diff --git a/README.md b/README.md index b31653e..642aee4 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work ## 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 specific external capability when automation needs it: diff --git a/setup/scripts/link-mcp b/setup/scripts/link-mcp index 8dbb6f6..79067a2 100755 --- a/setup/scripts/link-mcp +++ b/setup/scripts/link-mcp @@ -23,10 +23,13 @@ # failure mode than leaving two lines for them to paste once. # # opencode has no CLI for this either, but its config is plain JSON, so this -# stage merges two keys into it with jq: one mcp entry per server row, and the -# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md -# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in -# that file is left as it was. +# stage writes it whole from user/agents/opencode/config.json -- the tracked +# template carrying everything that is not a secret -- plus the MCP block built +# here from the server rows. The whole file is generated rather than merged into +# 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 @@ -37,6 +40,8 @@ PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" MCP_DIR="$PANAMA_PATH/user/agents/mcp" SERVERS="$MCP_DIR/servers" 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" DECISION="$STATE_DIR/user-content" @@ -177,29 +182,62 @@ fi $has_opencode || exit 0 -# opencode reads opencode.jsonc or opencode.json, whichever this machine has. -# The file holds tokens once this runs, so it is written private and swapped in -# whole, the way ~/.claude.json already holds them for Claude Code. +# opencode reads opencode.jsonc. The file holds tokens once this runs, so it is +# written private and swapped in whole, the way ~/.claude.json already holds +# them for Claude Code. OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode" opencode_config="$OPENCODE_DIR/opencode.jsonc" -[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json" -rules="$HOME/.claude/rules/*.md" -mkdir -p "$OPENCODE_DIR" -current='{"$schema": "https://opencode.ai/config.json"}' -[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")" - -# jq cannot read JSONC comments. A file that has them is left alone rather -# than rewritten without them. -if ! merged="$(jq --argjson mcp "$opencode_mcp" --arg rules "$rules" \ - '.mcp = ((.mcp // {}) + $mcp) | .instructions = (((.instructions // []) - [$rules]) + [$rules])' \ - <<<"$current" 2>/dev/null)"; then - warn "Could not parse $opencode_config as JSON; opencode MCP servers not written." +[[ -r "$TEMPLATE" ]] || { warn "No $TEMPLATE; opencode config not written."; exit 0; } +if ! jq -e . "$TEMPLATE" >/dev/null 2>&1; then + warn "$TEMPLATE is not valid JSON; opencode config not written." exit 0 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")" +{ + 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" -printf '%s\n' "$merged" >"$tmp" mv "$tmp" "$opencode_config" -log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config." + +log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the template written to $opencode_config." diff --git a/tests/contracts.manifest b/tests/contracts.manifest index 559f39a..13359e7 100644 --- a/tests/contracts.manifest +++ b/tests/contracts.manifest @@ -270,6 +270,7 @@ hermetic tests/setup/launcher-search-contract hermetic tests/setup/lid-contract hermetic tests/setup/migrations-contract hermetic tests/setup/nautilus-extensions-contract +hermetic tests/setup/opencode-config-contract hermetic tests/setup/package-lists-contract # Provenance uses local signed fixtures and stubs every network/package operation. hermetic tests/setup/package-provenance-contract diff --git a/tests/quickshell/declared-dependencies-contract b/tests/quickshell/declared-dependencies-contract index c088aec..50dd315 100755 --- a/tests/quickshell/declared-dependencies-contract +++ b/tests/quickshell/declared-dependencies-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 # aliases fail by naming the missing command, which is the honest outcome. # 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 # entry: RPM Fusion's ffmpeg CONFLICTS with the ffmpeg-free that Fedora ships diff --git a/tests/setup/opencode-config-contract b/tests/setup/opencode-config-contract new file mode 100755 index 0000000..e2539ab --- /dev/null +++ b/tests/setup/opencode-config-contract @@ -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' diff --git a/user/README.md b/user/README.md index fcf14b3..e58f5f5 100644 --- a/user/README.md +++ b/user/README.md @@ -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 diff --git a/user/agents/AGENTS.md b/user/agents/AGENTS.md index aa123ad..ae3031e 100644 --- a/user/agents/AGENTS.md +++ b/user/agents/AGENTS.md @@ -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. diff --git a/user/agents/agents/code-simplifier.md b/user/agents/agents/code-simplifier.md new file mode 100644 index 0000000..6701a12 --- /dev/null +++ b/user/agents/agents/code-simplifier.md @@ -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. diff --git a/user/agents/commands/code-review.md b/user/agents/commands/code-review.md new file mode 100644 index 0000000..7857cbc --- /dev/null +++ b/user/agents/commands/code-review.md @@ -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. + diff --git a/user/agents/mcp/bin/playwright b/user/agents/mcp/bin/playwright new file mode 100755 index 0000000..cdd893a --- /dev/null +++ b/user/agents/mcp/bin/playwright @@ -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 diff --git a/user/agents/mcp/servers b/user/agents/mcp/servers index c407948..4d6586b 100644 --- a/user/agents/mcp/servers +++ b/user/agents/mcp/servers @@ -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 diff --git a/user/agents/opencode/config.json b/user/agents/opencode/config.json new file mode 100644 index 0000000..8df513f --- /dev/null +++ b/user/agents/opencode/config.json @@ -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" } + ] +} diff --git a/user/agents/skills/frontend-design/SKILL.md b/user/agents/skills/frontend-design/SKILL.md new file mode 100644 index 0000000..6f374d8 --- /dev/null +++ b/user/agents/skills/frontend-design/SKILL.md @@ -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. diff --git a/user/bin/pwswitch b/user/bin/pwswitch new file mode 100755 index 0000000..3d7c360 --- /dev/null +++ b/user/bin/pwswitch @@ -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 diff --git a/user/manifest b/user/manifest index 8acab1f..de69538 100644 --- a/user/manifest +++ b/user/manifest @@ -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 diff --git a/user/ssh/config b/user/ssh/config index 9aa9512..75c73a3 100644 --- a/user/ssh/config +++ b/user/ssh/config @@ -1,3 +1,5 @@ +Include ~/.ssh/config.local + Host server.gib HostName server.gib User gib