From 254d72cfd4013d32b6a6e3eb562ded4b90a12b4b Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Sun, 12 Apr 2026 23:05:31 -0400 Subject: [PATCH] added opencode --- .gitignore | 3 + TEAM.yaml | 11 ++ flake.nix | 11 +- generate.sh | 227 +++++++++++++++++++++++++++++++++++++++ install.sh | 29 +++++ justfile | 2 +- opencode/config.json | 33 ++++++ schemas/team.schema.json | 3 +- 8 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 opencode/config.json diff --git a/.gitignore b/.gitignore index 72c3fc4..345c24c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ Thumbs.db settings.json claude/ codex/ +opencode/agents/ +opencode/AGENTS.md +opencode/opencode.json .claude .codex .direnv diff --git a/TEAM.yaml b/TEAM.yaml index 3661de1..45aba57 100644 --- a/TEAM.yaml +++ b/TEAM.yaml @@ -229,6 +229,7 @@ skills: applies_to: - claude - codex + - opencode install_mode: shared message-schema: id: message-schema @@ -238,6 +239,7 @@ skills: applies_to: - claude - codex + - opencode install_mode: shared orchestrate: id: orchestrate @@ -247,6 +249,7 @@ skills: applies_to: - claude - codex + - opencode install_mode: shared qa-checklist: id: qa-checklist @@ -256,6 +259,7 @@ skills: applies_to: - claude - codex + - opencode install_mode: shared worker-protocol: id: worker-protocol @@ -265,6 +269,7 @@ skills: applies_to: - claude - codex + - opencode install_mode: shared rules: @@ -282,33 +287,39 @@ rules: applies_to: - claude - codex + - opencode 02-responses: id: 02-responses source_file: rules/02-responses.md applies_to: - claude - codex + - opencode 03-git: id: 03-git source_file: rules/03-git.md applies_to: - claude - codex + - opencode 04-tools: id: 04-tools source_file: rules/04-tools.md applies_to: - claude - codex + - opencode 05-verification: id: 05-verification source_file: rules/05-verification.md applies_to: - claude - codex + - opencode 07-research: id: 07-research source_file: rules/07-research.md applies_to: - claude - codex + - opencode diff --git a/flake.nix b/flake.nix index dbbf296..cf5272a 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ packages = with pkgs; [ yq-go gettext + jq just ]; }; @@ -22,6 +23,7 @@ bash yq-go gettext + jq pythonEnv ]; bashBin = "${pkgs.bash}/bin/bash"; @@ -44,6 +46,10 @@ yq -e '.skills.order | type == "!!seq"' ./TEAM.yaml yq -e '.rules.order | type == "!!seq"' ./TEAM.yaml + # OpenCode base config must exist and be valid JSON + test -f ./opencode/config.json + jq empty ./opencode/config.json + # JSON Schema validation for protocol files python <<'PY' import json @@ -91,7 +97,7 @@ 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."; + meta.description = "Generate Claude, Codex, and OpenCode build artifacts from the authored protocol files."; }; validate = { @@ -123,7 +129,7 @@ ${validateCmd} ${bashBin} ./install.sh ''}/bin/install"; - meta.description = "Install generated artifacts into Claude and Codex config directories."; + meta.description = "Install generated artifacts into Claude, Codex, and OpenCode config directories."; }; }); @@ -133,6 +139,7 @@ bash yq-go gettext + jq pythonEnv ]; bashBin = "${pkgs.bash}/bin/bash"; diff --git a/generate.sh b/generate.sh index 5b7c080..c25fd97 100755 --- a/generate.sh +++ b/generate.sh @@ -28,6 +28,10 @@ CLAUDE_AGENTS_DIR="$CLAUDE_DIR/agents" CODEX_DIR="$SCRIPT_DIR/codex" CODEX_AGENTS_DIR="$CODEX_DIR/agents" +OPENCODE_DIR="$SCRIPT_DIR/opencode" +OPENCODE_AGENTS_DIR="$OPENCODE_DIR/agents" +OPENCODE_BASE_CONFIG="$OPENCODE_DIR/config.json" + # --------------------------------------------------------------------------- # Template variable values per target (KEY=VALUE pairs) # --------------------------------------------------------------------------- @@ -43,6 +47,12 @@ CODEX_VARS=( "SEARCH_TOOLS=Search the codebase" ) +OPENCODE_VARS=( + "PLANS_DIR=plans" + "WEB_SEARCH=via web search" + "SEARCH_TOOLS=Search the codebase" +) + # --------------------------------------------------------------------------- # extract_body — extracts everything after the second --- (YAML frontmatter) # --------------------------------------------------------------------------- @@ -249,6 +259,101 @@ map_portable_tool_to_claude() { esac } +# --------------------------------------------------------------------------- +# map_model_to_opencode — all models map to the single local model +# --------------------------------------------------------------------------- +map_model_to_opencode() { + echo "llama.cpp/qwen3-coder:a3b" +} + +# --------------------------------------------------------------------------- +# map_effort_to_temperature — maps effort to temperature float +# --------------------------------------------------------------------------- +map_effort_to_temperature() { + local effort="$1" + case "$effort" in + max) echo "0.1" ;; + high) echo "0.2" ;; + medium) echo "0.3" ;; + low) echo "0.5" ;; + *) echo "0.3" ;; + esac +} + +# --------------------------------------------------------------------------- +# map_permission_mode_to_opencode_mode — maps permission mode to agent mode +# --------------------------------------------------------------------------- +map_permission_mode_to_opencode_mode() { + local permission_mode="$1" + case "$permission_mode" in + plan) echo "subagent" ;; + *) echo "primary" ;; + esac +} + +# --------------------------------------------------------------------------- +# generate_opencode_permission_block — emits YAML permission block for agent +# $1 = tools (comma-separated Claude tool names) +# $2 = disallowed_tools (comma-separated Claude tool names) +# $3 = permission_mode (plan/acceptEdits/"") +# --------------------------------------------------------------------------- +generate_opencode_permission_block() { + local tools="$1" + local disallowed_tools="$2" + local permission_mode="$3" + + local edit_perm="deny" + local bash_perm="deny" + local webfetch_perm="deny" + + if [ "$permission_mode" = "plan" ]; then + # Plan-mode agents: read-only, no edits, no bash + edit_perm="deny" + bash_perm="deny" + # Researchers/reviewers still need web access + if echo "$tools" | grep -qE '\bWebFetch\b|\bWebSearch\b'; then + webfetch_perm="allow" + fi + else + # Check edit permission + if echo "$tools" | grep -qE '\bWrite\b|\bEdit\b'; then + edit_perm="allow" + fi + if echo "$disallowed_tools" | grep -qE '\bWrite\b|\bEdit\b'; then + edit_perm="deny" + fi + + # Check bash permission + if echo "$tools" | grep -q '\bBash\b'; then + bash_perm="ask" + fi + if echo "$disallowed_tools" | grep -q '\bBash\b'; then + bash_perm="deny" + fi + + # Check web permission + if echo "$tools" | grep -qE '\bWebFetch\b|\bWebSearch\b'; then + webfetch_perm="allow" + fi + fi + + echo "permission:" + echo " edit: ${edit_perm}" + + if [ "$bash_perm" = "ask" ]; then + echo " bash:" + echo " \"*\": ask" + echo " \"git status\": allow" + echo " \"git diff *\": allow" + echo " \"git log *\": allow" + elif [ "$bash_perm" = "deny" ]; then + echo " bash:" + echo " \"*\": deny" + fi + + echo " webfetch: ${webfetch_perm}" +} + # --------------------------------------------------------------------------- # json_escape — escapes a string for JSON string literal output # --------------------------------------------------------------------------- @@ -712,12 +817,134 @@ TOML echo "Generated: $CODEX_DIR/config.toml" } +# --------------------------------------------------------------------------- +# generate_opencode — produces opencode/ output directory +# --------------------------------------------------------------------------- +generate_opencode() { + echo "" + echo "=== Generating OpenCode output ===" + + # Clean generated outputs only (preserve user-authored config.json) + rm -rf "$OPENCODE_AGENTS_DIR" + rm -f "$OPENCODE_DIR/AGENTS.md" + rm -f "$OPENCODE_DIR/opencode.json" + mkdir -p "$OPENCODE_AGENTS_DIR" + + # Symlink skills + if [ -L "$OPENCODE_DIR/skills" ]; then + rm "$OPENCODE_DIR/skills" + fi + ln -s ../skills "$OPENCODE_DIR/skills" + echo "Symlinked: $OPENCODE_DIR/skills -> ../skills" + + # Generate agent .md files with OpenCode frontmatter + local agent_id + while IFS= read -r agent_id; do + [ -n "$agent_id" ] || continue + + local name description model effort permission_mode + local src_file dst_file body expanded_body + local max_turns tools_csv disallowed_tools_csv + local opencode_model opencode_temperature opencode_mode opencode_steps + + 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")" + + src_file="$SCRIPT_DIR/$(yq -r ".agents.items.${agent_id}.instruction_file" "$TEAM_YAML")" + dst_file="$OPENCODE_AGENTS_DIR/${name}.md" + + body="$(extract_body "$src_file")" + expanded_body="$(expand_body "$body" "${OPENCODE_VARS[@]}")" + + # Map to OpenCode equivalents + opencode_model="$(map_model_to_opencode "$model")" + opencode_temperature="$(map_effort_to_temperature "${effort:-medium}")" + opencode_mode="$(map_permission_mode_to_opencode_mode "$permission_mode")" + opencode_steps="${max_turns:-25}" + + { + echo "---" + echo "description: '$(yaml_escape_single_quoted "$description")'" + echo "mode: ${opencode_mode}" + echo "model: ${opencode_model}" + echo "temperature: ${opencode_temperature}" + echo "steps: ${opencode_steps}" + generate_opencode_permission_block "$tools_csv" "$disallowed_tools_csv" "$permission_mode" + echo "---" + echo "" + echo "$expanded_body" + } > "$dst_file" + + echo "Generated: $dst_file" + done < <(yq -r '.agents.order[]' "$TEAM_YAML") + + # Generate AGENTS.md — concatenate TEAM-ordered rules for opencode target + echo "" + echo "Generating opencode/AGENTS.md..." + { + echo "# Agent Team Instructions" + echo "" + echo "Agent-team specific protocols live in skills (orchestrate, conventions, worker-protocol, qa-checklist, message-schema)." + 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 "opencode" || continue + rules_file="$SCRIPT_DIR/$(yq -r ".rules.items.${rule_id}.source_file" "$TEAM_YAML")" + echo "" + cat "$rules_file" + done < <(yq -r '.rules.order[]' "$TEAM_YAML") + } > "$OPENCODE_DIR/AGENTS.md" + echo "Generated: $OPENCODE_DIR/AGENTS.md" + + # Generate merged opencode.json — base config + generated overlay + echo "" + echo "Generating opencode/opencode.json..." + + if [ ! -f "$OPENCODE_BASE_CONFIG" ]; then + echo "Error: missing base config at $OPENCODE_BASE_CONFIG" + exit 1 + fi + + # Build the generated overlay with global permissions from SETTINGS.yaml + local overlay_json + overlay_json="$(cat <<'OVERLAY' +{ + "permission": { + "edit": "ask", + "bash": { + "*": "ask" + }, + "webfetch": "allow", + "skill": { + "*": "allow" + } + }, + "compaction": { + "auto": true, + "prune": true + }, + "snapshot": true +} +OVERLAY +)" + + jq -s '.[0] * .[1]' "$OPENCODE_BASE_CONFIG" <(echo "$overlay_json") > "$OPENCODE_DIR/opencode.json" + echo "Generated: $OPENCODE_DIR/opencode.json" +} + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- prepare_settings_json generate_claude generate_codex +generate_opencode echo "" echo "Done." diff --git a/install.sh b/install.sh index 33b2902..9cadb3f 100755 --- a/install.sh +++ b/install.sh @@ -302,3 +302,32 @@ if [ -d "$SCRIPT_DIR/codex" ]; then create_file_symlink "$SCRIPT_DIR/codex/config.toml" "$CODEX_DIR/config.toml" "codex config.toml" fi fi + +# OpenCode integration (optional — only if opencode/ output exists) +OPENCODE_CONFIG_DIR="$HOME/.config/opencode" + +if [ -d "$SCRIPT_DIR/opencode" ]; then + echo "" + echo "OpenCode output found — installing to $OPENCODE_CONFIG_DIR" + mkdir -p "$OPENCODE_CONFIG_DIR" + + # Skills: symlink each skill directory into ~/.config/opencode/skills/ + install_team_skills_for_target "opencode" "$OPENCODE_CONFIG_DIR/skills" "opencode" + + # Generated agents + if [ -d "$SCRIPT_DIR/opencode/agents" ]; then + create_symlink "$SCRIPT_DIR/opencode/agents" "$OPENCODE_CONFIG_DIR/agents" "opencode agents" + else + echo "Run ./generate.sh first to generate OpenCode agent definitions" + fi + + # Generated AGENTS.md + if [ -f "$SCRIPT_DIR/opencode/AGENTS.md" ]; then + create_file_symlink "$SCRIPT_DIR/opencode/AGENTS.md" "$OPENCODE_CONFIG_DIR/AGENTS.md" "opencode AGENTS.md" + fi + + # Merged config — the generated opencode.json becomes the installed config.json + if [ -f "$SCRIPT_DIR/opencode/opencode.json" ]; then + create_file_symlink "$SCRIPT_DIR/opencode/opencode.json" "$OPENCODE_CONFIG_DIR/config.json" "opencode config" + fi +fi diff --git a/justfile b/justfile index 756834e..7c320fc 100644 --- a/justfile +++ b/justfile @@ -18,4 +18,4 @@ install: nix run .#install clean: - rm -rf settings.json claude codex + rm -rf settings.json claude codex opencode/agents opencode/AGENTS.md opencode/opencode.json diff --git a/opencode/config.json b/opencode/config.json new file mode 100644 index 0000000..d5628ee --- /dev/null +++ b/opencode/config.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "llama.cpp": { + "npm": "@ai-sdk/openai-compatible", + "name": "llama-server (local)", + "options": { + "baseURL": "http://192.168.0.23:8000/v1" + }, + "models": { + "qwen3-coder:a3b": { + "name": "Qwen3-Coder-30B-A3B-Instruct-Q8" + } + } + } + }, + "mcp": { + "fetch": { + "type": "local", + "command": ["uvx", "mcp-server-fetch", "--ignore-robots-txt"], + "enabled": true + }, + "searxng": { + "type": "local", + "command": ["npx", "-y", "mcp-searxng"], + "environment": { + "SEARXNG_URL": "http://192.168.0.23:8080" + }, + "enabled": true + } + }, + "model": "llama.cpp/qwen3-coder:a3b" +} diff --git a/schemas/team.schema.json b/schemas/team.schema.json index bc51291..0d9668c 100644 --- a/schemas/team.schema.json +++ b/schemas/team.schema.json @@ -56,7 +56,8 @@ "type": "string", "enum": [ "claude", - "codex" + "codex", + "opencode" ] }, "agent_item": {