feat: complete framework migration — PowerShell, adapters, guides, profiles, tests
Completes the bootstrap repo migration with remaining files: - PowerShell scripts (.ps1) for Windows support (bin/ + tools/) - Runtime adapters (claude, codex, generic, pi) - Guides (17 .md files) and profiles (domains, tech-stacks, workflows) - Wizard test suite (6 test files from bootstrap tests/) - Memory placeholder, audit history Bootstrap repo (mosaic/bootstrap) is now fully superseded: - All 335 files accounted for - 5 build config files (package.json, tsconfig, etc.) not needed — monorepo has its own at packages/mosaic/ - skills-local/ superseded by monorepo skills/ with mosaic-* naming - src/ already lives at packages/mosaic/src/
This commit is contained in:
283
packages/mosaic/framework/bin/mosaic-doctor.ps1
Normal file
283
packages/mosaic/framework/bin/mosaic-doctor.ps1
Normal file
@@ -0,0 +1,283 @@
|
||||
# mosaic-doctor.ps1
|
||||
# Audits Mosaic runtime state and detects drift across agent runtimes.
|
||||
# PowerShell equivalent of mosaic-doctor (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$FailOnWarn,
|
||||
[switch]$Verbose,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-doctor.ps1 [-FailOnWarn] [-Verbose] [-Help]
|
||||
|
||||
Audit Mosaic runtime state and detect drift across agent runtimes.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
$script:warnCount = 0
|
||||
|
||||
function Warn {
|
||||
param([string]$Message)
|
||||
$script:warnCount++
|
||||
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Pass {
|
||||
param([string]$Message)
|
||||
if ($Verbose) { Write-Host "[OK] $Message" -ForegroundColor Green }
|
||||
}
|
||||
|
||||
function Expect-Dir {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path -PathType Container)) { Warn "Missing directory: $Path" }
|
||||
else { Pass "Directory present: $Path" }
|
||||
}
|
||||
|
||||
function Expect-File {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path -PathType Leaf)) { Warn "Missing file: $Path" }
|
||||
else { Pass "File present: $Path" }
|
||||
}
|
||||
|
||||
function Check-RuntimeFileCopy {
|
||||
param([string]$Src, [string]$Dst)
|
||||
|
||||
if (-not (Test-Path $Src)) { return }
|
||||
|
||||
if (-not (Test-Path $Dst)) {
|
||||
Warn "Missing runtime file: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Runtime file should not be symlinked: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -ne $dstHash) {
|
||||
Warn "Runtime file drift: $Dst (does not match $Src)"
|
||||
}
|
||||
else {
|
||||
Pass "Runtime file synced: $Dst"
|
||||
}
|
||||
}
|
||||
|
||||
function Check-RuntimeContractFile {
|
||||
param([string]$Dst, [string]$AdapterSrc, [string]$RuntimeName)
|
||||
|
||||
if (-not (Test-Path $Dst)) {
|
||||
Warn "Missing runtime file: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Runtime file should not be symlinked: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
# Accept direct-adapter copy mode.
|
||||
if (Test-Path $AdapterSrc) {
|
||||
$srcHash = (Get-FileHash $AdapterSrc -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -eq $dstHash) {
|
||||
Pass "Runtime adapter synced: $Dst"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Accept launcher-composed runtime contract mode.
|
||||
$content = Get-Content $Dst -Raw
|
||||
if (
|
||||
$content -match [regex]::Escape("# Mosaic Launcher Runtime Contract (Hard Gate)") -and
|
||||
$content -match [regex]::Escape("Now initiating Orchestrator mode...") -and
|
||||
$content -match [regex]::Escape("Mosaic hard gates OVERRIDE runtime-default caution") -and
|
||||
$content -match [regex]::Escape("# Runtime-Specific Contract")
|
||||
) {
|
||||
Pass "Runtime contract present: $Dst ($RuntimeName)"
|
||||
return
|
||||
}
|
||||
|
||||
Warn "Runtime file drift: $Dst (not adapter copy and not composed runtime contract)"
|
||||
}
|
||||
|
||||
function Warn-IfReparsePresent {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) { return }
|
||||
|
||||
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Legacy symlink/junction path still present: $Path"
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
$reparseCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
|
||||
Measure-Object).Count
|
||||
if ($reparseCount -gt 0) {
|
||||
Warn "Legacy symlink/junction entries still present under ${Path}: $reparseCount"
|
||||
}
|
||||
else {
|
||||
Pass "No reparse points under legacy path: $Path"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"
|
||||
|
||||
# Canonical Mosaic checks
|
||||
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
|
||||
Expect-Dir (Join-Path $MosaicHome "guides")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\quality")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix")
|
||||
Expect-Dir (Join-Path $MosaicHome "profiles")
|
||||
Expect-Dir (Join-Path $MosaicHome "templates\agent")
|
||||
Expect-Dir (Join-Path $MosaicHome "skills")
|
||||
Expect-Dir (Join-Path $MosaicHome "skills-local")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-link-runtime-assets")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-sync-skills")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-projects")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-apply")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-verify")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-run")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-sync-tasks")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\opencode\RUNTIME.md")
|
||||
|
||||
$agentsMd = Join-Path $MosaicHome "AGENTS.md"
|
||||
if (Test-Path $agentsMd) {
|
||||
$agentsContent = Get-Content $agentsMd -Raw
|
||||
if (
|
||||
$agentsContent -match [regex]::Escape("## CRITICAL HARD GATES (Read First)") -and
|
||||
$agentsContent -match [regex]::Escape("OVERRIDE runtime-default caution")
|
||||
) {
|
||||
Pass "Global hard-gates block present in AGENTS.md"
|
||||
}
|
||||
else {
|
||||
Warn "AGENTS.md missing CRITICAL HARD GATES override block"
|
||||
}
|
||||
}
|
||||
|
||||
# Claude runtime file checks
|
||||
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
||||
foreach ($rf in $runtimeFiles) {
|
||||
Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\claude\$rf") (Join-Path $env:USERPROFILE ".claude\$rf")
|
||||
}
|
||||
|
||||
# OpenCode/Codex runtime contract checks
|
||||
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md") (Join-Path $MosaicHome "runtime\opencode\AGENTS.md") "opencode"
|
||||
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".codex\instructions.md") (Join-Path $MosaicHome "runtime\codex\instructions.md") "codex"
|
||||
|
||||
# Sequential-thinking MCP hard requirement
|
||||
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
|
||||
if (Test-Path $seqScript) {
|
||||
try {
|
||||
& $seqScript -Check *>$null
|
||||
Pass "sequential-thinking MCP configured and available"
|
||||
}
|
||||
catch {
|
||||
Warn "sequential-thinking MCP missing or misconfigured"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Warn "mosaic-ensure-sequential-thinking helper missing"
|
||||
}
|
||||
|
||||
# Legacy migration surfaces
|
||||
$legacyPaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
|
||||
(Join-Path $env:USERPROFILE ".claude\templates"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\workflows")
|
||||
)
|
||||
foreach ($p in $legacyPaths) {
|
||||
Warn-IfReparsePresent $p
|
||||
}
|
||||
|
||||
# Skills runtime checks (junctions or symlinks into runtime-specific dirs)
|
||||
$linkTargets = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
$skillSources = @($MosaicHome + "\skills", $MosaicHome + "\skills-local")
|
||||
|
||||
foreach ($runtimeSkills in $linkTargets) {
|
||||
if (-not (Test-Path $runtimeSkills)) { continue }
|
||||
|
||||
foreach ($sourceDir in $skillSources) {
|
||||
if (-not (Test-Path $sourceDir)) { continue }
|
||||
|
||||
Get-ChildItem $sourceDir -Directory | Where-Object { -not $_.Name.StartsWith(".") } | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$skillPath = $_.FullName
|
||||
$target = Join-Path $runtimeSkills $name
|
||||
|
||||
if (-not (Test-Path $target)) {
|
||||
Warn "Missing skill link: $target"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $target -Force -ErrorAction SilentlyContinue
|
||||
if (-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Non-junction skill entry: $target"
|
||||
return
|
||||
}
|
||||
|
||||
$targetResolved = $item.Target
|
||||
if (-not $targetResolved -or (Resolve-Path $targetResolved -ErrorAction SilentlyContinue).Path -ne (Resolve-Path $skillPath -ErrorAction SilentlyContinue).Path) {
|
||||
Warn "Drifted skill link: $target (expected -> $skillPath)"
|
||||
}
|
||||
else {
|
||||
Pass "Linked skill: $target"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Broken junctions/symlinks in managed runtime skill dirs
|
||||
$brokenLinks = 0
|
||||
foreach ($d in $linkTargets) {
|
||||
if (-not (Test-Path $d)) { continue }
|
||||
Get-ChildItem $d -Force -ErrorAction SilentlyContinue | Where-Object {
|
||||
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and -not (Test-Path $_.FullName)
|
||||
} | ForEach-Object { $brokenLinks++ }
|
||||
}
|
||||
if ($brokenLinks -gt 0) {
|
||||
Warn "Broken skill junctions/symlinks detected: $brokenLinks"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-doctor] warnings=$($script:warnCount)"
|
||||
if ($FailOnWarn -and $script:warnCount -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
114
packages/mosaic/framework/bin/mosaic-ensure-sequential-thinking.ps1
Executable file
114
packages/mosaic/framework/bin/mosaic-ensure-sequential-thinking.ps1
Executable file
@@ -0,0 +1,114 @@
|
||||
# mosaic-ensure-sequential-thinking.ps1
|
||||
param(
|
||||
[switch]$Check
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Pkg = "@modelcontextprotocol/server-sequential-thinking"
|
||||
|
||||
function Require-Binary {
|
||||
param([string]$Name)
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required binary missing: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Warm-Package {
|
||||
$null = & npx -y $Pkg --help 2>$null
|
||||
}
|
||||
|
||||
function Set-ClaudeConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".claude\settings.json"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
|
||||
$data = @{}
|
||||
if (Test-Path $path) {
|
||||
try { $data = Get-Content $path -Raw | ConvertFrom-Json -AsHashtable } catch { $data = @{} }
|
||||
}
|
||||
if (-not $data.ContainsKey("mcpServers") -or -not ($data["mcpServers"] -is [hashtable])) {
|
||||
$data["mcpServers"] = @{}
|
||||
}
|
||||
$data["mcpServers"]["sequential-thinking"] = @{
|
||||
command = "npx"
|
||||
args = @("-y", "@modelcontextprotocol/server-sequential-thinking")
|
||||
}
|
||||
$data | ConvertTo-Json -Depth 20 | Set-Content -Path $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Set-CodexConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".codex\config.toml"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
if (-not (Test-Path $path)) { New-Item -ItemType File -Path $path -Force | Out-Null }
|
||||
|
||||
$content = Get-Content $path -Raw
|
||||
$content = [regex]::Replace($content, "(?ms)^\[mcp_servers\.(sequential-thinking|sequential_thinking)\].*?(?=^\[|\z)", "")
|
||||
$content = $content.TrimEnd() + "`n`n[mcp_servers.sequential-thinking]`ncommand = `"npx`"`nargs = [`"-y`", `"@modelcontextprotocol/server-sequential-thinking`"]`n"
|
||||
Set-Content -Path $path -Value $content -Encoding UTF8
|
||||
}
|
||||
|
||||
function Set-OpenCodeConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".config\opencode\config.json"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
|
||||
$data = @{}
|
||||
if (Test-Path $path) {
|
||||
try { $data = Get-Content $path -Raw | ConvertFrom-Json -AsHashtable } catch { $data = @{} }
|
||||
}
|
||||
if (-not $data.ContainsKey("mcp") -or -not ($data["mcp"] -is [hashtable])) {
|
||||
$data["mcp"] = @{}
|
||||
}
|
||||
$data["mcp"]["sequential-thinking"] = @{
|
||||
type = "local"
|
||||
command = @("npx", "-y", "@modelcontextprotocol/server-sequential-thinking")
|
||||
enabled = $true
|
||||
}
|
||||
$data | ConvertTo-Json -Depth 20 | Set-Content -Path $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Test-Configs {
|
||||
$claudeOk = $false
|
||||
$codexOk = $false
|
||||
$opencodeOk = $false
|
||||
|
||||
$claudePath = Join-Path $env:USERPROFILE ".claude\settings.json"
|
||||
if (Test-Path $claudePath) {
|
||||
try {
|
||||
$c = Get-Content $claudePath -Raw | ConvertFrom-Json -AsHashtable
|
||||
$claudeOk = $c.ContainsKey("mcpServers") -and $c["mcpServers"].ContainsKey("sequential-thinking")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$codexPath = Join-Path $env:USERPROFILE ".codex\config.toml"
|
||||
if (Test-Path $codexPath) {
|
||||
$raw = Get-Content $codexPath -Raw
|
||||
$codexOk = $raw -match "\[mcp_servers\.(sequential-thinking|sequential_thinking)\]" -and $raw -match "@modelcontextprotocol/server-sequential-thinking"
|
||||
}
|
||||
|
||||
$opencodePath = Join-Path $env:USERPROFILE ".config\opencode\config.json"
|
||||
if (Test-Path $opencodePath) {
|
||||
try {
|
||||
$o = Get-Content $opencodePath -Raw | ConvertFrom-Json -AsHashtable
|
||||
$opencodeOk = $o.ContainsKey("mcp") -and $o["mcp"].ContainsKey("sequential-thinking")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (-not ($claudeOk -and $codexOk -and $opencodeOk)) {
|
||||
throw "Sequential-thinking MCP runtime config is incomplete"
|
||||
}
|
||||
}
|
||||
|
||||
Require-Binary node
|
||||
Require-Binary npx
|
||||
Warm-Package
|
||||
|
||||
if ($Check) {
|
||||
Test-Configs
|
||||
Write-Host "[mosaic-seq] sequential-thinking MCP is configured and available"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Set-ClaudeConfig
|
||||
Set-CodexConfig
|
||||
Set-OpenCodeConfig
|
||||
Write-Host "[mosaic-seq] sequential-thinking MCP configured for Claude, Codex, and OpenCode"
|
||||
144
packages/mosaic/framework/bin/mosaic-init.ps1
Normal file
144
packages/mosaic/framework/bin/mosaic-init.ps1
Normal file
@@ -0,0 +1,144 @@
|
||||
# mosaic-init.ps1 — Interactive SOUL.md generator (Windows)
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-init.ps1 # Interactive mode
|
||||
# mosaic-init.ps1 -Name "Jarvis" -Style direct # Flag overrides
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Role,
|
||||
[ValidateSet("direct", "friendly", "formal")]
|
||||
[string]$Style,
|
||||
[string]$Accessibility,
|
||||
[string]$Guardrails,
|
||||
[switch]$NonInteractive,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$Template = Join-Path $MosaicHome "templates\SOUL.md.template"
|
||||
$Output = Join-Path $MosaicHome "SOUL.md"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-init.ps1 [-Name <name>] [-Role <desc>] [-Style direct|friendly|formal]
|
||||
[-Accessibility <prefs>] [-Guardrails <rules>] [-NonInteractive]
|
||||
|
||||
Generate ~/.config/mosaic/SOUL.md - the universal agent identity contract.
|
||||
Interactive by default. Use flags to skip prompts.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Prompt-IfEmpty {
|
||||
param([string]$Current, [string]$PromptText, [string]$Default = "")
|
||||
|
||||
if ($Current) { return $Current }
|
||||
|
||||
if ($NonInteractive) {
|
||||
if ($Default) { return $Default }
|
||||
Write-Host "[mosaic-init] ERROR: Value required in non-interactive mode: $PromptText" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$display = if ($Default) { "$PromptText [$Default]" } else { $PromptText }
|
||||
$value = Read-Host $display
|
||||
if (-not $value -and $Default) { return $Default }
|
||||
return $value
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-init] Generating SOUL.md - your universal agent identity contract"
|
||||
Write-Host ""
|
||||
|
||||
$Name = Prompt-IfEmpty $Name "What name should agents use" "Assistant"
|
||||
$Role = Prompt-IfEmpty $Role "Agent role description" "execution partner and visibility engine"
|
||||
|
||||
if (-not $Style) {
|
||||
if ($NonInteractive) {
|
||||
$Style = "direct"
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-Host "Communication style:"
|
||||
Write-Host " 1) direct - Concise, no fluff, actionable"
|
||||
Write-Host " 2) friendly - Warm but efficient, conversational"
|
||||
Write-Host " 3) formal - Professional, structured, thorough"
|
||||
$choice = Read-Host "Choose [1/2/3] (default: 1)"
|
||||
$Style = switch ($choice) {
|
||||
"2" { "friendly" }
|
||||
"3" { "formal" }
|
||||
default { "direct" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$Accessibility = Prompt-IfEmpty $Accessibility "Accessibility preferences (or 'none')" "none"
|
||||
|
||||
if (-not $Guardrails -and -not $NonInteractive) {
|
||||
Write-Host ""
|
||||
$Guardrails = Read-Host "Custom guardrails (optional, press Enter to skip)"
|
||||
}
|
||||
|
||||
# Build behavioral principles
|
||||
$BehavioralPrinciples = switch ($Style) {
|
||||
"direct" {
|
||||
"1. Clarity over performance theater.`n2. Practical execution over abstract planning.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Visible state over hidden assumptions."
|
||||
}
|
||||
"friendly" {
|
||||
"1. Be helpful and approachable while staying efficient.`n2. Provide context and explain reasoning when helpful.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Visible state over hidden assumptions."
|
||||
}
|
||||
"formal" {
|
||||
"1. Maintain professional, structured communication.`n2. Provide thorough analysis with explicit tradeoffs.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Document decisions and rationale clearly."
|
||||
}
|
||||
}
|
||||
|
||||
if ($Accessibility -and $Accessibility -ne "none") {
|
||||
$BehavioralPrinciples += "`n5. $Accessibility."
|
||||
}
|
||||
|
||||
# Build communication style
|
||||
$CommunicationStyle = switch ($Style) {
|
||||
"direct" {
|
||||
"- Be direct, concise, and concrete.`n- Avoid fluff, hype, and anthropomorphic roleplay.`n- Do not simulate certainty when facts are missing.`n- Prefer actionable next steps and explicit tradeoffs."
|
||||
}
|
||||
"friendly" {
|
||||
"- Be warm and conversational while staying focused.`n- Explain your reasoning when it helps the user.`n- Do not simulate certainty when facts are missing.`n- Prefer actionable next steps with clear context."
|
||||
}
|
||||
"formal" {
|
||||
"- Use professional, structured language.`n- Provide thorough explanations with supporting detail.`n- Do not simulate certainty when facts are missing.`n- Present options with explicit tradeoffs and recommendations."
|
||||
}
|
||||
}
|
||||
|
||||
# Format custom guardrails
|
||||
$FormattedGuardrails = if ($Guardrails) { "- $Guardrails" } else { "" }
|
||||
|
||||
# Verify template
|
||||
if (-not (Test-Path $Template)) {
|
||||
Write-Host "[mosaic-init] ERROR: Template not found: $Template" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Generate SOUL.md
|
||||
$content = Get-Content $Template -Raw
|
||||
$content = $content -replace '\{\{AGENT_NAME\}\}', $Name
|
||||
$content = $content -replace '\{\{ROLE_DESCRIPTION\}\}', $Role
|
||||
$content = $content -replace '\{\{BEHAVIORAL_PRINCIPLES\}\}', $BehavioralPrinciples
|
||||
$content = $content -replace '\{\{COMMUNICATION_STYLE\}\}', $CommunicationStyle
|
||||
$content = $content -replace '\{\{CUSTOM_GUARDRAILS\}\}', $FormattedGuardrails
|
||||
|
||||
Set-Content -Path $Output -Value $content -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[mosaic-init] Generated: $Output"
|
||||
Write-Host "[mosaic-init] Agent name: $Name"
|
||||
Write-Host "[mosaic-init] Style: $Style"
|
||||
|
||||
# Push to runtime adapters
|
||||
$linkScript = Join-Path $MosaicHome "bin\mosaic-link-runtime-assets.ps1"
|
||||
if (Test-Path $linkScript) {
|
||||
Write-Host "[mosaic-init] Updating runtime adapters..."
|
||||
& $linkScript
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-init] Done. Launch with: mosaic claude"
|
||||
111
packages/mosaic/framework/bin/mosaic-link-runtime-assets.ps1
Normal file
111
packages/mosaic/framework/bin/mosaic-link-runtime-assets.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
# mosaic-link-runtime-assets.ps1
|
||||
# Syncs Mosaic runtime config files into agent runtime directories.
|
||||
# PowerShell equivalent of mosaic-link-runtime-assets (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$BackupStamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
|
||||
function Copy-FileManaged {
|
||||
param([string]$Src, [string]$Dst)
|
||||
|
||||
$parent = Split-Path $Dst -Parent
|
||||
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
|
||||
# Remove existing symlink/junction
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item $Dst -Force
|
||||
}
|
||||
|
||||
if (Test-Path $Dst) {
|
||||
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -eq $dstHash) { return }
|
||||
Rename-Item $Dst "$Dst.mosaic-bak-$BackupStamp"
|
||||
}
|
||||
|
||||
Copy-Item $Src $Dst -Force
|
||||
}
|
||||
|
||||
function Remove-LegacyPath {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path $Path)) { return }
|
||||
|
||||
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item $Path -Force
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
# Remove symlinks/junctions inside, then empty dirs
|
||||
Get-ChildItem $Path -Recurse -Force | Where-Object {
|
||||
$_.Attributes -band [System.IO.FileAttributes]::ReparsePoint
|
||||
} | Remove-Item -Force
|
||||
|
||||
Get-ChildItem $Path -Recurse -Directory -Force |
|
||||
Sort-Object { $_.FullName.Length } -Descending |
|
||||
Where-Object { (Get-ChildItem $_.FullName -Force | Measure-Object).Count -eq 0 } |
|
||||
Remove-Item -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Remove legacy compatibility paths
|
||||
$legacyPaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\debug-hook.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-handler.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-stdin.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-wrapper.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-queue-monitor.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\remediation-hook-handler.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\templates"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\workflows"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\jarvis-loop.json")
|
||||
)
|
||||
|
||||
foreach ($p in $legacyPaths) {
|
||||
Remove-LegacyPath $p
|
||||
}
|
||||
|
||||
# Claude-specific runtime files (settings, hooks — CLAUDE.md is now a thin pointer)
|
||||
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
||||
foreach ($rf in $runtimeFiles) {
|
||||
$src = Join-Path $MosaicHome "runtime\claude\$rf"
|
||||
if (-not (Test-Path $src)) { continue }
|
||||
$dst = Join-Path $env:USERPROFILE ".claude\$rf"
|
||||
Copy-FileManaged $src $dst
|
||||
}
|
||||
|
||||
# OpenCode runtime adapter
|
||||
$opencodeSrc = Join-Path $MosaicHome "runtime\opencode\AGENTS.md"
|
||||
if (Test-Path $opencodeSrc) {
|
||||
$opencodeDst = Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md"
|
||||
Copy-FileManaged $opencodeSrc $opencodeDst
|
||||
}
|
||||
|
||||
# Codex runtime adapter
|
||||
$codexSrc = Join-Path $MosaicHome "runtime\codex\instructions.md"
|
||||
if (Test-Path $codexSrc) {
|
||||
$codexDir = Join-Path $env:USERPROFILE ".codex"
|
||||
if (-not (Test-Path $codexDir)) { New-Item -ItemType Directory -Path $codexDir -Force | Out-Null }
|
||||
$codexDst = Join-Path $codexDir "instructions.md"
|
||||
Copy-FileManaged $codexSrc $codexDst
|
||||
}
|
||||
|
||||
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
|
||||
if (Test-Path $seqScript) {
|
||||
& $seqScript
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-link] Runtime assets synced (non-symlink mode)"
|
||||
Write-Host "[mosaic-link] Canonical source: $MosaicHome"
|
||||
@@ -0,0 +1,90 @@
|
||||
# mosaic-migrate-local-skills.ps1
|
||||
# Migrates runtime-local skill directories to Mosaic-managed junctions.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-migrate-local-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$Apply,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$LocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-migrate-local-skills.ps1 [-Apply] [-Help]
|
||||
|
||||
Migrate runtime-local skill directories (e.g. ~/.claude/skills/jarvis) to
|
||||
Mosaic-managed skills by replacing local directories with junctions to
|
||||
~/.config/mosaic/skills-local.
|
||||
|
||||
Default mode is dry-run.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Test-Path $LocalSkillsDir)) {
|
||||
Write-Host "[mosaic-local-skills] Missing local skills dir: $LocalSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$skillRoots = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
$count = 0
|
||||
|
||||
Get-ChildItem $LocalSkillsDir -Directory | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$src = $_.FullName
|
||||
|
||||
foreach ($root in $skillRoots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$target = Join-Path $root $name
|
||||
|
||||
# Already a junction/symlink — check if it points to the right place
|
||||
$existing = Get-Item $target -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$currentTarget = $existing.Target
|
||||
if ($currentTarget -and ($currentTarget -eq $src -or (Resolve-Path $currentTarget -ErrorAction SilentlyContinue).Path -eq (Resolve-Path $src -ErrorAction SilentlyContinue).Path)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
# Only migrate local directories containing SKILL.md
|
||||
if ((Test-Path $target -PathType Container) -and
|
||||
(Test-Path (Join-Path $target "SKILL.md")) -and
|
||||
-not ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint))) {
|
||||
|
||||
$count++
|
||||
if ($Apply) {
|
||||
$stamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
Rename-Item $target "$target.mosaic-bak-$stamp"
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $target -Target $src -ErrorAction Stop | Out-Null
|
||||
Write-Host "[mosaic-local-skills] migrated: $target -> $src"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-local-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $src $target -Recurse -Force
|
||||
Write-Host "[mosaic-local-skills] copied: $target <- $src"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] would migrate: $target -> $src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Apply) {
|
||||
Write-Host "[mosaic-local-skills] complete: migrated=$count"
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] dry-run: migratable=$count"
|
||||
Write-Host "[mosaic-local-skills] re-run with -Apply to migrate"
|
||||
}
|
||||
65
packages/mosaic/framework/bin/mosaic-release-upgrade.ps1
Normal file
65
packages/mosaic/framework/bin/mosaic-release-upgrade.ps1
Normal file
@@ -0,0 +1,65 @@
|
||||
# mosaic-release-upgrade.ps1 — Upgrade installed Mosaic framework release (Windows)
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-release-upgrade.ps1
|
||||
# mosaic-release-upgrade.ps1 -Ref main -Keep
|
||||
# mosaic-release-upgrade.ps1 -Ref v0.2.0 -Overwrite -Yes
|
||||
#
|
||||
param(
|
||||
[string]$Ref = $(if ($env:MOSAIC_BOOTSTRAP_REF) { $env:MOSAIC_BOOTSTRAP_REF } else { "main" }),
|
||||
[switch]$Keep,
|
||||
[switch]$Overwrite,
|
||||
[switch]$Yes,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) {
|
||||
$env:MOSAIC_REMOTE_INSTALL_URL
|
||||
} else {
|
||||
"https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1"
|
||||
}
|
||||
|
||||
$installMode = if ($Overwrite) { "overwrite" } elseif ($Keep) { "keep" } elseif ($env:MOSAIC_INSTALL_MODE) { $env:MOSAIC_INSTALL_MODE } else { "keep" }
|
||||
if ($installMode -notin @("keep", "overwrite")) {
|
||||
Write-Host "[mosaic-release-upgrade] Invalid install mode: $installMode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$currentVersion = "unknown"
|
||||
$mosaicCmd = Join-Path $MosaicHome "bin\mosaic.ps1"
|
||||
if (Test-Path $mosaicCmd) {
|
||||
try {
|
||||
$currentVersion = (& $mosaicCmd --version) -replace '^mosaic\s+', ''
|
||||
}
|
||||
catch {
|
||||
$currentVersion = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-release-upgrade] Current version: $currentVersion"
|
||||
Write-Host "[mosaic-release-upgrade] Target ref: $Ref"
|
||||
Write-Host "[mosaic-release-upgrade] Install mode: $installMode"
|
||||
Write-Host "[mosaic-release-upgrade] Installer URL: $RemoteInstallerUrl"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[mosaic-release-upgrade] Dry run: no changes applied."
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Yes) {
|
||||
$confirmation = Read-Host "Proceed with Mosaic release upgrade? [y/N]"
|
||||
if ($confirmation -notin @("y", "Y", "yes", "YES")) {
|
||||
Write-Host "[mosaic-release-upgrade] Aborted."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$env:MOSAIC_BOOTSTRAP_REF = $Ref
|
||||
$env:MOSAIC_INSTALL_MODE = $installMode
|
||||
$env:MOSAIC_HOME = $MosaicHome
|
||||
|
||||
Invoke-RestMethod -Uri $RemoteInstallerUrl | Invoke-Expression
|
||||
|
||||
126
packages/mosaic/framework/bin/mosaic-sync-skills.ps1
Normal file
126
packages/mosaic/framework/bin/mosaic-sync-skills.ps1
Normal file
@@ -0,0 +1,126 @@
|
||||
# mosaic-sync-skills.ps1
|
||||
# Syncs canonical skills and links them into agent runtime skill directories.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-sync-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$LinkOnly,
|
||||
[switch]$NoLink,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$SkillsRepoUrl = if ($env:MOSAIC_SKILLS_REPO_URL) { $env:MOSAIC_SKILLS_REPO_URL } else { "https://git.mosaicstack.dev/mosaic/agent-skills.git" }
|
||||
$SkillsRepoDir = if ($env:MOSAIC_SKILLS_REPO_DIR) { $env:MOSAIC_SKILLS_REPO_DIR } else { Join-Path $MosaicHome "sources\agent-skills" }
|
||||
$MosaicSkillsDir = Join-Path $MosaicHome "skills"
|
||||
$MosaicLocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-sync-skills.ps1 [-LinkOnly] [-NoLink] [-Help]
|
||||
|
||||
Sync canonical skills into ~/.config/mosaic/skills and link all Mosaic skills
|
||||
into runtime skill directories using directory junctions.
|
||||
|
||||
Options:
|
||||
-LinkOnly Skip git clone/pull and only relink
|
||||
-NoLink Sync canonical skills but do not update runtime links
|
||||
-Help Show help
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($d in @($MosaicHome, $MosaicSkillsDir, $MosaicLocalSkillsDir)) {
|
||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||||
}
|
||||
|
||||
# Fetch skills from git
|
||||
if (-not $LinkOnly) {
|
||||
if (Test-Path (Join-Path $SkillsRepoDir ".git")) {
|
||||
Write-Host "[mosaic-skills] Updating skills source: $SkillsRepoDir"
|
||||
git -C $SkillsRepoDir pull --rebase
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-skills] Cloning skills source to: $SkillsRepoDir"
|
||||
$parentDir = Split-Path $SkillsRepoDir -Parent
|
||||
if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null }
|
||||
git clone $SkillsRepoUrl $SkillsRepoDir
|
||||
}
|
||||
|
||||
$sourceSkillsDir = Join-Path $SkillsRepoDir "skills"
|
||||
if (-not (Test-Path $sourceSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Missing source skills dir: $sourceSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sync: remove old, copy new
|
||||
if (Test-Path $MosaicSkillsDir) {
|
||||
Get-ChildItem $MosaicSkillsDir | Remove-Item -Recurse -Force
|
||||
}
|
||||
Copy-Item "$sourceSkillsDir\*" $MosaicSkillsDir -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $MosaicSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Canonical skills dir missing: $MosaicSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($NoLink) {
|
||||
Write-Host "[mosaic-skills] Canonical sync completed (link update skipped)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Link-SkillIntoTarget {
|
||||
param([string]$SkillPath, [string]$TargetDir)
|
||||
|
||||
$name = Split-Path $SkillPath -Leaf
|
||||
if ($name.StartsWith(".")) { return }
|
||||
|
||||
$linkPath = Join-Path $TargetDir $name
|
||||
|
||||
# Already a junction/symlink — recreate
|
||||
$existing = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Remove-Item $linkPath -Force
|
||||
}
|
||||
elseif ($existing) {
|
||||
Write-Host "[mosaic-skills] Preserve existing runtime-specific entry: $linkPath"
|
||||
return
|
||||
}
|
||||
|
||||
# Try junction first, fall back to copy
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $linkPath -Target $SkillPath -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $SkillPath $linkPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
$linkTargets = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
foreach ($target in $linkTargets) {
|
||||
if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null }
|
||||
|
||||
# Link canonical skills
|
||||
Get-ChildItem $MosaicSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
|
||||
# Link local skills
|
||||
if (Test-Path $MosaicLocalSkillsDir) {
|
||||
Get-ChildItem $MosaicLocalSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Linked skills into: $target"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Complete"
|
||||
437
packages/mosaic/framework/bin/mosaic.ps1
Normal file
437
packages/mosaic/framework/bin/mosaic.ps1
Normal 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: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user