Generate opencode's config from a tracked template, with matching commands, agents and pwswitch

This commit is contained in:
Gabriel Brown committed 2026-10-30 13:44:29 -04:00
1 parent 68c7d6a3ea
commit a987cbab66
16 files changed
+531 -25

No files matched your search

+1 -1
View File
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
## Tests ## Tests
192 of them, under `tests/`. `tests/contracts.manifest` classifies every 193 of them, under `tests/`. `tests/contracts.manifest` classifies every
contract by the capabilities it needs. Run the hermetic set, or grant a contract by the capabilities it needs. Run the hermetic set, or grant a
specific external capability when automation needs it: specific external capability when automation needs it:
+59 -21
View File
@@ -23,10 +23,13 @@
# failure mode than 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 # 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 # stage writes it whole from user/agents/opencode/config.json -- the tracked
# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md # template carrying everything that is not a secret -- plus the MCP block built
# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in # here from the server rows. The whole file is generated rather than merged into
# that file is left as it was. # because the point of the template is that every machine ends up with the same
# opencode experience, and a merged file drifts the moment one machine is edited
# by hand. The output says so at the top, and anything worth changing goes in
# the template, where it reaches the other machines too.
set -euo pipefail set -euo pipefail
@@ -37,6 +40,8 @@ PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
MCP_DIR="$PANAMA_PATH/user/agents/mcp" MCP_DIR="$PANAMA_PATH/user/agents/mcp"
SERVERS="$MCP_DIR/servers" SERVERS="$MCP_DIR/servers"
ENV_FILE="$MCP_DIR/env" ENV_FILE="$MCP_DIR/env"
TEMPLATE="$PANAMA_PATH/user/agents/opencode/config.json"
PANAMA_OLD="$PANAMA_PATH/config/old"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
DECISION="$STATE_DIR/user-content" DECISION="$STATE_DIR/user-content"
@@ -177,29 +182,62 @@ fi
$has_opencode || exit 0 $has_opencode || exit 0
# opencode reads opencode.jsonc or opencode.json, whichever this machine has. # opencode reads opencode.jsonc. The file holds tokens once this runs, so it is
# The file holds tokens once this runs, so it is written private and swapped in # written private and swapped in whole, the way ~/.claude.json already holds
# whole, the way ~/.claude.json already holds them for Claude Code. # them for Claude Code.
OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode" OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
opencode_config="$OPENCODE_DIR/opencode.jsonc" opencode_config="$OPENCODE_DIR/opencode.jsonc"
[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json"
rules="$HOME/.claude/rules/*.md"
mkdir -p "$OPENCODE_DIR" [[ -r "$TEMPLATE" ]] || { warn "No $TEMPLATE; opencode config not written."; exit 0; }
current='{"$schema": "https://opencode.ai/config.json"}' if ! jq -e . "$TEMPLATE" >/dev/null 2>&1; then
[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")" warn "$TEMPLATE is not valid JSON; opencode config not written."
# 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 exit 0
fi fi
# Nothing at the destination is destroyed, the promise link-user and link-skills
# make. A file this stage did not write is somebody's own work and goes to
# config/old/ under a name that says where it came from. One carrying the marker
# was written by an earlier run and is replaced in place, so re-running never
# buries its own output and config/old/ does not grow on every upgrade.
MARKER='generated by setup/scripts/link-mcp'
displace() {
local destination="$1" backup
[[ -e "$destination" || -L "$destination" ]] || return 0
if [[ -L "$destination" ]]; then
rm -f "$destination"
return 0
fi
grep -qF "$MARKER" "$destination" 2>/dev/null && return 0
backup="$PANAMA_OLD/mcp-$(printf '%s' "${destination#"$HOME"/}" | tr '/' '-')"
[[ -e "$backup" ]] && backup="$backup.$(date +%s)"
mv "$destination" "$backup"
log "Moved existing $destination to $backup"
}
mkdir -p "$OPENCODE_DIR" "$PANAMA_OLD"
# Two names for one file would leave opencode choosing between them, and the one
# this stage does not write would win or lose on discovery order.
displace "${opencode_config%.jsonc}.json"
displace "$opencode_config"
# The template is portable prose: it names the rules directory as ~/ so it does
# not carry this machine's login. opencode resolves a leading ~/ on its own, but
# expanding it here keeps the generated file readable the way a person would
# have written it by hand when they went looking for where the path came from.
merged="$(jq --argjson mcp "$opencode_mcp" --arg home "$HOME" \
'.mcp = $mcp
| .instructions = [(.instructions // [])[]
| if startswith("~/") then $home + .[1:] else . end]' \
<<<"$(cat "$TEMPLATE")")"
tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")" tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")"
{
printf '// %s\n' "$MARKER"
printf '// Edit user/agents/opencode/config.json in the Panama checkout instead.\n'
printf '// This file is replaced whenever ./install or panama update runs.\n'
printf '%s\n' "$merged"
} >"$tmp"
chmod 600 "$tmp" chmod 600 "$tmp"
printf '%s\n' "$merged" >"$tmp"
mv "$tmp" "$opencode_config" mv "$tmp" "$opencode_config"
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config."
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the template written to $opencode_config."
+1
View File
@@ -270,6 +270,7 @@ hermetic tests/setup/launcher-search-contract
hermetic tests/setup/lid-contract hermetic tests/setup/lid-contract
hermetic tests/setup/migrations-contract hermetic tests/setup/migrations-contract
hermetic tests/setup/nautilus-extensions-contract hermetic tests/setup/nautilus-extensions-contract
hermetic tests/setup/opencode-config-contract
hermetic tests/setup/package-lists-contract hermetic tests/setup/package-lists-contract
# Provenance uses local signed fixtures and stubs every network/package operation. # Provenance uses local signed fixtures and stubs every network/package operation.
hermetic tests/setup/package-provenance-contract hermetic tests/setup/package-provenance-contract
@@ -59,7 +59,10 @@ SELF_INSTALLED='^(bun|claude|codex|node|npm|pnpm)$'
# fresh machine to keep five aliases company. Where docker is absent the # fresh machine to keep five aliases company. Where docker is absent the
# aliases fail by naming the missing command, which is the honest outcome. # aliases fail by naming the missing command, which is the honest outcome.
# Anything added here needs that same property: absence must be loud. # Anything added here needs that same property: absence must be loud.
OPTIONAL='^(docker)$' # opencode is the other kind of optional: link-mcp probes for it with
# `command -v` and writes its config only where someone installed it with
# opencode's own installer, the same way it treats claude.
OPTIONAL='^(docker|opencode)$'
# Provided by a package swap rather than a list entry. ffmpeg cannot be a list # Provided by a package swap rather than a list entry. ffmpeg cannot be a list
# entry: RPM Fusion's ffmpeg CONFLICTS with the ffmpeg-free that Fedora ships # entry: RPM Fusion's ffmpeg CONFLICTS with the ffmpeg-free that Fedora ships
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# The opencode config, generated rather than linked.
#
# Every other personal destination is a symlink or a one-time copy, so getting
# one wrong is visible and cheap to undo. This one is written whole by
# setup/scripts/link-mcp, and it is the only file in the set that carries
# secrets -- which is the reason it is generated at all. Three properties are
# worth pinning:
#
# 1. The tracked template is the source of truth and holds no secret. It is
# readable by anyone who finds the repository, so a token in it is a
# disclosure rather than a mistake.
# 2. The generation is faithful: the template's keys survive, the MCP block
# appears, and a leading ~/ is resolved so the output is readable.
# 3. Nothing is destroyed and nothing churns. A file somebody wrote by hand
# goes to config/old/, and running the stage again does not fill it up.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
linker="$repo_dir/setup/scripts/link-mcp"
template="$repo_dir/user/agents/opencode/config.json"
servers="$repo_dir/user/agents/mcp/servers"
findings=()
note() { findings+=("$1"); }
[[ -x "$linker" ]] || { printf 'opencode config contract: %s is not executable\n' "$linker" >&2; exit 1; }
# ── 1. The template holds no secret ──────────────────────────────────────────
if [[ ! -r "$template" ]]; then
note 'user/agents/opencode/config.json is missing, so nothing generates an opencode config'
else
jq -e . "$template" >/dev/null 2>&1 \
|| note 'user/agents/opencode/config.json is not valid JSON'
if grep -qE 'BEGIN [A-Z ]*PRIVATE KEY|sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]' "$template" 2>/dev/null; then
note 'user/agents/opencode/config.json carries something that looks like a token'
fi
fi
[[ -r "$servers" ]] || note 'user/agents/mcp/servers is missing, so no MCP servers can be registered'
# ── 2 and 3, against a throwaway home ────────────────────────────────────────
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
checkout="$work/Panama"
mkdir -p "$checkout/setup/scripts" "$checkout/user/agents/mcp/bin" "$checkout/user/agents/opencode"
cp "$linker" "$checkout/setup/scripts/link-mcp"
cp "$template" "$checkout/user/agents/opencode/config.json"
printf '#!/usr/bin/env bash\nexit 0\n' >"$checkout/user/agents/mcp/bin/gamma"
chmod +x "$checkout/user/agents/mcp/bin/gamma"
cat >"$checkout/user/agents/mcp/servers" <<'FIXTURE'
# a comment, then a row that needs a token, one that needs none, and a stdio one
alpha http https://alpha.example/mcp ALPHA_TOKEN
beta http https://beta.example/mcp
gamma stdio bin/gamma
FIXTURE
printf 'ALPHA_TOKEN=sekrit-token-value\n' >"$checkout/user/agents/mcp/env"
# link-mcp registers through whichever runtime it finds, and gives up entirely
# when it finds neither. A stub is enough to prove the config generation.
mkdir -p "$work/bin"
printf '#!/usr/bin/env bash\nexit 0\n' >"$work/bin/opencode"
chmod +x "$work/bin/opencode"
home="$work/home"
state="$work/state"
old="$checkout/config/old"
# Every XDG directory is pinned to the throwaway home. XDG_CONFIG_HOME is
# usually set in the environment this runs from, and leaving it alone would send
# the generated config at the real ~/.config/opencode rather than at the fixture.
mkdir -p "$home/.config/opencode" "$state/panama"
run() {
HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_STATE_HOME="$state" \
PANAMA_PATH="$checkout" PATH="$work/bin:$PATH" \
"$checkout/setup/scripts/link-mcp" >"$work/log" 2>&1
}
generated="$home/.config/opencode/opencode.jsonc"
# A file somebody wrote by hand is there before this stage ever runs.
printf '{"model": "someone-elses-hand-edit"}\n' >"$generated"
printf 'yes\n' >"$state/panama/user-content"
if ! run; then
note "link-mcp failed against a throwaway home: $(tail -1 "$work/log")"
fi
# ── The hand-edit is preserved, not deleted ──────────────────────────────────
if ! grep -rq 'someone-elses-hand-edit' "$old" 2>/dev/null; then
note 'a config written by hand was not preserved in config/old/'
fi
# ── The generation is faithful ───────────────────────────────────────────────
body="$(sed '/^\/\//d' "$generated" 2>/dev/null)"
jq -e . <<<"$body" >/dev/null 2>&1 \
|| note 'the generated opencode config is not valid JSON once its header comment is stripped'
grep -qF 'generated by setup/scripts/link-mcp' "$generated" 2>/dev/null \
|| note 'the generated opencode config does not say who wrote it or where to edit instead'
if ! jq -e --arg home "$home" '.instructions | index($home + "/.claude/rules/*.md")' <<<"$body" >/dev/null 2>&1; then
note 'the generated opencode config did not resolve the ~/ rules path to this home'
fi
jq -e '.permissions | length > 0' <<<"$body" >/dev/null 2>&1 \
|| note 'the template permissions did not reach the generated opencode config'
jq -e '.mcp.alpha.headers.Authorization == "sekrit-token-value"' <<<"$body" >/dev/null 2>&1 \
|| note 'a token-carrying server was registered without its Authorization header'
jq -e '.mcp.beta | has("headers") | not' <<<"$body" >/dev/null 2>&1 \
|| note 'a server that needs no token was given a header anyway'
jq -e --arg launcher "$checkout/user/agents/mcp/bin/gamma" '.mcp.gamma.command == [$launcher]' <<<"$body" >/dev/null 2>&1 \
|| note 'a stdio server was not registered against its launcher in this checkout'
# The file holds tokens, so it is private. Checked on the file rather than on
# the umask, because the umask is not what a later reader sees.
mode="$(stat -c '%a' "$generated" 2>/dev/null)"
[[ "$mode" == 600 ]] || note "the generated opencode config is mode $mode, not 600"
# The template is readable by anybody who finds the repository. The tokens live
# in the generated file and in user/agents/mcp/env, never here.
if grep -qF 'sekrit-token-value' "$template" 2>/dev/null; then
note 'the tracked opencode template carries an MCP token'
fi
# ── Re-running replaces, and does not churn ──────────────────────────────────
before="$(find "$old" -type f 2>/dev/null | wc -l)"
run
after="$(find "$old" -type f 2>/dev/null | wc -l)"
[[ "$before" == "$after" ]] \
|| note 're-running link-mcp buried its own output in config/old/, so it grows every upgrade'
grep -qF 'generated by setup/scripts/link-mcp' "$generated" 2>/dev/null \
|| note 're-running link-mcp did not rewrite the config it generates'
# A machine that never said yes is left alone, the same gate every other
# personal destination obeys.
rm -f "$generated"
printf 'no\n' >"$state/panama/user-content"
run
[[ ! -e "$generated" ]] \
|| note 'an opencode config was generated on a machine that never said yes'
if (( ${#findings[@]} > 0 )); then
printf 'opencode config contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'opencode config contract: PASS\n'
+22 -1
View File
@@ -14,8 +14,12 @@ and one file says where each piece goes.
| --- | --- | --- | | --- | --- | --- |
| `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/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. opencode reads both directories itself. | | `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` | opencode loads these through `instructions` in its config, which `setup/scripts/link-mcp` writes. | | `agents/rules/` | `~/.claude/rules` | opencode loads these through `instructions` in its config, which `setup/scripts/link-mcp` generates. |
| `agents/commands/` | `~/.config/opencode/commands` | Claude Code carries its commands as plugins, which opencode cannot read. These are the same commands in opencode's own format. |
| `agents/agents/` | `~/.config/opencode/agents` | The same again for subagents. |
| `agents/opencode/config.json` | read by `setup/scripts/link-mcp` | Not linked. The opencode config is generated from it, because that file also has to carry the MCP servers and their tokens, and a symlink cannot be half written. |
| `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. |
| `bin/pwswitch` | `~/.local/bin/pwswitch` | `pwswitch off` makes sudo passwordless for an agent session, `pwswitch on` restores the password. |
| `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. |
`manifest` is the authority; this table is a summary of it. `manifest` is the authority; this table is a summary of it.
@@ -44,6 +48,23 @@ Two consequences, recorded rather than fixed:
A tool that installs directly into a home skill directory creates a A tool that installs directly into a home skill directory creates a
machine-local skill until it is moved into the checkout. machine-local skill until it is moved into the checkout.
## What link-mcp generates
The opencode config is the one destination here that is generated rather than linked or
copied. It cannot be a symlink because it has to carry the MCP servers and their tokens,
and a token is exactly what cannot live in this repository. So `setup/scripts/link-mcp`
builds the whole file: `agents/opencode/config.json` supplies everything that is not a
secret, `agents/mcp/servers` supplies the server list, and the machine-local
`agents/mcp/env` supplies the tokens.
The result is `~/.config/opencode/opencode.jsonc`, written private and replaced in
place. It opens with a comment saying where to edit it, and that comment is the whole
warning: **hand-edits to the generated file do not survive an upgrade.** Put the change in
`agents/opencode/config.json` and it reaches every machine.
Anything already at that path that this stage did not write moves to `config/old/`, the
same promise everything else here makes.
## It is off unless you say yes ## It is off unless you say yes
The installer asks, naming the destinations, and the default is no. Nothing here The installer asks, naming the destinations, and the default is no. Nothing here
+64 -1
View File
@@ -105,6 +105,68 @@ and record who decided what, because that history is useful.
- When it comes to personal projects, I am much less strict, but I prefer a title that makes it very clear what the MR is about. - When it comes to personal projects, I am much less strict, but I prefer a title that makes it very clear what the MR is about.
- No confusing or complicated language. - No confusing or complicated language.
---
# Operating context
Where things live, what is sensitive, and who may see it. This used to sit in one tool's
settings file, so only whichever agent read that file ever knew it.
## Source control and hosting
- I keep my own projects on `git.gbrown.org`, my self-hosted Git host. Ksense work lives in
the client's own GitLab, and that is where the merge requests go.
- `agentchat.gbrown.org` is my agent-to-agent message hub. It is personal infrastructure,
and it is not a place to publish anything sensitive outside the trust boundary.
- I have no internal package registry and no trusted cloud buckets. Nothing gets uploaded
to a bucket.
## Secrets
- Infisical carries staging and development secrets.
- `JIRA_CREDENTIALS` comes from `~/.bashrc`. Jira ticket work always uses my own
credentials, never the Jira credentials staging Infisical carries.
- `.claude/docs/env/prod/.env` and `.claude/docs/env/ci/.env` are read-only, and every
read needs explicit permission. Ask every time, even when I approved the same read
earlier in the same session.
## Sensitive locations
These are mine alone plus the cleared Ksense coworkers named at the end of this file, and
only at a "named and specific" bar, meaning I say which file and why. They never go to a
paste site, a snippet host, or an external tool:
- Jira credentials and the prod/CI env files above.
- `~/.infisical/secrets-backup/`.
- `~/.config/context7/credentials.json`.
- `~/.claude/.credentials.json`.
- Client project env files, such as `~/Documents/Code/GibSend/.env*` and
`~/Documents/Code/StPeteIT/.env*`.
## Network
My personal homelab. Everything under `gbrown.org` is inside the trust boundary:
`plex.`, `git.`, `chat.`, `radarr.`, `immich.`, `jellyfin.`, `home.`, and
`agentchat.`. Local hosts are `macnano.gib` and `nano.gib`, and the LAN is
`192.168.1.2` and `192.168.2.2`. The NanoKVM boards on those two hosts give an agent
console access to the machine, including what SSH cannot reach, so treat that access as
root-equivalent.
## Targets that need my explicit say-so
- Any host, namespace, or resource where `prod` or `production` is a whole word or a whole
name segment. Command Center production is the one I care about most.
- IAM, RBAC, networking, quota, and node-pool resources in any IaC, and anything whose
name or tag carries `prod` or `production`.
## CLIs I expect you to know
- `ctx7` for current library documentation.
- `infisical` for secrets.
- `claude`, `clauded`, and `claudedr` are Claude Code wrappers.
---
# Coworkers & Others that you will be working with & Interacting with when working on Ksense Projects # Coworkers & Others that you will be working with & Interacting with when working on Ksense Projects
- Conrad Rohleder - Project Manager - Conrad Rohleder - Project Manager
@@ -115,7 +177,8 @@ and record who decided what, because that history is useful.
- For example, he once asked for a JSON blob of our schema so he could better understand it. - For example, he once asked for a JSON blob of our schema so he could better understand it.
- Conrad is awesome & is great to work with. - Conrad is awesome & is great to work with.
- He is somewhat skeptical of AI & its ability to complete work. - He is somewhat skeptical of AI & its ability to complete work.
- Henry Nguyen - Tech Owner (Tech Lead) - Henry Nguyen - Tech Owner (Tech Lead) for Command Center only
- Henry is only involved in Command Center. On other projects (for example 360 Shop View), don't route reviews or sign-offs through him.
- Henry is who I interact with second most, just behind Conrad. - Henry is who I interact with second most, just behind Conrad.
- Henry reviews all of my code & up until recently, would also write all the dev reviews for all the stories I completed as well. Nowadays, I write them myself & he reviews those too. - Henry reviews all of my code & up until recently, would also write all the dev reviews for all the stories I completed as well. Nowadays, I write them myself & he reviews those too.
- Henry likes work to be very considerate & he always prefers solutions that result in 0 downtime. - Henry likes work to be very considerate & he always prefers solutions that result in 0 downtime.
+35
View File
@@ -0,0 +1,35 @@
---
description: Simplify recently changed code for clarity while leaving behaviour identical
mode: subagent
---
You simplify code. You never change what it does, only how it does it.
Work only on code changed in this session, or on the specific files you are pointed at.
Anything wider is out of scope unless you are asked for it.
What you do:
- Cut complexity that is not earning its keep: nesting that flattens, abstractions with one
caller, names that need a comment to be understood, values threaded through layers that
never use them.
- Keep the behaviour exactly as it was. Same outputs, same errors, same edge cases, same
failure modes.
- Follow the guidance that governs the code you are touching. Where that guidance and your
own instinct disagree, the guidance wins.
- Prefer explicit over compact. A clear line beats a clever one, and a name that says what
something is beats a shorter name that does not.
- Delete comments that restate the code. Keep comments that explain a reason, and update
them when the code they describe changed.
What you do not do:
- You do not refactor beyond the change. Renaming a public API, reshuffling a module, or
tidying a file you were not asked about is somebody else's job.
- You do not merge concerns into one expression to save a line. Nested ternaries and dense
one-liners are not simplification.
- You do not remove an abstraction just because it has one caller today.
- You do not add tests, types, or documentation as a side effect of simplifying.
Report what you changed and why, briefly. When a simplification you considered would have
changed behaviour, say that you left it alone and why.
+38
View File
@@ -0,0 +1,38 @@
---
description: Review a diff or merge request for real bugs and missed requirements
---
Review the change for real problems. Do not edit anything.
Target: $ARGUMENTS
When the target names a merge request or pull request, review that change. When it names a
file or directory, review what is uncommitted there. When it is empty, review the current
working diff.
How to review:
1. Read the change, then read the code around it. Context you did not read is context you
will misjudge.
2. Look for real problems only: logic that is wrong, a case that was missed, a contract
broken, data lost, an error swallowed, a race introduced, something the change claims to
do that it does not do.
3. Check the change against whatever guidance governs the code it touches. Guidance is
guidance for writing code, so not all of it applies to a review.
4. Read the git history of the lines being modified. A change that looks fine in isolation
often breaks a reason the code is the way it is.
5. Leave these out: style, naming, formatting, import order, missing tests, missing
documentation, and anything a linter, typechecker, or compiler will catch. CI runs those.
Also leave out problems on lines the change did not touch.
Output:
- Order findings by severity, highest first.
- Each finding gets the file and line, what is wrong, and why it matters. Link to code
rather than quoting long stretches of it.
- Say which requirement or guideline is broken when that is the reason.
- When a finding is a judgement call rather than a definite bug, say so plainly instead of
dressing it up as certain.
- If there is nothing worth reporting, say that and stop. Do not invent findings to fill
the space.
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Playwright's MCP server, for driving a real browser: pages, screenshots,
# forms, end-to-end runs. This is what the ticket skill's proof screenshots are
# captured with, so it belongs on every machine rather than only where a Claude
# Code plugin happened to be installed.
#
# link-mcp registers this file as a stdio server. It takes no token, so its
# row in user/agents/mcp/servers names no variable and nothing arrives in
# MCP_TOKEN.
set -euo pipefail
exec npx -y @playwright/mcp@latest
+5
View File
@@ -38,6 +38,11 @@ penpot http https://penpot.gbrown.org/mcp/stream?userToken={TOKEN} PE
# The hosted mcp.sentry.dev only speaks to sentry.io. # The hosted mcp.sentry.dev only speaks to sentry.io.
sentry-gbrown stdio bin/sentry-gbrown SENTRY_GBROWN_TOKEN sentry-gbrown stdio bin/sentry-gbrown SENTRY_GBROWN_TOKEN
# Playwright's MCP server. Browser automation: pages, screenshots, forms,
# end-to-end runs. The ticket skill captures its proof screenshots this way.
# No token, hence no variable in the last column.
playwright stdio bin/playwright
# T3 Code environments, one per machine. Agents read projects and threads and, # T3 Code environments, one per machine. Agents read projects and threads and,
# with more than read-only access, start and message threads there. No token: # with more than read-only access, start and message threads there. No token:
# each is an OAuth server that asks for a pairing code from that machine on # each is an OAuth server that asks for a pairing code from that machine on
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://opencode.ai/config.json",
"instructions": ["~/.claude/rules/*.md"],
"permissions": [
{ "action": "read", "resource": "/etc/**", "effect": "allow" },
{ "action": "shell", "resource": "coredumpctl list*", "effect": "allow" },
{ "action": "shell", "resource": "coredumpctl info*", "effect": "allow" },
{ "action": "shell", "resource": "journalctl*", "effect": "allow" },
{ "action": "shell", "resource": "rpm -q*", "effect": "allow" },
{ "action": "shell", "resource": "panama doctor*", "effect": "allow" },
{ "action": "shell", "resource": "lspci*", "effect": "allow" },
{ "action": "shell", "resource": "lsusb*", "effect": "allow" },
{ "action": "shell", "resource": "getent hosts*", "effect": "allow" }
]
}
@@ -0,0 +1,71 @@
---
name: frontend-design
description: Make deliberate visual design choices for new UI or a reshaping of an existing one, and run the mock-then-decide workflow before touching real components.
---
# Frontend Design
Distinctive UI comes from decisions made on purpose. Templated UI comes from decisions made
by habit. This is about catching the second kind before it ships.
## Start from the thing itself
If the brief does not say what the product is, who it is for, and what the screen is
supposed to accomplish, work that out first and confirm it before designing anything. A
dashboard for people reconciling invoices and a booking page for a surf school want
different palettes, different type, different density, and pretending otherwise is how you
end up with the same page twice.
Name the subject in one sentence, name the audience in one, name the job of the screen in
one. Design against those three sentences. When a choice cannot be justified from them, it
is decoration.
## Choices to make deliberately
**Type carries the personality.** Pick the family on purpose rather than reaching for the
usual one. Decide a scale up front and hold to it, so a heading is a heading everywhere and
not a heading in one place and a slightly larger paragraph in another. You rarely need more
than two families.
**Colour is a system, not a pick.** Name the roles before the values: what is background,
what is surface, what is text, what is the one accent. The accent is the only one that gets
to be loud, and it earns that by being used sparingly.
**Structure is information.** Borders, rules, labels, numbering, eyebrows and dividers
should be telling the reader something about the content. If a device is only there to fill
a gap, it is ornament. Numbered markers in particular only make sense when the content is
genuinely a sequence.
**Motion answers the user.** Motion that reacts to what somebody did is useful, because it
shows what changed. Motion that runs on its own is filling space. Do not use continuously
repainting effects such as pulse, shimmer, blur or spinners. They peg the GPU on high
refresh displays and read as generated.
## Tells to avoid
These are the shapes that show up again and again in generated UI, and they are the fastest
way to make a page look like nobody chose it:
- A cream background with a serif display face and a warm terracotta accent.
- A near-black background with one acid green or vermilion accent.
- A headline with exactly one word italicised, bolded or coloured.
- All-caps labels used as decoration rather than because the label is short and needs it.
- A label above every block of content, naming what the content already makes obvious.
- The same fade-and-slide entrance applied to every section, and a hover lift on every card.
- A hero that is a big number, a small label and a gradient, when the subject has something
more characteristic to show.
## Workflow
This is the rule from my preferences, spelled out:
1. Build several genuinely distinct directions, not three variations of one idea. Different
palettes, different type, different structure. If two mocks would be described the same
way in one sentence, they are the same mock.
2. Build them as static pages. Real content, not grey boxes labelled "image here", because
the content is where most of the design decisions turn out to matter.
3. Publish them with a simple http server, report the URL, and stop.
4. Wait for a decision before implementing anything in real components.
Do not edit real components first. Do not keep polishing past the point where a choice
could be made, and do not pick a winner yourself and build it.
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Toggle passwordless sudo for the current user.
# pwswitch on -> sudo asks for a password again (default behavior)
# pwswitch off -> sudo runs without a password
# Works by adding/removing a drop-in file in /etc/sudoers.d.
set -euo pipefail
user="${SUDO_USER:-$USER}"
# "zz-" prefix so it sorts last and overrides earlier rules.
dropin="/etc/sudoers.d/zz-passwordswitch-${user}"
case "${1:-}" in
off)
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
echo "${user} ALL=(ALL) NOPASSWD: ALL" >"$tmp"
# Validate before installing so a bad file can never lock sudo.
sudo visudo -cqf "$tmp"
sudo install -m 0440 -o root -g root "$tmp" "$dropin"
echo "Passwordless sudo ENABLED for ${user}. Run '$(basename "$0") on' when done."
;;
on)
sudo rm -f "$dropin"
sudo -k
echo "Password required for sudo again."
;;
*)
echo "Usage: $(basename "$0") on|off" >&2
exit 1
;;
esac
+13
View File
@@ -46,9 +46,22 @@ link agents/rules ~/.claude/rules
# what the SSH Hosts launcher command reads. # what the SSH Hosts launcher command reads.
link ssh/config ~/.ssh/config link ssh/config ~/.ssh/config
# ── Scripts ──────────────────────────────────────────────────────────────────
# Personal commands wanted on every machine, desktop or server. ~/.local/bin is
# on PATH in both shell configs. pwswitch toggles passwordless sudo through a
# sudoers.d drop-in, so an agent can run sudo and the password comes back after.
link bin/pwswitch ~/.local/bin/pwswitch
# ── Espanso ────────────────────────────────────────────────────────────────── # ── Espanso ──────────────────────────────────────────────────────────────────
# The personal expansion triggers. Copied rather than linked, because this is a # The personal expansion triggers. Copied rather than linked, because this is a
# file each machine may add its own triggers to. It lands before setup-identity # file each machine may add its own triggers to. It lands before setup-identity
# runs, and that stage only seeds a file that does not exist yet, so what is # runs, and that stage only seeds a file that does not exist yet, so what is
# tracked here wins over the name and email the interview would have written. # tracked here wins over the name and email the interview would have written.
copy espanso/identity.yml ~/.config/espanso/match/identity.yml copy espanso/identity.yml ~/.config/espanso/match/identity.yml
# ── OpenCode ─────────────────────────────────────────────────────────────────
# Claude Code carries its commands and agents as plugins, which opencode cannot
# read. These two directories hold the same capabilities in opencode's own
# format, so the command I type in one tool exists in the other.
linkdir agents/commands ~/.config/opencode/commands
linkdir agents/agents ~/.config/opencode/agents
+2
View File
@@ -1,3 +1,5 @@
Include ~/.ssh/config.local
Host server.gib Host server.gib
HostName server.gib HostName server.gib
User gib User gib