build(adapters): generate orchestration guidance lazily

This commit is contained in:
fraujulian committed 2026-09-16 22:44:42 +02:00
1 parent 74d62fe878
commit 6d4ab9ddac
5 files changed
+60 -54

No files matched your search

+1
View File
@@ -0,0 +1 @@
Read skills/rules/rules-orchestration/SKILL.md from your installed configuration before acting as a subagent.
+1
View File
@@ -5,6 +5,7 @@ decision-rule.md rules-decision-rule unresolved requirements, ambiguous behavior
definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
git.md rules-git Git operations such as commits, branches, or merges git.md rules-git Git operations such as commits, branches, or merges
microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
orchestration.md rules-orchestration delegation, subagent work, or long-running task state
refactoring.md rules-refactoring refactoring existing code without changing behavior refactoring.md rules-refactoring refactoring existing code without changing behavior
security.md rules-security any software development task: implementing, modifying, debugging, reviewing, testing, or configuring code, scripts, hooks, or infrastructure security.md rules-security any software development task: implementing, modifying, debugging, reviewing, testing, or configuring code, scripts, hooks, or infrastructure
security-auth.md rules-security-auth authentication, authorization, sessions, tokens, permissions, or tenant-boundary work security-auth.md rules-security-auth authentication, authorization, sessions, tokens, permissions, or tenant-boundary work
1 rule_file skill_name trigger
5 definition-of-done.md rules-definition-of-done validating that a task is complete or ready for review
6 git.md rules-git Git operations such as commits, branches, or merges
7 microsoft.md rules-microsoft Microsoft 365, Azure DevOps, or Teams operations
8 orchestration.md rules-orchestration delegation, subagent work, or long-running task state
9 refactoring.md rules-refactoring refactoring existing code without changing behavior
10 security.md rules-security any software development task: implementing, modifying, debugging, reviewing, testing, or configuring code, scripts, hooks, or infrastructure
11 security-auth.md rules-security-auth authentication, authorization, sessions, tokens, permissions, or tenant-boundary work
+1
View File
@@ -0,0 +1 @@
Read rules/orchestration.md from your installed configuration before acting as a subagent.
+31 -29
View File
@@ -116,23 +116,24 @@ Load rule files when their subject applies:
- rules/wpf/index.md for WPF work. - rules/wpf/index.md for WPF work.
- rules/ui-ux/index.md for UI or UX decisions. - rules/ui-ux/index.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work. - rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/orchestration.md for delegation, subagent work, or long-running task state.
- rules/git.md for Git operations. - rules/git.md for Git operations.
- rules/refactoring.md for refactoring work. - rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion. - rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains. - rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
'@ '@
foreach ($platform in @('windows','linux')) { foreach ($shell in @('powershell','bash')) {
New-Item (Join-Path $output "codex-$platform") -ItemType Directory -Force | Out-Null New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
New-Item (Join-Path $output "claude-$platform") -ItemType Directory -Force | Out-Null New-Item (Join-Path $output "claude-$shell") -ItemType Directory -Force | Out-Null
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$platform/skills") Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$shell/skills")
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$platform/skills") Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$shell/skills")
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$platform/rules") Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$shell/rules")
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$platform/hooks") Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$shell/hooks")
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$platform/hooks") Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$shell/hooks")
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$platform/statusline") Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$shell/statusline")
$claudeRulesDir = Join-Path $output "claude-$platform/rules" $claudeRulesDir = Join-Path $output "claude-$shell/rules"
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
$claudeRuleSkillLines = @() $claudeRuleSkillLines = @()
@@ -140,7 +141,7 @@ foreach ($platform in @('windows','linux')) {
$ruleSourcePath = Join-Path $shared "rules/$($entry.rule_file)" $ruleSourcePath = Join-Path $shared "rules/$($entry.rule_file)"
$isDirectory = Test-Path -LiteralPath $ruleSourcePath -PathType Container $isDirectory = Test-Path -LiteralPath $ruleSourcePath -PathType Container
$ruleBodyPath = if ($isDirectory) { Join-Path $ruleSourcePath 'index.md' } else { $ruleSourcePath } $ruleBodyPath = if ($isDirectory) { Join-Path $ruleSourcePath 'index.md' } else { $ruleSourcePath }
$skillDir = Join-Path $output "claude-$platform/skills/rules/$($entry.skill_name)" $skillDir = Join-Path $output "claude-$shell/skills/rules/$($entry.skill_name)"
New-Item $skillDir -ItemType Directory -Force | Out-Null New-Item $skillDir -ItemType Directory -Force | Out-Null
$ruleContent = Get-Content $ruleBodyPath -Raw $ruleContent = Get-Content $ruleBodyPath -Raw
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: $(Quote-Toml "Use for $($entry.trigger).")`r`n---`r`n`r`n$ruleContent" $skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: $(Quote-Toml "Use for $($entry.trigger).")`r`n---`r`n`r`n$ruleContent"
@@ -158,23 +159,24 @@ foreach ($platform in @('windows','linux')) {
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading) $agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)
$agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent" $agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "codex-$platform/AGENTS.md") $agentsContent -Encoding UTF8 Set-Content (Join-Path $output "codex-$shell/AGENTS.md") $agentsContent -Encoding UTF8
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading) $claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent" $claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "claude-$platform/CLAUDE.md") $claudeContent -Encoding UTF8 Set-Content (Join-Path $output "claude-$shell/CLAUDE.md") $claudeContent -Encoding UTF8
Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$platform/config.toml") -Force Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$shell/config.toml") -Force
Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$platform/settings.json") -Force Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$shell/settings.json") -Force
foreach ($client in @('codex','claude')) { foreach ($client in @('codex','claude')) {
$agentsOutput = Join-Path $output "$client-$platform/agents" $agentsOutput = Join-Path $output "$client-$shell/agents"
New-Item $agentsOutput -ItemType Directory -Force | Out-Null New-Item $agentsOutput -ItemType Directory -Force | Out-Null
foreach ($agentDir in $sourceAgentDirs) { foreach ($agentDir in $sourceAgentDirs) {
$metadata = Join-Path $agentDir.FullName 'agent.yml' $metadata = Join-Path $agentDir.FullName 'agent.yml'
$name = Read-Field $metadata 'name' $name = Read-Field $metadata 'name'
$description = Read-Field $metadata 'description' $description = Read-Field $metadata 'description'
$instructions = Get-Content (Join-Path $agentDir.FullName 'instructions.md') -Raw $loader = (Get-Content (Join-Path $root "adapters/$client/orchestration-loader.txt") -Raw -Encoding UTF8).Trim()
$instructions = $loader + "`n`n" + (Get-Content (Join-Path $agentDir.FullName 'instructions.md') -Raw -Encoding UTF8)
if ($client -eq 'codex') { if ($client -eq 'codex') {
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8 Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
} else { } else {
@@ -185,21 +187,21 @@ foreach ($platform in @('windows','linux')) {
foreach ($client in @('codex','claude')) { foreach ($client in @('codex','claude')) {
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' } $file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$path = Join-Path $output "$client-$platform/$file" $path = Join-Path $output "$client-$shell/$file"
$command = if ($platform -eq 'windows') { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'bash' } $command = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__' } else { 'bash' }
$script = if ($platform -eq 'windows') { 'flashbang.ps1' } else { 'flashbang.sh' } $script = if ($shell -eq 'powershell') { 'flashbang.ps1' } else { 'flashbang.sh' }
$statusLineScript = if ($platform -eq 'windows') { 'statusline.ps1' } else { 'statusline.sh' } $statusLineScript = if ($shell -eq 'powershell') { 'statusline.ps1' } else { 'statusline.sh' }
$content = Get-Content -LiteralPath $path -Raw $content = Get-Content -LiteralPath $path -Raw
$compactScript = if ($platform -eq 'windows') { 'Record-Compact.ps1' } else { 'record-compact.sh' } $compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
$pointerScript = if ($platform -eq 'windows') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' } $pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__WINDOWS_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__WINDOWS_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript) $content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
Set-Content -LiteralPath $path -Value $content -Encoding UTF8 Set-Content -LiteralPath $path -Value $content -Encoding UTF8
} }
$tomlPath = Join-Path $output "codex-$platform/config.toml" $tomlPath = Join-Path $output "codex-$shell/config.toml"
$settingsPath = Join-Path $output "claude-$platform/settings.json" $settingsPath = Join-Path $output "claude-$shell/settings.json"
& python (Join-Path $root 'scripts/validate-config.py') $tomlPath $settingsPath & python (Join-Path $root 'scripts/validate-config.py') $tomlPath $settingsPath
if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $platform." } if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $shell." }
Test-CodexSchema $tomlPath Test-CodexSchema $tomlPath
} }
@@ -207,10 +209,10 @@ $leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is # __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output. -cmatch keeps this case-sensitive so Python # expected to remain in generated output. -cmatch keeps this case-sensitive so Python
# dunder names (__main__, __name__) in flashbang.sh are not mistaken for placeholders. # dunder names (__main__, __name__) in flashbang.sh are not mistaken for placeholders.
$content = (Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__', '' $content = (Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__', ''
if ($content -cmatch '__[A-Z0-9_]+__') { $_.FullName } if ($content -cmatch '__[A-Z0-9_]+__') { $_.FullName }
}) })
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" } if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
if ($Summary) { Write-Output 'Build: PASS | 4 packages' } else { Write-Output 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux' } if ($Summary) { Write-Output 'Build: PASS | 4 packages' } else { Write-Output 'PASS build: codex-powershell, claude-powershell, codex-bash, claude-bash' }
exit 0 exit 0
+26 -25
View File
@@ -112,22 +112,23 @@ Load rule files when their subject applies:
- rules/wpf/index.md for WPF work. - rules/wpf/index.md for WPF work.
- rules/ui-ux/index.md for UI or UX decisions. - rules/ui-ux/index.md for UI or UX decisions.
- rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work. - rules/microsoft.md for Microsoft 365, Azure DevOps, or Teams work.
- rules/orchestration.md for delegation, subagent work, or long-running task state.
- rules/git.md for Git operations. - rules/git.md for Git operations.
- rules/refactoring.md for refactoring work. - rules/refactoring.md for refactoring work.
- rules/definition-of-done.md when validating completion. - rules/definition-of-done.md when validating completion.
- rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains. - rules/decision-rule.md when requirements are unresolved, behavior is ambiguous, or a meaningful technical choice remains.
BLOCK BLOCK
for platform in windows linux; do for shell in powershell bash; do
mkdir -p "$output/codex-$platform" "$output/claude-$platform" mkdir -p "$output/codex-$shell" "$output/claude-$shell"
copy_directory "$shared/skills" "$output/codex-$platform/skills" copy_directory "$shared/skills" "$output/codex-$shell/skills"
copy_directory "$shared/skills" "$output/claude-$platform/skills" copy_directory "$shared/skills" "$output/claude-$shell/skills"
copy_directory "$shared/rules" "$output/codex-$platform/rules" copy_directory "$shared/rules" "$output/codex-$shell/rules"
copy_directory "$shared/hooks" "$output/codex-$platform/hooks" copy_directory "$shared/hooks" "$output/codex-$shell/hooks"
copy_directory "$shared/hooks" "$output/claude-$platform/hooks" copy_directory "$shared/hooks" "$output/claude-$shell/hooks"
copy_directory "$shared/statusline" "$output/claude-$platform/statusline" copy_directory "$shared/statusline" "$output/claude-$shell/statusline"
mkdir -p "$output/claude-$platform/rules" mkdir -p "$output/claude-$shell/rules"
claude_rule_loading_list="" claude_rule_loading_list=""
for i in "${!rule_skill_names[@]}"; do for i in "${!rule_skill_names[@]}"; do
@@ -137,7 +138,7 @@ for i in "${!rule_skill_names[@]}"; do
rule_source="$shared/rules/$rule_file" rule_source="$shared/rules/$rule_file"
rule_body="$rule_source" rule_body="$rule_source"
[ -d "$rule_source" ] && rule_body="$rule_source/index.md" [ -d "$rule_source" ] && rule_body="$rule_source/index.md"
skill_dir="$output/claude-$platform/skills/rules/$skill_name" skill_dir="$output/claude-$shell/skills/rules/$skill_name"
mkdir -p "$skill_dir" mkdir -p "$skill_dir"
{ printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "Use for $trigger.")"; cat "$rule_body"; } > "$skill_dir/SKILL.md" { printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "Use for $trigger.")"; cat "$rule_body"; } > "$skill_dir/SKILL.md"
if [ -d "$rule_source/references" ]; then if [ -d "$rule_source/references" ]; then
@@ -158,22 +159,22 @@ shared_template=$(<"$shared/global-instructions.md")
general_content=$(<"$shared/rules/general.md") general_content=$(<"$shared/rules/general.md")
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading} agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$platform/AGENTS.md" printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$shell/AGENTS.md"
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading} claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$platform/CLAUDE.md" printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$shell/CLAUDE.md"
cp "$root/adapters/codex/config/config.toml" "$output/codex-$platform/config.toml" cp "$root/adapters/codex/config/config.toml" "$output/codex-$shell/config.toml"
cp "$root/adapters/claude/config/settings.json" "$output/claude-$platform/settings.json" cp "$root/adapters/claude/config/settings.json" "$output/claude-$shell/settings.json"
for client in codex claude; do for client in codex claude; do
agents_output="$output/$client-$platform/agents" agents_output="$output/$client-$shell/agents"
mkdir -p "$agents_output" mkdir -p "$agents_output"
for agent_dir in "${agent_dirs[@]}"; do for agent_dir in "${agent_dirs[@]}"; do
metadata="${agent_dir}agent.yml" metadata="${agent_dir}agent.yml"
name=$(read_field "$metadata" name) name=$(read_field "$metadata" name)
description=$(read_field "$metadata" description) description=$(read_field "$metadata" description)
instructions=$(<"${agent_dir}instructions.md") instructions=$(<"$root/adapters/$client/orchestration-loader.txt")$'\n\n'$(<"${agent_dir}instructions.md")
if [ "$client" = codex ]; then if [ "$client" = codex ]; then
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml" printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
else else
@@ -190,19 +191,19 @@ for client in codex claude; do
statusline_script=statusline.sh statusline_script=statusline.sh
compact_script=record-compact.sh compact_script=record-compact.sh
pointer_script=show-session-state-pointer.sh pointer_script=show-session-state-pointer.sh
if [ "$platform" = windows ]; then if [ "$shell" = powershell ]; then
command='powershell -NoProfile -ExecutionPolicy Bypass -File' command='__POWERSHELL_COMMAND__'
script=flashbang.ps1 script=flashbang.ps1
statusline_script=statusline.ps1 statusline_script=statusline.ps1
compact_script=Record-Compact.ps1 compact_script=Record-Compact.ps1
pointer_script=Show-SessionStatePointer.ps1 pointer_script=Show-SessionStatePointer.ps1
fi fi
path="$output/$client-$platform/$file" path="$output/$client-$shell/$file"
content=$(<"$path") content=$(<"$path")
content=${content//__HOOK_COMMAND__/$command} content=${content//__HOOK_COMMAND__/$command}
content=${content//__WINDOWS_HOOK_COMMAND__/$command} content=${content//__POWERSHELL_HOOK_COMMAND__/$command}
content=${content//__HOOK_SCRIPT__/$script} content=${content//__HOOK_SCRIPT__/$script}
content=${content//__WINDOWS_HOOK_SCRIPT__/$script} content=${content//__POWERSHELL_HOOK_SCRIPT__/$script}
content=${content//__COMPACT_SCRIPT__/$compact_script} content=${content//__COMPACT_SCRIPT__/$compact_script}
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script} content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
content=${content//__STATUSLINE_COMMAND__/$command} content=${content//__STATUSLINE_COMMAND__/$command}
@@ -210,8 +211,8 @@ for client in codex claude; do
printf '%s\n' "$content" > "$path" printf '%s\n' "$content" > "$path"
done done
toml_path="$output/codex-$platform/config.toml" toml_path="$output/codex-$shell/config.toml"
settings_path="$output/claude-$platform/settings.json" settings_path="$output/claude-$shell/settings.json"
python3 "$root/scripts/validate-config.py" "$toml_path" "$settings_path" python3 "$root/scripts/validate-config.py" "$toml_path" "$settings_path"
if command -v codex >/dev/null; then if command -v codex >/dev/null; then
schema_home=$(mktemp -d) schema_home=$(mktemp -d)
@@ -229,11 +230,11 @@ done
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is # __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output, so it is excluded from the leftover check. A # expected to remain in generated output, so it is excluded from the leftover check. A
# single tree-wide grep (rather than one process per file) keeps this fast. # single tree-wide grep (rather than one process per file) keeps this fast.
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$output" 2>/dev/null | sort -u | grep -v '^__AI_CONFIG_ROOT__$' || true) leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$output" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__)$' || true)
if [ -n "$leftover_tokens" ]; then if [ -n "$leftover_tokens" ]; then
offending_files=$(grep -RlE '__[A-Z0-9_]+__' "$output" 2>/dev/null | tr '\n' ' ') offending_files=$(grep -RlE '__[A-Z0-9_]+__' "$output" 2>/dev/null | tr '\n' ' ')
printf 'Unresolved template placeholders (%s) in: %s\n' "$(printf '%s' "$leftover_tokens" | tr '\n' ' ')" "$offending_files" >&2 printf 'Unresolved template placeholders (%s) in: %s\n' "$(printf '%s' "$leftover_tokens" | tr '\n' ' ')" "$offending_files" >&2
exit 1 exit 1
fi fi
if [ "$summary" = true ]; then printf 'Build: PASS | 4 packages\n'; else printf 'PASS build: codex-windows, claude-windows, codex-linux, claude-linux\n'; fi if [ "$summary" = true ]; then printf 'Build: PASS | 4 packages\n'; else printf 'PASS build: codex-powershell, claude-powershell, codex-bash, claude-bash\n'; fi