CI / Capabilities and shell quality (push) Successful in 14s
CI / Python tests and compilation (push) Successful in 13s
CI / PowerShell tests on Ubuntu (push) Failing after 15s
CI / CI configuration security checks (push) Successful in 3s
CI / Build and inspect generated packages (push) Successful in 10s
CI / Qwen retrieval integration (push) Successful in 14m24s
CI / Bash integration and installation guards (push) Failing after 34s
CI / Installed client configuration smoke tests (push) Successful in 42s
202 lines
8.7 KiB
Bash
202 lines
8.7 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
summary=false
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--summary) summary=true; shift ;;
|
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
|
shared="$root/shared"
|
|
output="$root/generated"
|
|
plugin_manifest="$root/adapters/plugins.tsv"
|
|
capability_manifest="$root/adapters/claude/capabilities.tsv"
|
|
|
|
[ -f "$plugin_manifest" ] || { printf 'Missing plugin manifest: %s\n' "$plugin_manifest" >&2; exit 1; }
|
|
awk -F '\t' 'NR == 1 { next } NF == 0 { next } $1 == "" || $3 == "" || $6 == "" { print "Invalid plugin manifest row." > "/dev/stderr"; exit 1 } seen[$1]++ { print "Duplicate plugin name in manifest: " $1 > "/dev/stderr"; exit 1 }' "$plugin_manifest"
|
|
[ -f "$capability_manifest" ] || { printf 'Missing capability manifest: %s\n' "$capability_manifest" >&2; exit 1; }
|
|
declare -A agent_tools
|
|
while IFS=$'\t' read -r role tools; do
|
|
[ "$role" != role ] || continue
|
|
[ -n "$role" ] && [ -n "$tools" ] || { printf 'Invalid capability manifest row.\n' >&2; exit 1; }
|
|
agent_tools[$role]="$tools"
|
|
done < "$capability_manifest"
|
|
|
|
copy_directory() {
|
|
local source=$1 destination=$2
|
|
mkdir -p "$destination"
|
|
cp -R "$source"/. "$destination"/
|
|
}
|
|
|
|
read_field() {
|
|
local path=$1 name=$2 value
|
|
value=$(sed -n "s/^${name}:[[:space:]]*//p" "$path" | head -n 1)
|
|
[ -n "$value" ] || { printf 'Missing %s in %s\n' "$name" "$path" >&2; exit 1; }
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
quote_toml() {
|
|
local value=$1
|
|
value=${value//\\/\\\\}
|
|
value=${value//\"/\\\"}
|
|
value=${value//$'\r'/}
|
|
value=${value//$'\n'/\\n}
|
|
printf '"%s"' "$value"
|
|
}
|
|
|
|
session_output=$(bash "$shared/hooks/scripts/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; }
|
|
if [ "$summary" = true ]; then
|
|
printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d'
|
|
else
|
|
printf '%s\n' "$session_output"
|
|
fi
|
|
rm -rf -- "$output"
|
|
|
|
agent_dirs=("$shared"/agents/*/)
|
|
declare -A agent_seen
|
|
for agent_dir in "${agent_dirs[@]}"; do
|
|
name=$(read_field "${agent_dir}agent.yml" name)
|
|
[ -z "${agent_seen[$name]+x}" ] || { printf 'Duplicate agent name: %s\n' "$name" >&2; exit 1; }
|
|
agent_seen[$name]=1
|
|
done
|
|
|
|
for shell in powershell bash; do
|
|
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
|
|
copy_directory "$shared/skills" "$output/codex-$shell/skills"
|
|
copy_directory "$shared/skills" "$output/claude-$shell/skills"
|
|
copy_directory "$shared/rules" "$output/codex-$shell/rules"
|
|
copy_directory "$shared/hooks" "$output/codex-$shell/hooks"
|
|
copy_directory "$shared/hooks" "$output/claude-$shell/hooks"
|
|
copy_directory "$shared/statusline" "$output/claude-$shell/statusline"
|
|
|
|
declare -A rule_skill_names=()
|
|
while IFS= read -r rule_path; do
|
|
case "$rule_path" in
|
|
general.md) continue ;;
|
|
*/index.md) skill_name="rules-${rule_path%/index.md}"; rule_body="$shared/rules/$rule_path"; references_dir="${rule_body%/index.md}/references" ;;
|
|
*/*.md)
|
|
rule_directory=${rule_path%%/*}
|
|
rule_relative=${rule_path#*/}
|
|
rule_relative=${rule_relative%.md}
|
|
rule_relative=${rule_relative#references/}
|
|
rule_relative=${rule_relative//\//-}
|
|
skill_name="rules-$rule_directory-$rule_relative"
|
|
rule_body="$shared/rules/$rule_path"
|
|
references_dir=''
|
|
;;
|
|
*.md) skill_name="rules-${rule_path%.md}"; rule_body="$shared/rules/$rule_path"; references_dir='' ;;
|
|
*) continue ;;
|
|
esac
|
|
if [[ -n "${rule_skill_names[$skill_name]:-}" ]]; then
|
|
printf 'Duplicate generated rule skill name: %s\n' "$skill_name" >&2
|
|
exit 1
|
|
fi
|
|
rule_skill_names[$skill_name]=1
|
|
skill_dir="$output/claude-$shell/skills/rules/$skill_name"
|
|
mkdir -p "$skill_dir"
|
|
rule_title=$(sed -n 's/^#\{1,6\} *//p' "$rule_body" | head -n 1)
|
|
if [ -z "$rule_title" ]; then printf 'Missing rule title in %s\n' "$rule_body" >&2; exit 1; fi
|
|
rule_description="Use when the task concerns $rule_title."
|
|
{ printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "$rule_description")"; cat "$rule_body"; } > "$skill_dir/SKILL.md"
|
|
if [ -n "$references_dir" ] && [ -d "$references_dir" ]; then
|
|
copy_directory "$references_dir" "$skill_dir/references"
|
|
fi
|
|
done < <(find "$shared/rules" -mindepth 1 -type f -name '*.md' -printf '%P\n' | sort)
|
|
|
|
shared_template=$(<"$shared/global-instructions.md")
|
|
general_content=$(<"$shared/rules/general.md")
|
|
credential_helper_extension='sh'
|
|
[ "$shell" != powershell ] || credential_helper_extension='ps1'
|
|
|
|
agents_content=${shared_template//__CLIENT__/codex}
|
|
agents_content=${agents_content//__SHELL__/$credential_helper_extension}
|
|
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$shell/AGENTS.md"
|
|
|
|
claude_content=${shared_template//__CLIENT__/claude}
|
|
claude_content=${claude_content//__SHELL__/$credential_helper_extension}
|
|
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-$shell/config.toml"
|
|
cp "$root/adapters/claude/config/settings.json" "$output/claude-$shell/settings.json"
|
|
|
|
for client in codex claude; do
|
|
agents_output="$output/$client-$shell/agents"
|
|
mkdir -p "$agents_output"
|
|
for agent_dir in "${agent_dirs[@]}"; do
|
|
metadata="${agent_dir}agent.yml"
|
|
name=$(read_field "$metadata" name)
|
|
description=$(read_field "$metadata" description)
|
|
instructions=$(<"$root/adapters/$client/orchestration-loader.txt")$'\n\n'$(<"${agent_dir}instructions.md")
|
|
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"
|
|
else
|
|
tools=${agent_tools[$name]:-}
|
|
shell_tool=Bash
|
|
[ "$shell" != powershell ] || shell_tool=PowerShell
|
|
tools=${tools//__SHELL__/$shell_tool}
|
|
if [ -n "$tools" ]; then printf '%s\nname: %s\ndescription: %s\ntools: %s\n%s\n\n%s\n' '---' "$name" "$description" "$tools" '---' "$instructions" > "$agents_output/$name.md"; else printf '%s\nname: %s\ndescription: %s\n%s\n\n%s\n' '---' "$name" "$description" '---' "$instructions" > "$agents_output/$name.md"; fi
|
|
fi
|
|
done
|
|
done
|
|
|
|
for client in codex claude; do
|
|
file=config.toml
|
|
[ "$client" != claude ] || file=settings.json
|
|
command=bash
|
|
script=flashbang.sh
|
|
statusline_script=statusline.sh
|
|
compact_script=record-compact.sh
|
|
pointer_script=show-session-state-pointer.sh
|
|
if [ "$shell" = powershell ]; then
|
|
command='__POWERSHELL_COMMAND__'
|
|
script=flashbang.ps1
|
|
statusline_script=statusline.ps1
|
|
compact_script=Record-Compact.ps1
|
|
pointer_script=Show-SessionStatePointer.ps1
|
|
fi
|
|
path="$output/$client-$shell/$file"
|
|
content=$(<"$path")
|
|
sandbox='{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}'
|
|
[ "$shell" != powershell ] || sandbox='{"enabled": false}'
|
|
content=${content//__HOOK_COMMAND__/$command}
|
|
content=${content//__POWERSHELL_HOOK_COMMAND__/$command}
|
|
content=${content//__HOOK_SCRIPT__/$script}
|
|
content=${content//__POWERSHELL_HOOK_SCRIPT__/$script}
|
|
content=${content//__COMPACT_SCRIPT__/$compact_script}
|
|
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
|
|
content=${content//__STATUSLINE_COMMAND__/$command}
|
|
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
|
content=${content//__CLAUDE_SANDBOX__/$sandbox}
|
|
printf '%s\n' "$content" > "$path"
|
|
done
|
|
|
|
toml_path="$output/codex-$shell/config.toml"
|
|
settings_path="$output/claude-$shell/settings.json"
|
|
python3 "$root/scripts/checks/validate-config.py" "$toml_path" "$settings_path"
|
|
if command -v codex >/dev/null; then
|
|
schema_home=$(mktemp -d)
|
|
cp "$toml_path" "$schema_home/config.toml"
|
|
CODEX_HOME="$schema_home" codex --strict-config --help >/dev/null
|
|
rm -rf -- "$schema_home"
|
|
elif [ "${REQUIRE_CODEX_SCHEMA:-false}" = true ]; then
|
|
printf 'Codex CLI is required for schema validation.\n' >&2
|
|
exit 1
|
|
else
|
|
printf 'WARN Codex CLI unavailable; skipped Codex schema validation.\n' >&2
|
|
fi
|
|
done
|
|
|
|
# __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
|
|
# 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 -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__)$' || true)
|
|
if [ -n "$leftover_tokens" ]; then
|
|
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
|
|
exit 1
|
|
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
|