feat(manifest): restructure factory.yaml with core/modules schema + manifest-driven bootstrap
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 <noreply@anthropic.com>
This commit is contained in:
100
bootstrap.ps1
100
bootstrap.ps1
@@ -16,6 +16,60 @@ function Test-Command {
|
|||||||
$null = Get-Command $Name -ErrorAction SilentlyContinue
|
$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 {
|
function Write-Step {
|
||||||
param([string]$Msg)
|
param([string]$Msg)
|
||||||
Write-Host "`n▶ $Msg" -ForegroundColor Cyan
|
Write-Host "`n▶ $Msg" -ForegroundColor Cyan
|
||||||
@@ -174,49 +228,57 @@ $lines = @(
|
|||||||
|
|
||||||
Write-Ok "Wrote $HOME_TOML"
|
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"
|
Write-Step "Building MCP servers"
|
||||||
|
|
||||||
$libPath = "$factoryLocal\lib"
|
$libPath = "$factoryLocal\lib"
|
||||||
|
|
||||||
if (Test-Path $libPath) {
|
if (Test-Path $libPath) {
|
||||||
# TypeScript servers (npm install + npm run build)
|
foreach ($component in $manifest.core_components) {
|
||||||
foreach ($server in @("projects-meta-mcp", "wiki-graph")) {
|
$srcPath = "$factoryLocal\$($component.source)"
|
||||||
$serverPath = "$libPath\$server"
|
if (-not (Test-Path $srcPath)) { continue }
|
||||||
if (Test-Path $serverPath) {
|
|
||||||
Write-Host " Building $server..."
|
if ($component.type -eq "typescript") {
|
||||||
Push-Location $serverPath
|
Write-Host " Building $($component.id) v$($component.version)..."
|
||||||
|
Push-Location $srcPath
|
||||||
try {
|
try {
|
||||||
npm install --silent 2>&1 | Out-Null
|
npm install --silent 2>&1 | Out-Null
|
||||||
npm run build 2>&1 | Out-Null
|
npm run build 2>&1 | Out-Null
|
||||||
if ($LASTEXITCODE -ne 0) {
|
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 {
|
} else {
|
||||||
Write-Ok "$server built"
|
Write-Ok "$($component.id) v$($component.version) built"
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
}
|
} elseif ($component.type -eq "python") {
|
||||||
}
|
Write-Host " Setting up $($component.id) v$($component.version)..."
|
||||||
|
Push-Location $srcPath
|
||||||
# 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 {
|
try {
|
||||||
python -m venv .venv 2>&1 | Out-Null
|
python -m venv .venv 2>&1 | Out-Null
|
||||||
& ".venv\Scripts\pip.exe" install -e . --quiet
|
& ".venv\Scripts\pip.exe" install -e . --quiet
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Gap "interns-mcp: pip install failed"
|
Write-Gap "$($component.id): pip install failed"
|
||||||
} else {
|
} else {
|
||||||
Write-Ok "interns-mcp installed"
|
Write-Ok "$($component.id) v$($component.version) installed"
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Skip "lib/ not found — skipping MCP server build"
|
Write-Skip "lib/ not found — skipping MCP server build"
|
||||||
}
|
}
|
||||||
|
|||||||
228
factory.yaml
228
factory.yaml
@@ -1,141 +1,139 @@
|
|||||||
# factory.yaml — L1 manifest
|
# factory.yaml — manifest компонентов factory
|
||||||
# Variables: {gitea}, {projects_dir}, {factory}, {home}
|
#
|
||||||
# Substitution happens at runtime from ~/.config/factory/home.toml
|
# 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)
|
# ── Core (always installed) ──────────────────────────────────────────────────
|
||||||
git_host: "https://git.kzntsv.site"
|
#
|
||||||
git_org: "OpeItcLoc03"
|
# 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
|
core:
|
||||||
components:
|
components:
|
||||||
# === Core infrastructure ===
|
|
||||||
|
|
||||||
- name: dot-common
|
- id: projects-meta-mcp
|
||||||
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]
|
|
||||||
|
|
||||||
- name: projects-meta-mcp
|
|
||||||
display_name: "Projects-meta MCP server"
|
display_name: "Projects-meta MCP server"
|
||||||
# Subdirectory of .common repo — no standalone git source
|
source: lib/projects-meta-mcp
|
||||||
target: "{projects_dir}/.common/lib/projects-meta-mcp"
|
version: "2.25.0"
|
||||||
deps: [dot-common]
|
type: typescript
|
||||||
setup_skill: setup-projects-meta
|
install:
|
||||||
|
run:
|
||||||
|
- npm install --silent
|
||||||
|
- npm run build
|
||||||
health_check:
|
health_check:
|
||||||
type: mcp_tool
|
type: mcp_tool
|
||||||
tool: mcp__projects-meta__meta_status
|
tool: mcp__projects-meta__meta_status
|
||||||
clients: [claude-code, copilot-cli]
|
|
||||||
|
|
||||||
- name: interns-mcp
|
- 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
|
||||||
|
|
||||||
|
- id: interns-mcp
|
||||||
display_name: "Interns MCP server"
|
display_name: "Interns MCP server"
|
||||||
# Subdirectory of .common repo — no standalone git source
|
source: lib/interns-mcp
|
||||||
target: "{projects_dir}/.common/lib/interns-mcp"
|
version: "0.3.3"
|
||||||
deps: [dot-common]
|
type: python
|
||||||
setup_skill: setup-interns
|
install:
|
||||||
|
venv: .venv
|
||||||
|
pip: "-e ."
|
||||||
health_check:
|
health_check:
|
||||||
type: mcp_tool
|
type: mcp_tool
|
||||||
tool: mcp__interns__bulk_text_read
|
tool: mcp__interns__bulk_text_read
|
||||||
clients: [claude-code, copilot-cli]
|
|
||||||
|
|
||||||
# === Knowledge ===
|
- id: skills
|
||||||
|
display_name: "Claude skills"
|
||||||
- name: dot-wiki
|
source: skills
|
||||||
display_name: "Shared project wiki"
|
version: "bundled"
|
||||||
source:
|
type: skills
|
||||||
type: git
|
install:
|
||||||
url: "{git_host}/{git_org}/projects-wiki"
|
copy_to: "{home}/.claude/skills/"
|
||||||
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]
|
|
||||||
|
|
||||||
- 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]
|
|
||||||
|
|
||||||
# === 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:
|
health_check:
|
||||||
type: directory_exists
|
type: directory_exists
|
||||||
path: "{home}/.claude/skills"
|
path: "{home}/.claude/skills"
|
||||||
clients: [claude-code]
|
|
||||||
|
|
||||||
# === Fixtures (optional) ===
|
# ── 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-organization
|
modules:
|
||||||
display_name: "Organization roster & personas"
|
|
||||||
source:
|
- id: tasks
|
||||||
type: git
|
|
||||||
url: "{git_host}/{git_org}/.organization"
|
|
||||||
branch: master
|
|
||||||
target: "{projects_dir}/.organization"
|
|
||||||
optional: true
|
optional: true
|
||||||
health_check:
|
description: "Task management — projects-meta board + per-project .tasks/"
|
||||||
type: file_exists
|
creates:
|
||||||
path: "{target}/roster.md"
|
- type: local_dir
|
||||||
clients: [claude-code, copilot-cli, gemini-cli]
|
path: "{projects_dir}/.tasks"
|
||||||
|
- type: claude_md_snippet
|
||||||
|
content: "use task management system"
|
||||||
|
- type: setup_skill
|
||||||
|
skill: setup-tasks
|
||||||
|
|
||||||
- name: dot-templates
|
- id: wiki
|
||||||
display_name: "Project templates"
|
|
||||||
source:
|
|
||||||
type: git
|
|
||||||
url: "{git_host}/{git_org}/.templates"
|
|
||||||
branch: master
|
|
||||||
target: "{projects_dir}/.templates"
|
|
||||||
optional: true
|
optional: true
|
||||||
health_check:
|
description: "Shared knowledge wiki — linked plaintext, no RAG"
|
||||||
type: file_exists
|
creates:
|
||||||
path: "{target}/project/.gitkeep"
|
- type: gitea_repo
|
||||||
clients: [claude-code, copilot-cli, gemini-cli]
|
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
|
- id: workshop
|
||||||
display_name: "Meeting room / brainstorm archive"
|
|
||||||
source:
|
|
||||||
type: git
|
|
||||||
url: "{git_host}/{git_org}/.meeting-room"
|
|
||||||
branch: master
|
|
||||||
target: "{projects_dir}/.meeting-room"
|
|
||||||
optional: true
|
optional: true
|
||||||
health_check:
|
description: "Boss-zone — design work, brainstorms, archived decisions"
|
||||||
type: file_exists
|
creates:
|
||||||
path: "{target}/.brainstorm/.gitkeep"
|
- type: gitea_repo
|
||||||
clients: [claude-code, copilot-cli, gemini-cli]
|
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:
|
- id: admin
|
||||||
# file_exists: checks if file exists at {path}
|
optional: true
|
||||||
# directory_exists: checks if directory exists at {path}
|
description: "Ops zone — infrastructure administration and runbooks"
|
||||||
# mcp_tool: checks if MCP tool is available (via ~/.claude.json)
|
creates:
|
||||||
# command: runs command and checks exit code 0
|
- type: gitea_repo
|
||||||
# custom: runs custom health check script
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user