From b7a086e918020ead07f2c235b9893264e9895c29 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:13:42 -0400 Subject: [PATCH 1/9] register portable home path check --- manifests/doctor-checks.json | 1 + 1 file changed, 1 insertion(+) diff --git a/manifests/doctor-checks.json b/manifests/doctor-checks.json index 7524319..aaa37ba 100644 --- a/manifests/doctor-checks.json +++ b/manifests/doctor-checks.json @@ -9,6 +9,7 @@ "manifest-boundaries.ps1", "model-tiers.ps1", "policy-contracts.ps1", + "portable-home-paths.ps1", "required-files.ps1", "restart-recovery.ps1", "retired-plugins.ps1", From 8b93f1f1aa5a8426dd4ce9c8469367f52dfd9380 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:14:05 -0400 Subject: [PATCH 2/9] clarify Claude agent derivation --- manifests/portable-files.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifests/portable-files.toml b/manifests/portable-files.toml index 9b6c80a..b603a05 100644 --- a/manifests/portable-files.toml +++ b/manifests/portable-files.toml @@ -86,8 +86,8 @@ derived_skills = [ "write-a-skill", ] -# These reviewer agents are maintained explicitly because their Claude -# frontmatter and Codex read-only sandbox contracts are platform-specific. +# No Claude agent is maintained as a separate source. Every Claude agent +# derives from codex/agents/*.toml at install time; see derived_agents below. agents = [] # Other Claude agents derive from codex/agents/.toml at install time. From 9c7fbddb65ed7fb13366dfcf607de715fd4eb823 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:14:39 -0400 Subject: [PATCH 3/9] bind Claude frontmatter to roster --- scripts/doctor/checks/claude.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/doctor/checks/claude.ps1 b/scripts/doctor/checks/claude.ps1 index b0f188e..d71b639 100644 --- a/scripts/doctor/checks/claude.ps1 +++ b/scripts/doctor/checks/claude.ps1 @@ -159,4 +159,12 @@ else { $problems.Add("claude derived agent missing frontmatter entry: $agent") } } + + # Reverse binding: every frontmatter entry must be a declared derived + # agent, so an orphan left behind after removing a derived agent fails. + foreach ($agent in $script:ClaudeDerivedAgentFrontmatter.Keys) { + if ($manifestClaudeDerivedAgents -notcontains $agent) { + $problems.Add("claude frontmatter entry is not a declared derived agent: $agent") + } + } } From ba18be35412201888ac5f66ce4e04b0246d4a580 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:14:56 -0400 Subject: [PATCH 4/9] bind portable home paths --- scripts/doctor/checks/portable-home-paths.ps1 | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 scripts/doctor/checks/portable-home-paths.ps1 diff --git a/scripts/doctor/checks/portable-home-paths.ps1 b/scripts/doctor/checks/portable-home-paths.ps1 new file mode 100644 index 0000000..4d728f5 --- /dev/null +++ b/scripts/doctor/checks/portable-home-paths.ps1 @@ -0,0 +1,55 @@ +# Home paths are executable truth in scripts/common.ps1 (Get-CodexHome, +# Get-AgentsHome, Get-ClaudeHome). The portable manifest documents the same +# paths as strings for maintainers. This check extracts the default directory +# names from common.ps1 and requires the parsed manifest values to match the +# complete documented expressions exactly. +$commonPath = Join-Path $repoRoot "scripts\common.ps1" +$manifestPath = Join-Path $repoRoot "manifests\portable-files.toml" +if (-not (Test-Path -LiteralPath $commonPath -PathType Leaf) -or -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + $problems.Add("missing common.ps1 or portable-files.toml for home path check") +} +else { + $common = Get-Content -Raw -LiteralPath $commonPath + + $codexMatch = [regex]::Match($common, 'Join-Path \$env:USERPROFILE "([^"]+)"') + $homeMatches = [regex]::Matches($common, 'Join-Path \$HOME "([^"]+)"') + if (-not $codexMatch.Success -or $homeMatches.Count -lt 2) { + $problems.Add("could not extract home directory defaults from common.ps1") + } + else { + $defaults = @{ + codex = $codexMatch.Groups[1].Value + agents = $homeMatches[0].Groups[1].Value + claude = $homeMatches[1].Groups[1].Value + } + $manifest = (Get-PortableGeneratedData).manifest + + function Get-PortableManifestString { + param($Section, $Key) + $sectionValue = Get-PortableJsonProperty -Object $manifest -Name $Section + return Get-PortableJsonProperty -Object $sectionValue -Name $Key + } + + function Assert-Equals { + param($Section, $Key, $Expected) + $value = Get-PortableManifestString $Section $Key + if ($null -eq $value) { + $problems.Add("portable manifest [$Section].$Key is missing") + } + elseif ($value -cne $Expected) { + $problems.Add("portable manifest [$Section].$Key documents '$value' but expected '$Expected'") + } + } + + $codexHome = '$CODEX_HOME or %USERPROFILE%\' + $defaults.codex + $agentsHome = '$HOME\' + $defaults.agents + $claudeHome = '$HOME\' + $defaults.claude + + Assert-Equals "codex" "home" $codexHome + Assert-Equals "agents" "home" $agentsHome + Assert-Equals "agents" "skills_dir" ($agentsHome + "\skills") + Assert-Equals "claude" "home" $claudeHome + Assert-Equals "claude" "skills_dir" ($claudeHome + "\skills") + Assert-Equals "claude" "agents_dir" ($claudeHome + "\agents") + } +} From 548535c19047f47816084f76c28b1abbc13d0014 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:15:20 -0400 Subject: [PATCH 5/9] expect portable path cluster --- scripts/doctor/checks/source-of-truth.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/doctor/checks/source-of-truth.ps1 b/scripts/doctor/checks/source-of-truth.ps1 index 2197154..6a355bf 100644 --- a/scripts/doctor/checks/source-of-truth.ps1 +++ b/scripts/doctor/checks/source-of-truth.ps1 @@ -97,7 +97,7 @@ else { } } - foreach ($expected in 1..13) { + foreach ($expected in 1..14) { if ($seenIds -notcontains "$expected") { $problems.Add("source-of-truth register is missing cluster row: $expected") } From 52ccdf1687a70f8931d325955914d431b15b97bf Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:15:53 -0400 Subject: [PATCH 6/9] register portable path binding --- local-docs/source-of-truth.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/local-docs/source-of-truth.md b/local-docs/source-of-truth.md index 52a06d8..d1c0cde 100644 --- a/local-docs/source-of-truth.md +++ b/local-docs/source-of-truth.md @@ -39,6 +39,7 @@ Status values: | 11 | Routing source reference (checklist prose deferred) | workflows/addition-intake.md | pin | policy-contracts.ps1 | consolidated | | 12 | Glossary terms | glossary.md | link | editorial convention | accepted | | 13 | Shared runtime-global prose | manifests/policy-contracts.json | pin | policy-contracts.ps1 | consolidated | +| 14 | Portable runtime home paths | scripts/common.ps1 home resolvers | pin | portable-home-paths.ps1 | consolidated | ## Intentional separations @@ -101,14 +102,14 @@ audits do not re-flag them. ## Status summary -Of the 13 fact families, 12 are consolidated or already canonical and 1 is +Of the 14 fact families, 13 are consolidated or already canonical and 1 is accepted as an intentional separation (cluster 12 glossary). The surface-specific update-together checklist prose (part of cluster 11) is deferred as editorial work. The consolidations are enforced mechanically: new manifests and doctor checks (`retired-skills`, -`model-tiers`, `generated-artifacts`, `source-of-truth`, `template-anchors`), -generators under `scripts/generators/`, policy-contract pins, git-derived -required files, and dynamic doctor dispatch. +`model-tiers`, `generated-artifacts`, `source-of-truth`, `template-anchors`, +`portable-home-paths`), generators under `scripts/generators/`, policy-contract +pins, git-derived required files, and dynamic doctor dispatch. Required files (cluster 4) use the git index as the source of repository membership. The architecturally mandatory entrypoints the old hand-written list From 8349b1c509e15557b68930f0e6e32157074335bf Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:22:24 -0400 Subject: [PATCH 7/9] bind workflow home path prose --- scripts/doctor/checks/portable-home-paths.ps1 | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/doctor/checks/portable-home-paths.ps1 b/scripts/doctor/checks/portable-home-paths.ps1 index 4d728f5..727ccf7 100644 --- a/scripts/doctor/checks/portable-home-paths.ps1 +++ b/scripts/doctor/checks/portable-home-paths.ps1 @@ -1,12 +1,17 @@ # Home paths are executable truth in scripts/common.ps1 (Get-CodexHome, -# Get-AgentsHome, Get-ClaudeHome). The portable manifest documents the same -# paths as strings for maintainers. This check extracts the default directory -# names from common.ps1 and requires the parsed manifest values to match the -# complete documented expressions exactly. +# Get-AgentsHome, Get-ClaudeHome). The portable manifest and portable-config +# workflow document the same paths for maintainers. This check extracts the +# default directory names from common.ps1 and requires every documented copy to +# match the complete expected expressions exactly. $commonPath = Join-Path $repoRoot "scripts\common.ps1" $manifestPath = Join-Path $repoRoot "manifests\portable-files.toml" -if (-not (Test-Path -LiteralPath $commonPath -PathType Leaf) -or -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - $problems.Add("missing common.ps1 or portable-files.toml for home path check") +$workflowPath = Join-Path $repoRoot "workflows\portable-config.md" +if ( + -not (Test-Path -LiteralPath $commonPath -PathType Leaf) -or + -not (Test-Path -LiteralPath $manifestPath -PathType Leaf) -or + -not (Test-Path -LiteralPath $workflowPath -PathType Leaf) +) { + $problems.Add("missing common.ps1, portable-files.toml, or portable-config.md for home path check") } else { $common = Get-Content -Raw -LiteralPath $commonPath @@ -51,5 +56,17 @@ else { Assert-Equals "claude" "home" $claudeHome Assert-Equals "claude" "skills_dir" ($claudeHome + "\skills") Assert-Equals "claude" "agents_dir" ($claudeHome + "\agents") + + $workflowLines = @(Get-Content -LiteralPath $workflowPath) + $expectedWorkflowLines = @( + '- `-CodexHome`, then `$env:CODEX_HOME`, then `%USERPROFILE%\' + $defaults.codex + '`;' + '- `-AgentsHome`, then `$HOME\' + $defaults.agents + '`;' + '- `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.`' + ) + foreach ($expectedLine in $expectedWorkflowLines) { + if ($workflowLines -cnotcontains $expectedLine) { + $problems.Add("portable-config.md home path line missing or stale: $expectedLine") + } + } } } From 71062c1ae2dd3a26325a3dde2cd24ba32ad9abba Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:22:52 -0400 Subject: [PATCH 8/9] correct workflow Claude path line --- scripts/doctor/checks/portable-home-paths.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/doctor/checks/portable-home-paths.ps1 b/scripts/doctor/checks/portable-home-paths.ps1 index 727ccf7..90dced8 100644 --- a/scripts/doctor/checks/portable-home-paths.ps1 +++ b/scripts/doctor/checks/portable-home-paths.ps1 @@ -61,7 +61,7 @@ else { $expectedWorkflowLines = @( '- `-CodexHome`, then `$env:CODEX_HOME`, then `%USERPROFILE%\' + $defaults.codex + '`;' '- `-AgentsHome`, then `$HOME\' + $defaults.agents + '`;' - '- `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.`' + '- `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.' ) foreach ($expectedLine in $expectedWorkflowLines) { if ($workflowLines -cnotcontains $expectedLine) { From b48faf8a4bfcee8f60c3663b51cad5a5a1949c26 Mon Sep 17 00:00:00 2001 From: Ario Barin Ostovary <89481178+ariobarin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:29:24 -0400 Subject: [PATCH 9/9] bind every runtime home copy --- scripts/doctor/checks/portable-home-paths.ps1 | 172 +++++++++++++----- 1 file changed, 123 insertions(+), 49 deletions(-) diff --git a/scripts/doctor/checks/portable-home-paths.ps1 b/scripts/doctor/checks/portable-home-paths.ps1 index 90dced8..1b9f4aa 100644 --- a/scripts/doctor/checks/portable-home-paths.ps1 +++ b/scripts/doctor/checks/portable-home-paths.ps1 @@ -1,21 +1,76 @@ # Home paths are executable truth in scripts/common.ps1 (Get-CodexHome, -# Get-AgentsHome, Get-ClaudeHome). The portable manifest and portable-config -# workflow document the same paths for maintainers. This check extracts the -# default directory names from common.ps1 and requires every documented copy to -# match the complete expected expressions exactly. -$commonPath = Join-Path $repoRoot "scripts\common.ps1" -$manifestPath = Join-Path $repoRoot "manifests\portable-files.toml" -$workflowPath = Join-Path $repoRoot "workflows\portable-config.md" -if ( - -not (Test-Path -LiteralPath $commonPath -PathType Leaf) -or - -not (Test-Path -LiteralPath $manifestPath -PathType Leaf) -or - -not (Test-Path -LiteralPath $workflowPath -PathType Leaf) -) { - $problems.Add("missing common.ps1, portable-files.toml, or portable-config.md for home path check") +# Get-AgentsHome, Get-ClaudeHome). The manifest, launchers, operator docs, and +# focused workflow docs repeat those defaults for different audiences. This +# check derives every expected copy from common.ps1 so a default change cannot +# leave an accepted runtime route or current instruction stale. +$paths = @{ + common = Join-Path $repoRoot "scripts\common.ps1" + manifest = Join-Path $repoRoot "manifests\portable-files.toml" + portableWorkflow = Join-Path $repoRoot "workflows\portable-config.md" + claudeWorkflow = Join-Path $repoRoot "workflows\claude-config.md" + whichLlmWorkflow = Join-Path $repoRoot "workflows\which-llm-skill.md" + readme = Join-Path $repoRoot "README.md" + contributing = Join-Path $repoRoot "CONTRIBUTING.md" + doctor = Join-Path $repoRoot "scripts\doctor.ps1" + hookPowerShell = Join-Path $repoRoot "codex\hooks\portable_guard.ps1" + hookShell = Join-Path $repoRoot "codex\hooks\portable_guard.sh" + hooksJson = Join-Path $repoRoot "codex\hooks.json" + hooksReadme = Join-Path $repoRoot "codex\hooks\README.md" +} + +$missing = @($paths.GetEnumerator() | Where-Object { -not (Test-Path -LiteralPath $_.Value -PathType Leaf) }) +if ($missing.Count -gt 0) { + foreach ($entry in $missing) { + $problems.Add("missing $($entry.Value) for home path check") + } } else { - $common = Get-Content -Raw -LiteralPath $commonPath + function Get-PortableManifestString { + param($Manifest, $Section, $Key) + $sectionValue = Get-PortableJsonProperty -Object $Manifest -Name $Section + return Get-PortableJsonProperty -Object $sectionValue -Name $Key + } + + function Assert-Equals { + param($Actual, $Expected, [string]$Label) + if ($null -eq $Actual) { + $problems.Add("$Label is missing") + } + elseif ($Actual -cne $Expected) { + $problems.Add("$Label documents '$Actual' but expected '$Expected'") + } + } + + function Normalize-PortableWhitespace { + param([string]$Text) + return [regex]::Replace($Text, '\s+', ' ').Trim() + } + + function Assert-Contains { + param([string]$Text, [string]$Expected, [string]$Label) + if ($Text.IndexOf($Expected, [System.StringComparison]::Ordinal) -lt 0) { + $problems.Add("$Label is missing or stale: $Expected") + } + } + + function Assert-NormalizedContains { + param([string]$Text, [string]$Expected, [string]$Label) + $normalizedText = Normalize-PortableWhitespace $Text + $normalizedExpected = Normalize-PortableWhitespace $Expected + if ($normalizedText.IndexOf($normalizedExpected, [System.StringComparison]::Ordinal) -lt 0) { + $problems.Add("$Label is missing or stale: $normalizedExpected") + } + } + + function Assert-MatchCount { + param([string]$Text, [string]$Expected, [int]$Count, [string]$Label) + $actual = [regex]::Matches($Text, [regex]::Escape($Expected)).Count + if ($actual -ne $Count) { + $problems.Add("$Label expected $Count copies of '$Expected' but found $actual") + } + } + $common = Get-Content -Raw -LiteralPath $paths.common $codexMatch = [regex]::Match($common, 'Join-Path \$env:USERPROFILE "([^"]+)"') $homeMatches = [regex]::Matches($common, 'Join-Path \$HOME "([^"]+)"') if (-not $codexMatch.Success -or $homeMatches.Count -lt 2) { @@ -29,44 +84,63 @@ else { } $manifest = (Get-PortableGeneratedData).manifest - function Get-PortableManifestString { - param($Section, $Key) - $sectionValue = Get-PortableJsonProperty -Object $manifest -Name $Section - return Get-PortableJsonProperty -Object $sectionValue -Name $Key - } - - function Assert-Equals { - param($Section, $Key, $Expected) - $value = Get-PortableManifestString $Section $Key - if ($null -eq $value) { - $problems.Add("portable manifest [$Section].$Key is missing") - } - elseif ($value -cne $Expected) { - $problems.Add("portable manifest [$Section].$Key documents '$value' but expected '$Expected'") - } - } - $codexHome = '$CODEX_HOME or %USERPROFILE%\' + $defaults.codex $agentsHome = '$HOME\' + $defaults.agents $claudeHome = '$HOME\' + $defaults.claude + $codexTilde = '~/' + $defaults.codex + $claudeTilde = '~/' + $defaults.claude + $agentsSlash = '$HOME/' + $defaults.agents + $claudeSlash = '$HOME/' + $defaults.claude - Assert-Equals "codex" "home" $codexHome - Assert-Equals "agents" "home" $agentsHome - Assert-Equals "agents" "skills_dir" ($agentsHome + "\skills") - Assert-Equals "claude" "home" $claudeHome - Assert-Equals "claude" "skills_dir" ($claudeHome + "\skills") - Assert-Equals "claude" "agents_dir" ($claudeHome + "\agents") - - $workflowLines = @(Get-Content -LiteralPath $workflowPath) - $expectedWorkflowLines = @( - '- `-CodexHome`, then `$env:CODEX_HOME`, then `%USERPROFILE%\' + $defaults.codex + '`;' - '- `-AgentsHome`, then `$HOME\' + $defaults.agents + '`;' - '- `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.' - ) - foreach ($expectedLine in $expectedWorkflowLines) { - if ($workflowLines -cnotcontains $expectedLine) { - $problems.Add("portable-config.md home path line missing or stale: $expectedLine") - } - } + Assert-Equals (Get-PortableManifestString $manifest "codex" "home") $codexHome "portable manifest [codex].home" + Assert-Equals (Get-PortableManifestString $manifest "agents" "home") $agentsHome "portable manifest [agents].home" + Assert-Equals (Get-PortableManifestString $manifest "agents" "skills_dir") ($agentsHome + "\skills") "portable manifest [agents].skills_dir" + Assert-Equals (Get-PortableManifestString $manifest "claude" "home") $claudeHome "portable manifest [claude].home" + Assert-Equals (Get-PortableManifestString $manifest "claude" "skills_dir") ($claudeHome + "\skills") "portable manifest [claude].skills_dir" + Assert-Equals (Get-PortableManifestString $manifest "claude" "agents_dir") ($claudeHome + "\agents") "portable manifest [claude].agents_dir" + + $portableWorkflow = Get-Content -Raw -LiteralPath $paths.portableWorkflow + Assert-Contains $portableWorkflow ('- `-CodexHome`, then `$env:CODEX_HOME`, then `%USERPROFILE%\' + $defaults.codex + '`;') "portable-config Codex resolution" + Assert-Contains $portableWorkflow ('- `-AgentsHome`, then `$HOME\' + $defaults.agents + '`;') "portable-config agents resolution" + Assert-Contains $portableWorkflow ('- `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.') "portable-config Claude resolution" + + $readme = Get-Content -Raw -LiteralPath $paths.readme + $readmeResolution = 'Scripts use `-CodexHome`, then `$env:CODEX_HOME`, then `%USERPROFILE%\' + $defaults.codex + '`; `-AgentsHome`, then `$HOME\' + $defaults.agents + '`; and `-ClaudeHome`, then `$HOME\' + $defaults.claude + '`.' + Assert-NormalizedContains $readme $readmeResolution "README home resolution" + Assert-Contains $readme ($codexTilde + "/AGENTS.md") "README Codex install root" + Assert-Contains $readme ($claudeTilde + "/CLAUDE.md") "README Claude install root" + Assert-Contains $readme ($agentsSlash + "/skills") "README agents skill root" + Assert-Contains $readme ($claudeSlash + "/skills") "README Claude skill root" + + $contributing = Get-Content -Raw -LiteralPath $paths.contributing + Assert-Contains $contributing ($agentsSlash + "/skills") "CONTRIBUTING agents skill root" + + $claudeWorkflow = Get-Content -Raw -LiteralPath $paths.claudeWorkflow + Assert-Contains $claudeWorkflow ($claudeSlash + "/CLAUDE.md") "Claude workflow instruction root" + Assert-Contains $claudeWorkflow $claudeSlash "Claude workflow default root" + + $whichLlmWorkflow = Get-Content -Raw -LiteralPath $paths.whichLlmWorkflow + Assert-Contains $whichLlmWorkflow ($agentsHome + "\skills\which-llm\pick.py") "which-llm installed pick path" + Assert-Contains $whichLlmWorkflow ($agentsHome + "\skills\which-llm\query.py") "which-llm installed query path" + + $doctor = Get-Content -Raw -LiteralPath $paths.doctor + Assert-Contains $doctor ('$liveHome = Join-Path $env:USERPROFILE "' + $defaults.codex + '"') "doctor Codex fallback" + Assert-Contains $doctor ('$agentsHomePath = Join-Path $HOME "' + $defaults.agents + '"') "doctor agents fallback" + Assert-Contains $doctor ('$claudeHomePath = Join-Path $HOME "' + $defaults.claude + '"') "doctor Claude fallback" + + $hookPowerShell = Get-Content -Raw -LiteralPath $paths.hookPowerShell + Assert-Contains $hookPowerShell ('$codexHome = Join-Path $env:USERPROFILE "' + $defaults.codex + '"') "PowerShell hook Codex fallback" + + $hookShell = Get-Content -Raw -LiteralPath $paths.hookShell + Assert-Contains $hookShell ('codex_home=$HOME/' + $defaults.codex) "shell hook Codex fallback" + + $hooksJson = Get-Content -Raw -LiteralPath $paths.hooksJson + $hookShellDefault = '${CODEX_HOME:-$HOME/' + $defaults.codex + '}/hooks/portable_guard.sh' + $hookWindowsDefault = "Join-Path `$env:USERPROFILE '$($defaults.codex)'" + Assert-MatchCount $hooksJson $hookShellDefault 2 "hook JSON shell Codex fallback" + Assert-MatchCount $hooksJson $hookWindowsDefault 2 "hook JSON Windows Codex fallback" + + $hooksReadme = Get-Content -Raw -LiteralPath $paths.hooksReadme + Assert-Contains $hooksReadme $codexTilde "hooks README Codex default" } }