From 0710baae2926af29e4bdc5d76fdbcfb9f3814d86 Mon Sep 17 00:00:00 2001 From: vitya Date: Thu, 11 Jun 2026 13:34:09 +0300 Subject: [PATCH] feat(manifest): restructure factory.yaml with core/modules schema + manifest-driven bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit factory.yaml: new schema (schema_version 1, factory_version 0.3.0) - core.components[] with id/source/version/type/install/health_check - versions from bundled lib/: projects-meta-mcp 2.25.0, wiki-graph 0.3.1, interns-mcp 0.3.3 - modules[]: tasks, wiki, workshop, admin — each with creates[] describing gitea_repo / local_clone / claude_md_snippet / setup_skill actions - removed hardcoded git_host/git_org (those belong in ~/.config/factory/home.toml per design) bootstrap.ps1: add Read-FactoryManifest + manifest-driven MCP build loop - indent-aware YAML parser prevents health_check.type from clobbering component.type - MCP build loop now reads from manifest instead of hardcoded server names - reports factory version at startup Co-Authored-By: Claude Sonnet 4.6 --- bootstrap.ps1 | 114 +++++++++++++++++------ factory.yaml | 248 +++++++++++++++++++++++++------------------------- 2 files changed, 211 insertions(+), 151 deletions(-) diff --git a/bootstrap.ps1 b/bootstrap.ps1 index 84702eb..e63123e 100644 --- a/bootstrap.ps1 +++ b/bootstrap.ps1 @@ -16,6 +16,60 @@ function Test-Command { $null = Get-Command $Name -ErrorAction SilentlyContinue } +function Read-FactoryManifest { + param([string]$FactoryPath) + + $yamlPath = "$FactoryPath\factory.yaml" + if (-not (Test-Path $yamlPath)) { + Write-Gap "factory.yaml not found at $yamlPath" + return $null + } + + $lines = Get-Content $yamlPath + $manifest = @{ + schema_version = "" + factory_version = "" + core_components = [System.Collections.Generic.List[hashtable]]::new() + modules = [System.Collections.Generic.List[hashtable]]::new() + } + + $section = "" + $currentItem = $null + $propPrefix = "" # exact indent of direct item properties (avoids nested key collisions) + + foreach ($line in $lines) { + if ($line -match '^\s*#' -or $line -match '^\s*$') { continue } + + if ($line -match '^schema_version:\s*"([^"]+)"') { $manifest.schema_version = $matches[1]; continue } + if ($line -match '^factory_version:\s*"([^"]+)"') { $manifest.factory_version = $matches[1]; continue } + + if ($line -match '^core:') { $section = ""; $currentItem = $null; continue } + if ($line -match '^\s+components:') { $section = "core_components"; $currentItem = $null; continue } + if ($line -match '^modules:') { $section = "modules"; $currentItem = $null; continue } + + # List item start — detect indent to build exact property prefix + if ($line -match '^(\s+)-\s+id:\s+(\S+)') { + $itemIndent = $matches[1].Length + $propPrefix = " " * ($itemIndent + 2) + $currentItem = @{ id = $matches[2]; type = ""; source = ""; version = ""; description = ""; optional = $true } + if ($section -eq "core_components") { $manifest.core_components.Add($currentItem) } + elseif ($section -eq "modules") { $manifest.modules.Add($currentItem) } + continue + } + + if (-not $currentItem) { continue } + + # Match only at direct-property indent (prevents health_check.type clobbering component.type) + if ($line -match "^${propPrefix}type:\s+(\S+)") { $currentItem.type = $matches[1]; continue } + if ($line -match "^${propPrefix}source:\s+(\S+)") { $currentItem.source = $matches[1]; continue } + if ($line -match "^${propPrefix}version:\s+`"([^`"]+)`"") { $currentItem.version = $matches[1]; continue } + if ($line -match "^${propPrefix}description:\s+`"([^`"]+)`"") { $currentItem.description = $matches[1]; continue } + if ($line -match "^${propPrefix}optional:\s+(\S+)") { $currentItem.optional = ($matches[1] -eq "true"); continue } + } + + return $manifest +} + function Write-Step { param([string]$Msg) Write-Host "`n▶ $Msg" -ForegroundColor Cyan @@ -174,47 +228,55 @@ $lines = @( Write-Ok "Wrote $HOME_TOML" -# Build MCP servers from lib/ +# Read factory manifest +Write-Step "Reading factory manifest" + +$manifest = Read-FactoryManifest -FactoryPath $factoryLocal +if (-not $manifest) { exit 1 } +Write-Ok "factory v$($manifest.factory_version) (schema $($manifest.schema_version))" +$coreIds = ($manifest.core_components | ForEach-Object { $_.id }) -join ", " +$moduleIds = ($manifest.modules | ForEach-Object { $_.id }) -join ", " +Write-Host " Core: $coreIds" +Write-Host " Modules: $moduleIds" + +# Build MCP servers from lib/ (driven by manifest core.components) Write-Step "Building MCP servers" $libPath = "$factoryLocal\lib" if (Test-Path $libPath) { - # TypeScript servers (npm install + npm run build) - foreach ($server in @("projects-meta-mcp", "wiki-graph")) { - $serverPath = "$libPath\$server" - if (Test-Path $serverPath) { - Write-Host " Building $server..." - Push-Location $serverPath + foreach ($component in $manifest.core_components) { + $srcPath = "$factoryLocal\$($component.source)" + if (-not (Test-Path $srcPath)) { continue } + + if ($component.type -eq "typescript") { + Write-Host " Building $($component.id) v$($component.version)..." + Push-Location $srcPath try { npm install --silent 2>&1 | Out-Null npm run build 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { - Write-Gap "$server: build failed (npm run build exited $LASTEXITCODE)" + Write-Gap "$($component.id): build failed (npm run build exited $LASTEXITCODE)" } else { - Write-Ok "$server built" + Write-Ok "$($component.id) v$($component.version) built" } } finally { Pop-Location } - } - } - - # Python server (interns-mcp) — venv + pip install - $internsMcpPath = "$libPath\interns-mcp" - if (Test-Path $internsMcpPath) { - Write-Host " Setting up interns-mcp..." - Push-Location $internsMcpPath - try { - python -m venv .venv 2>&1 | Out-Null - & ".venv\Scripts\pip.exe" install -e . --quiet - if ($LASTEXITCODE -ne 0) { - Write-Gap "interns-mcp: pip install failed" - } else { - Write-Ok "interns-mcp installed" + } elseif ($component.type -eq "python") { + Write-Host " Setting up $($component.id) v$($component.version)..." + Push-Location $srcPath + try { + python -m venv .venv 2>&1 | Out-Null + & ".venv\Scripts\pip.exe" install -e . --quiet + if ($LASTEXITCODE -ne 0) { + Write-Gap "$($component.id): pip install failed" + } else { + Write-Ok "$($component.id) v$($component.version) installed" + } + } finally { + Pop-Location } - } finally { - Pop-Location } } } else { diff --git a/factory.yaml b/factory.yaml index d7afa1e..530ca28 100644 --- a/factory.yaml +++ b/factory.yaml @@ -1,141 +1,139 @@ -# factory.yaml — L1 manifest -# Variables: {gitea}, {projects_dir}, {factory}, {home} -# Substitution happens at runtime from ~/.config/factory/home.toml +# factory.yaml — manifest компонентов factory +# +# Runtime variables (expanded from ~/.config/factory/home.toml): +# {git_host} — e.g. https://git.kzntsv.site +# {git_org} — e.g. OpeItcLoc03 +# {projects_dir} — e.g. C:\Users\alice\projects +# {factory} — {projects_dir}\.factory +# {home} — user home dir (~) -version: "0.2.0" +schema_version: "1" +factory_version: "0.3.0" -# Git host configuration (overrides {gitea} default) -git_host: "https://git.kzntsv.site" -git_org: "OpeItcLoc03" +# ── Core (always installed) ────────────────────────────────────────────────── +# +# Everything in core ships inside the factory repo: +# lib/ — MCP servers (built locally on install) +# skills/ — skill files (copied to ~/.claude/skills/) +# +# bootstrap.ps1 reads this list to drive the build loop. -# Components to install/update -components: - # === Core infrastructure === +core: + components: - - name: dot-common - display_name: ".common shared libs" - source: - type: git - url: "{git_host}/{git_org}/common" - branch: master - target: "{projects_dir}/.common" - health_check: - type: file_exists - path: "{target}/scripts/claude-switch.ps1" - clients: [claude-code, copilot-cli, gemini-cli] + - id: projects-meta-mcp + display_name: "Projects-meta MCP server" + source: lib/projects-meta-mcp + version: "2.25.0" + type: typescript + install: + run: + - npm install --silent + - npm run build + health_check: + type: mcp_tool + tool: mcp__projects-meta__meta_status - - name: projects-meta-mcp - display_name: "Projects-meta MCP server" - # Subdirectory of .common repo — no standalone git source - target: "{projects_dir}/.common/lib/projects-meta-mcp" - deps: [dot-common] - setup_skill: setup-projects-meta - health_check: - type: mcp_tool - tool: mcp__projects-meta__meta_status - clients: [claude-code, copilot-cli] + - id: wiki-graph + display_name: "Wiki-graph MCP server" + source: lib/wiki-graph + version: "0.3.1" + type: typescript + install: + run: + - npm install --silent + - npm run build + health_check: + type: mcp_tool + tool: mcp__wiki-graph__stats - - name: interns-mcp - display_name: "Interns MCP server" - # Subdirectory of .common repo — no standalone git source - target: "{projects_dir}/.common/lib/interns-mcp" - deps: [dot-common] - setup_skill: setup-interns - health_check: - type: mcp_tool - tool: mcp__interns__bulk_text_read - clients: [claude-code, copilot-cli] + - id: interns-mcp + display_name: "Interns MCP server" + source: lib/interns-mcp + version: "0.3.3" + type: python + install: + venv: .venv + pip: "-e ." + health_check: + type: mcp_tool + tool: mcp__interns__bulk_text_read - # === Knowledge === + - id: skills + display_name: "Claude skills" + source: skills + version: "bundled" + type: skills + install: + copy_to: "{home}/.claude/skills/" + health_check: + type: directory_exists + path: "{home}/.claude/skills" - - name: dot-wiki - display_name: "Shared project wiki" - source: - type: git - url: "{git_host}/{git_org}/projects-wiki" - branch: main - target: "{projects_dir}/.wiki" - deps: [projects-meta-mcp] - setup_skill: setup-wiki - health_check: - type: file_exists - path: "{target}/CLAUDE.md" - clients: [claude-code, copilot-cli, gemini-cli] +# ── Modules (selected at install time) ────────────────────────────────────── +# +# Each module is optional. bootstrap.ps1 asks the user which to install. +# creates[] describes what bootstrap creates on the user's machine/Gitea. +# +# create types: +# local_dir — create a local directory +# gitea_repo — create a repo on the user's Gitea via API +# local_clone — git clone a repo locally +# claude_md_snippet — append a trigger line to CLAUDE.md +# setup_skill — run a setup-* skill post-install - - name: dot-tasks - display_name: "Local task board" - target: "{projects_dir}/.tasks" - deps: [dot-wiki] - setup_skill: setup-tasks - health_check: - type: file_exists - path: "{target}/STATUS.md" - clients: [claude-code, copilot-cli, gemini-cli] +modules: - # === Tooling === - - - name: claude-skills - display_name: "Claude skills collection" - source: - type: git - url: "{git_host}/{git_org}/claude-skills" - branch: master - target: "{projects_dir}/claude-skills" - deps: [dot-common] - post_install: - shell: pwsh - script: "{target}/scripts/install.ps1" - shell_alt: bash - script_alt: "{target}/scripts/install.sh" - health_check: - type: directory_exists - path: "{home}/.claude/skills" - clients: [claude-code] - - # === Fixtures (optional) === - - - name: dot-organization - display_name: "Organization roster & personas" - source: - type: git - url: "{git_host}/{git_org}/.organization" - branch: master - target: "{projects_dir}/.organization" + - id: tasks optional: true - health_check: - type: file_exists - path: "{target}/roster.md" - clients: [claude-code, copilot-cli, gemini-cli] + description: "Task management — projects-meta board + per-project .tasks/" + creates: + - type: local_dir + path: "{projects_dir}/.tasks" + - type: claude_md_snippet + content: "use task management system" + - type: setup_skill + skill: setup-tasks - - name: dot-templates - display_name: "Project templates" - source: - type: git - url: "{git_host}/{git_org}/.templates" - branch: master - target: "{projects_dir}/.templates" + - id: wiki optional: true - health_check: - type: file_exists - path: "{target}/project/.gitkeep" - clients: [claude-code, copilot-cli, gemini-cli] + description: "Shared knowledge wiki — linked plaintext, no RAG" + creates: + - type: gitea_repo + name: "projects-wiki" + visibility: private + description: "Shared project wiki" + init: true + - type: local_clone + repo: "{git_host}/{git_org}/projects-wiki" + path: "{projects_dir}/.wiki" + - type: claude_md_snippet + content: "use project wiki" + - type: setup_skill + skill: setup-wiki - - name: dot-meeting-room - display_name: "Meeting room / brainstorm archive" - source: - type: git - url: "{git_host}/{git_org}/.meeting-room" - branch: master - target: "{projects_dir}/.meeting-room" + - id: workshop optional: true - health_check: - type: file_exists - path: "{target}/.brainstorm/.gitkeep" - clients: [claude-code, copilot-cli, gemini-cli] + description: "Boss-zone — design work, brainstorms, archived decisions" + creates: + - type: gitea_repo + name: ".workshop" + visibility: private + description: "Design workspace and brainstorm archive" + init: true + - type: local_clone + repo: "{git_host}/{git_org}/.workshop" + path: "{projects_dir}/.workshop" -# Health check types: -# file_exists: checks if file exists at {path} -# directory_exists: checks if directory exists at {path} -# mcp_tool: checks if MCP tool is available (via ~/.claude.json) -# command: runs command and checks exit code 0 -# custom: runs custom health check script + - id: admin + optional: true + description: "Ops zone — infrastructure administration and runbooks" + creates: + - type: gitea_repo + name: ".admin" + visibility: private + description: "Infrastructure ops and admin notes" + init: true + - type: local_clone + repo: "{git_host}/{git_org}/.admin" + path: "{projects_dir}/.admin"