Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85160ffef2 | ||
|
|
59192143c5 | ||
|
|
2f2e3b88e8 | ||
|
|
9156a7e05c | ||
|
|
39a2e8e9ca | ||
|
|
6206565d95 | ||
|
|
cceb7a707b |
@@ -12,6 +12,10 @@
|
|||||||
/config/dot/espanso/match/identity.yml
|
/config/dot/espanso/match/identity.yml
|
||||||
# Ignore backups of old config files
|
# Ignore backups of old config files
|
||||||
/config/old
|
/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!
|
# Ignore Wireguard config of course!
|
||||||
/config/wg/**
|
/config/wg/**
|
||||||
# Ignore Neovim lazy-lock file
|
# Ignore Neovim lazy-lock file
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## 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
|
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:
|
||||||
|
|
||||||
|
|||||||
@@ -244,10 +244,20 @@ local shipped_mode = "4500x3000@60"
|
|||||||
local shipped_scale = 1.5
|
local shipped_scale = 1.5
|
||||||
local shipped_transform = 0
|
local shipped_transform = 0
|
||||||
|
|
||||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
-- 8-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
|
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset
|
||||||
-- a lower mode, drop this to 8 first.
|
-- retrains it and blanks the screen. 8bpc keeps headroom on the link.
|
||||||
local shipped_bitdepth = 10
|
--
|
||||||
|
-- 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.
|
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||||
local shipped_cm = "auto"
|
local shipped_cm = "auto"
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ hl.window_rule({
|
|||||||
no_dim = true,
|
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.
|
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||||
hl.window_rule({
|
hl.window_rule({
|
||||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||||
@@ -269,16 +279,28 @@ hl.layer_rule({
|
|||||||
ignore_alpha = 0.2,
|
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({
|
hl.layer_rule({
|
||||||
name = "qs-overlay",
|
name = "qs-overlay",
|
||||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
match = { namespace = "^qs-(overview|screen-intelligence)$" },
|
||||||
blur = true,
|
blur = true,
|
||||||
ignore_alpha = 0.4,
|
ignore_alpha = 0.4,
|
||||||
dim_around = true,
|
dim_around = true,
|
||||||
no_screen_share = 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
|
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||||
-- read as washed out rather than as glass.
|
-- read as washed out rather than as glass.
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ PanelWindow {
|
|||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fullscreen overlays must not reserve space, or every window on the
|
// Ignore, and nothing else: reserve no space (or every window on the
|
||||||
// workspace gets resized as the picker opens and closes.
|
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||||
exclusiveZone: 0
|
// (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.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ PanelWindow {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
// A dock that reserved space would not be intellihiding.
|
// A dock that reserved space would not be intellihiding.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
// 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 }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
// 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
|
// 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
|
// output including under the bar and the dock, which is what makes it
|
||||||
// visible from wherever the eyes happen to be.
|
// visible from wherever the eyes happen to be.
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ PanelWindow {
|
|||||||
screen: root.modelData
|
screen: root.modelData
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
implicitWidth: root.desiredWidth
|
implicitWidth: root.desiredWidth
|
||||||
implicitHeight: 64
|
implicitHeight: 64
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||||
|
|||||||
@@ -34,11 +34,23 @@ Item {
|
|||||||
visible: root.rowCount > 0
|
visible: root.rowCount > 0
|
||||||
implicitHeight: rows.implicitHeight
|
implicitHeight: rows.implicitHeight
|
||||||
|
|
||||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
// Probing I2C takes on the order of a second, so it waits until the quick
|
||||||
// is actually on screen rather than running at shell startup. Monitors do
|
// settings panel is actually on screen rather than running at shell
|
||||||
// not come and go, so once is enough.
|
// 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()
|
// 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
|
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||||
Process {
|
Process {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Variants {
|
|||||||
implicitWidth: 260
|
implicitWidth: 260
|
||||||
implicitHeight: 172
|
implicitHeight: 172
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
mask: Region {}
|
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
|
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||||
landed is reading the value back.
|
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,
|
**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
|
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||||
its control under the label below 520px. Test narrow.
|
its control under the label below 520px. Test narrow.
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ PanelWindow {
|
|||||||
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: 0
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-popover-welcome"
|
WlrLayershell.namespace: "qs-popover-welcome"
|
||||||
|
|||||||
@@ -341,7 +341,10 @@ esac'
|
|||||||
property bool record: false
|
property bool record: false
|
||||||
property bool intelligence: false
|
property bool intelligence: false
|
||||||
// A handful of frames at 60Hz, enough for the compositor to recomposite
|
// 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
|
interval: 90
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const r = commitDelay.rect;
|
const r = commitDelay.rect;
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ fi
|
|||||||
if [[ "$PANAMA_ROLE" == server ]]; then
|
if [[ "$PANAMA_ROLE" == server ]]; then
|
||||||
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
# The two an upgrade drops. Both exist only to act on interview answers, and
|
||||||
|
|||||||
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
|
# Reads Hyprland option descriptions from the live compositor to verify enum
|
||||||
# mappings.
|
# mappings.
|
||||||
live-compositor tests/quickshell/enum-hypr-map-contract
|
live-compositor tests/quickshell/enum-hypr-map-contract
|
||||||
|
hermetic tests/quickshell/exclusion-idiom-contract
|
||||||
hermetic tests/quickshell/fingerprint-contract
|
hermetic tests/quickshell/fingerprint-contract
|
||||||
# Reads the host firewall state through the production firewall helper.
|
# Reads the host firewall state through the production firewall helper.
|
||||||
live-host tests/quickshell/firewall-contract
|
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
|
# Merge Requests
|
||||||
|
|
||||||
- Make sure titles follow conventions from the repo.
|
- 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 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.
|
||||||
- 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!
|
- 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
|
- Hunter Southworth - Engineering Manager / Senior Developer
|
||||||
- Kelson - Owner of Ksense
|
- 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
|
- 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.
|
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
|
## Verification tools available
|
||||||
|
|
||||||
The goal isn't "a plausible-sounding plan" or "code that compiles" — it's a plan and
|
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:
|
Process, per deliverable:
|
||||||
|
|
||||||
1. Write `<slug>.md`.
|
1. Write `<slug>.md`.
|
||||||
2. Write `<slug>.typ` from it, per the styling notes above.
|
2. Run the prose bar over `<slug>.md` before building anything from it. A spike
|
||||||
3. Compile: `typst compile <slug>.typ <slug>.pdf`. `typst` should already be on PATH;
|
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.
|
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
|
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,
|
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
|
cramped or excessive spacing, a diagram that rendered wrong. A successful compile
|
||||||
only means valid Typst, not that it looks right — actually look.
|
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.
|
PDF genuinely looks right, not just until it compiles without erroring.
|
||||||
|
|
||||||
If `.claude/docs/` is tracked by git in this repo (check with
|
If `.claude/docs/` is tracked by git in this repo (check with
|
||||||
@@ -756,13 +817,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
|
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.
|
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
|
Before saving, run the prose bar (see **The prose bar** above) over the Summary and
|
||||||
the Summary and Additional Notes ONLY: puffery, superficial -ing phrases, "not just
|
Additional Notes ONLY. The pasted handoff is generated output and is never edited.
|
||||||
X but Y", vague attributions, rule-of-three padding. The pasted handoff is generated
|
Henry reads every one of these, and prose that reads as machine-generated costs the
|
||||||
output and is never edited, per the rule above. Skip unslop's "Adding soul" section
|
MR its credibility before anyone looks at the diff.
|
||||||
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.
|
|
||||||
9. Fill the Jira ticket fields directly, by issue type. Rich text fields are ADF:
|
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 markdown with `python3 ~/.agents/skills/review-ticket/scripts/review2adf.py
|
||||||
render <file.md>` and PUT via `{"fields": {...}}` to `/rest/api/3/issue/<KEY>`.
|
render <file.md>` and PUT via `{"fields": {...}}` to `/rest/api/3/issue/<KEY>`.
|
||||||
@@ -771,11 +829,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
|
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.
|
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
|
**Run the prose bar** (see **The prose bar** above) over every field before rendering
|
||||||
PM. Run the `unslop` skill's pattern detection over the markdown before rendering it
|
it to ADF, proof and mitigation cells included. Everything here is read by the Lead
|
||||||
to ADF, skipping its "Adding soul" section — Jira fields want plain, factual, and
|
and the PM.
|
||||||
specific. This does not apply to proof cells, which are references to artifacts and
|
|
||||||
test names rather than prose.
|
|
||||||
|
|
||||||
**Proof first.** Before filling any proof column, capture working feature proof
|
**Proof first.** Before filling any proof column, capture working feature proof
|
||||||
yourself wherever possible: run the app (`run` skill) and screenshot the real
|
yourself wherever possible: run the app (`run` skill) and screenshot the real
|
||||||
@@ -835,13 +891,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
|
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`.
|
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_10261` User Story Patch Notes: the same change in user-facing words.
|
||||||
- `customfield_10142` Working Feature Proof: ALWAYS fill this on a Story, never
|
- `customfield_10142` Working Feature Proof: leave it alone. Write nothing here.
|
||||||
leave it empty. It renders as its own panel in the ticket's testing section, so
|
This field is where Gib drops the proof attachments, and dragging files into it
|
||||||
an empty field reads as unfilled testing even when every test-table cell is
|
replaces whatever text is sitting there, so a written list is destroyed by the
|
||||||
complete (this happened on KACP-23143). At minimum it lists the files staged in
|
upload it was describing. Naming each artifact is already the job of the Test
|
||||||
`proof/` for Gib to attach, one line each saying what the artifact shows, plus a
|
Cases proof cells, which reference it by filename. Step 10 tells Gib which files
|
||||||
pointer that per-row proof lives in the Test Cases table. A headline artifact (a
|
to upload. Do not PUT this field on a Story.
|
||||||
demo capture, a before/after pair) leads the list when one exists.
|
|
||||||
|
|
||||||
**Spike** (deliverables, not code): no test or proof fields to fill. The output is
|
**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
|
the deliverables directory, and step 10 tells Gib which files to upload where (the
|
||||||
|
|||||||
Reference in New Issue
Block a user