Compare commits
9
Commits
c1bbc69c8a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130137485e | ||
|
|
5ebebc8aa2 | ||
|
|
85160ffef2 | ||
|
|
59192143c5 | ||
|
|
2f2e3b88e8 | ||
|
|
9156a7e05c | ||
|
|
39a2e8e9ca | ||
|
|
6206565d95 | ||
|
|
cceb7a707b |
@@ -12,6 +12,10 @@
|
||||
/config/dot/espanso/match/identity.yml
|
||||
# Ignore backups of old config files
|
||||
/config/old
|
||||
# MCP bearer tokens. The server list in user/agents/mcp/servers is tracked
|
||||
# and names which variable carries each token; the tokens themselves are
|
||||
# machine-local, because this repo is public.
|
||||
/user/agents/mcp/env
|
||||
# Ignore Wireguard config of course!
|
||||
/config/wg/**
|
||||
# Ignore Neovim lazy-lock file
|
||||
|
||||
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
189 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
190 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:
|
||||
|
||||
|
||||
@@ -244,10 +244,20 @@ local shipped_mode = "4500x3000@60"
|
||||
local shipped_scale = 1.5
|
||||
local shipped_transform = 0
|
||||
|
||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3, so this relies on DSC. If the display fails to light up or falls back to
|
||||
-- a lower mode, drop this to 8 first.
|
||||
local shipped_bitdepth = 10
|
||||
-- 8-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset
|
||||
-- retrains it and blanks the screen. 8bpc keeps headroom on the link.
|
||||
--
|
||||
-- Related: directScanoutPolicy is 0 in Panama settings (2026-09-13). With
|
||||
-- scanout on, a fullscreen game whose buffer depth differs from the desktop
|
||||
-- (games ship both 8- and 10-bit swapchains) makes Hyprland change the output
|
||||
-- format, and on amdgpu a format change is a full modeset. Compositing always
|
||||
-- keeps the format fixed, so the link never retrains mid-game.
|
||||
--
|
||||
-- vrrPolicy is also 0 there. VRR on this panel loses sync and blacks out
|
||||
-- (seen on GNOME in July 2026 and again here); a 60Hz panel gains little
|
||||
-- from it anyway.
|
||||
local shipped_bitdepth = 8
|
||||
|
||||
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||
local shipped_cm = "auto"
|
||||
|
||||
@@ -125,6 +125,16 @@ hl.window_rule({
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
-- Steam games never get true fullscreen. Everything Hyprland does only for a
|
||||
-- real fullscreen window (direct scanout, VRR, tearing, auto HDR) makes this
|
||||
-- panel's marginal DP link retrain and blank (2026-09-13). "1 2" keeps the
|
||||
-- window maximized internally while the game believes it is fullscreen, which
|
||||
-- is what borderless windowed looks like from the game's side.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam_app_\\d+$" },
|
||||
fullscreen_state = "1 2",
|
||||
})
|
||||
|
||||
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||
@@ -269,16 +279,28 @@ hl.layer_rule({
|
||||
ignore_alpha = 0.2,
|
||||
})
|
||||
|
||||
-- Overview, capture and local screen-reading UI dim the desktop behind them.
|
||||
-- Overview and local screen-reading UI dim the desktop behind them.
|
||||
hl.layer_rule({
|
||||
name = "qs-overlay",
|
||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
||||
match = { namespace = "^qs-(overview|screen-intelligence)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.4,
|
||||
dim_around = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- The screenshot picker paints an opaque frozen frame edge to edge, so blur and
|
||||
-- dim would be compositor work on pixels nobody sees. no_anim is load-bearing:
|
||||
-- services/Capture.qml unmaps the picker and runs grim 90ms later, and the
|
||||
-- 200ms layersOut fade would otherwise put the half-faded picker, dimming and
|
||||
-- all, into every screenshot.
|
||||
hl.layer_rule({
|
||||
name = "qs-capture",
|
||||
match = { namespace = "^qs-capture$" },
|
||||
no_anim = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||
-- read as washed out rather than as glass.
|
||||
|
||||
@@ -27,9 +27,13 @@ PanelWindow {
|
||||
right: true
|
||||
}
|
||||
|
||||
// Fullscreen overlays must not reserve space, or every window on the
|
||||
// workspace gets resized as the picker opens and closes.
|
||||
exclusiveZone: 0
|
||||
// Ignore, and nothing else: reserve no space (or every window on the
|
||||
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||
// (or the window starts under the bar, the full-output freeze frame is
|
||||
// squeezed into a shorter box, and every selection lands one bar-height
|
||||
// off in the real capture). Never pair this with exclusiveZone; see
|
||||
// tests/quickshell/exclusion-idiom-contract.
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
|
||||
@@ -57,7 +57,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||
|
||||
@@ -55,7 +55,6 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
|
||||
// A dock that reserved space would not be intellihiding.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -40,7 +40,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
||||
|
||||
@@ -44,7 +44,6 @@ PanelWindow {
|
||||
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
||||
// output including under the bar and the dock, which is what makes it
|
||||
// visible from wherever the eyes happen to be.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ PanelWindow {
|
||||
screen: root.modelData
|
||||
anchors.bottom: true
|
||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: root.desiredWidth
|
||||
implicitHeight: 64
|
||||
|
||||
@@ -22,7 +22,6 @@ PanelWindow {
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -34,11 +34,23 @@ Item {
|
||||
visible: root.rowCount > 0
|
||||
implicitHeight: rows.implicitHeight
|
||||
|
||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
||||
// is actually on screen rather than running at shell startup. Monitors do
|
||||
// not come and go, so once is enough.
|
||||
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
||||
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
||||
// Probing I2C takes on the order of a second, so it waits until the quick
|
||||
// settings panel is actually on screen rather than running at shell
|
||||
// startup. Monitors do not come and go, so once is enough.
|
||||
//
|
||||
// The trigger is the panel opening, not this item becoming visible: on a
|
||||
// machine with no backlight, `visible` stays false until a scan has found
|
||||
// a DDC display, so keying the scan off it would never scan at all.
|
||||
Connections {
|
||||
target: ShellState
|
||||
function onQuickSettingsOpenChanged(): void { root.scanIfOpen(); }
|
||||
}
|
||||
Component.onCompleted: root.scanIfOpen()
|
||||
|
||||
function scanIfOpen(): void {
|
||||
if (ShellState.quickSettingsOpen && !Brightness.scanned)
|
||||
Brightness.refresh();
|
||||
}
|
||||
|
||||
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||
Process {
|
||||
|
||||
@@ -22,7 +22,6 @@ Variants {
|
||||
implicitWidth: 260
|
||||
implicitHeight: 172
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
mask: Region {}
|
||||
|
||||
|
||||
@@ -383,6 +383,13 @@ Lua-configured Hyprland, prints the refusal to stdout, and exits 0. `eval` exits
|
||||
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||
landed is reading the value back.
|
||||
|
||||
**`exclusionMode: ExclusionMode.Ignore` goes alone.** Quickshell's `exclusiveZone`
|
||||
setter flips `exclusionMode` back to `Normal` as a side effect, so a window that
|
||||
declares both is at the mercy of which property the QML engine applies last.
|
||||
The 2026-09-14 Qt update changed that order and every full-screen overlay slid
|
||||
under the bar. `tests/quickshell/exclusion-idiom-contract` fails any file that
|
||||
pairs them.
|
||||
|
||||
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
|
||||
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||
its control under the label below 520px. Test narrow.
|
||||
|
||||
@@ -78,7 +78,6 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-welcome"
|
||||
|
||||
@@ -341,7 +341,10 @@ esac'
|
||||
property bool record: false
|
||||
property bool intelligence: false
|
||||
// A handful of frames at 60Hz, enough for the compositor to recomposite
|
||||
// the output without the overlay on it.
|
||||
// the output without the overlay on it. Only enough because the
|
||||
// qs-capture layer rule in hypr/rules.lua sets no_anim: with the
|
||||
// default 200ms layersOut fade the half-faded picker would still be
|
||||
// on screen when grim reads it.
|
||||
interval: 90
|
||||
onTriggered: {
|
||||
const r = commitDelay.rect;
|
||||
|
||||
@@ -343,7 +343,7 @@ fi
|
||||
if [[ "$PANAMA_ROLE" == server ]]; then
|
||||
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
||||
else
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user link-mcp change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
fi
|
||||
|
||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
||||
|
||||
@@ -1405,8 +1405,17 @@ soft "the multimedia group update" \
|
||||
sync
|
||||
log "Swapping ffmpeg-free for ffmpeg"
|
||||
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
||||
# mesa-va-drivers-freeworld Provides mesa-va-drivers, so once the swap has been
|
||||
# done it is the only thing providing the name we ask dnf to remove. `dnf swap`
|
||||
# then resolves to removing and installing the same package in one transaction
|
||||
# and fails with "conflicting requests", which reports install-packages as a
|
||||
# failed stage on every subsequent run. Guard on the end state instead.
|
||||
if rpm -q mesa-va-drivers-freeworld >/dev/null 2>&1; then
|
||||
log "mesa-va-drivers-freeworld is already installed; swap not needed"
|
||||
else
|
||||
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
||||
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
||||
fi
|
||||
log "Upgrading Multimedia group with optional packages"
|
||||
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
||||
log "Installing GStreamer plugins (bad, good, base)"
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# MCP servers, registered with the agent runtimes on this machine.
|
||||
#
|
||||
# An MCP server is a URL plus a bearer token, and the token is the whole reason
|
||||
# this is a stage rather than a manifest line. Panama is a public repository, so
|
||||
# the tokens cannot live in it, and neither runtime keeps its server list in a
|
||||
# file that could be symlinked anyway: Codex writes them into config.toml beside
|
||||
# dozens of unrelated settings, and Claude Code into ~/.claude.json. There is no
|
||||
# file to point at, so this registers them through the runtime's own CLI.
|
||||
#
|
||||
# What is tracked is user/agents/mcp/servers, which names each server and which
|
||||
# variable carries its token. What is not tracked is user/agents/mcp/env, which
|
||||
# holds the tokens. A new machine gets the servers by dropping its own env file
|
||||
# beside the tracked one and re-running ./install.
|
||||
#
|
||||
# This is personal content, so it obeys the same interview decision link-user
|
||||
# does. A machine that never said yes registers nothing.
|
||||
#
|
||||
# Only Claude Code is handled. Codex stores its servers inside config.toml, and
|
||||
# rewriting a section of somebody's live TOML is a worse failure mode than
|
||||
# leaving two lines for them to paste once.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; }
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
MCP_DIR="$PANAMA_PATH/user/agents/mcp"
|
||||
SERVERS="$MCP_DIR/servers"
|
||||
ENV_FILE="$MCP_DIR/env"
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
DECISION="$STATE_DIR/user-content"
|
||||
|
||||
[[ -r "$SERVERS" ]] || { log "No MCP server list; nothing to register."; exit 0; }
|
||||
|
||||
# The same gate link-user uses, read the same way, so one answer governs all
|
||||
# personal content rather than two stages disagreeing about it.
|
||||
decision="$([[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no')"
|
||||
if [[ "$decision" != "yes" ]]; then
|
||||
log "Personal content is not enabled on this machine; no MCP servers registered."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v claude >/dev/null 2>&1; then
|
||||
log "Claude Code is not installed; nothing to register."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Tokens are optional. A machine without the env file still registers any server
|
||||
# that needs no header, and says which ones it skipped rather than failing.
|
||||
if [[ -r "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
warn "No $ENV_FILE; servers needing a token will be skipped."
|
||||
fi
|
||||
|
||||
registered=0
|
||||
skipped=0
|
||||
|
||||
while read -r name transport url token_var _rest; do
|
||||
case "${name:-}" in ''|'#'*) continue ;; esac
|
||||
if [[ -z "${transport:-}" || -z "${url:-}" ]]; then
|
||||
warn "Ignoring malformed row for '$name'."
|
||||
continue
|
||||
fi
|
||||
|
||||
header=()
|
||||
if [[ -n "${token_var:-}" ]]; then
|
||||
token="${!token_var:-}"
|
||||
if [[ -z "$token" ]]; then
|
||||
warn "Skipping $name: $token_var is not set in $ENV_FILE."
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
header=(-H "Authorization: $token")
|
||||
fi
|
||||
|
||||
# Re-registering is how this stays idempotent across upgrades, and how a
|
||||
# rotated token reaches the runtime. Removing first avoids the CLI refusing
|
||||
# a name it already knows. Neither call may print the token, so both are
|
||||
# quiet unless they fail.
|
||||
claude mcp remove "$name" -s user >/dev/null 2>&1 || true
|
||||
if claude mcp add --transport "$transport" "$name" "$url" "${header[@]}" \
|
||||
-s user >/dev/null 2>&1; then
|
||||
log "Registered $name."
|
||||
registered=$((registered + 1))
|
||||
else
|
||||
warn "Could not register $name."
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
done <"$SERVERS"
|
||||
|
||||
log "MCP servers: $registered registered, $skipped skipped."
|
||||
log "Claude Code loads them at start, so restart a running session to pick them up."
|
||||
@@ -82,6 +82,7 @@ live-desktop tests/quickshell/dock-position-contract
|
||||
# Reads Hyprland option descriptions from the live compositor to verify enum
|
||||
# mappings.
|
||||
live-compositor tests/quickshell/enum-hypr-map-contract
|
||||
hermetic tests/quickshell/exclusion-idiom-contract
|
||||
hermetic tests/quickshell/fingerprint-contract
|
||||
# Reads the host firewall state through the production firewall helper.
|
||||
live-host tests/quickshell/firewall-contract
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# A window that wants the whole output must ask for ExclusionMode.Ignore and
|
||||
# nothing else. Quickshell's exclusiveZone setter forces exclusionMode back to
|
||||
# Normal as a side effect, so a window declaring both
|
||||
#
|
||||
# exclusiveZone: 0
|
||||
# exclusionMode: ExclusionMode.Ignore
|
||||
#
|
||||
# ends up with whichever property the QML engine applied last, and that order
|
||||
# is not ours to control: the 2026-09-14 Qt/Quickshell update flipped it, every
|
||||
# full-screen overlay slid down under the bar, and the screenshot picker's
|
||||
# frozen frame stopped lining up with the screen it was a picture of. This pins
|
||||
# the order-independent idiom: Ignore alone, never paired with exclusiveZone.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
qs="$repo_dir/config/dot/quickshell"
|
||||
|
||||
fail() { printf 'exclusion idiom contract: %s\n' "$1" >&2; exit 1; }
|
||||
[[ -d "$qs/modules" ]] || fail "missing $qs/modules"
|
||||
|
||||
python3 - "$qs" <<'PY'
|
||||
import re, sys, pathlib
|
||||
qs = pathlib.Path(sys.argv[1])
|
||||
problems = []
|
||||
ignore = re.compile(r'\bexclusionMode\s*:\s*ExclusionMode\.Ignore\b')
|
||||
zone = re.compile(r'^\s*(?:WlrLayershell\.)?exclusiveZone\s*:', re.M)
|
||||
checked = 0
|
||||
for path in sorted(qs.rglob('*.qml')):
|
||||
text = path.read_text()
|
||||
if not ignore.search(text):
|
||||
continue
|
||||
checked += 1
|
||||
for m in zone.finditer(text):
|
||||
line = text.count('\n', 0, m.start()) + 1
|
||||
problems.append(f"{path.relative_to(qs)}:{line}: exclusiveZone set in a file that uses "
|
||||
"ExclusionMode.Ignore; drop it, Ignore already means -1")
|
||||
if checked == 0:
|
||||
problems.append("no window uses ExclusionMode.Ignore; the overlays this pins are gone")
|
||||
if problems:
|
||||
print('\n'.join(problems), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
@@ -62,6 +62,40 @@ I wanted to share some of my preferences here so we can be more aligned as we wo
|
||||
|
||||
---
|
||||
|
||||
# The prose bar
|
||||
|
||||
Anything a person other than me will read gets a pattern pass before it ships. MR and PR
|
||||
descriptions, ticket fields, deliverable documents, reports, anything handed to a
|
||||
teammate or a client. Invoke the `unslop` skill and apply its pattern detection.
|
||||
|
||||
- Skip its "Adding soul" section. Opinions and deliberate mess are right for an essay and
|
||||
wrong for a risk table or a status field. These want plain, factual, and specific.
|
||||
- No em dashes, en dashes, semicolons, or arrow glyphs in prose. This is stricter than
|
||||
unslop's own punctuation pattern, and mine wins. Grep the file before calling it done.
|
||||
- Passive voice is the usual offender in technical writing, not puffery. "X was verified"
|
||||
wants to be "I verified X", which is shorter and says who did it.
|
||||
- Short reference cells still count. Three sentences wrapped around a filename is prose.
|
||||
- Never rewrite generated output. If a tool produced a block, an audit verdict or a test
|
||||
report, regenerate it rather than editing its words.
|
||||
- Write it as me, in first person. Never use my name or a stand-in for it. Not "Gib
|
||||
decided", not "the author", not "per the developer". If a sentence needs an actor, it
|
||||
is "I".
|
||||
- Never flag one of my own decisions as a deviation. Don't write that something was out
|
||||
of scope, that an AC or dev review said otherwise, that an earlier story should have
|
||||
caught it, or that a direction was reversed. I write the tickets, so I am allowed to
|
||||
change them, and a dev review is a proposal rather than a contract. Calling it out
|
||||
reads as my own AI tattling on me. It waves a red flag at a nothing burger, costs the
|
||||
PM time investigating a non-issue, and leaves them thinking I did something wrong.
|
||||
State what the change is and why it belongs where it is, then stop.
|
||||
- If a decision feels big enough that you want to narrate it, ask me about it before
|
||||
writing rather than annotating it in the document. Being aligned first is the point,
|
||||
and the writing should read as though we always were.
|
||||
|
||||
Files only I read are exempt: plans, scratch notes, working records. Those can name me
|
||||
and record who decided what, because that history is useful.
|
||||
|
||||
---
|
||||
|
||||
# Merge Requests
|
||||
|
||||
- Make sure titles follow conventions from the repo.
|
||||
@@ -84,5 +118,8 @@ I wanted to share some of my preferences here so we can be more aligned as we wo
|
||||
- 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.
|
||||
- Despite the fact that Command Center does not have many users & the impact of it being down for a few minutes is small, Henry still leans on the side of solutions to problems that don't result in prod being down ever, even for just a few minutes during the build process. So our solutions should always keep that in mind. Any code that isn't considering everything & could result in a bug will probably be flagged by him, so its worthwhile to do right the first time!
|
||||
- Josh - Tech Lead for the APSCA project (apsca_next)
|
||||
- On APSCA, Josh is my lead instead of Henry. He reviews my code and writes the dev reviews, spikes, and proposed schemas on the APSCA stories.
|
||||
- His dev reviews are detailed and often include draft code, schema, and state machines. Treat them as the starting point for a story, not as a finished spec.
|
||||
- Hunter Southworth - Engineering Manager / Senior Developer
|
||||
- Kelson - Owner of Ksense
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# MCP servers registered with the local agent runtimes.
|
||||
#
|
||||
# <name> <transport> <url> <token variable>
|
||||
#
|
||||
# The token variable names an entry in user/agents/mcp/env, which is ignored by
|
||||
# git. This file carries no secret, so it is tracked like the rest of user/ and
|
||||
# a new machine gets the same servers by dropping its own env file beside it.
|
||||
#
|
||||
# A row with no token variable registers without an Authorization header.
|
||||
#
|
||||
# Lines beginning with # are comments and blank lines are ignored.
|
||||
|
||||
# The NanoKVM boards. These give an agent full console control of each machine,
|
||||
# including the parts SSH cannot reach: firmware, a locked screen, and the GUI
|
||||
# prompts Xcode and the keychain raise during a signed iOS build.
|
||||
nanokvm-fedora http https://nano.gbrown.org/api/mcp NANOKVM_FEDORA_TOKEN
|
||||
nanokvm-mac http https://macnano.gbrown.org/api/mcp NANOKVM_MAC_TOKEN
|
||||
|
||||
# Bill Tracker's hosted MCP server. No token: it is an OAuth server of its own
|
||||
# and Claude Code signs in through Gib's Auth on first use.
|
||||
bill-tracker http https://mcp.billtracker.gbrown.org/mcp
|
||||
@@ -108,6 +108,63 @@ rerun it, and verify the device log instead of accepting the fallback transcript
|
||||
- Don't check a checklist box (in `mr.md` or anywhere else) unless you actually verified
|
||||
it. Leave it unchecked and say why in the notes rather than guessing.
|
||||
|
||||
## The prose bar (anything another person reads)
|
||||
|
||||
Everything this skill produces for someone else gets a pattern pass before it ships:
|
||||
`mr.md`'s Summary and Additional Notes, every Jira field including the proof and
|
||||
mitigation cells, and every deliverable document. Call the Skill tool with "unslop" and
|
||||
apply its pattern detection.
|
||||
|
||||
- **Skip unslop's "Adding soul" section.** It asks for opinions, varied rhythm, and some
|
||||
mess. That is right for an essay and wrong for a mitigation cell Henry is auditing.
|
||||
These want plain, factual, and specific.
|
||||
- **Never edit generated output.** The pre-MR handoff pasted into `mr.md` is the audit's
|
||||
own words. If it reads badly, regenerate it, don't rewrite it.
|
||||
- **Proof cells are prose too.** A cell that wraps three sentences around a filename is
|
||||
writing, not a citation, and it picks up passive voice and jargon like any other
|
||||
paragraph. Real examples caught this way: "affordance" for button, "vacuously" for
|
||||
"for the wrong reason", "the test was confirmed able to fail" for "I emptied the
|
||||
legacy render and confirmed the test fails".
|
||||
- **Passive voice is the usual offender here,** not puffery. Technical writing drifts
|
||||
into "X was verified" when "I verified X" is shorter and says who did it.
|
||||
|
||||
### Write as Gabriel, and never flag his own decisions
|
||||
|
||||
Everything that ships under his name is written **as him**, in first person. Never write
|
||||
his name or any stand-in for it. Not "Gib decided", not "the author", not "per the
|
||||
developer". When a sentence needs an actor, it is "I".
|
||||
|
||||
**Never frame one of his decisions as a deviation.** Do not write that something "was not
|
||||
in this story's scope", that an AC or dev review said otherwise, that an earlier story
|
||||
"should have caught it", or that a direction was "reversed". State what the change is and
|
||||
why it belongs where it is, then stop.
|
||||
|
||||
He writes the tickets himself, so he is entitled to change them, and a dev review is a
|
||||
proposal rather than a contract. Calling the change out reads as his own AI tattling on
|
||||
him. It waves a red flag at a nothing burger, costs the PM time investigating a
|
||||
non-issue, and leaves them assuming he did something wrong. The real story is normally
|
||||
mundane and good: the better component already existed and cost almost nothing to use.
|
||||
|
||||
Compare, on KACP-23058:
|
||||
|
||||
> Bad: "This was not in this story's scope. KACP-23059 shipped that page still on the
|
||||
> legacy tables, and Gib reversed the reuse-legacy direction on 2026-09-02."
|
||||
>
|
||||
> Good: "Both detail pages now share one set of grid components instead of two divergent
|
||||
> implementations, so the move ships with the components it shares."
|
||||
|
||||
Same information, no flag attached. **If a decision feels big enough that you want to
|
||||
narrate it, that is the signal to ask him about it before writing, not to annotate it in
|
||||
the document.** Being aligned first is the point, and the writing should read as though
|
||||
you always were.
|
||||
|
||||
This governs `mr.md`, `customfield_10260` and `10261`, every other Jira field, and PM
|
||||
deliverables. It does not govern files only he reads. `plan.md`, `bugs.md`, audits and
|
||||
working notes can name him and record who decided what, because that history is useful.
|
||||
|
||||
The House style punctuation rule above is the stricter one where the two overlap. Keep
|
||||
it. Personal working files (`plan.md`, `ticket.md`) are exempt, nobody else reads them.
|
||||
|
||||
## Verification tools available
|
||||
|
||||
The goal isn't "a plausible-sounding plan" or "code that compiles" — it's a plan and
|
||||
@@ -226,15 +283,19 @@ method for producing accurate screenshots, not part of the deliverable's story.
|
||||
Process, per deliverable:
|
||||
|
||||
1. Write `<slug>.md`.
|
||||
2. Write `<slug>.typ` from it, per the styling notes above.
|
||||
3. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
||||
2. Run the prose bar over `<slug>.md` before building anything from it. A spike
|
||||
deliverable is the most prose-heavy thing this skill produces and it goes straight to
|
||||
Conrad, who is skeptical of AI. Fixing it here means fixing it once, rather than in
|
||||
the markdown and again in the Typst.
|
||||
3. Write `<slug>.typ` from it, per the styling notes above.
|
||||
4. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
||||
if it isn't, tell the user rather than silently skipping the PDF.
|
||||
4. Actually look at the compiled PDF using the Read tool (it reads PDFs directly, page
|
||||
5. Actually look at the compiled PDF using the Read tool (it reads PDFs directly, page
|
||||
by page for longer documents). Check every page for real layout problems: text or
|
||||
a table overflowing a page, an awkward page break splitting a table or diagram,
|
||||
cramped or excessive spacing, a diagram that rendered wrong. A successful compile
|
||||
only means valid Typst, not that it looks right — actually look.
|
||||
5. If anything looks wrong, fix `<slug>.typ` and go back to step 3. Repeat until the
|
||||
6. If anything looks wrong, fix `<slug>.typ` and go back to step 4. Repeat until the
|
||||
PDF genuinely looks right, not just until it compiles without erroring.
|
||||
|
||||
If `.claude/docs/` is tracked by git in this repo (check with
|
||||
@@ -690,6 +751,35 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
write `mr.md` before that verdict is reached.
|
||||
Findings that need no code change (mentions, disclosures) get addressed in
|
||||
`mr.md`'s Additional Notes or the Jira fields as usual.
|
||||
|
||||
**The verdict is the gate and it is not negotiable.** Ready to Open MR is what
|
||||
goes on record with the Lead, so never reason your way past it because the
|
||||
remaining items look harmless. That judgment is not yours to make.
|
||||
|
||||
**Don't confuse the verdict with the handoff's recommendation.** They are
|
||||
different fields in different files for different readers. The audit's
|
||||
`Verdict` says whether the branch is ready to become an MR, and that is the
|
||||
gate. The handoff's `Recommendation` (`Merge`, `Merge after conditions`) is
|
||||
advice to the maintainer about merging it, and `Merge after conditions` is
|
||||
normal and expected at a Ready verdict. Never treat the recommendation as a
|
||||
blocker or report it as one.
|
||||
|
||||
**Reruns converge by giving the audit more, not by repeating the command.**
|
||||
When a rerun is needed, the user is running it, so every rerun costs them.
|
||||
Make each one count:
|
||||
- **Feed it context on the first run.** It reviews the diff, and left alone it
|
||||
re-derives intent from the branch name and re-raises decisions that are
|
||||
already settled. Ask the user to point it at the ticket's
|
||||
`resources/ticket.md` and `plan.md`. This is the cheapest thing that reduces
|
||||
repeat findings.
|
||||
- **Answer its decisions explicitly in the rerun.** When it parks a finding
|
||||
under "Decide before opening", write the decision and its reasoning into the
|
||||
prompt for the next run, so it can reclassify rather than re-ask. A silent
|
||||
rerun on an unchanged diff re-asks the same question.
|
||||
- **Say so when it finds new things in unchanged code.** That means the earlier
|
||||
run was an incomplete pass, not that the branch got worse. Tell the user
|
||||
plainly, because it is the audit's problem to fix and it changes how much
|
||||
weight the next run deserves.
|
||||
8. Read `~/.agents/skills/ticket/templates/mr.md` — this is the org's MR template,
|
||||
copied into this skill so it still works even though the original
|
||||
`.claude/docs/mr/template.md` no longer exists in the command-center repo. `mr.md`
|
||||
@@ -714,7 +804,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
verified" if you actually ran a build and it passed).
|
||||
- Replace `<!-- Paste the Pre-MR handoff here -->` with the exact content of the
|
||||
latest `pre-mr-review` handoff file (the one with verdict Ready to Open MR) —
|
||||
paste it verbatim, don't summarize it.
|
||||
paste it verbatim, don't summarize it. Its `Recommendation` line is the
|
||||
audit's advice to the maintainer and is left exactly as written, including
|
||||
`Merge after conditions`.
|
||||
|
||||
The handoff section of `mr.md` is GENERATED OUTPUT, not something you author.
|
||||
It is the block that states whether the branch is ready to merge, so it must be
|
||||
@@ -756,13 +848,10 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
content. A reviewer should be able to read the whole file in about a minute; if it
|
||||
has grown past roughly 120 lines including the handoff, it has drifted.
|
||||
|
||||
Before saving, call the Skill tool with "unslop" and apply its pattern detection to
|
||||
the Summary and Additional Notes ONLY: puffery, superficial -ing phrases, "not just
|
||||
X but Y", vague attributions, rule-of-three padding. The pasted handoff is generated
|
||||
output and is never edited, per the rule above. Skip unslop's "Adding soul" section
|
||||
too — an MR description wants plain and factual, not voice. Henry reads every one of
|
||||
these, and prose that reads as machine-generated costs the MR its credibility before
|
||||
anyone looks at the diff.
|
||||
Before saving, run the prose bar (see **The prose bar** above) over the Summary and
|
||||
Additional Notes ONLY. The pasted handoff is generated output and is never edited.
|
||||
Henry reads every one of these, and prose that reads as machine-generated costs the
|
||||
MR its credibility before anyone looks at the diff.
|
||||
9. Fill the Jira ticket fields directly, by issue type. Rich text fields are ADF:
|
||||
render markdown with `python3 ~/.agents/skills/review-ticket/scripts/review2adf.py
|
||||
render <file.md>` and PUT via `{"fields": {...}}` to `/rest/api/3/issue/<KEY>`.
|
||||
@@ -771,11 +860,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
field-to-type map below is verified against the project's edit screens, don't PUT
|
||||
a field to a type that doesn't carry it.
|
||||
|
||||
**Same prose bar as `mr.md`.** Every field authored here is read by the Lead and the
|
||||
PM. Run the `unslop` skill's pattern detection over the markdown before rendering it
|
||||
to ADF, skipping its "Adding soul" section — Jira fields want plain, factual, and
|
||||
specific. This does not apply to proof cells, which are references to artifacts and
|
||||
test names rather than prose.
|
||||
**Run the prose bar** (see **The prose bar** above) over every field before rendering
|
||||
it to ADF, proof and mitigation cells included. Everything here is read by the Lead
|
||||
and the PM.
|
||||
|
||||
**Proof first.** Before filling any proof column, capture working feature proof
|
||||
yourself wherever possible: run the app (`run` skill) and screenshot the real
|
||||
@@ -835,13 +922,12 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
|
||||
handled and what covers it. Deliver the number asked for, not more, and pick the
|
||||
ones a reviewer most benefits from. This field, not `mr.md`.
|
||||
- `customfield_10261` User Story Patch Notes: the same change in user-facing words.
|
||||
- `customfield_10142` Working Feature Proof: ALWAYS fill this on a Story, never
|
||||
leave it empty. It renders as its own panel in the ticket's testing section, so
|
||||
an empty field reads as unfilled testing even when every test-table cell is
|
||||
complete (this happened on KACP-23143). At minimum it lists the files staged in
|
||||
`proof/` for Gib to attach, one line each saying what the artifact shows, plus a
|
||||
pointer that per-row proof lives in the Test Cases table. A headline artifact (a
|
||||
demo capture, a before/after pair) leads the list when one exists.
|
||||
- `customfield_10142` Working Feature Proof: leave it alone. Write nothing here.
|
||||
This field is where Gib drops the proof attachments, and dragging files into it
|
||||
replaces whatever text is sitting there, so a written list is destroyed by the
|
||||
upload it was describing. Naming each artifact is already the job of the Test
|
||||
Cases proof cells, which reference it by filename. Step 10 tells Gib which files
|
||||
to upload. Do not PUT this field on a Story.
|
||||
|
||||
**Spike** (deliverables, not code): no test or proof fields to fill. The output is
|
||||
the deliverables directory, and step 10 tells Gib which files to upload where (the
|
||||
|
||||
Reference in New Issue
Block a user