Generate opencode's config from a tracked template, with matching commands, agents and pwswitch
This commit is contained in:
1 parent
68c7d6a3ea
commit
a987cbab66
16 files changed
+531
-25
No files matched your search
@@ -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
@@ -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."
|
||||||
@@ -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
|
||||||
|
|||||||
Executable
+159
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
Executable
+11
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
Executable
+31
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
Include ~/.ssh/config.local
|
||||||
|
|
||||||
Host server.gib
|
Host server.gib
|
||||||
HostName server.gib
|
HostName server.gib
|
||||||
User gib
|
User gib
|
||||||
|
|||||||
Reference in new issue
Block a user