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

+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/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. |
| `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. |
`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
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
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.
- 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
- 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.
- Conrad is awesome & is great to work with.
- 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 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.
+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.
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,
# 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
+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.
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 ──────────────────────────────────────────────────────────────────
# 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
# 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.
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
HostName server.gib
User gib