mirror of
https://github.com/itme-brain/agent-team.git
synced 2026-05-08 10:40:12 -04:00
refactor(build): drive generation from shared configs
This commit is contained in:
parent
84882d3b9c
commit
511076e059
4 changed files with 507 additions and 117 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,7 +10,9 @@ settings.local.json
|
|||
Thumbs.db
|
||||
|
||||
# Generated output (derived from source templates via generate.sh)
|
||||
settings.json
|
||||
claude/
|
||||
codex/
|
||||
.claude
|
||||
.codex
|
||||
.direnv
|
||||
|
|
|
|||
492
generate.sh
492
generate.sh
|
|
@ -2,8 +2,9 @@
|
|||
set -euo pipefail
|
||||
|
||||
# generate.sh — generates both Claude and Codex output directories from
|
||||
# shared agent source files. Agent source files (agents/*.md) are the
|
||||
# single source of truth; this script derives tool-specific equivalents.
|
||||
# shared agent source files plus a vendor-neutral runtime config.
|
||||
# Agent source files (agents/*.md) are the single source of truth; this
|
||||
# script derives tool-specific equivalents.
|
||||
#
|
||||
# Template variables in agent bodies are expanded per-target:
|
||||
# ${PLANS_DIR} — where plans live (.claude/plans vs plans)
|
||||
|
|
@ -17,6 +18,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||
AGENTS_SRC="$SCRIPT_DIR/agents"
|
||||
RULES_DIR="$SCRIPT_DIR/rules"
|
||||
CLAUDE_MD="$SCRIPT_DIR/CLAUDE.md"
|
||||
SETTINGS_SHARED_YAML="$SCRIPT_DIR/SETTINGS.yaml"
|
||||
TEAM_YAML="$SCRIPT_DIR/TEAM.yaml"
|
||||
SETTINGS_JSON="$SCRIPT_DIR/settings.json"
|
||||
|
||||
CLAUDE_DIR="$SCRIPT_DIR/claude"
|
||||
|
|
@ -48,14 +51,6 @@ extract_body() {
|
|||
awk 'BEGIN{fm=0} /^---$/{if(fm==0){fm=1;next} if(fm==1){fm=2;next}} fm==2{print}' "$file"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# extract_frontmatter_block — extracts the raw frontmatter including delimiters
|
||||
# ---------------------------------------------------------------------------
|
||||
extract_frontmatter_block() {
|
||||
local file="$1"
|
||||
awk 'BEGIN{fm=0} /^---$/{if(fm==0){fm=1;print;next} if(fm==1){print;exit}} fm==1{print}' "$file"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# expand_body — runs envsubst on body text, substituting only our 3 variables
|
||||
# $1 = body text
|
||||
|
|
@ -75,6 +70,301 @@ expand_body() {
|
|||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# yaml_escape_single_quoted — escapes text for YAML single-quoted scalars
|
||||
# ---------------------------------------------------------------------------
|
||||
yaml_escape_single_quoted() {
|
||||
printf '%s' "$1" | sed "s/'/''/g"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# csv_from_yaml_array — joins YAML array values from stdin with ", "
|
||||
# ---------------------------------------------------------------------------
|
||||
csv_from_yaml_array() {
|
||||
local first=1
|
||||
local item
|
||||
while IFS= read -r item; do
|
||||
[ -n "$item" ] || continue
|
||||
if [ "$first" -eq 0 ]; then
|
||||
printf ', '
|
||||
fi
|
||||
printf '%s' "$item"
|
||||
first=0
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_team_protocol — validates TEAM protocol fields and referenced files
|
||||
# ---------------------------------------------------------------------------
|
||||
validate_team_protocol() {
|
||||
[ -f "$TEAM_YAML" ] || {
|
||||
echo "Error: missing $TEAM_YAML"
|
||||
exit 1
|
||||
}
|
||||
|
||||
yq -e '.version == 1' "$TEAM_YAML" > /dev/null
|
||||
yq -e '.agents.order and .agents.items and .skills.order and .skills.items and .rules.order and .rules.items' "$TEAM_YAML" > /dev/null
|
||||
|
||||
local section id ids_in_order
|
||||
for section in agents skills rules; do
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
yq -e ".${section}.items.${id}" "$TEAM_YAML" > /dev/null
|
||||
[ "$(yq -r ".${section}.items.${id}.id" "$TEAM_YAML")" = "$id" ] || {
|
||||
echo "Error: TEAM ${section} item '${id}' has mismatched id field"
|
||||
exit 1
|
||||
}
|
||||
done < <(yq -r ".${section}.order[]" "$TEAM_YAML")
|
||||
|
||||
ids_in_order="$(yq -r ".${section}.order[]" "$TEAM_YAML")"
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
printf '%s\n' "$ids_in_order" | grep -qx "$id" || {
|
||||
echo "Error: TEAM ${section} item '${id}' missing from order list"
|
||||
exit 1
|
||||
}
|
||||
done < <(yq -r ".${section}.items | keys | .[]" "$TEAM_YAML")
|
||||
done
|
||||
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
local path
|
||||
path="$SCRIPT_DIR/$(yq -r ".agents.items.${id}.instruction_file" "$TEAM_YAML")"
|
||||
[ -f "$path" ] || {
|
||||
echo "Error: missing agent instruction file for '${id}': $path"
|
||||
exit 1
|
||||
}
|
||||
done < <(yq -r '.agents.order[]' "$TEAM_YAML")
|
||||
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
local path
|
||||
path="$SCRIPT_DIR/$(yq -r ".skills.items.${id}.instruction_file" "$TEAM_YAML")"
|
||||
[ -f "$path" ] || {
|
||||
echo "Error: missing skill instruction file for '${id}': $path"
|
||||
exit 1
|
||||
}
|
||||
done < <(yq -r '.skills.order[]' "$TEAM_YAML")
|
||||
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
local path
|
||||
path="$SCRIPT_DIR/$(yq -r ".rules.items.${id}.source_file" "$TEAM_YAML")"
|
||||
[ -f "$path" ] || {
|
||||
echo "Error: missing rule source file for '${id}': $path"
|
||||
exit 1
|
||||
}
|
||||
done < <(yq -r '.rules.order[]' "$TEAM_YAML")
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_shared_settings — validates the shared protocol fields we rely on
|
||||
# ---------------------------------------------------------------------------
|
||||
validate_shared_settings() {
|
||||
[ -f "$SETTINGS_SHARED_YAML" ] || {
|
||||
echo "Error: missing $SETTINGS_SHARED_YAML"
|
||||
exit 1
|
||||
}
|
||||
|
||||
yq -e '.version == 1' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '.model.class == "fast" or .model.class == "balanced" or .model.class == "powerful"' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '.model.reasoning == "low" or .model.reasoning == "medium" or .model.reasoning == "high" or .model.reasoning == "max"' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '.runtime.filesystem == "read-only" or .runtime.filesystem == "workspace-write"' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '.runtime.approval == "manual" or .runtime.approval == "guarded-auto" or .runtime.approval == "full-auto"' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '(.runtime.network_access | type) == "!!bool"' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
yq -e '
|
||||
(.runtime.tools // []) as $tools |
|
||||
(
|
||||
$tools |
|
||||
map(
|
||||
select(
|
||||
. == "shell" or
|
||||
. == "read" or
|
||||
. == "edit" or
|
||||
. == "write" or
|
||||
. == "glob" or
|
||||
. == "grep" or
|
||||
. == "web_fetch" or
|
||||
. == "web_search"
|
||||
)
|
||||
) |
|
||||
length
|
||||
) == ($tools | length)
|
||||
' "$SETTINGS_SHARED_YAML" > /dev/null
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_model_class_to_claude — maps shared model.class to Claude model value
|
||||
# ---------------------------------------------------------------------------
|
||||
map_model_class_to_claude() {
|
||||
local model_class="$1"
|
||||
case "$model_class" in
|
||||
fast) echo "haiku" ;;
|
||||
powerful) echo "opus" ;;
|
||||
balanced) echo "sonnet" ;;
|
||||
*) echo "sonnet" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_approval_intent_to_codex_policy — shared approval intent to Codex value
|
||||
# ---------------------------------------------------------------------------
|
||||
map_approval_intent_to_codex_policy() {
|
||||
local approval_intent="$1"
|
||||
case "$approval_intent" in
|
||||
manual) echo "on-request" ;;
|
||||
full-auto) echo "never" ;;
|
||||
guarded-auto) echo "untrusted" ;;
|
||||
*) echo "untrusted" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_filesystem_intent_to_claude_mode — shared filesystem to Claude mode
|
||||
# ---------------------------------------------------------------------------
|
||||
map_filesystem_intent_to_claude_mode() {
|
||||
local filesystem="$1"
|
||||
case "$filesystem" in
|
||||
read-only) echo "plan" ;;
|
||||
workspace-write) echo "acceptEdits" ;;
|
||||
*) echo "acceptEdits" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_portable_tool_to_claude — shared runtime tool to Claude allow-list name
|
||||
# ---------------------------------------------------------------------------
|
||||
map_portable_tool_to_claude() {
|
||||
local tool="$1"
|
||||
case "$tool" in
|
||||
shell) echo "Bash" ;;
|
||||
read) echo "Read" ;;
|
||||
edit) echo "Edit" ;;
|
||||
write) echo "Write" ;;
|
||||
glob) echo "Glob" ;;
|
||||
grep) echo "Grep" ;;
|
||||
web_fetch) echo "WebFetch" ;;
|
||||
web_search) echo "WebSearch" ;;
|
||||
*) echo "$tool" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# json_escape — escapes a string for JSON string literal output
|
||||
# ---------------------------------------------------------------------------
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# json_array_from_lines — renders stdin as a compact JSON string array
|
||||
# ---------------------------------------------------------------------------
|
||||
json_array_from_lines() {
|
||||
local first=1
|
||||
local item
|
||||
|
||||
printf '['
|
||||
while IFS= read -r item; do
|
||||
[ -n "$item" ] || continue
|
||||
if [ "$first" -eq 0 ]; then
|
||||
printf ', '
|
||||
fi
|
||||
printf '"%s"' "$(json_escape "$item")"
|
||||
first=0
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# generate_legacy_settings_json — emits Claude-compatible settings.json
|
||||
# from SETTINGS.yaml so downstream generation stays backward-compatible
|
||||
# ---------------------------------------------------------------------------
|
||||
generate_legacy_settings_json() {
|
||||
local model_class model_reasoning runtime_filesystem runtime_approval
|
||||
local claude_model claude_default_mode codex_approval_policy codex_network_access
|
||||
local allow_json deny_json ask_json claude_md_excludes_json
|
||||
|
||||
model_class="$(yq -r '.model.class' "$SETTINGS_SHARED_YAML")"
|
||||
model_reasoning="$(yq -r '.model.reasoning' "$SETTINGS_SHARED_YAML")"
|
||||
runtime_filesystem="$(yq -r '.runtime.filesystem' "$SETTINGS_SHARED_YAML")"
|
||||
runtime_approval="$(yq -r '.runtime.approval' "$SETTINGS_SHARED_YAML")"
|
||||
|
||||
claude_model="$(map_model_class_to_claude "$model_class")"
|
||||
claude_default_mode="$(map_filesystem_intent_to_claude_mode "$runtime_filesystem")"
|
||||
codex_approval_policy="$(yq -r '.targets.codex.approval_policy // ""' "$SETTINGS_SHARED_YAML")"
|
||||
codex_network_access="$(yq -r '.targets.codex.network_access // .runtime.network_access // false' "$SETTINGS_SHARED_YAML")"
|
||||
|
||||
if [ -z "$codex_approval_policy" ] || [ "$codex_approval_policy" = "null" ]; then
|
||||
codex_approval_policy="$(map_approval_intent_to_codex_policy "$runtime_approval")"
|
||||
fi
|
||||
|
||||
allow_json="$(
|
||||
yq -r '.runtime.tools[]' "$SETTINGS_SHARED_YAML" \
|
||||
| while IFS= read -r tool; do
|
||||
map_portable_tool_to_claude "$tool"
|
||||
done \
|
||||
| json_array_from_lines
|
||||
)"
|
||||
|
||||
deny_json="$(
|
||||
{
|
||||
yq -r '.safety.protected_paths[]' "$SETTINGS_SHARED_YAML" | while IFS= read -r path; do
|
||||
printf 'Read(%s)\n' "$path"
|
||||
printf 'Write(%s)\n' "$path"
|
||||
printf 'Edit(%s)\n' "$path"
|
||||
done
|
||||
} | json_array_from_lines
|
||||
)"
|
||||
|
||||
ask_json="$(
|
||||
yq -r '.safety.dangerous_shell_commands.ask[]' "$SETTINGS_SHARED_YAML" \
|
||||
| while IFS= read -r cmd; do
|
||||
printf 'Bash(%s)\n' "$cmd"
|
||||
done \
|
||||
| json_array_from_lines
|
||||
)"
|
||||
|
||||
claude_md_excludes_json="$(
|
||||
yq -r '(.targets.claude.claude_md_excludes // [".claude/agent-memory/**"])[]' "$SETTINGS_SHARED_YAML" \
|
||||
| json_array_from_lines
|
||||
)"
|
||||
|
||||
cat > "$SETTINGS_JSON" <<JSON
|
||||
{
|
||||
"\$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"attribution": {
|
||||
"commit": "",
|
||||
"pr": ""
|
||||
},
|
||||
"permissions": {
|
||||
"allow": ${allow_json},
|
||||
"deny": ${deny_json},
|
||||
"ask": ${ask_json},
|
||||
"defaultMode": "${claude_default_mode}"
|
||||
},
|
||||
"model": "${claude_model}",
|
||||
"effortLevel": "${model_reasoning}",
|
||||
"codex": {
|
||||
"approvalPolicy": "${codex_approval_policy}",
|
||||
"networkAccess": ${codex_network_access}
|
||||
},
|
||||
"claudeMdExcludes": ${claude_md_excludes_json}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# prepare_settings_json — ensures the Claude-compatible settings.json
|
||||
# artifact exists from the shared runtime config
|
||||
# ---------------------------------------------------------------------------
|
||||
prepare_settings_json() {
|
||||
echo "Using shared config: $SETTINGS_SHARED_YAML"
|
||||
validate_shared_settings
|
||||
validate_team_protocol
|
||||
generate_legacy_settings_json
|
||||
echo "Generated compatibility artifact: $SETTINGS_JSON"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_model — maps Claude model name to Codex model name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -103,7 +393,7 @@ map_effort() {
|
|||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_sandbox_mode — determines Codex sandbox_mode from agent frontmatter
|
||||
# map_sandbox_mode — determines Codex sandbox_mode from agent metadata
|
||||
# $1 = permissionMode value (plan / acceptEdits / "")
|
||||
# $2 = tools list (comma-separated)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -129,6 +419,44 @@ map_sandbox_mode() {
|
|||
echo "read-only"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_default_sandbox_mode — determines Codex sandbox_mode from shared config
|
||||
# $1 = Claude permissions.defaultMode value
|
||||
# ---------------------------------------------------------------------------
|
||||
map_default_sandbox_mode() {
|
||||
local default_mode="$1"
|
||||
|
||||
case "$default_mode" in
|
||||
plan) echo "read-only" ;;
|
||||
acceptEdits) echo "workspace-write" ;;
|
||||
*) echo "workspace-write" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_approval_policy — determines Codex approval_policy from shared config
|
||||
# $1 = Claude permissions.defaultMode value
|
||||
# $2 = optional Codex approval override from shared config
|
||||
# ---------------------------------------------------------------------------
|
||||
map_approval_policy() {
|
||||
local default_mode="$1"
|
||||
local override="$2"
|
||||
|
||||
if [ -n "$override" ] && [ "$override" != "null" ]; then
|
||||
echo "$override"
|
||||
return
|
||||
fi
|
||||
|
||||
# Closest intent mapping:
|
||||
# - plan remains guarded
|
||||
# - acceptEdits becomes Codex's closest "edit without constant prompts" mode
|
||||
case "$default_mode" in
|
||||
plan) echo "on-request" ;;
|
||||
acceptEdits) echo "untrusted" ;;
|
||||
*) echo "untrusted" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# generate_claude — produces claude/ output directory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -154,28 +482,72 @@ generate_claude() {
|
|||
ln -s ../skills "$CLAUDE_DIR/skills"
|
||||
echo "Symlinked: $CLAUDE_DIR/skills -> ../skills"
|
||||
|
||||
# Generate agent .md files with expanded template variables
|
||||
for agent_file in "$AGENTS_SRC"/*.md; do
|
||||
[ -f "$agent_file" ] || continue
|
||||
# Generate agent .md files from TEAM metadata + markdown instruction body
|
||||
local agent_id
|
||||
while IFS= read -r agent_id; do
|
||||
[ -n "$agent_id" ] || continue
|
||||
|
||||
local agent_basename
|
||||
agent_basename="$(basename "$agent_file")"
|
||||
local dst_file="$CLAUDE_AGENTS_DIR/$agent_basename"
|
||||
local name description model effort permission_mode
|
||||
local src_file dst_file body expanded_body
|
||||
local max_turns background memory isolation
|
||||
local tools_csv disallowed_tools_csv
|
||||
|
||||
# Extract frontmatter and body separately
|
||||
local frontmatter body expanded_body
|
||||
frontmatter="$(extract_frontmatter_block "$agent_file")"
|
||||
body="$(extract_body "$agent_file")"
|
||||
name="$(yq -r ".agents.items.${agent_id}.name" "$TEAM_YAML")"
|
||||
description="$(yq -r ".agents.items.${agent_id}.description" "$TEAM_YAML")"
|
||||
model="$(yq -r ".agents.items.${agent_id}.model" "$TEAM_YAML")"
|
||||
effort="$(yq -r ".agents.items.${agent_id}.effort // \"\"" "$TEAM_YAML")"
|
||||
permission_mode="$(yq -r ".agents.items.${agent_id}.permission_mode // \"\"" "$TEAM_YAML")"
|
||||
tools_csv="$(yq -r ".agents.items.${agent_id}.tools[]" "$TEAM_YAML" | csv_from_yaml_array)"
|
||||
disallowed_tools_csv="$(yq -r ".agents.items.${agent_id}.disallowed_tools // [] | .[]" "$TEAM_YAML" | csv_from_yaml_array)"
|
||||
max_turns="$(yq -r ".agents.items.${agent_id}.max_turns // \"\"" "$TEAM_YAML")"
|
||||
background="$(yq -r ".agents.items.${agent_id}.background // \"\"" "$TEAM_YAML")"
|
||||
memory="$(yq -r ".agents.items.${agent_id}.memory // \"\"" "$TEAM_YAML")"
|
||||
isolation="$(yq -r ".agents.items.${agent_id}.isolation // \"\"" "$TEAM_YAML")"
|
||||
|
||||
src_file="$SCRIPT_DIR/$(yq -r ".agents.items.${agent_id}.instruction_file" "$TEAM_YAML")"
|
||||
dst_file="$CLAUDE_AGENTS_DIR/${name}.md"
|
||||
|
||||
body="$(extract_body "$src_file")"
|
||||
expanded_body="$(expand_body "$body" "${CLAUDE_VARS[@]}")"
|
||||
|
||||
# Reassemble: frontmatter + expanded body
|
||||
{
|
||||
echo "$frontmatter"
|
||||
echo "---"
|
||||
echo "name: '$(yaml_escape_single_quoted "$name")'"
|
||||
echo "description: '$(yaml_escape_single_quoted "$description")'"
|
||||
echo "model: '$(yaml_escape_single_quoted "$model")'"
|
||||
if [ -n "$effort" ] && [ "$effort" != "null" ]; then
|
||||
echo "effort: '$(yaml_escape_single_quoted "$effort")'"
|
||||
fi
|
||||
if [ -n "$permission_mode" ] && [ "$permission_mode" != "null" ]; then
|
||||
echo "permissionMode: '$(yaml_escape_single_quoted "$permission_mode")'"
|
||||
fi
|
||||
echo "tools: '$(yaml_escape_single_quoted "$tools_csv")'"
|
||||
if [ -n "$disallowed_tools_csv" ] && [ "$disallowed_tools_csv" != "null" ]; then
|
||||
echo "disallowedTools: '$(yaml_escape_single_quoted "$disallowed_tools_csv")'"
|
||||
fi
|
||||
if [ "$background" = "true" ]; then
|
||||
echo "background: true"
|
||||
fi
|
||||
if [ -n "$memory" ] && [ "$memory" != "null" ]; then
|
||||
echo "memory: '$(yaml_escape_single_quoted "$memory")'"
|
||||
fi
|
||||
if [ -n "$isolation" ] && [ "$isolation" != "null" ]; then
|
||||
echo "isolation: '$(yaml_escape_single_quoted "$isolation")'"
|
||||
fi
|
||||
if [ -n "$max_turns" ] && [ "$max_turns" != "null" ]; then
|
||||
echo "maxTurns: $max_turns"
|
||||
fi
|
||||
echo "skills:"
|
||||
yq -r ".agents.items.${agent_id}.skills[]" "$TEAM_YAML" | while IFS= read -r skill; do
|
||||
echo " - $(yaml_escape_single_quoted "$skill")"
|
||||
done
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "$expanded_body"
|
||||
} > "$dst_file"
|
||||
|
||||
echo "Generated: $dst_file"
|
||||
done
|
||||
done < <(yq -r '.agents.order[]' "$TEAM_YAML")
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -189,28 +561,23 @@ generate_codex() {
|
|||
rm -rf "$CODEX_DIR"
|
||||
mkdir -p "$CODEX_AGENTS_DIR"
|
||||
|
||||
# Generate agent .toml files
|
||||
# Generate agent .toml files from TEAM metadata + markdown instruction body
|
||||
echo "Generating Codex agent definitions..."
|
||||
for agent_file in "$AGENTS_SRC"/*.md; do
|
||||
[ -f "$agent_file" ] || continue
|
||||
local agent_id
|
||||
while IFS= read -r agent_id; do
|
||||
[ -n "$agent_id" ] || continue
|
||||
|
||||
local agent_basename
|
||||
agent_basename="$(basename "$agent_file" .md)"
|
||||
local dst_file="$CODEX_AGENTS_DIR/${agent_basename}.toml"
|
||||
|
||||
# Extract YAML frontmatter using yq
|
||||
local frontmatter
|
||||
frontmatter="$(yq --front-matter=extract '.' "$agent_file")"
|
||||
|
||||
# Extract individual fields from frontmatter
|
||||
local name description model effort permission_mode tools disallowed_tools
|
||||
name="$(echo "$frontmatter" | yq '.name // ""')"
|
||||
description="$(echo "$frontmatter" | yq '.description // ""')"
|
||||
model="$(echo "$frontmatter" | yq '.model // ""')"
|
||||
effort="$(echo "$frontmatter" | yq '.effort // ""')"
|
||||
permission_mode="$(echo "$frontmatter" | yq '.permissionMode // ""')"
|
||||
tools="$(echo "$frontmatter" | yq '.tools // ""')"
|
||||
disallowed_tools="$(echo "$frontmatter" | yq '.disallowedTools // ""')"
|
||||
local src_file dst_file
|
||||
name="$(yq -r ".agents.items.${agent_id}.name" "$TEAM_YAML")"
|
||||
description="$(yq -r ".agents.items.${agent_id}.description" "$TEAM_YAML")"
|
||||
model="$(yq -r ".agents.items.${agent_id}.model" "$TEAM_YAML")"
|
||||
effort="$(yq -r ".agents.items.${agent_id}.effort // \"\"" "$TEAM_YAML")"
|
||||
permission_mode="$(yq -r ".agents.items.${agent_id}.permission_mode // \"\"" "$TEAM_YAML")"
|
||||
tools="$(yq -r ".agents.items.${agent_id}.tools[]" "$TEAM_YAML" | csv_from_yaml_array)"
|
||||
disallowed_tools="$(yq -r ".agents.items.${agent_id}.disallowed_tools // [] | .[]" "$TEAM_YAML" | csv_from_yaml_array)"
|
||||
src_file="$SCRIPT_DIR/$(yq -r ".agents.items.${agent_id}.instruction_file" "$TEAM_YAML")"
|
||||
dst_file="$CODEX_AGENTS_DIR/${name}.toml"
|
||||
|
||||
# Map to Codex equivalents
|
||||
local codex_model codex_effort codex_sandbox
|
||||
|
|
@ -220,7 +587,7 @@ generate_codex() {
|
|||
|
||||
# Extract and expand body with Codex variable values
|
||||
local body expanded_body
|
||||
body="$(extract_body "$agent_file")"
|
||||
body="$(extract_body "$src_file")"
|
||||
expanded_body="$(expand_body "$body" "${CODEX_VARS[@]}")"
|
||||
|
||||
# Build developer_instructions: append disallowedTools note if present
|
||||
|
|
@ -245,42 +612,48 @@ ${developer_instructions}
|
|||
TOML
|
||||
|
||||
echo "Generated: $dst_file"
|
||||
done
|
||||
done < <(yq -r '.agents.order[]' "$TEAM_YAML")
|
||||
|
||||
# Generate AGENTS.md — concatenate rules/*.md with tool-agnostic header
|
||||
# Generate AGENTS.md — concatenate TEAM-ordered rules with tool-agnostic header
|
||||
echo ""
|
||||
echo "Generating codex/AGENTS.md..."
|
||||
{
|
||||
echo "# Agent Team Instructions"
|
||||
echo ""
|
||||
echo "Agent-team specific protocols live in skills (orchestrate, conventions, worker-protocol, qa-checklist, message-schema, project)."
|
||||
for rules_file in "$RULES_DIR"/*.md; do
|
||||
local rule_id rules_file
|
||||
while IFS= read -r rule_id; do
|
||||
[ -n "$rule_id" ] || continue
|
||||
yq -r ".rules.items.${rule_id}.applies_to[]" "$TEAM_YAML" | grep -qx "codex" || continue
|
||||
rules_file="$SCRIPT_DIR/$(yq -r ".rules.items.${rule_id}.source_file" "$TEAM_YAML")"
|
||||
echo ""
|
||||
cat "$rules_file"
|
||||
done
|
||||
done < <(yq -r '.rules.order[]' "$TEAM_YAML")
|
||||
} > "$CODEX_DIR/AGENTS.md"
|
||||
echo "Generated: $CODEX_DIR/AGENTS.md"
|
||||
|
||||
# Generate config.toml — derive sandbox_mode from settings.json defaultMode
|
||||
# Generate config.toml — derive sandbox/approval defaults from shared config
|
||||
echo ""
|
||||
echo "Generating codex/config.toml..."
|
||||
|
||||
local default_mode
|
||||
default_mode="$(yq -r '.permissions.defaultMode // "acceptEdits"' "$SETTINGS_JSON")"
|
||||
local default_mode codex_approval_override codex_network_access
|
||||
default_mode="$(map_filesystem_intent_to_claude_mode "$(yq -r '.runtime.filesystem' "$SETTINGS_SHARED_YAML")")"
|
||||
codex_approval_override="$(yq -r '.targets.codex.approval_policy // ""' "$SETTINGS_SHARED_YAML")"
|
||||
codex_network_access="$(yq -r '.targets.codex.network_access // .runtime.network_access // false' "$SETTINGS_SHARED_YAML")"
|
||||
|
||||
# Map Claude defaultMode to Codex sandbox_mode
|
||||
local config_sandbox
|
||||
case "$default_mode" in
|
||||
plan) config_sandbox="read-only" ;;
|
||||
acceptEdits) config_sandbox="workspace-write" ;;
|
||||
*) config_sandbox="workspace-write" ;;
|
||||
esac
|
||||
local config_sandbox config_approval
|
||||
config_sandbox="$(map_default_sandbox_mode "$default_mode")"
|
||||
config_approval="$(map_approval_policy "$default_mode" "$codex_approval_override")"
|
||||
|
||||
cat > "$CODEX_DIR/config.toml" <<TOML
|
||||
#:schema https://developers.openai.com/codex/config-schema.json
|
||||
model = "gpt-5.3-codex"
|
||||
model_reasoning_effort = "medium"
|
||||
sandbox_mode = "${config_sandbox}"
|
||||
approval_policy = "on-request"
|
||||
approval_policy = "${config_approval}"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = ${codex_network_access}
|
||||
TOML
|
||||
echo "Generated: $CODEX_DIR/config.toml"
|
||||
}
|
||||
|
|
@ -288,6 +661,7 @@ TOML
|
|||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
prepare_settings_json
|
||||
generate_claude
|
||||
generate_codex
|
||||
|
||||
|
|
|
|||
76
install.sh
76
install.sh
|
|
@ -9,6 +9,7 @@ CLAUDE_DIR="$HOME/.claude"
|
|||
AGENTS_SRC="$SCRIPT_DIR/claude/agents"
|
||||
SKILLS_SRC="$SCRIPT_DIR/skills"
|
||||
RULES_SRC="$SCRIPT_DIR/rules"
|
||||
TEAM_YAML="$SCRIPT_DIR/TEAM.yaml"
|
||||
AGENTS_DST="$CLAUDE_DIR/agents"
|
||||
SKILLS_DST="$CLAUDE_DIR/skills"
|
||||
RULES_DST="$CLAUDE_DIR/rules"
|
||||
|
|
@ -122,6 +123,48 @@ create_file_symlink() {
|
|||
echo "Linked: $dst -> $src"
|
||||
}
|
||||
|
||||
# Return one skill id per line for a target platform from TEAM.yaml.
|
||||
# Falls back to empty output when TEAM.yaml is unavailable.
|
||||
list_team_skills_for_target() {
|
||||
local target="$1"
|
||||
|
||||
if [ ! -f "$TEAM_YAML" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate TEAM parseability before resolving inventory.
|
||||
yq -e '.version == 1 and has("skills") and (.skills | has("order")) and (.skills | has("items"))' "$TEAM_YAML" > /dev/null
|
||||
|
||||
local skill_id applies
|
||||
while IFS= read -r skill_id; do
|
||||
[ -n "$skill_id" ] || continue
|
||||
applies="$(yq -r ".skills.items.\"$skill_id\".applies_to[]? // \"\"" "$TEAM_YAML")"
|
||||
if printf '%s\n' "$applies" | grep -Fxq "$target"; then
|
||||
printf '%s\n' "$skill_id"
|
||||
fi
|
||||
done < <(yq -r '.skills.order[]' "$TEAM_YAML")
|
||||
}
|
||||
|
||||
# Resolve a TEAM skill id to its source directory using instruction_file.
|
||||
resolve_skill_dir_from_team() {
|
||||
local skill_id="$1"
|
||||
|
||||
if [ ! -f "$TEAM_YAML" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local instruction_file skill_dir
|
||||
instruction_file="$(
|
||||
yq -r ".skills.items.\"$skill_id\".instruction_file // \"\"" "$TEAM_YAML"
|
||||
)"
|
||||
[ -n "$instruction_file" ] || return 1
|
||||
|
||||
skill_dir="$(dirname "$SCRIPT_DIR/$instruction_file")"
|
||||
[ -d "$skill_dir" ] || return 1
|
||||
|
||||
printf '%s\n' "$skill_dir"
|
||||
}
|
||||
|
||||
create_symlink "$AGENTS_SRC" "$AGENTS_DST" "agents"
|
||||
create_symlink "$SKILLS_SRC" "$SKILLS_DST" "skills"
|
||||
create_symlink "$RULES_SRC" "$RULES_DST" "rules"
|
||||
|
|
@ -139,10 +182,35 @@ if [ -d "$SCRIPT_DIR/codex" ]; then
|
|||
# Skills: symlink each skill directory into ~/.codex/skills/
|
||||
# (Can't replace the whole directory — .system/ must remain intact)
|
||||
mkdir -p "$CODEX_DIR/skills"
|
||||
for skill_dir in "$SKILLS_SRC"/*/; do
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
create_symlink "$skill_dir" "$CODEX_DIR/skills/$skill_name" "codex skill: $skill_name"
|
||||
done
|
||||
if [ -f "$TEAM_YAML" ]; then
|
||||
team_codex_skills_tmp="$(mktemp)"
|
||||
if ! list_team_skills_for_target "codex" > "$team_codex_skills_tmp" 2>/dev/null; then
|
||||
echo "Warning: TEAM.yaml exists but could not be parsed; falling back to directory-based Codex skill install."
|
||||
for skill_dir in "$SKILLS_SRC"/*/; do
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
create_symlink "$skill_dir" "$CODEX_DIR/skills/$skill_name" "codex skill: $skill_name"
|
||||
done
|
||||
rm -f "$team_codex_skills_tmp"
|
||||
else
|
||||
# TEAM is authoritative when present, including the explicit zero-skills case.
|
||||
while IFS= read -r skill_id; do
|
||||
[ -n "$skill_id" ] || continue
|
||||
skill_dir="$(resolve_skill_dir_from_team "$skill_id" || true)"
|
||||
if [ -z "$skill_dir" ]; then
|
||||
echo "Warning: TEAM.yaml skill '$skill_id' has no valid instruction_file directory; skipping."
|
||||
continue
|
||||
fi
|
||||
create_symlink "$skill_dir" "$CODEX_DIR/skills/$skill_id" "codex skill: $skill_id"
|
||||
done < "$team_codex_skills_tmp"
|
||||
rm -f "$team_codex_skills_tmp"
|
||||
fi
|
||||
else
|
||||
# Legacy fallback only when TEAM.yaml is absent.
|
||||
for skill_dir in "$SKILLS_SRC"/*/; do
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
create_symlink "$skill_dir" "$CODEX_DIR/skills/$skill_name" "codex skill: $skill_name"
|
||||
done
|
||||
fi
|
||||
|
||||
# Generated agents
|
||||
if [ -d "$SCRIPT_DIR/codex/agents" ]; then
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"attribution": {
|
||||
"commit": "",
|
||||
"pr": ""
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash",
|
||||
"Read",
|
||||
"Edit",
|
||||
"Write",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"WebFetch",
|
||||
"WebSearch"
|
||||
],
|
||||
"deny": [
|
||||
"Read(~/.ssh/**)",
|
||||
"Read(~/.aws/**)",
|
||||
"Read(~/.gnupg/**)",
|
||||
"Read(**/.env*)",
|
||||
"Write(~/.ssh/**)",
|
||||
"Write(~/.aws/**)",
|
||||
"Write(~/.gnupg/**)",
|
||||
"Write(**/.env*)",
|
||||
"Edit(~/.ssh/**)",
|
||||
"Edit(~/.aws/**)",
|
||||
"Edit(~/.gnupg/**)",
|
||||
"Edit(**/.env*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(rm *)",
|
||||
"Bash(rmdir *)",
|
||||
"Bash(git push --force*)",
|
||||
"Bash(git push -f*)",
|
||||
"Bash(git reset --hard*)",
|
||||
"Bash(git clean *)",
|
||||
"Bash(chmod *)",
|
||||
"Bash(dd *)",
|
||||
"Bash(mkfs*)",
|
||||
"Bash(shred *)",
|
||||
"Bash(kill *)",
|
||||
"Bash(killall *)",
|
||||
"Bash(sudo *)"
|
||||
],
|
||||
"defaultMode": "acceptEdits"
|
||||
},
|
||||
"model": "sonnet",
|
||||
"effortLevel": "medium",
|
||||
"claudeMdExcludes": [
|
||||
".claude/agent-memory/**"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue