3 Commits
Author SHA1 Message Date
Gabriel Brown 48e5ea9a39 Let agents invoke the ticket skill, like its create-ticket and review-ticket siblings 2026-10-22 18:24:30 -04:00
Gabriel Brown ffad7e56ea Give the Mac mini a zsh config with the Panama prompt and SSH tmux 2026-10-12 08:06:20 -04:00
Gabriel Brown 34bc1663ed Give opencode the same agent instructions, rules and MCP servers as Claude Code
opencode already reads skills from ~/.claude/skills and ~/.agents/skills, so
those needed nothing. The rest did.

The manifest now links agents/AGENTS.md to ~/.config/opencode/AGENTS.md as
well, beside the Claude Code and Codex names.

opencode keeps its MCP servers in its own JSON config, with no file to point
a symlink at, so link-mcp now writes them there too. It builds one entry per
row of user/agents/mcp/servers with the same tokens it gives Claude Code,
and adds ~/.claude/rules/*.md under instructions, since opencode does not
read that directory by itself. Only those two keys change. The file is
written private and swapped in whole because it now holds tokens, and a
config with JSONC comments is left alone with a warning rather than
rewritten without them.
2026-10-07 01:06:44 -04:00
6 changed files with 124 additions and 19 deletions

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env zsh
# The Mac mini's shell. macOS stays on zsh: its bash is 3.2, and
# config/bash/shell leans on Linux-only tools (stat -c, ksshaskpass, the nvm
# profile script), so this is a small zsh counterpart rather than a port.
# Linked to ~/.zshrc. Homebrew's PATH comes from ~/.zprofile (brew shellenv).
export PANAMA_PATH="$HOME/.local/share/Panama"
export EDITOR="nvim"
export SUDO_EDITOR="$EDITOR"
export BUN_INSTALL="$HOME/.bun"
export PATH="$HOME/.local/bin:$BUN_INSTALL/bin:$PANAMA_PATH/bin:$PATH"
# History shared across sessions, so tmux panes see each other's commands.
HISTFILE="$HOME/.zsh_history"
HISTSIZE=50000
SAVEHIST=50000
setopt SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE
# Emacs keys, like bash's readline: ctrl-a/e/r work as they do on Linux.
bindkey -e
autoload -Uz compinit && compinit
[ -s "$BUN_INSTALL/_bun" ] && source "$BUN_INSTALL/_bun"
# Same SSH behaviour as config/bash/shell: an SSH login lands in a tmux
# session that survives a dropped connection. PANAMA_SSH_TMUX=off opts out.
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && -o interactive &&
"${PANAMA_SSH_TMUX:-on}" != "off" ]] && command -v tmux >/dev/null 2>&1; then
exec tmux new-session -A -s main
fi
alias c="clear"
alias n="nvim"
alias n.="nvim ."
alias ..="cd .."
alias ...="cd ../.."
alias ....="cd ../../.."
alias letscode="ranger ~/Documents/Documents/Code"
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
alias ls='eza -lh --group-directories-first --icons=auto'
alias lsa='ls -a'
alias lt='eza --tree --level=2 --long --icons=auto --git'
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; }
alias decompress="tar -xzf"
# Xcode cannot sign from an SSH session while the login keychain is locked
# (errSecInternalComponent). Run this once per session before a build.
alias unlock-keychain='security unlock-keychain ~/Library/Keychains/login.keychain-db'
eval "$(zoxide init zsh)"
source <(fzf --zsh)
eval "$(oh-my-posh init zsh --config "$PANAMA_PATH/config/dot/ohmyposh/gib.omp.json")"
+64 -9
View File
@@ -18,9 +18,15 @@
# This is personal content, so it obeys the same interview decision link-user # This is personal content, so it obeys the same interview decision link-user
# does. A machine that never said yes registers nothing. # does. A machine that never said yes registers nothing.
# #
# Only Claude Code is handled. Codex stores its servers inside config.toml, and # Claude Code and opencode are handled. Codex stores its servers inside
# rewriting a section of somebody's live TOML is a worse failure mode than # config.toml, and rewriting a section of somebody's live TOML is a worse
# leaving two lines for them to paste once. # failure mode than leaving two lines for them to paste once.
#
# opencode has no CLI for this either, but its config is plain JSON, so this
# stage merges two keys into it with jq: one mcp entry per server row, and the
# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md
# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in
# that file is left as it was.
set -euo pipefail set -euo pipefail
@@ -44,8 +50,12 @@ if [[ "$decision" != "yes" ]]; then
exit 0 exit 0
fi fi
if ! command -v claude >/dev/null 2>&1; then has_claude=false
log "Claude Code is not installed; nothing to register." has_opencode=false
command -v claude >/dev/null 2>&1 && has_claude=true
command -v opencode >/dev/null 2>&1 && has_opencode=true
if ! $has_claude && ! $has_opencode; then
log "Neither Claude Code nor opencode is installed; nothing to register."
exit 0 exit 0
fi fi
@@ -62,6 +72,8 @@ fi
registered=0 registered=0
skipped=0 skipped=0
# The opencode mcp object, built up one server at a time.
opencode_mcp='{}'
while read -r name transport target token_var _rest; do while read -r name transport target token_var _rest; do
case "${name:-}" in ''|'#'*) continue ;; esac case "${name:-}" in ''|'#'*) continue ;; esac
@@ -103,6 +115,20 @@ while read -r name transport target token_var _rest; do
fi fi
fi fi
auth=""
[[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"
if [[ "$transport" == stdio ]]; then
entry="$(jq -cn --arg c "$launcher" --arg t "$token" \
'{type: "local", command: [$c]} + (if $t == "" then {} else {environment: {MCP_TOKEN: $t}} end)')"
else
entry="$(jq -cn --arg u "$url" --arg a "$auth" \
'{type: "remote", url: $u} + (if $a == "" then {} else {headers: {Authorization: $a}} end)')"
fi
opencode_mcp="$(jq -c --arg n "$name" --argjson e "$entry" '.[$n] = $e' <<<"$opencode_mcp")"
$has_claude || continue
# A server already registered exactly as this row would register it is left # A server already registered exactly as this row would register it is left
# alone. Removing and re-adding it would throw away the OAuth session of a # alone. Removing and re-adding it would throw away the OAuth session of a
# server that signs in on its own (bill-tracker, atlassian), so every # server that signs in on its own (bill-tracker, atlassian), so every
@@ -114,8 +140,6 @@ while read -r name transport target token_var _rest; do
'.mcpServers[$n] // {} | {command, env: (if (.env // {}) == {} then null else .env end)}' \ '.mcpServers[$n] // {} | {command, env: (if (.env // {}) == {} then null else .env end)}' \
"$HOME/.claude.json" 2>/dev/null || true)" "$HOME/.claude.json" 2>/dev/null || true)"
else else
auth=""
[[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"
want="$(jq -cn --arg u "$url" --arg a "$auth" \ want="$(jq -cn --arg u "$url" --arg a "$auth" \
'{url: $u, auth: (if $a == "" then null else $a end)}')" '{url: $u, auth: (if $a == "" then null else $a end)}')"
have="$(jq -c --arg n "$name" \ have="$(jq -c --arg n "$name" \
@@ -146,5 +170,36 @@ while read -r name transport target token_var _rest; do
fi fi
done <"$SERVERS" done <"$SERVERS"
log "MCP servers: $registered registered, $skipped skipped." if $has_claude; then
log "Claude Code loads them at start, so restart a running session to pick them up." log "MCP servers: $registered registered, $skipped skipped."
log "Claude Code loads them at start, so restart a running session to pick them up."
fi
$has_opencode || exit 0
# opencode reads opencode.jsonc or opencode.json, whichever this machine has.
# The file holds tokens once this runs, so it is written private and swapped in
# whole, the way ~/.claude.json already holds them for Claude Code.
OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
opencode_config="$OPENCODE_DIR/opencode.jsonc"
[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json"
rules="$HOME/.claude/rules/*.md"
mkdir -p "$OPENCODE_DIR"
current='{"$schema": "https://opencode.ai/config.json"}'
[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")"
# jq cannot read JSONC comments. A file that has them is left alone rather
# than rewritten without them.
if ! merged="$(jq --argjson mcp "$opencode_mcp" --arg rules "$rules" \
'.mcp = ((.mcp // {}) + $mcp) | .instructions = (((.instructions // []) - [$rules]) + [$rules])' \
<<<"$current" 2>/dev/null)"; then
warn "Could not parse $opencode_config as JSON; opencode MCP servers not written."
exit 0
fi
tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")"
chmod 600 "$tmp"
printf '%s\n' "$merged" >"$tmp"
mv "$tmp" "$opencode_config"
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config."
+3 -3
View File
@@ -12,9 +12,9 @@ and one file says where each piece goes.
| Path | Goes to | Why | | Path | Goes to | Why |
| --- | --- | --- | | --- | --- | --- |
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` | Two tools, two names, one file. These were byte-identical copies before this, waiting to disagree. | | `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.config/opencode/AGENTS.md` | Three tools, one file. These were byte-identical copies before this, waiting to disagree. |
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. | | `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. opencode reads both directories itself. |
| `agents/rules/` | `~/.claude/rules` | | | `agents/rules/` | `~/.claude/rules` | opencode loads these through `instructions` in its config, which `setup/scripts/link-mcp` writes. |
| `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. | | `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. |
| `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. | | `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. |
-1
View File
@@ -1,7 +1,6 @@
--- ---
name: ticket name: ticket
description: End-to-end Jira ticket workflow — fetch a ticket into a live ticket page under .claude/docs/epics/, plan it, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket. description: End-to-end Jira ticket workflow — fetch a ticket into a live ticket page under .claude/docs/epics/, plan it, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
disable-model-invocation: true
--- ---
# Ticket Workflow # Ticket Workflow
@@ -1,7 +1,3 @@
interface: interface:
display_name: Ticket Workflow display_name: Ticket Workflow
short_description: End-to-end Jira ticket workflow: fetch, plan, implement, review, MR doc. short_description: End-to-end Jira ticket workflow: fetch, plan, implement, review, MR doc.
policy:
# Pairs with `disable-model-invocation: true` in SKILL.md. Keep the two in sync:
# this skill is user-invoked (`/ticket KACP-11111`) in both harnesses or neither.
allow_implicit_invocation: false
+3 -2
View File
@@ -23,10 +23,11 @@
# Gabriel's agent instructions on their machine. # Gabriel's agent instructions on their machine.
# ── Agents ─────────────────────────────────────────────────────────────────── # ── Agents ───────────────────────────────────────────────────────────────────
# One file, two names. Claude Code reads CLAUDE.md and Codex reads AGENTS.md, # One file, three names. Claude Code reads CLAUDE.md, Codex and opencode read
# and before this they were two byte-identical copies waiting to disagree. # AGENTS.md, and before this they were byte-identical copies waiting to disagree.
link agents/AGENTS.md ~/.claude/CLAUDE.md link agents/AGENTS.md ~/.claude/CLAUDE.md
link agents/AGENTS.md ~/.codex/AGENTS.md link agents/AGENTS.md ~/.codex/AGENTS.md
link agents/AGENTS.md ~/.config/opencode/AGENTS.md
# Skills, at both paths that look for them. Each tracked skill is linked from # Skills, at both paths that look for them. Each tracked skill is linked from
# this checkout so edits stay shared and `panama update` offers to commit them. # this checkout so edits stay shared and `panama update` offers to commit them.