From eda47480123d3bfd710a8a754347ae1f9df96bcf Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Thu, 2 Apr 2026 13:26:08 -0400 Subject: [PATCH] feat(workflow): add flake and just entrypoints --- CLAUDE.md | 2 +- README.md | 149 ++++++++++++++++++++++++++++++++++++++++----- flake.nix | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++--- justfile | 21 +++++++ 4 files changed, 323 insertions(+), 25 deletions(-) create mode 100644 justfile diff --git a/CLAUDE.md b/CLAUDE.md index 1e2879f..cf054e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ # Global Claude Code Instructions -Rules are modularized in `~/.claude/rules/` and loaded automatically. +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). diff --git a/README.md b/README.md index c56b495..06a9e4f 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,46 @@ # agent-team -A portable agent team configuration for Claude Code and Codex CLI. Clone it, run `./generate.sh` + `./install.sh`, and both tools get a full team of specialized subagents and shared skills — on any machine. +A portable agent team configuration for Claude Code and Codex CLI. Clone it, run the flake entrypoints or the `just` wrapper, and both tools get a full team of specialized subagents and shared skills on any machine. ## Quick install ```bash -git clone ~/agent-team -cd ~/agent-team +git clone +cd agent-team nix develop # enter devShell with yq + envsubst -./generate.sh # generate Claude + Codex config from templates -./install.sh # symlinks into ~/.claude/ and ~/.codex/ (if present) +nix run .#check # validate protocols + generate artifacts +nix run .#install # install into your Claude/Codex config dirs ``` -`generate.sh` expands shared agent templates into tool-specific outputs for Claude Code and Codex CLI. `install.sh` symlinks the results into `~/.claude/` and `~/.codex/`. Works on Linux, macOS, and Windows (Git Bash). +The supported user-facing entrypoints are the flake apps and the `just` wrapper. `generate.sh` and `install.sh` remain the internal implementation layer behind them. Works on Linux, macOS, and Windows (Git Bash). + +## Nix entrypoints + +The flake exposes formal workflow entrypoints: + +```bash +nix run .#validate # syntax + protocol presence/basic shape checks +nix run .#build # generate settings.json + claude/ + codex/ +nix run .#check # validate + build +nix run .#install # run install.sh +nix flake check # run flake checks (validate + build in sandboxed check derivations) +``` + +`just` is also supported as a convenience wrapper over those same flake commands: + +```bash +just validate +just build +just check +just install +just clean # removes generated artifacts: settings.json + claude/ + codex/ +``` + +`generate.sh` and `install.sh` are kept as internal implementation details for portability and debugging, but they are no longer the primary documented workflow. ## Maintenance -**Symlink fragility:** `~/.claude/CLAUDE.md` and `~/.claude/settings.json` are installed as symlinks by `install.sh`. Some tools (including Claude Code itself when writing settings) resolve symlinks to regular files on write, silently breaking the link. If edits to the repo are no longer reflected in `~/.claude/`, re-run `./install.sh` to restore the symlinks. +**Symlink fragility:** the generated Claude files are installed as symlinks by `install.sh`. Some tools (including Claude Code itself when writing settings) resolve symlinks to regular files on write, silently breaking the link. If edits to the repo are no longer reflected in your Claude config dir, re-run `./install.sh` to restore the symlinks. ## Agents @@ -43,7 +67,7 @@ nix develop # enter devShell with yq + envsubst ## Rules -Global instructions are modularized in `rules/` and auto-loaded by Claude Code from `~/.claude/rules/` on every session. Each file covers a focused topic (git workflow, Nix preferences, response style, etc.). Agent-team specific protocols live in skills, not rules. +Global instructions are modularized in `rules/` and auto-loaded by Claude Code from the installed Claude config on every session. Each file covers a focused topic (git workflow, Nix preferences, response style, etc.). Agent-team specific protocols live in skills, not rules. ## How to use @@ -65,7 +89,7 @@ For simple tasks, invoke an agent directly: ### Codex CLI -Agents are available as named agents in `~/.codex/agents/`. Invoke them with: +Agents are available as named agents in the installed Codex config. Invoke them with: ``` codex --agent worker "Fix the broken pagination in the user list endpoint" @@ -73,17 +97,112 @@ codex --agent worker "Fix the broken pagination in the user list endpoint" ## Dual-target generation -Agent source files in `agents/` are the single source of truth. `generate.sh` derives tool-specific outputs for both Claude Code and [OpenAI Codex CLI](https://github.com/openai/codex). +This repo uses two authored protocol files: + +- [SETTINGS.yaml](SETTINGS.yaml) for runtime policy (filesystem, approvals, network, model intent) +- [TEAM.yaml](TEAM.yaml) for team inventory metadata (agents, skills, rules) + +Long-form instructions remain authored in Markdown (`agents/*.md`, `skills/*/SKILL.md`, `rules/*.md`). + +Runtime policy is documented in [spec/agent-runtime-v1.md](spec/agent-runtime-v1.md) and described by [schemas/agent-runtime.schema.json](schemas/agent-runtime.schema.json). Team inventory is documented in [spec/team-protocol-v1.md](spec/team-protocol-v1.md). `generate.sh` derives tool-specific outputs for both Claude Code and [OpenAI Codex CLI](https://github.com/openai/codex). ### What gets generated | Source | Generated | Location | |---|---|---| -| `agents/*.md` (templates) | `claude/agents/*.md` | `~/.claude/agents/` | -| `agents/*.md` (templates) | `codex/agents/*.toml` | `~/.codex/agents/` | -| `rules/*.md` | `codex/AGENTS.md` | `~/.codex/AGENTS.md` | -| `settings.json` | `codex/config.toml` | `~/.codex/config.toml` | -| `skills/` | (shared as-is) | `~/.claude/skills/` + `~/.agents/skills/` | +| `TEAM.yaml` + `agents/*.md` | `claude/agents/*.md` | Claude config dir | +| `TEAM.yaml` + `agents/*.md` | `codex/agents/*.toml` | Codex config dir | +| `SETTINGS.yaml` | `settings.json` (compatibility artifact, generated) | repo root | +| `SETTINGS.yaml` | `claude/settings.json` | Claude config dir | +| `SETTINGS.yaml` | `codex/config.toml` | Codex config dir | +| `TEAM.yaml` + `rules/*.md` | `codex/AGENTS.md` | Codex config dir | +| `TEAM.yaml` + `skills/*/SKILL.md` | installed skill dirs | installed skill dirs | + +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. + +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. + +Shared runtime intent is generated conservatively across tools: + +| Shared source | Claude Code | Codex CLI | +|---|---|---| +| `runtime.filesystem = read-only` | `permissions.defaultMode = "plan"` | `sandbox_mode = "read-only"` | +| `runtime.filesystem = workspace-write` | `permissions.defaultMode = "acceptEdits"` | `sandbox_mode = "workspace-write"` | +| `runtime.approval = manual` | partially represented | `approval_policy = "on-request"` | +| `runtime.approval = guarded-auto` | partially represented | `approval_policy = "untrusted"` | +| `runtime.approval = full-auto` | partially represented | `approval_policy = "never"` | + +Codex does not support Claude's 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: + +```yaml +targets: + codex: + approval_policy: untrusted + network_access: false + claude: + claude_md_excludes: + - .claude/agent-memory/** +``` + +## Shared protocol + +The protocol source is YAML because it is easier to read and annotate than JSON or TOML while still being easy to validate with JSON Schema. + +- Runtime policy: [SETTINGS.yaml](SETTINGS.yaml) +- Runtime schema: [schemas/agent-runtime.schema.json](schemas/agent-runtime.schema.json) +- Runtime spec: [spec/agent-runtime-v1.md](spec/agent-runtime-v1.md) +- Team/inventory spec: [spec/team-protocol-v1.md](spec/team-protocol-v1.md) + +The protocol is intentionally small in v1: + +- portable model tier and reasoning level +- filesystem access intent +- approval intent +- network access +- portable tool classes +- protected paths +- dangerous shell command prompts +- limited target-specific escape hatches + +Example: + +```yaml +version: 1 + +model: + class: balanced + reasoning: medium + +runtime: + filesystem: workspace-write + approval: guarded-auto + network_access: false + tools: + - shell + - read + - edit + - write + - glob + - grep + - web_fetch + - web_search + +safety: + protected_paths: + - ~/.ssh/** + - ~/.aws/** + - ~/.gnupg/** + - "**/.env*" + dangerous_shell_commands: + ask: + - rm * + - git reset --hard* + - sudo * +``` ## Model mapping diff --git a/flake.nix b/flake.nix index 3d58609..3b96dd7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,18 +1,176 @@ { inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - outputs = { nixpkgs, ... }: + outputs = { self, nixpkgs, ... }: let systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); - in { + in + { devShells = forAllSystems (pkgs: { - default = pkgs.mkShell { - packages = with pkgs; [ - yq-go - gettext - codex - ]; - }; + default = pkgs.mkShell { + packages = with pkgs; [ + yq-go + gettext + just + ]; + }; + }); + + apps = forAllSystems (pkgs: let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ pyyaml jsonschema ]); + runtimeInputs = with pkgs; [ + bash + yq-go + gettext + pythonEnv + ]; + bashBin = "${pkgs.bash}/bin/bash"; + + validateCmd = '' + # Script syntax checks + ${bashBin} -n ./generate.sh + ${bashBin} -n ./install.sh + + # Protocol file presence checks + test -f ./SETTINGS.yaml + test -f ./TEAM.yaml + test -f ./schemas/agent-runtime.schema.json + test -f ./schemas/team.schema.json + + # Basic protocol shape checks + yq -e '.version == 1' ./SETTINGS.yaml + yq -e '.version == 1' ./TEAM.yaml + yq -e '.agents.order | type == "!!seq"' ./TEAM.yaml + yq -e '.skills.order | type == "!!seq"' ./TEAM.yaml + yq -e '.rules.order | type == "!!seq"' ./TEAM.yaml + + # JSON Schema validation for protocol files + python <<'PY' + import json + from pathlib import Path + + import yaml + from jsonschema import validate + + root = Path(".") + settings_data = yaml.safe_load((root / "SETTINGS.yaml").read_text()) + team_data = yaml.safe_load((root / "TEAM.yaml").read_text()) + settings_schema = json.loads((root / "schemas/agent-runtime.schema.json").read_text()) + team_schema = json.loads((root / "schemas/team.schema.json").read_text()) + + validate(instance=settings_data, schema=settings_schema) + validate(instance=team_data, schema=team_schema) + PY + ''; + + mkAppScript = name: text: + pkgs.writeShellApplication { + inherit name runtimeInputs text; + }; + in { + build = { + type = "app"; + program = "${mkAppScript "build" '' + set -euo pipefail + test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; } + ${bashBin} ./generate.sh + ''}/bin/build"; + meta.description = "Generate Claude and Codex build artifacts from the authored protocol files."; + }; + + validate = { + type = "app"; + program = "${mkAppScript "validate" '' + set -euo pipefail + test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; } + ${validateCmd} + ''}/bin/validate"; + meta.description = "Validate scripts and protocol files."; + }; + + check = { + type = "app"; + program = "${mkAppScript "check" '' + set -euo pipefail + test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; } + ${validateCmd} + ${bashBin} ./generate.sh + ''}/bin/check"; + meta.description = "Run validation and generation together."; + }; + + install = { + type = "app"; + program = "${mkAppScript "install" '' + set -euo pipefail + test -f ./install.sh || { echo "Run this command from the repository root."; exit 1; } + ${bashBin} ./install.sh + ''}/bin/install"; + meta.description = "Install generated artifacts into Claude and Codex config directories."; + }; + }); + + checks = forAllSystems (pkgs: let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ pyyaml jsonschema ]); + runtimeInputs = with pkgs; [ + bash + yq-go + gettext + pythonEnv + ]; + bashBin = "${pkgs.bash}/bin/bash"; + + validateCmd = '' + ${bashBin} -n ./generate.sh + ${bashBin} -n ./install.sh + test -f ./SETTINGS.yaml + test -f ./TEAM.yaml + test -f ./schemas/agent-runtime.schema.json + test -f ./schemas/team.schema.json + yq -e '.version == 1' ./SETTINGS.yaml + yq -e '.version == 1' ./TEAM.yaml + yq -e '.agents.order | type == "!!seq"' ./TEAM.yaml + yq -e '.skills.order | type == "!!seq"' ./TEAM.yaml + yq -e '.rules.order | type == "!!seq"' ./TEAM.yaml + + python <<'PY' + import json + from pathlib import Path + + import yaml + from jsonschema import validate + + root = Path(".") + settings_data = yaml.safe_load((root / "SETTINGS.yaml").read_text()) + team_data = yaml.safe_load((root / "TEAM.yaml").read_text()) + settings_schema = json.loads((root / "schemas/agent-runtime.schema.json").read_text()) + team_schema = json.loads((root / "schemas/team.schema.json").read_text()) + + validate(instance=settings_data, schema=settings_schema) + validate(instance=team_data, schema=team_schema) + PY + ''; + + mkCheck = name: text: + pkgs.runCommand name { nativeBuildInputs = runtimeInputs; src = ./.; } '' + mkdir -p "$TMPDIR/repo" + cp -R "$src"/. "$TMPDIR/repo" + chmod -R u+w "$TMPDIR/repo" + cd "$TMPDIR/repo" + ${text} + touch "$out" + ''; + in { + validate = mkCheck "agent-team-validate-check" '' + set -euxo pipefail + ${validateCmd} + ''; + + build = mkCheck "agent-team-build-check" '' + set -euxo pipefail + ${validateCmd} + ${bashBin} ./generate.sh + ''; }); }; } diff --git a/justfile b/justfile new file mode 100644 index 0000000..756834e --- /dev/null +++ b/justfile @@ -0,0 +1,21 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +default: help + +help: + @just --list + +validate: + nix run .#validate + +build: + nix run .#build + +check: + nix run .#check + +install: + nix run .#install + +clean: + rm -rf settings.json claude codex