diff --git a/CLAUDE.md b/CLAUDE.md index cf054e5..4391add 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ # Global Claude Code Instructions Rules are modularized in `rules/` and loaded automatically by the generated Claude config. -Agent-team specific protocols live in skills (orchestrate, conventions, worker-protocol, qa-checklist, message-schema, project). +Agent-team specific protocols live in skills (orchestrate, conventions, worker-protocol, qa-checklist, message-schema). diff --git a/README.md b/README.md index b780659..07ad725 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ just clean # removes generated artifacts: settings.json + claude/ + cod | `worker-protocol` | Output format, feedback handling, and operational procedures for worker agents | | `qa-checklist` | Self-validation checklist workers run before returning results | | `message-schema` | Typed YAML frontmatter envelopes for all inter-agent communication | -| `project` | Instructs agents to check for and ingest a project-specific skill file before starting work | ## Rules @@ -120,6 +119,7 @@ Runtime policy is documented in [spec/agent-runtime-v1.md](spec/agent-runtime-v1 | `SETTINGS.yaml` | `claude/settings.json` | Claude adapter output | | `SETTINGS.yaml` | `codex/config.toml` | Codex adapter output | | `TEAM.yaml` + `rules/*.md` | `codex/AGENTS.md` | Codex adapter output | +| `TEAM.yaml` + `skills/*/SKILL.md` | `codex/skills -> ../skills` | Codex adapter output | | `TEAM.yaml` + `skills/*/SKILL.md` | installed skill dirs | target install output | All final config files are generated artifacts. The authored protocol sources are `SETTINGS.yaml`, `TEAM.yaml`, and Markdown instruction content. The primary workflows are `nix run .#build` / `nix run .#install` or the equivalent `just` commands. @@ -129,6 +129,7 @@ Narrow compatibility caveats: - TEAM schema is intentionally rigid/repo-specific in v1. Inventory changes require schema updates in lockstep. - Claude generated agent frontmatter is normalized by generator serialization (field order/quoting), which may produce non-semantic diffs. - Codex skill installation is TEAM-authoritative when `TEAM.yaml` is present. Legacy directory fallback is used only when TEAM is absent or unparseable. +- Codex custom-agent files do not preserve every TEAM agent field. `background`, `memory`, and `isolation` have no documented per-agent equivalents in current Codex docs. TEAM `skills` are mapped into per-agent Codex `skills.config` entries. Shared runtime intent is generated conservatively across tools: @@ -140,7 +141,13 @@ Shared runtime intent is generated conservatively across tools: | `runtime.approval = guarded-auto` | partially represented | `approval_policy = "untrusted"` | | `runtime.approval = full-auto` | partially represented | `approval_policy = "never"` | -The adapters do not expose identical config surfaces. For example, Codex does not support Claude-style per-tool `allow` / `deny` / `ask` patterns directly. The shared protocol keeps the intent portable, then adapters derive the closest target behavior. Use target-specific fields only where there is no shared equivalent: +The adapters do not expose identical config surfaces. For example, Codex does not support Claude-style per-tool `allow` / `deny` / `ask` patterns directly. The shared protocol keeps the intent portable, then adapters derive the closest target behavior. + +`runtime.approval` and `runtime.network_access` are the primary source of truth. `targets.codex.approval_policy` and `targets.codex.network_access` are compatibility overrides for exceptional cases only. When set, they override the Codex-derived value. + +This repo intentionally sets those Codex overrides to `approval_policy: never` and `network_access: true`. The reason is not that Codex has no approval controls at all, but that it lacks Claude-equivalent pattern-level permission controls for tool/path `allow` / `deny` / `ask`. In this repo, Codex therefore runs with a deliberately more permissive top-level policy than the portable runtime defaults. + +Use target-specific fields only when you intentionally need a target-only override: ```yaml targets: @@ -234,7 +241,6 @@ Each project repo can extend the team with local config in `.claude/`: - `.claude/CLAUDE.md` — project-specific instructions (architecture notes, domain conventions, stack details) - `.claude/agents/` — project-local agent overrides or additions -- `.claude/skills/project.md` — skill file that agents automatically ingest before starting work (see the `project` skill) Commit `.claude/` with the project so the team has context wherever it runs. @@ -242,7 +248,7 @@ Commit `.claude/` with the project so the team has context wherever it runs. Two memory systems coexist: -- **Manual memory** (`.claude/memory/`) — curated context files with YAML frontmatter, indexed by `MEMORY.md`. Loaded as part of the CLAUDE.md hierarchy on every session. Use this for project decisions, user preferences, and reference pointers. +- **Project memory** (`memory/`) — curated context files with YAML frontmatter, indexed by `MEMORY.md`. This is the portable, instruction-level memory source shared across targets. - **Agent memory** (`.claude/agent-memory/`) — Claude Code's built-in runtime memory, written automatically by agents with `memory: project` scope. Excluded from CLAUDE.md context via `claudeMdExcludes` to avoid polluting the context window. -Commit both directories with the repo so memory persists across machines and sessions. +Commit both directories when used so memory persists across machines and sessions. diff --git a/SETTINGS.yaml b/SETTINGS.yaml index e5cd4f5..0844c7b 100644 --- a/SETTINGS.yaml +++ b/SETTINGS.yaml @@ -47,5 +47,8 @@ targets: claude_md_excludes: - .claude/agent-memory/** codex: - approval_policy: untrusted - network_access: false + # Intentional target override: Codex does not expose Claude-equivalent + # per-tool/path allow/deny/ask controls, so this repo runs Codex in + # full-auto with network enabled by default. + approval_policy: never + network_access: true diff --git a/TEAM.yaml b/TEAM.yaml index 5ebe433..379150b 100644 --- a/TEAM.yaml +++ b/TEAM.yaml @@ -25,9 +25,9 @@ agents: - Grep - WebFetch - WebSearch - - Bash - Write - disallowed_tools: [] + disallowed_tools: + - Edit max_turns: 35 skills: - conventions @@ -39,7 +39,7 @@ agents: description: Use after implementation — audits for security vulnerabilities and validates runtime behavior. Builds, tests, and probes acceptance criteria. Never modifies code. model: sonnet effort: "" - permission_mode: "" + permission_mode: acceptEdits tools: - Read - Glob @@ -82,17 +82,16 @@ agents: documenter: id: documenter name: documenter - description: Use when asked to write or update documentation — READMEs, API references, architecture overviews, inline doc comments, or changelogs. Reads code first, writes accurate docs. Never modifies source code. + description: Use when asked to write or update documentation — READMEs, API references, architecture overviews, inline doc comments, or changelogs. Reads code first and updates documentation artifacts only. model: sonnet effort: high - permission_mode: "" + permission_mode: acceptEdits tools: - Read - Write - Edit - Glob - Grep - - Bash disallowed_tools: [] max_turns: 20 skills: @@ -136,7 +135,6 @@ agents: - Read - Glob - Grep - - Bash - WebFetch - WebSearch disallowed_tools: @@ -157,7 +155,6 @@ agents: - Read - Glob - Grep - - Bash - WebFetch - WebSearch disallowed_tools: diff --git a/flake.nix b/flake.nix index 3b96dd7..dbbf296 100644 --- a/flake.nix +++ b/flake.nix @@ -60,6 +60,22 @@ validate(instance=settings_data, schema=settings_schema) validate(instance=team_data, schema=team_schema) + + # TEAM referenced files must exist on disk. + for agent_id in team_data["agents"]["order"]: + instruction_file = team_data["agents"]["items"][agent_id]["instruction_file"] + if not (root / instruction_file).is_file(): + raise FileNotFoundError(f"Missing agent instruction file: {instruction_file}") + + for skill_id in team_data["skills"]["order"]: + instruction_file = team_data["skills"]["items"][skill_id]["instruction_file"] + if not (root / instruction_file).is_file(): + raise FileNotFoundError(f"Missing skill instruction file: {instruction_file}") + + for rule_id in team_data["rules"]["order"]: + source_file = team_data["rules"]["items"][rule_id]["source_file"] + if not (root / source_file).is_file(): + raise FileNotFoundError(f"Missing rule source file: {source_file}") PY ''; @@ -104,6 +120,7 @@ program = "${mkAppScript "install" '' set -euo pipefail test -f ./install.sh || { echo "Run this command from the repository root."; exit 1; } + ${validateCmd} ${bashBin} ./install.sh ''}/bin/install"; meta.description = "Install generated artifacts into Claude and Codex config directories."; @@ -148,6 +165,22 @@ validate(instance=settings_data, schema=settings_schema) validate(instance=team_data, schema=team_schema) + + # TEAM referenced files must exist on disk. + for agent_id in team_data["agents"]["order"]: + instruction_file = team_data["agents"]["items"][agent_id]["instruction_file"] + if not (root / instruction_file).is_file(): + raise FileNotFoundError(f"Missing agent instruction file: {instruction_file}") + + for skill_id in team_data["skills"]["order"]: + instruction_file = team_data["skills"]["items"][skill_id]["instruction_file"] + if not (root / instruction_file).is_file(): + raise FileNotFoundError(f"Missing skill instruction file: {instruction_file}") + + for rule_id in team_data["rules"]["order"]: + source_file = team_data["rules"]["items"][rule_id]["source_file"] + if not (root / source_file).is_file(): + raise FileNotFoundError(f"Missing rule source file: {source_file}") PY ''; diff --git a/generate.sh b/generate.sh index abd2123..5b8002a 100755 --- a/generate.sh +++ b/generate.sh @@ -435,11 +435,11 @@ map_default_sandbox_mode() { # --------------------------------------------------------------------------- # map_approval_policy — determines Codex approval_policy from shared config -# $1 = Claude permissions.defaultMode value +# $1 = runtime.approval value (manual / guarded-auto / full-auto) # $2 = optional Codex approval override from shared config # --------------------------------------------------------------------------- map_approval_policy() { - local default_mode="$1" + local runtime_approval="$1" local override="$2" if [ -n "$override" ] && [ "$override" != "null" ]; then @@ -447,14 +447,7 @@ map_approval_policy() { 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 + map_approval_intent_to_codex_policy "$runtime_approval" } # --------------------------------------------------------------------------- @@ -560,6 +553,8 @@ generate_codex() { # Clean and recreate output directories rm -rf "$CODEX_DIR" mkdir -p "$CODEX_AGENTS_DIR" + ln -s ../skills "$CODEX_DIR/skills" + echo "Symlinked: $CODEX_DIR/skills -> ../skills" # Generate agent .toml files from TEAM metadata + markdown instruction body echo "Generating Codex agent definitions..." @@ -568,6 +563,7 @@ generate_codex() { [ -n "$agent_id" ] || continue local name description model effort permission_mode tools disallowed_tools + local agent_skills 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")" @@ -576,6 +572,7 @@ generate_codex() { 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)" + agent_skills="$(yq -r ".agents.items.${agent_id}.skills[]" "$TEAM_YAML")" src_file="$SCRIPT_DIR/$(yq -r ".agents.items.${agent_id}.instruction_file" "$TEAM_YAML")" dst_file="$CODEX_AGENTS_DIR/${name}.toml" @@ -599,6 +596,13 @@ generate_codex() { You do NOT have access to these tools: ${disallowed_tools}" fi + # TOML multiline basic strings use """ delimiters; reject raw delimiter + # sequences in instruction bodies so generated TOML remains parseable. + if printf '%s' "$developer_instructions" | grep -q '"""'; then + echo "Error: agent instruction contains raw triple quotes (\"\"\") which break TOML in $src_file" + exit 1 + fi + # Write TOML output cat > "$dst_file" <> "$dst_file" <> "$dst_file" < "$CODEX_DIR/config.toml" < $src" } +ensure_directory() { + local dst="$1" + local name="$2" + + if [ -L "$dst" ]; then + echo "Removing existing symlink: $dst" + rm "$dst" + elif [ -f "$dst" ]; then + local backup="${dst}.backup.$(date +%Y%m%d%H%M%S)" + echo "Backing up existing $name file to: $backup" + mv "$dst" "$backup" + fi + + mkdir -p "$dst" +} + # Symlink a single file create_file_symlink() { local src="$1" @@ -165,11 +183,95 @@ resolve_skill_dir_from_team() { printf '%s\n' "$skill_dir" } +install_team_skills_for_target() { + local target="$1" + local dst_root="$2" + local label_prefix="$3" + + ensure_directory "$dst_root" "$label_prefix skills" + + local skill_dir skill_name skill_dst skill_id skill_dir_path + local expected_skills_tmp + expected_skills_tmp="$(mktemp)" + + cleanup_skill_symlinks() { + local expected_file="$1" + local existing_path existing_name + + for existing_path in "$dst_root"/*; do + [ -e "$existing_path" ] || [ -L "$existing_path" ] || continue + [ -L "$existing_path" ] || continue + + existing_name="$(basename "$existing_path")" + if [ -s "$expected_file" ] && grep -Fxq "$existing_name" "$expected_file"; then + continue + fi + + echo "Removing stale symlink: $existing_path" + rm "$existing_path" + done + } + + if [ -f "$TEAM_YAML" ]; then + local team_skills_tmp + team_skills_tmp="$(mktemp)" + if ! list_team_skills_for_target "$target" > "$team_skills_tmp" 2>/dev/null; then + echo "Warning: TEAM.yaml exists but could not be parsed; falling back to directory-based ${label_prefix} skill install." + for skill_dir in "$SKILLS_SRC"/*/; do + skill_name="$(basename "$skill_dir")" + printf '%s\n' "$skill_name" >> "$expected_skills_tmp" + done + cleanup_skill_symlinks "$expected_skills_tmp" + for skill_dir in "$SKILLS_SRC"/*/; do + skill_name="$(basename "$skill_dir")" + create_symlink "$skill_dir" "$dst_root/$skill_name" "${label_prefix} skill: $skill_name" + done + rm -f "$team_skills_tmp" + rm -f "$expected_skills_tmp" + return + fi + + while IFS= read -r skill_id; do + [ -n "$skill_id" ] || continue + printf '%s\n' "$skill_id" >> "$expected_skills_tmp" + done < "$team_skills_tmp" + + cleanup_skill_symlinks "$expected_skills_tmp" + + while IFS= read -r skill_id; do + [ -n "$skill_id" ] || continue + skill_dir_path="$(resolve_skill_dir_from_team "$skill_id" || true)" + if [ -z "$skill_dir_path" ]; then + echo "Warning: TEAM.yaml skill '$skill_id' has no valid instruction_file directory; skipping." + continue + fi + create_symlink "$skill_dir_path" "$dst_root/$skill_id" "${label_prefix} skill: $skill_id" + done < "$team_skills_tmp" + rm -f "$team_skills_tmp" + rm -f "$expected_skills_tmp" + return + fi + + for skill_dir in "$SKILLS_SRC"/*/; do + skill_name="$(basename "$skill_dir")" + printf '%s\n' "$skill_name" >> "$expected_skills_tmp" + done + + cleanup_skill_symlinks "$expected_skills_tmp" + + for skill_dir in "$SKILLS_SRC"/*/; do + skill_name="$(basename "$skill_dir")" + create_symlink "$skill_dir" "$dst_root/$skill_name" "${label_prefix} skill: $skill_name" + done + + rm -f "$expected_skills_tmp" +} + create_symlink "$AGENTS_SRC" "$AGENTS_DST" "agents" -create_symlink "$SKILLS_SRC" "$SKILLS_DST" "skills" create_symlink "$RULES_SRC" "$RULES_DST" "rules" create_file_symlink "$CLAUDE_MD_SRC" "$CLAUDE_MD_DST" "CLAUDE.md" create_file_symlink "$SETTINGS_SRC" "$SETTINGS_DST" "settings.json" +install_team_skills_for_target "claude" "$CLAUDE_DIR/skills" "claude" # Codex CLI integration (optional — only if codex/ output exists) CODEX_DIR="$HOME/.codex" @@ -181,36 +283,7 @@ 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" - 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 + install_team_skills_for_target "codex" "$CODEX_DIR/skills" "codex" # Generated agents if [ -d "$SCRIPT_DIR/codex/agents" ]; then diff --git a/rules/01-session.md b/rules/01-session.md index 49f6ecb..941bb29 100644 --- a/rules/01-session.md +++ b/rules/01-session.md @@ -10,3 +10,4 @@ - Use `MEMORY.md` in that directory as the index (one line per entry pointing to a file) - Memory files use frontmatter: `name`, `description`, `type` (user/feedback/project/reference) - Commit `memory/` with the repo so memory persists across machines and sessions +- Tool-specific runtime memory (for example `.claude/agent-memory/`) is optional and does not replace `memory/` as the project source of truth diff --git a/schemas/agent-runtime.schema.json b/schemas/agent-runtime.schema.json index 1a977d6..8f0ef23 100644 --- a/schemas/agent-runtime.schema.json +++ b/schemas/agent-runtime.schema.json @@ -149,10 +149,12 @@ "on-request", "untrusted", "never" - ] + ], + "description": "Codex-only compatibility override. Prefer runtime.approval as the portable source of truth." }, "network_access": { - "type": "boolean" + "type": "boolean", + "description": "Codex-only compatibility override. Prefer runtime.network_access as the portable source of truth." } } } diff --git a/schemas/team.schema.json b/schemas/team.schema.json index 3be7545..45f7c09 100644 --- a/schemas/team.schema.json +++ b/schemas/team.schema.json @@ -367,7 +367,7 @@ "inventory_skills": { "type": "object", "additionalProperties": false, - "description": "Skill inventory for protocol v1. This schema enforces exact order, exact keys, and key/id equality for the current repository inventory.", + "description": "Skill inventory for protocol v1. This schema enforces exact order, exact keys, and key/id equality for the current repository inventory. The fixed v1 inventory does not include a separate 'project' skill entry.", "required": [ "order", "items" diff --git a/spec/agent-runtime-v1.md b/spec/agent-runtime-v1.md index 4bf333d..a172ff8 100644 --- a/spec/agent-runtime-v1.md +++ b/spec/agent-runtime-v1.md @@ -21,7 +21,7 @@ Version 1 standardizes: - portable tool classes - protected path rules - dangerous shell command prompts -- target-specific escape hatches only when the target exposes settings with no shared equivalent +- a narrow set of target-specific escape hatches for compatibility overrides Version 1 does not attempt to standardize: @@ -50,13 +50,20 @@ Version 1 does not attempt to standardize: ### `targets` -Target blocks are escape hatches, not the main schema. Use them only where a runtime exposes a knob with no shared equivalent. +Target blocks are escape hatches, not the main schema. Current target-specific fields: - `targets.claude.claude_md_excludes` -- `targets.codex.approval_policy` -- `targets.codex.network_access` +- `targets.codex.approval_policy` (optional override of derived approval) +- `targets.codex.network_access` (optional override of derived network access) + +Authority rules: + +- `runtime.approval` and `runtime.network_access` are the portable source of truth. +- Codex target fields exist for explicit compatibility overrides and should normally be omitted. +- When Codex target fields are set, they intentionally override the derived Codex value. +- In this repo, `targets.codex.approval_policy` and `targets.codex.network_access` are intentionally set so Codex runs with `approval_policy = "never"` and network enabled by default. This is a deliberate target-specific compatibility choice, not an accidental divergence. ## Adapter rules @@ -81,15 +88,16 @@ Lossiness: - `runtime.filesystem = read-only` -> `sandbox_mode = "read-only"` - `runtime.filesystem = workspace-write` -> `sandbox_mode = "workspace-write"` -- `runtime.approval = manual` -> `approval_policy = "on-request"` -- `runtime.approval = guarded-auto` -> `approval_policy = "untrusted"` -- `runtime.approval = full-auto` -> `approval_policy = "never"` +- `runtime.approval = manual` -> `approval_policy = "on-request"` (unless overridden) +- `runtime.approval = guarded-auto` -> `approval_policy = "untrusted"` (unless overridden) +- `runtime.approval = full-auto` -> `approval_policy = "never"` (unless overridden) - `runtime.network_access` -> `[sandbox_workspace_write].network_access` Lossiness: - Codex does not expose Claude-style per-tool `allow` / `deny` / `ask` pattern controls in `config.toml`. - Protected paths and dangerous command prompts are therefore only partially representable in Codex config today. +- Codex does expose coarse approval controls, including `approval_policy` and documented granular approval categories, but not the same pattern-level permission model Claude exposes. ## Compatibility contract diff --git a/spec/team-protocol-v1.md b/spec/team-protocol-v1.md index ef2c56a..a3acf58 100644 --- a/spec/team-protocol-v1.md +++ b/spec/team-protocol-v1.md @@ -79,9 +79,10 @@ Each agent entry includes metadata required for adapter generation: Each skill entry includes lightweight metadata and content reference: - `id` +- `name` - `description` - `instruction_file` -- optional target/install metadata +- target/install metadata (`applies_to`, `install_mode`) Skill prose remains in `skills/*/SKILL.md`. @@ -110,6 +111,7 @@ Current target behavior: - `codex/config.toml` - `codex/AGENTS.md` - `codex/agents/*.toml` + - `codex/skills` symlinked to the shared skill directories for relative `skills.config` references ## Validation Requirements @@ -128,6 +130,7 @@ TEAM validation enforces schema + runtime checks for: - Existing YAML frontmatter in `agents/*.md` may remain for editorial continuity, but generation does not use it for team metadata. - Output diffs that are purely formatting-related are acceptable; semantic behavior changes are not unless explicitly documented. - TEAM schema is intentionally rigid/repo-specific in v1; inventory additions/removals require schema updates in lockstep. +- Agent metadata is not fully portable across targets. Current Codex custom-agent docs cover session-style fields such as `model`, `model_reasoning_effort`, `sandbox_mode`, `mcp_servers`, and `skills.config`, but do not document per-agent equivalents for TEAM's `background`, `memory`, or `isolation` fields. ## Out of Scope