diff --git a/codex/skills/compass/SKILL.md b/codex/skills/compass/SKILL.md index 00227fe..d891250 100644 --- a/codex/skills/compass/SKILL.md +++ b/codex/skills/compass/SKILL.md @@ -33,8 +33,8 @@ maintenance flow from memory. `manifests/`, `scripts/`. - Reviewed config fragments: `codex/config.review.toml`; do not treat it as a direct replacement for live `config.toml`. -- Installed skill additions: update `manifests/portable-files.toml` and - `scripts/common.ps1` together. +- Installed skill additions: update `manifests/portable-files.toml`; + `scripts/common.ps1` reads that manifest for the install map. ## Validate The Repo Change diff --git a/manifests/portable-files.toml b/manifests/portable-files.toml index 4b5718b..1ade525 100644 --- a/manifests/portable-files.toml +++ b/manifests/portable-files.toml @@ -53,11 +53,20 @@ reason = "Live config.toml includes generated runtime paths, marketplaces, proje # from the repo's codex/ directory into the live Codex home. The [agents] # section maps repo skills into the user skill home. files = [ + ".gitattributes", + ".gitignore", "AGENTS.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE", "README.md", + "SECURITY.md", + "SUPPORT.md", + "philosophy.md", ] dirs = [ + ".github", "local-docs", "manifests", "scripts", diff --git a/scripts/common.ps1 b/scripts/common.ps1 index 7e5aead..4fd21ac 100644 --- a/scripts/common.ps1 +++ b/scripts/common.ps1 @@ -32,25 +32,27 @@ function Get-AgentsHome { } function Get-PortableSkillNames { + $manifestPath = Join-Path (Get-RepoRoot) "manifests\portable-files.toml" + if (-not (Test-Path -LiteralPath $manifestPath)) { + throw "missing portable manifest: $manifestPath" + } + + $manifestText = Get-Content -Raw -LiteralPath $manifestPath + $sectionPattern = "(?ms)^\[agents\]\s*(.*?)(?=^\[|\z)" + $sectionMatch = [regex]::Match($manifestText, $sectionPattern) + if (-not $sectionMatch.Success) { + throw "missing agents section in portable manifest" + } + + $skillsPattern = "(?ms)^\s*skills\s*=\s*\[(.*?)^\s*\]" + $skillsMatch = [regex]::Match($sectionMatch.Groups[1].Value, $skillsPattern) + if (-not $skillsMatch.Success) { + throw "missing portable skill list in manifest" + } + return @( - "action-items-to-prs", - "benchmark-infra-reviewer", - "benchmark-run-operator", - "compass", - "git-branch-resolver", - "grill-me", - "orchestration-controller", - "pr-review-loop", - "specialist-review", - "subagent-driven-development", - "to-prd", - "update-compass", - "using-codex-goals", - "webmcp-eval-triage", - "webmcp-tool-authoring", - "webmcp-verify-tool", - "workspace-steward", - "write-a-skill" + [regex]::Matches($skillsMatch.Groups[1].Value, '"([^"]+)"') | + ForEach-Object { $_.Groups[1].Value } ) } @@ -203,8 +205,7 @@ function Copy-PortableItem { ) if (-not (Test-Path $Source)) { - Write-Host "skip missing source: $Source" - return + throw "missing portable source: $Source" } if ($AllowedRoot) { diff --git a/scripts/doctor/checks/manifest-boundaries.ps1 b/scripts/doctor/checks/manifest-boundaries.ps1 index 8577db9..1624b55 100644 --- a/scripts/doctor/checks/manifest-boundaries.ps1 +++ b/scripts/doctor/checks/manifest-boundaries.ps1 @@ -1,6 +1,70 @@ $manifestPath = Join-Path $repoRoot "manifests\portable-files.toml" $manifestText = Get-Content -Raw -LiteralPath $manifestPath +function ConvertTo-GitPath { + param([string]$Path) + + return $Path.Replace("\", "/").Trim("/") +} + +function Get-ManifestRepoPaths { + $paths = New-Object System.Collections.Generic.List[string] + + foreach ($file in @(Get-ManifestArrayValues -Text $manifestText -Section "repo_only" -Key "files")) { + $paths.Add((ConvertTo-GitPath -Path $file)) + } + + foreach ($file in @(Get-ManifestArrayValues -Text $manifestText -Section "codex" -Key "files")) { + $paths.Add((ConvertTo-GitPath -Path (Join-Path "codex" $file))) + } + + $configReviewFile = Get-ManifestStringValue -Section "config" -Key "review_file" + if ($configReviewFile) { + $paths.Add((ConvertTo-GitPath -Path (Join-Path "codex" $configReviewFile))) + } + + return @($paths) +} + +function Get-ManifestRepoDirPrefixes { + $prefixes = New-Object System.Collections.Generic.List[string] + + foreach ($dir in @(Get-ManifestArrayValues -Text $manifestText -Section "repo_only" -Key "dirs")) { + $prefixes.Add("$(ConvertTo-GitPath -Path $dir)/") + } + + foreach ($dir in @(Get-ManifestArrayValues -Text $manifestText -Section "codex" -Key "dirs")) { + $prefixes.Add("$(ConvertTo-GitPath -Path (Join-Path "codex" $dir))/") + } + + foreach ($skill in @(Get-ManifestArrayValues -Text $manifestText -Section "agents" -Key "skills")) { + $prefixes.Add("$(ConvertTo-GitPath -Path (Join-Path (Join-Path "codex" "skills") $skill))/") + } + + return @($prefixes) +} + +function Get-ManifestStringValue { + param( + [string]$Section, + [string]$Key + ) + + $sectionPattern = "(?ms)^\[$([regex]::Escape($Section))\]\s*(.*?)(?=^\[|\z)" + $sectionMatch = [regex]::Match($manifestText, $sectionPattern) + if (-not $sectionMatch.Success) { + return $null + } + + $keyPattern = "(?m)^\s*$([regex]::Escape($Key))\s*=\s*`"([^`"]+)`"" + $keyMatch = [regex]::Match($sectionMatch.Groups[1].Value, $keyPattern) + if (-not $keyMatch.Success) { + return $null + } + + return $keyMatch.Groups[1].Value +} + $blockedNames = @(Get-ManifestArrayValues -Text $manifestText -Section "local_only" -Key "files") if ($blockedNames.Count -eq 0) { $problems.Add("missing local-only files in portable manifest") @@ -39,3 +103,45 @@ foreach ($dir in Get-DoctorChildItem -Kind Directory) { $problems.Add("blocked local-only directory in repo tree: $($dir.FullName)") } } + +if (Get-Command git -ErrorAction SilentlyContinue) { + $trackedRepoFiles = @(& git -C $repoRoot ls-files) + if ($LASTEXITCODE -ne 0) { + $problems.Add("could not list tracked files for manifest boundary check") + } + else { + $allowedFiles = @(Get-ManifestRepoPaths) + $allowedDirPrefixes = @(Get-ManifestRepoDirPrefixes) + foreach ($trackedFile in $trackedRepoFiles) { + $repoPath = ConvertTo-GitPath -Path $trackedFile + $allowed = $allowedFiles -contains $repoPath + if (-not $allowed) { + foreach ($prefix in $allowedDirPrefixes) { + if ($repoPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + $allowed = $true + break + } + } + } + + if (-not $allowed) { + $problems.Add("tracked file outside portable manifest boundary: $repoPath") + } + } + } +} + +foreach ($path in @(Get-ManifestRepoPaths)) { + $fullPath = Join-Path $repoRoot ($path.Replace("/", "\")) + if (-not (Test-Path -LiteralPath $fullPath)) { + $problems.Add("manifest-listed file missing from repo: $path") + } +} + +foreach ($prefix in @(Get-ManifestRepoDirPrefixes)) { + $path = $prefix.TrimEnd("/") + $fullPath = Join-Path $repoRoot ($path.Replace("/", "\")) + if (-not (Test-Path -LiteralPath $fullPath)) { + $problems.Add("manifest-listed directory missing from repo: $path") + } +} diff --git a/scripts/doctor/checks/skills.ps1 b/scripts/doctor/checks/skills.ps1 index 3da61fb..bd04147 100644 --- a/scripts/doctor/checks/skills.ps1 +++ b/scripts/doctor/checks/skills.ps1 @@ -18,43 +18,3 @@ foreach ($skillFile in $skillFiles) { $problems.Add("skill description over $maxDescriptionLength chars: $($skillFile.FullName)") } } - -$manifestPath = Join-Path $repoRoot "manifests\portable-files.toml" -$manifestText = Get-Content -Raw -LiteralPath $manifestPath -$skillsMatch = [regex]::Match($manifestText, "(?ms)^skills\s*=\s*\[(.*?)^\]") -if (-not $skillsMatch.Success) { - $problems.Add("missing skills allowlist in portable manifest") -} -else { - $manifestSkills = @( - [regex]::Matches($skillsMatch.Groups[1].Value, '"([^"]+)"') | - ForEach-Object { $_.Groups[1].Value } | - Sort-Object -Unique - ) - - $skillsRoot = [System.IO.Path]::GetFullPath((Join-Path (Join-Path $repoRoot "codex") "skills")).TrimEnd("\") - $mappedSkills = @( - Get-PortableFileMap -RepoRoot $repoRoot -CodexHome $liveHome -AgentsHome $agentsHomePath | - Where-Object { - $_.Type -eq "dir" -and - [System.IO.Path]::GetFullPath($_.RepoPath).TrimEnd("\").StartsWith( - "$skillsRoot\", - [System.StringComparison]::OrdinalIgnoreCase - ) - } | - ForEach-Object { Split-Path -Leaf $_.RepoPath } | - Sort-Object -Unique - ) - - foreach ($skill in $manifestSkills) { - if ($mappedSkills -notcontains $skill) { - $problems.Add("skill in manifest missing from install map: $skill") - } - } - - foreach ($skill in $mappedSkills) { - if ($manifestSkills -notcontains $skill) { - $problems.Add("skill in install map missing from manifest: $skill") - } - } -} diff --git a/scripts/verify-live.ps1 b/scripts/verify-live.ps1 index 2ff330f..2f98a68 100644 --- a/scripts/verify-live.ps1 +++ b/scripts/verify-live.ps1 @@ -12,8 +12,10 @@ $repoRoot = Get-RepoRoot $liveHome = Get-CodexHome -CodexHome $CodexHome $agentsHome = Get-AgentsHome -AgentsHome $AgentsHome $items = Get-PortableFileMap -RepoRoot $repoRoot -CodexHome $liveHome -AgentsHome $agentsHome +$retiredItems = Get-RetiredPortableFileMap -CodexHome $liveHome -AgentsHome $agentsHome $drift = New-Object System.Collections.Generic.List[string] $missing = New-Object System.Collections.Generic.List[string] +$retired = New-Object System.Collections.Generic.List[string] function Get-RelativeFileMap { param([string]$Root) @@ -60,6 +62,12 @@ foreach ($item in $items) { } } +foreach ($item in $retiredItems) { + if (Test-Path $item.LivePath) { + $retired.Add($item.LivePath) + } +} + Write-Host "repo: $repoRoot" Write-Host "codex: $liveHome" Write-Host "agents: $agentsHome" @@ -79,11 +87,18 @@ if ($drift.Count -gt 0) { Write-Host " $path" } } -elseif ($missing.Count -eq 0) { +if ($retired.Count -gt 0) { + Write-Host "" + Write-Host "retired portable copies:" + foreach ($path in $retired) { + Write-Host " $path" + } +} +elseif ($missing.Count -eq 0 -and $drift.Count -eq 0) { Write-Host "portable files match live allowlist" } -if ($RequireInSync -and ($missing.Count -gt 0 -or $drift.Count -gt 0)) { +if ($RequireInSync -and ($missing.Count -gt 0 -or $drift.Count -gt 0 -or $retired.Count -gt 0)) { exit 1 } diff --git a/workflows/addition-intake.md b/workflows/addition-intake.md index 80a642f..8c08c8e 100644 --- a/workflows/addition-intake.md +++ b/workflows/addition-intake.md @@ -77,7 +77,7 @@ Skills and agents need extra review because they shape future agent behavior. - Install user skills into `$HOME/.agents/skills`; keep `codex/skills/` as the reviewed source tree. - Add each portable skill name to `manifests/portable-files.toml`. -- Add each portable skill to `Get-PortableFileMap` in `scripts/common.ps1`. +- `scripts/common.ps1` reads portable skill names from that manifest. - When removing a previously portable user skill, keep its legacy Codex-home retirement entry and add an explicit user-home retirement entry to `Get-RetiredPortableFileMap` so `install.ps1 -Apply` actually uninstalls @@ -86,7 +86,7 @@ Skills and agents need extra review because they shape future agent behavior. should never mutate state need `sandbox_mode = "read-only"`. Reviewers that must run tests, commands, browser checks, or plugin-backed validation should be non-editing by instruction instead of blocked by sandbox. -- Run `.\scripts\doctor.ps1` to catch manifest, install-map, and agent sandbox +- Run `.\scripts\doctor.ps1` to catch manifest boundary and agent sandbox drift. ## Stale Guidance Sweep