Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex/skills/compass/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions manifests/portable-files.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 21 additions & 20 deletions scripts/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip TOML comments before extracting skills

If a maintainer comments out or annotates an entry inside [agents].skills, for example # "old-skill", or an inline note containing a quoted skill name, this regex still captures the quoted text as an active skill even though TOML would ignore it. That makes install, snapshot, and retired-copy maps continue acting on commented-out skills, and the doctor check uses the same style of extraction so it would not catch the mismatch.

Useful? React with 👍 / 👎.

)
}

Expand Down Expand Up @@ -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) {
Expand Down
106 changes: 106 additions & 0 deletions scripts/doctor/checks/manifest-boundaries.ps1
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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")
}
}
40 changes: 0 additions & 40 deletions scripts/doctor/checks/skills.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
19 changes: 17 additions & 2 deletions scripts/verify-live.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions workflows/addition-intake.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down