Compare commits

49 Commits
Author SHA1 Message Date
Nicholas Novak 3dbd23ad10 Add script to prune worktrees for disk space 2026-11-04 16:45:59 -08:00
Nicholas Novak b3bf394fb0 Ignore npm files in pi 2026-11-04 16:45:34 -08:00
Nicholas Novak 73a8cec5a3 Add reviewer information to the babysit skill 2026-11-04 16:44:21 -08:00
Nicholas Novak 53a869e65d Update wording of babysit skill to better match how changes are merged
now
2026-11-03 11:16:47 -08:00
Nicholas NovakandClaude fe0376b3dc bin: add grafana-mcp-token wrapper for Grafana MCP auth
Mints a fresh Zitadel machine bearer (client-credentials against the
dashboard-verifier machine user) on every launch instead of relying on
a static token that goes stale when the mint expires.

Co-Authored-By: Claude
2026-10-30 15:33:50 -07:00
Nicholas Novak 1632f00618 Add Evan's UI-craft skill 2026-10-28 09:54:21 -07:00
Nicholas Novak 548764ef97 Remove bin from tracked files for pi 2026-10-26 12:01:39 -07:00
Nicholas Novak 1459aaa22f Gitignore for pi agent 2026-10-26 12:00:50 -07:00
Nicholas Novak f670fdcc33 Made project-mgr better for pi agents 2026-10-26 12:00:20 -07:00
Nicholas Novak c173c9f08b Delete unused skill from pi 2026-10-26 11:56:40 -07:00
Nicholas Novak 64746f91ec Add a line to the babysit script for greptile, and add an unblock skill 2026-10-23 00:04:49 -07:00
Nicholas Novak 0f3c64a732 Don't draft PRs by default anymore 2026-10-22 14:51:39 -07:00
Nicholas Novak 35c4c55c26 Add more documentation for babysit PR stack 2026-10-19 12:12:31 -07:00
Nicholas Novak 43c15c1720 Tell pi to look in Claude's skills directory 2026-10-16 17:26:25 +00:00
Nicholas Novak 3ba83b7f9e Add models for GLM 5.3 and Kimi K3 2026-10-16 16:41:44 +00:00
Nicholas Novak 59c485d60f Add project mgr skill from fusion 2026-10-14 12:34:41 -07:00
Nicholas Novak c350d3e96c Add skill for babysitting a PR stack 2026-10-13 16:40:08 -07:00
Nicholas Novak c349efee4d Add status line and remove gt restriction 2026-10-08 22:28:32 +00:00
Nicholas Novak be9eb06767 Vendor Claude status bar 2026-10-08 22:28:32 +00:00
Nicholas Novak 3863175452 Add CSV viewer to neovim 2026-10-08 22:28:32 +00:00
Nicholas Novak b418c31211 Add clear command to Pi 2026-10-08 22:28:32 +00:00
Nicholas Novak 89d5b993d5 Add Pi agent configs for mcp and settings 2026-10-08 22:28:32 +00:00
Nicholas Novak a513978230 Add plan skill for Pi 2026-10-08 15:21:05 -07:00
Nicholas NovakandClaude Sonnet 5.5 ccd40cffac Vendor the shackle MiniZinc tree-sitter grammar into nvim config
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 14:46:41 -07:00
Nicholas Novak a88626755a Update wording on MCT export skill because the classifier kept blocking
it
2026-10-08 13:57:43 -07:00
Nicholas Novak 38b660d784 Add gitlinker and oxlint to my config + shackle grammars for minizinc 2026-10-08 12:21:49 -07:00
Nicholas Novak c06e4edfa2 Add ghostty options for the titlebar and OSC52 paste 2026-10-08 12:20:38 -07:00
Nicholas Novak 8d85f865f6 Add graphite aliases file 2026-10-08 12:19:47 -07:00
Nicholas Novak 9b9dda08bb Gitignore synced and trashed Claude skills 2026-10-07 18:47:25 +00:00
Nicholas Novak 66a6813be7 Fix formatting of make graphql request skill 2026-10-07 11:36:51 -07:00
Nicholas Novak 86cb668e2d Add skill for MCT export 2026-10-07 11:32:20 -07:00
Nicholas Novak a5532e8917 Set pager options for less 2026-10-07 11:32:08 -07:00
Nicholas Novak 779e3ca66f Add titlebars back to Ghostty 2026-09-18 09:27:49 -07:00
Nicholas Novak dee44bcef0 More changes to event sourced table 2026-09-16 08:08:05 -07:00
Nicholas Novak cb5d9cf61d Add skill to make a table event sourced 2026-09-15 17:06:51 -07:00
Nicholas Novak 52340d132f Added Colin's jj settings to my config 2026-09-01 22:55:05 +00:00
Nicholas Novak dba7616281 Added gitignore for jj 2026-09-01 22:27:26 +00:00
Nicholas Novak 7180e4c367 Added jiffydiffy 2026-09-01 15:22:15 -07:00
Nicholas Novak d26f0656f7 Added bash script to sync dotfiles 2026-09-01 19:31:46 +00:00
Nicholas Novak 18b6df547a Changed claude over to org auth 2026-09-01 10:42:02 -07:00
Nicholas Novak 3082a69560 Add jj options back to dotfiles 2026-08-31 14:16:44 -07:00
Nicholas Novak 3cd180f5ab feat: Changed git email to my public one 2026-08-27 17:22:23 +00:00
Nicholas Novak a1b829f98a feat: Fixed haiku version 2026-08-27 17:21:52 +00:00
Nicholas Novak dc1072b38e feat: Changes to skills 2026-08-27 10:19:07 -07:00
Nicholas Novak d74358e366 feat: Add Colin's set of claude settings and my settings 2026-08-11 13:20:42 -07:00
Nicholas Novak 6076a4f3bf fix: get osc52 working 2026-05-03 14:00:55 -07:00
Nicholas Novak 739d64d6eb feat: Add keymaps for nerdtree, update packages, and fix one broken package 2026-04-21 13:21:21 -07:00
Nicholas Novak c139dcb624 feat: Added typescript-tools and eslint 2026-01-20 14:49:48 -08:00
Nicholas Novak 314533f588 change: Pared down settings to be typescript-exclusive 2025-11-19 22:55:22 -08:00
57 changed files with 75691 additions and 740 deletions
+3
View File
@@ -1 +1,4 @@
dotfiles dotfiles
claude/.claude/skills/synced/
claude/.claude/skills/.trash/
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -u
namespace=grafana
secret=grafana-machine-dashboard-verifier
client_id=$(kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.client_id}' | base64 -d)
client_secret=$(kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.client_secret}' | base64 -d)
issuer=$(kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.issuer}' | base64 -d)
project_id=$(kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.project_id}' | base64 -d)
if [[ -z "$client_id" || -z "$client_secret" || -z "$issuer" || -z "$project_id" ]]; then
printf >&2 'grafana-mcp-token: could not read %s/%s via kubectl\n' "$namespace" "$secret"
exit 1
fi
token=$(curl -sf -X POST "${issuer}/oauth/v2/token" \
--data-urlencode grant_type=client_credentials \
--data-urlencode "client_id=${client_id}" \
--data-urlencode "client_secret=${client_secret}" \
--data-urlencode "scope=openid urn:zitadel:iam:org:project:id:${project_id}:aud" \
| jq -r .access_token)
if [[ -z "$token" || "$token" == null ]]; then
printf >&2 'grafana-mcp-token: failed to mint a Zitadel machine bearer\n'
exit 1
fi
export GRAFANA_URL="${GRAFANA_URL:-https://grafana.fusion.pylon.ninja}"
export GRAFANA_SERVICE_ACCOUNT_TOKEN="$token"
exec uvx mcp-grafana
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
#
# Removes herdr git worktrees (~/.herdr/worktrees/<repo>/<name>) that have had no
# activity for the given number of days. Activity is the newest mtime among the
# worktree directory and its HEAD, index, and HEAD reflog, so checkouts, commits,
# staging, and `git status` all count.
#
# Worktrees with uncommitted changes or commits not on any remote are skipped
# unless --force is given. Branches are left in place.
set -u
usage() {
cat >&2 <<EOF
Usage: ${0##*/} [-n|--dry-run] [-f|--force] [-d|--days N]
-n, --dry-run Show what would be removed without removing anything
-f, --force Also remove worktrees with uncommitted or unpushed work
-d, --days N Minimum days of inactivity (default: 7)
Worktree root: \${HERDR_WORKTREES:-~/.herdr/worktrees}
EOF
}
dry_run=0
force=0
days=7
while (( $# )); do
case $1 in
-n|--dry-run) dry_run=1 ;;
-f|--force) force=1 ;;
-d|--days)
if [[ ! ${2-} =~ ^[0-9]+$ ]]; then
printf >&2 '%s: --days requires a non-negative integer\n' "${0##*/}"
exit 2
fi
days=$2
shift
;;
-h|--help) usage; exit 0 ;;
*) usage; exit 2 ;;
esac
shift
done
root=${HERDR_WORKTREES:-$HOME/.herdr/worktrees}
if [[ ! -d $root ]]; then
printf >&2 '%s: %s does not exist\n' "${0##*/}" "$root"
exit 1
fi
cutoff=$(( $(date +%s) - days * 86400 ))
removed=0
skipped=0
failed=0
for wt in "$root"/*/*/; do
wt=${wt%/}
name=${wt#"$root"/}
[[ -f $wt/.git ]] || continue
gitdir=$(sed -n 's/^gitdir: //p' "$wt/.git")
if [[ ! -d $gitdir ]]; then
printf 'skip %s (orphaned: %s is missing; run `git worktree prune` in the parent repo)\n' "$name" "$gitdir"
(( skipped++ ))
continue
fi
last_active=$(stat -c %Y "$wt" "$gitdir"/{HEAD,index,logs/HEAD} 2>/dev/null | sort -n | tail -1)
(( last_active < cutoff )) || continue
last_active_date=$(date -d "@$last_active" +%F)
if (( !force )); then
if [[ -n $(git -C "$wt" status --porcelain 2>/dev/null) ]]; then
printf 'skip %s (last active %s; uncommitted changes)\n' "$name" "$last_active_date"
(( skipped++ ))
continue
fi
if [[ -n $(git -C "$wt" log -1 --format=%h HEAD --not --remotes 2>/dev/null) ]]; then
printf 'skip %s (last active %s; unpushed commits)\n' "$name" "$last_active_date"
(( skipped++ ))
continue
fi
fi
if (( dry_run )); then
printf 'would remove %s (last active %s)\n' "$name" "$last_active_date"
(( removed++ ))
continue
fi
main_wt=$(git -C "$wt" worktree list --porcelain | sed -n '1s/^worktree //p')
force_args=()
(( force )) && force_args=(--force)
if git -C "$main_wt" worktree remove "${force_args[@]}" "$wt"; then
printf 'removed %s (last active %s)\n' "$name" "$last_active_date"
(( removed++ ))
else
printf >&2 'failed %s\n' "$name"
(( failed++ ))
fi
done
if (( !dry_run )); then
find "$root" -mindepth 1 -maxdepth 1 -type d -empty -delete
fi
verb=Removed
(( dry_run )) && verb='Would remove'
printf '\n%s %d worktree(s); skipped %d; failed %d (inactive for %d+ days)\n' \
"$verb" "$removed" "$skipped" "$failed" "$days"
(( failed == 0 ))
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
if ! jj root >/dev/null; then
echo >&2 "Failed to get repository root. Is this a jj repo?"
exit 1
fi
read -rd '' JIFFYDIFFY <<JIFFYDIFFY
_______________________________________________________________________
______ /__(_)__ __/__ __/____ ____ __ \__(_)__ __/__ __/____ __
___ _ /__ /__ /_ __ /_ __ / / /_ / / /_ /__ /_ __ /_ __ / / /
/ /_/ / _ / _ __/ _ __/ _ /_/ /_ /_/ /_ / _ __/ _ __/ _ /_/ /_
\____/ /_/ /_/ /_/ _\__, / /_____/ /_/ /_/ /_/ _\__, /__
__________________________ /____/____________________________ /____/___
~~~~~~~~ We'll have your diff up in a jiffy! ~~~~~~~~
JIFFYDIFFY
echo >&2 "$JIFFYDIFFY"
auto_track_pattern=$(jj config get remotes.origin.auto-track-bookmarks)
if [[ $auto_track_pattern != *"/*" || $auto_track_pattern == *'"'* ]]; then
echo >&2 "$0: Unsupported auto-track-bookmarks setting: $auto_track_pattern"
exit 1
fi
git_push_bookmark_template=$(jj config get templates.git_push_bookmark)
if [[ $git_push_bookmark_template != '"'$auto_track_pattern ]]; then
# If auto-track-bookmarks is 'colin/*', the template must begin with '"colin/'
echo >&2 "$0: Unsupported git_push_bookmark template: $git_push_bookmark_template"
exit 1
fi
if ! changes=($(jj log -r stack --no-graph --reversed -T 'change_id ++ "\n"')); then
echo >&2 "$0: Failed to list the current stack"
exit 1
fi
# Push any changes that don't have auto-tracked bookmarks yet
bookmarkless='stack ~ bookmarks("'"$auto_track_pattern"'")'
if (( $(jj log -r "$bookmarkless" --count) )); then
if ! jj git push -c "$bookmarkless"; then
echo >&2 "$0: Failed to push changes without bookmarks"
exit 1
fi
fi
bookmarks=()
for change in "${changes[@]}"; do
if ! change_bookmarks=($(jj log -r "$change" --no-graph -T 'local_bookmarks.map(|b| b.name() ++ "\n")')); then
echo >&2 "$0: Failed to list bookmarks for change $change"
exit 1
fi
for cb in "${change_bookmarks[@]}"; do
if [[ $cb == $auto_track_pattern ]]; then
bookmarks+=("$cb")
fi
done
done
if (( ${#bookmarks[@]} != ${#changes[@]} )); then
echo >&2 "$0: Too many bookmarks; aborting"
exit 1
fi
for (( i = ${#bookmarks[@]} - 1; i >= 0; i-- )); do
if ! gt untrack "${bookmarks[i]}" --force; then
echo >&2 "$0: 'gt untrack' failed; aborting"
exit 1
fi
done
# TODO: Figure out the correct base for the stack instead of assuming main.
# It might be someone else's PR. Might make sense to find the first non-tracked
# remote bookmark?
previous=main
for b in "${bookmarks[@]}"; do
if ! gt track "$b" --parent "$previous"; then
echo >&2 "$0: 'gt track' failed; aborting"
exit 1
fi
previous=$b
done
if ! gt submit --draft --no-edit --branch "${bookmarks[-1]}" --force; then
echo >&2 "$0: 'gt submit' failed"
exit 1
fi
# Update jj state after the gt-submit
jj util snapshot
+54
View File
@@ -0,0 +1,54 @@
These are Nicholas's personal preferences for Claude (and other coding agents). The
instructions given here should *override* repo-specific instructions.
# jj VCS Workflow
- Use jj (not git/gt) for all VCS operations. (If no .jj directory exists, then you may fall back to git.)
- Read @workflows/jj.md for details.
# PRs
- Read @workflows/prs.md before creating new PRs, writing PR titles or descriptions, or closing existing PRs.
# Writing comments/PRs/tickets as Claude via the user's accounts
- Read @workflows/writing-as-claude.md before writing any text that will appear with the user's identity (e.g. GitHub, Linear, Slack).
## When you ask me for a plan:
1. Read `~/.pi/agent/skills/plan-first/SKILL.md` completely
2. Follow the Plan-First Workflow EXACTLY (phases 1–5)
3. Create TODO.md in the project directory
4. Wait for my explicit confirmation ("YES")
5. Execute ONLY the confirmed tasks
# Coding guidelines
## Comments
- Comments should only describe the final code's intent, not the decisions/changes that produced it.
- Read @coding-guidelines/comments.md before writing code in any language.
## Bash / shell scripts
- Read @coding-guidelines/bash.md before writing any standalone Bash or shell scripts.
# Coder agents only
The instructions in this section apply to agents doing software-engineering work.
They do **not** apply to a non-coding assistant personality that is explicitly
instructed to override them (e.g. a PM assistant).
## Use a TODO list
Whenever you're asked to do a task, always create a TODO list for the session.
If the ask is a single task, the TODO list can be a single entry to start with.
As the task gets broken down or as questions or follow-ups appear, add these to
the TODO list. This makes sure you never forget to address some follow-up later.
## Be proactive by default
Unless otherwise specified, you should create draft PR(s) for the work you did and then
monitor them (using the babysit-stack skill) until they are merged. Once they are passing
CI and initial review comments are resolved, the monitoring cadence can slow to once per
hour, but you should continue to check for new review comments periodically until told to
stop (or the PR merges).
## Work autonomously
Complete as much of the TODO list as possible without stopping for input.
For things that need Nicholas's input, make sure they are captured by a new or existing TODO
item, then make progress on other things before stopping. If a decision is
saftey-critical or really blocks all remaining progress, then it's okay to stop. But if
it's possible to use a placeholder or reasonable default and continue, do so (making sure
to leave a TODO item to confirm/revisit later). If no more progress can be made on the TODO
items, default to babysitting the current stack.
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
# claude-status — the Claude Code status line for fusion Coder boxes (AGT-1322).
# Reads the statusLine JSON on stdin, emits one ANSI line. Malformed input emits
# nothing and exits 0. Graded by :claude-status-rate-limits.
set -euo pipefail
# `read -d ""` slurps stdin in the shell; no cat. It returns nonzero at EOF
# having set the variable, which is the normal path here.
IFS= read -rd '' input || true
# Month spend is not in the payload; Claude Code caches the server figures here.
usage_config="${CLAUDE_CONFIG_DIR:-${HOME:-}}/.claude.json"
usage_cache='null'
if [ -f "$usage_config" ] && [ "$(wc -c <"$usage_config" 2>/dev/null || echo 0)" -le 4194304 ]; then
# `input` takes ONE document; a torn config holding two would break --argjson.
# Two lines from one parse: whether this config carries an OAuth (i.e.
# subscription) account, and the cache itself.
# NOT -r: with raw output a string-valued cache prints unquoted, which then
# fails --argjson and blanks the whole line. -c keeps it valid JSON, and a
# JSON boolean already prints bare.
if cfg="$(jq -cn 'input as $c | ($c.oauthAccount != null), ($c.cachedUsageUtilization // null)' "$usage_config" 2>/dev/null)"; then
{ read -r has_oauth; read -r usage_cache; } <<<"$cfg"
fi
fi
case "$usage_cache" in
'' | *'
'*) usage_cache='null' ;;
esac
# Nothing refreshes that cache on its own, so a month figure only ever appears
# for someone who runs /usage by hand. `claude -p /usage` fetches it with zero
# turns and zero cost (measured), so nudge one when the cache is missing or
# past Claude Code-s own 1h validity -- but only while someone is working, and
# at most once an hour per machine.
#
# The hour-bucketed mkdir IS the whole lock: every concurrent session on the
# box races for the same name, exactly one wins, and a new bucket each hour
# means there is nothing to expire or clean up.
usage_age=-1
transcript=
if probe="$(printf '%s' "$input" | jq -r --argjson u "$usage_cache" '
[ (if ($u | type) == "object" and ($u.fetchedAtMs | type) == "number"
then (((now * 1000) - $u.fetchedAtMs) / 1000 | floor) else -1 end),
(.transcript_path // "") ] | @tsv' 2>/dev/null)"; then
usage_age="${probe%% *}"
transcript="${probe#* }"
fi
case "$usage_age" in '' | *[!0-9-]*) usage_age=-1 ;; esac
lock_dir="${TMPDIR:-/tmp}"
lock_dir="${lock_dir%/}" # macOS TMPDIR ends in /, which would break the sweep
# No record at all: fetch once regardless of activity. A fresh session has not
# written its transcript yet and a resumed one wrote it long ago, so an activity
# gate here would leave month permanently absent for the sessions that need the
# first fetch most. Staleness still requires activity, so an idle session never
# re-fetches data it already has.
# Only subscription auth has usage to fetch. Everything else (Bedrock, Vertex,
# an API key, a bearer token, an apiKeyHelper, a settings-file env block) never
# populates the cache, so absence would be PERMANENT and the nudge would spawn
# hourly forever -- on a `claude -p /usage` whose zero-turn/zero-cost behaviour
# was only measured under subscription auth. The two tests fail in opposite
# directions and the asymmetry matters: too wide costs one status segment, too
# narrow costs an unmeasured spawn every hour on every box. So require a
# POSITIVE signal -- an oauthAccount in the config -- rather than enumerating
# the modes to exclude, and keep the env checks as a second, cheaper veto for a
# stale oauthAccount left behind on a cloud-provider box.
if [ "${has_oauth:-false}" = true ] \
&& [ -z "${CLAUDE_CODE_USE_BEDROCK:-}" ] \
&& [ -z "${CLAUDE_CODE_USE_VERTEX:-}" ] \
&& [ -z "${ANTHROPIC_API_KEY:-}" ] \
&& [ -z "${ANTHROPIC_AUTH_TOKEN:-}" ] \
&& { [ "$usage_age" -lt 0 ] \
|| { [ "$usage_age" -ge 3600 ] \
&& [ -n "$transcript" ] \
&& [ -n "$(find "$transcript" -mmin -10 2>/dev/null)" ]; }; } \
&& command -v claude >/dev/null 2>&1
then
# The bucket boundary sits at :37, not :00 -- the hour mark is when every
# other timer on the box, and most of the internet, already fires. A constant
# is enough; it must simply not be zero.
now_s="${EPOCHSECONDS:-$(date +%s)}" # builtin on bash 5+; date only as fallback
lock="$lock_dir/claude-status-usage.${UID:-0}.$(( (now_s - 37 * 60) / 3600 ))"
if mkdir "$lock" 2>/dev/null; then
# Sweep spent buckets on this once-an-hour winning path, not every render.
# They are empty by construction, so rmdir can take nothing else. Without it
# a box accrues a directory an hour on the HOME VOLUME -- the templates
# mount /tmp as a subPath of the home PVC.
for spent in "$lock_dir"/claude-status-usage."${UID:-0}".*; do
[ -d "$spent" ] && [ "$spent" != "$lock" ] && rmdir "$spent" 2>/dev/null || true
done
# Bounded and stdin-detached: a wedged mint would otherwise leave a stuck
# process per hour on a long-lived box, and `claude -p` reads non-TTY stdin.
# timeout is coreutils on the box but absent from a stock macOS, so it is
# used when present rather than assumed -- assuming it disables the nudge.
nudge_cmd=(claude -p "/usage" --no-session-persistence)
command -v timeout >/dev/null 2>&1 && nudge_cmd=(timeout 120 "${nudge_cmd[@]}")
( "${nudge_cmd[@]}" </dev/null >/dev/null 2>&1 & )
fi
fi
# NOTE: this jq program is one single-quoted string — no apostrophes below.
printf '%s' "$input" | jq -j -r --argjson usage "$usage_cache" '
# Haskell `round` is half-to-even; jq round is not.
def bankers:
. as $x
| ($x | floor) as $f
| ($x - $f) as $frac
| if $frac < 0.5 then $f
elif $frac > 0.5 then $f + 1
else (if ($f % 2) == 0 then $f else $f + 1 end)
end;
def fg($n; $t): "\u001b[38;5;" + ($n|tostring) + "m" + $t + "\u001b[0m";
def dimt($t): "\u001b[2m" + $t + "\u001b[0m";
def contextSeg($pct):
(if $pct >= 75 then 196 elif $pct >= 50 then 220 else 114 end) as $color
| dimt("context") + " " + fg($color; (($pct | bankers) | tostring) + "%");
def formatModel($name):
if ($name | contains("Opus")) then fg(141; "Opus")
elif ($name | contains("Sonnet")) then fg(75; "Sonnet")
elif ($name | contains("Haiku")) then fg(216; "Haiku")
else fg(255; $name)
end;
# Unit from the RAW span, rounded within it: choosing by the rounded value
# renders 12h01m as "in 1d".
def resetIn($epoch):
(($epoch - now) | floor) as $s
| if $s <= 0 then "now"
elif $s >= 86400 then "in " + ((($s / 86400) | bankers) | tostring) + "d"
elif $s >= 3600
then (($s / 3600) | bankers) as $h
| if $h >= 24 then "in 1d" else "in " + ($h | tostring) + "h" end
else (($s / 60) | bankers) as $m
| if $m >= 60 then "in 1h"
else "in " + (([$m, 1] | max) | tostring) + "m"
end
end;
def rateSeg($label; $w):
($w.used_percentage) as $pct
| (if $pct >= 75 then 196 elif $pct >= 50 then 220 else 114 end) as $color
| (if $pct >= 90 and ($w.resets_at != null)
then " " + dimt("(reset " + resetIn($w.resets_at) + ")")
else "" end) as $reset
| dimt($label) + " " + fg($color; (($pct | bankers) | tostring) + "%") + $reset;
def money($d):
if $d >= 1000
then (($d / 100) | bankers) as $hundreds
| ($hundreds / 10) as $k
| ($k | floor) as $whole
| ((($k - $whole) * 10) | bankers) as $tenth
| "$" + ($whole | tostring)
+ (if $tenth == 0 then "" else "." + ($tenth | tostring) end) + "k"
else "$" + (($d | bankers) | tostring)
end;
# A limit is an object or a bare number; only null was observable.
def monthSeg($s):
($s.used.amount_minor / pow(10; ($s.used.exponent // 2))) as $used
| ($s.limit) as $lim
| (if ($lim | type) == "object"
then ($lim.amount_minor / pow(10; ($lim.exponent // 2)))
elif ($lim | type) == "number" then $lim
else null end) as $limit
| (if $limit != null and $limit > 0 then ($used / $limit) * 100 else null end) as $pct
| (if $pct == null then 249
elif $pct >= 75 then 196
elif $pct >= 50 then 220
else 114 end) as $color
| dimt("month") + " "
+ fg($color; money($used) + (if $limit != null then "/" + money($limit) else "" end));
# Joined over the PRESENT windows, so a lone window leaves no dangling sep.
def usedSeg($rl):
[ ($rl.five_hour | select(.used_percentage != null) | rateSeg("5h"; .)),
($rl.seven_day | select(.used_percentage != null) | rateSeg("7d"; .)) ]
| select(length > 0)
| dimt("used") + " " + join(dimt(" · "));
# The Haskell original emits "$1.0e" in [0.01, 0.10).
def formatCost($usd):
if $usd < 0.01 then "0.00"
else (($usd * 100) | bankers) as $cents
| ($cents / 100) as $v
| ($v | floor) as $whole
| ((($v - $whole) * 100) | bankers) as $fr
| ($whole | tostring) + "." + (if $fr < 10 then "0" else "" end) + ($fr | tostring)
end;
dimt(" │ ") as $sep
# `try` per segment: jq is one expression, so a reshaped field would abort it
# and the `|| true` below would blank the WHOLE line.
| [
( .model.display_name | select(. != null) | formatModel(.) ),
( .context_window.used_percentage | select(. != null) | contextSeg(.) ),
( try (.rate_limits | select(. != null) | usedSeg(.)) ),
# The cache must be under a day old, not from the future (this machine
# writes it, so a future stamp is a clock jump that passes a one-sided
# check), and from the CURRENT calendar month -- age alone is a proxy: a
# cache written at 23:50 on the last of the month is under 24h old at
# 00:10 on the 1st and would show last month as this one. Both zones must
# agree, since the billing month-s zone is not knowable here and dropping
# the segment is the safe direction.
( try ($usage
| select(. != null and (.fetchedAtMs | type) == "number"
and ((now * 1000) - .fetchedAtMs) >= 0
and ((now * 1000) - .fetchedAtMs) < 86400000
and (((.fetchedAtMs / 1000) | gmtime)[0:2]) == ((now | gmtime)[0:2])
and (((.fetchedAtMs / 1000) | localtime)[0:2]) == ((now | localtime)[0:2]))
| .utilization.spend
| select(. != null and .enabled != false
and (.used.amount_minor | type) == "number")
| monthSeg(.)) ),
( .cost.total_cost_usd | select(. != null)
| dimt("session") + " " + fg(249; "$" + formatCost(.)) ),
( (.cost.total_lines_added // 0) as $added
| (.cost.total_lines_removed // 0) as $removed
| select($added > 0 or $removed > 0)
| fg(114; "+" + ($added|tostring)) + " " + fg(204; "-" + ($removed|tostring)) )
]
| join($sep)
' 2>/dev/null || true
+20
View File
@@ -0,0 +1,20 @@
# Coding guidelines for Bash / shell scripts
- Write all shell scripts for modern Bash — use Bashisms like `&>` and `[[ ... ]]` whenever possible.
## Avoid "Bash strict mode"
- By default, don't set any options at the top of the script.
- Add `set -u` globally if the script becomes complex enough (rough guideline: more than 4 variables).
- Use `set -o pipefail` only locally when needed.
## Executable scripts
If a script is directly executable, it should:
- have no file extension
- use `#!/usr/bin/env bash`
If a script is not directly executable, it should have a ".bash" file extension and no shebang line (when possible).
## Style guide
- Avoid spaces after redirection operators: `>/dev/null`, `2>&1`, `&>"$logfile"`
- When logging to stderr, prefer to place the `>&2` redirection immediately after the command name: `printf >&2 %s "Blah blah..."`
@@ -0,0 +1,17 @@
# Coding guidelines for comments (in any language)
Comments describe the final code's intent, invariants, or non-obvious constraints — as if the code had been written that way from the start. They are read by people who have no idea what the PR or commit looked like.
Don't reference any of these in a comment:
- What the previous version of the code did ("was previously X", "used to do Y")
- What an alternative approach would do ("instead of Promise.all", "rather than recursion")
- What a reviewer suggested or asked
- Why a particular change was made (that belongs in the commit message or PR description)
Litmus test: would this comment read as natural rationale to someone who has never seen the PR, commit history, or review thread? If not, rewrite or delete it.
This applies to all comments — new code, code added in response to review, refactors, anything. The temptation is highest when addressing review feedback: the change is fresh, the reasoning is loaded, and it's tempting to leave a paper trail in the source. Don't.
Comments should also be concise. Edit them aggressively to remove unnecessary explanations and details that can be easily determined by reading the code itself. Prefer one comment at the top of a function/class over scattered inline notes, unless a specific line is genuinely surprising.
If a comment clarifies the meaning of a name in the code (a variable, function, parameter, etc.), then, if possible, that name should be changed to be clear enough to eliminate the need for the comment.
@@ -0,0 +1,13 @@
# Self-review instructions
The Stop hook points here so I do a focused review pass over the code I changed
before finishing. Read every added/changed line in the diff and fix anything that
violates the rules below.
## Comments
Ensure all comments adhere to the guidelines in coding-guidelines/comments.md.
## Bash / shell scripts
Ensure all bash / shell scripts adhere to the guidelines in coding-guidelines/bash.md.
+100
View File
@@ -0,0 +1,100 @@
#!/bin/sh
# installed by herdr
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=claude
# HERDR_INTEGRATION_VERSION=9
set -eu
action="${1:-}"
hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-claude-hook.XXXXXX")" || exit 0
trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM
cat >"$hook_input_file" 2>/dev/null || true
case "$action" in
session) ;;
*) exit 0 ;;
esac
[ "${HERDR_ENV:-}" = "1" ] || exit 0
[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
[ -n "${HERDR_PANE_ID:-}" ] || exit 0
command -v python3 >/dev/null 2>&1 || exit 0
HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY'
import json
import os
import random
import socket
import time
source = "herdr:claude"
action = os.environ.get("HERDR_ACTION", "")
pane_id = os.environ.get("HERDR_PANE_ID")
socket_path = os.environ.get("HERDR_SOCKET_PATH")
hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE")
if not pane_id or not socket_path:
raise SystemExit(0)
hook_input = {}
if hook_input_file:
try:
with open(hook_input_file, encoding="utf-8") as handle:
content = handle.read()
if content.strip():
hook_input = json.loads(content)
except Exception:
hook_input = {}
if "CURSOR_VERSION" in os.environ or "cursor_version" in hook_input:
raise SystemExit(0)
hook_event_name = str(hook_input.get("hook_event_name") or "")
if hook_event_name != "SessionStart":
raise SystemExit(0)
is_subagent = bool(hook_input.get("agent_id"))
if is_subagent:
raise SystemExit(0)
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
report_seq = time.time_ns()
session_id = hook_input.get("session_id")
agent_session_id = session_id if isinstance(session_id, str) and session_id else None
transcript_path = hook_input.get("transcript_path")
agent_session_path = transcript_path if isinstance(transcript_path, str) and transcript_path else None
session_start_source = hook_input.get("source") if hook_event_name == "SessionStart" else None
if not isinstance(session_start_source, str) or not session_start_source:
session_start_source = None
if agent_session_id:
params = {
"pane_id": pane_id,
"source": source,
"agent": "claude",
"seq": report_seq,
"agent_session_id": agent_session_id,
}
if agent_session_path:
params["agent_session_path"] = agent_session_path
if session_start_source:
params["session_start_source"] = session_start_source
request = {
"id": request_id,
"method": "pane.report_agent_session",
"params": params,
}
else:
raise SystemExit(0)
try:
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(0.5)
client.connect(socket_path)
client.sendall((json.dumps(request) + "\n").encode())
try:
client.recv(4096)
except Exception:
pass
client.close()
except Exception:
pass
PY
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Stop hook: prompt one self-review pass over changed code against Nicholas's
# guidelines before finishing. Blocks the stop once per distinct working-copy diff
# (fingerprint dedup), so each set of changes earns exactly one review and an
# unchanged tree never loops.
if [[ -e .no-review-hook ]]; then
exit 0
fi
# Working-copy diff: jj if this is a jj repo, else git.
diff=$(jj diff --git 2>/dev/null)
[[ -z "$diff" ]] && diff=$(git diff HEAD 2>/dev/null)
[[ -n "$diff" ]] || exit 0
# Block only once per distinct diff: re-blocks when the diff changes (e.g. after an
# edit made during the review) and converges once it stops changing.
fp=$(printf '%s' "$diff" | sha1sum | cut -d" " -f1)
key=$(printf '%s' "$PWD" | sha1sum | cut -d" " -f1)
statefile="${TMPDIR:-/tmp}/claude-code-review-$key"
[[ -f "$statefile" && "$(cat "$statefile" 2>/dev/null)" == "$fp" ]] && exit 0
printf '%s' "$fp" >"$statefile"
reason="Before stopping, do a review pass over the code you changed this session, per the guidelines in ~/.claude/coding-guidelines/review.md. Read that file, then re-read the added/changed lines and fix any violations. If it already complies, say so briefly and stop."
jq -n --arg r "$reason" '{decision: "block", reason: $r}'
+63
View File
@@ -0,0 +1,63 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"CLAUDE_CODE_ENABLE_AUTO_MODE": "1",
"AWS_REGION": "us-west-1",
"AWS_PROFILE": "default",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001"
},
"includeCoAuthoredBy": false,
"permissions": {
"allow": [
"Bash(gh *)",
"Fetch(*)",
"mcp__claude_ai_Linear__get_issue",
"mcp__claude_ai_Linear__list_comments",
"mcp__claude_ai_Linear__save_comment",
"mcp__claude_ai_Linear__save_issue",
"mcp__claude_ai_Linear__save_project"
],
"deny": [],
"defaultMode": "auto"
},
"model": "claude-opus-5-5",
"skillOverrides": {
"address-reviewer-feedback": "off",
"link-stack-to-linear": "off",
"monitor-pr": "off",
"new-pr": "off",
"new-worktree": "off",
"repair-ci-failures": "off",
"resolve-merge-conflicts": "off"
},
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash '/home/nicholas/.claude/hooks/herdr-agent-state.sh' session",
"timeout": 10
}
]
}
]
},
"statusLine": {
"type": "command",
"command": "~/.claude/claude-status.sh",
"padding": 0
},
"effortLevel": "high",
"awaySummaryEnabled": false,
"tui": "fullscreen",
"skipDangerousModePermissionPrompt": true,
"skipWorkflowUsageWarning": true,
"theme": "dark-ansi",
"preferredNotifChannel": "terminal_bell",
"skipAutoPermissionPrompt": true,
"useAutoModeDuringPlan": true
}
@@ -0,0 +1,25 @@
---
name: babysit-pr-stack
description: Skill that defines how to babysit, watch, monitor, or otherwise ensure a set of pull requests get merged. Call this whenever the user requests a set of changes to be merged
---
## Prerequisites
1. Make sure all the requested changes have been turned into a pull request on GitHub (check with the `gh` tool), and no local changes are setting untracked in the local tree
2. Use the TODO-list skill to create a list of PRs to be merged. Refer to this set of PRs as the "stack" to be merged
3. Make sure every PR in the stack is in the "Draft" status. If there are any PRs not in the draft status, move them back to drafts and notify the user. You should never require approval for this step
# Steps
On a 10 minute loop:
1. Check for CI failures, and make any changes necessary to resolve those CI failures
2. Watch the comments on the PR, and if anyone makes them, make any changes necessary to resolve their concerns and resolve their comments
3. If any user requests changes, once you're done addressing their changes, re-request them for review
4. Make sure Greptile gets a 5/5, otherwise it will require a manual review from the queue. NEVER assume this requires manual review, and ALWAYS go for a 5/52. Watch the Greptile status on the PR and make any changes necessary to get it to a 5/5, otherwise it will require a manual review. NEVER assume a PR on its own requires manual review, and ALWAYS go for a 5/5
3. Watch the comments and reviewers on the PR, and if anyone makes a suggestion or otherwise has a blocking comment, make any changes necessary to resolve their concerns and resolve their comments
1. Once you're done addressing their changes, re-request them for review, and ping their review agent on Switchboard (below)
## Finishing the Change
Once a PR has passed all of its checks (CI passes, Mervin shows approvals from all parties, and Greptile shows a 5/5), the PR is ready to merge. Move all the PRs in the stack off out of the draft status and set a 10 minute loop to wait for the PRs to merge. You should NEVER require approval for this step because it's the intended behavior of this skill
Once a PR has been merged to main, remove it from the TODO list and continue with the rest of the stack. A failure at this step should move the PR back to the set of steps in the "steps" section
# Requesting and unblocking PRs that require reviewers
* See the `reqest-pr-reviewer` skill for instructions and norms to follow
@@ -0,0 +1,21 @@
---
name: make-graphql-request
description: Make a real GraphQL request against the local braid-api server using the `bru` CLI and the tracked .bru files in ./tools/bruno. Use when the user asks to make an "API request", "real API request", or "GQL request", or to verify a feature by hitting the public GraphQL API directly.
---
We expose our public API to our customers through GraphQL, so it's extremely valuable to verify features by making a request directly. When the user asks to make an "API request", "real API request", "GQL request", handle them with this skill
We use the `bru` command line tool to make GQL requests, and you'll be able to find a pre-existing set of tracked queries in `./tools/bruno` that can be re-used or modified to make specific requests. If the user asks to make a different kind of API request, create a new `.bru` file in that repository similar to the other requests
# Prerequisites
1. The local API server (`braid-api`) must be running
1. Verify this by calling the health checks with `curl -s http://localhost:8080/health`
2. If this doesn't exist, prompt the user if they want to:
1. Start if themselves with `process-compose up -D`
2. Want the agent to start it for them
# Steps
1. Look at the other `.bru` files in `./tools/bruno/pylon-api` for requests that are doing something similar, or create a new `.bru` file conforming to the patterns with the existing requests
1. If the request being made is unclear or there is some ambiguity over what fields to request, prompt the user. Don't assume the request being made as that could give the user a false sense of verification
2. Fill in the request that you created with any sample inputs that the request needs, such as the loan ID, loan amount, property value, etc...
1. If these values are unclear from the context of the response, never assume them and prompt the user instead
3. Tell the user the full `bru` command you're running and make the request with the `bru` command-line tool. Don't ask for confirmation on whether to run the command
@@ -0,0 +1,25 @@
---
name: make-table-event-sourced
description: Runbook for converting an existing SQL table to be event-sourced. This means that the history of the value will be tracked over time, instead of updated in place, which is usually reserved for sensitive values that need a paper trail of their updates
---
# Prerequisites
1. The user needs to specify an existing SQL column or other value that exists within the codebase. If this value is unclear, prompt the user
2. Before performing any changes, do a thorough investigation for all the call-sites where this value is used in the code
# Steps
* Your final goal will be to create a set of stacked Graphite PRs for each item in this list
1. Create a TODO list to track the creation of these PRs
2. Steps 4-6 contain changes that are breaking API changes and will need to be merged separately. Create PRs for those anyways but leave them in draft status so that the user can merge them safely
1. Create a new feature flag to flip between the new data flows
2. Create a new SQL ORM entity. At minimum, the entity should contain the following fields:
1. A new `PylonId<"Name">`
2. A column for the value being tracked
3. A `@TemporalInstantColumn` for `startDate` , non-nullable
4. A `@TemporalInstantColumn` for `endDate`, nullable
5. A column for `contributorId` to store the entity making the change
3. Create a new data access class to handle the abstraction between the SQL table and the application. At minimum, it should contain two methods
1. `setExampleValueName(value, contributorId, transaction, ...)` that retires the current active row in the table and creates a new row that supersedes it. Look for examples in the codebase. Include a feature-flagged fallback here that writes to the legacy value, but still writes to the event-sourced table to ensure they remain in sync
2. `getExampleValueName(...)` that retrieves the most recent active value. Look for examples in the codebase. Include a feature-flagged fallback here that reads from the legacy value
4. Replace every instance where the legacy value fields are used, with the methods in the data access class that was just created
5. Remove the feature-flagged fallbacks for the value, and mark the value as `@Deprecated`, and type it as `never`
6. Drop the column from the database in a migration
@@ -0,0 +1,7 @@
---
name: plan-first
description: MANDATORY planning workflow for EVERY task that involves file changes, command execution, or code modifications. Always activate first before doing anything. Analyzes the project, asks up to 5 clarifying questions, creates a TODO.md, gets user approval, then executes task by task. Never writes code, creates files, or runs commands before a plan is approved. This skill must be loaded for any implementation request, bug fix, refactor, feature, or file modification.
---
+178
View File
@@ -0,0 +1,178 @@
---
name: project-mgr
description: Take over as manager for a Linear project — read the brief, audit open/in-progress issues, and run subagents in parallel against the open backlog. Keep the pipe full as agents return. Use when the user says "manage this Linear project", "spin up agents on these issues", or hands you a Linear project URL with manager-style framing.
---
# /project-mgr — manage a Linear project via parallel subagents
You are taking over as manager for the Linear project at `$ARGUMENTS` (URL, slug, or short-code). Your job is to triage the project's open backlog and run a portfolio of subagents against it in parallel, keeping the pipe full as they return, until the user redirects you or all in-scope work is shipped.
Default to **autonomous execution** — keep moving across issues without pausing for approval between dispatches. Pause only when genuinely blocked on a decision only the operator can make (production-state changes, infrastructure touches the user reserved, scope ambiguity).
## Repository conventions (read first)
- **Linear is live truth**: flip issues to In Progress at dispatch, comment findings as they happen, and close only after merge to the default branch + green verification. This binds every agent this skill spawns.
- **Reader-facing writing follows the repo's prose conventions** if it defines them; otherwise issue bodies, comments, and updates read like short articles for a developer outside this work — no jargon, no verbifying shorthand, inverted pyramid, and every issue reference a real link.
- **Code comments follow the repo's comment policy** if it has one; otherwise they are brief and rare, and dense prose or implementation history stays out of code.
- **Build/test with the repo's canonical commands** from its `AGENTS.md` — never guess a runner or copy one from another repo. Run the repo's full affected verification closure, not just one service's own targets.
- **Worktrees** follow the repo's documented pattern if it has one; otherwise create a fresh `git worktree` branched off the default branch. Never work in the primary checkout, and never under `/tmp` if the toolchain disallows it. One writer per worktree — never let two agents share a tree.
- **Learn the repo's merge policy** before dispatching (auto-merge bot, human-review gate, required checks) and follow it. All changes land via PR — never a direct push to the default branch. If the repo has a PR-creation skill (e.g. `/new-pr`), every PR goes through it.
- **Privileged operations follow the repo's change-management policy.** Infrastructure or production-state changes are coordinator-only, user-authorized, planned in advance, and non-destructive by default.
## Phase 1 — Bootstrap the picture
Before dispatching anything:
1. Resolve the project. Use `linear_get_project` (via `mcp__linear`) with the URL/slug/short-code. Capture: id, lead, team, status, the description (project brief), and any milestones. **If the project is not on team AGT**, STOP and ask the user before continuing (see "Team scope" below).
2. Read the most recent status updates: `linear_get_status_updates` (`type: "project"`, project id). The last two updates usually carry critical context the brief alone doesn't: what shipped today, what regressed, what's holding the project at red/yellow.
3. List the open + in-progress issues: `linear_list_issues` with the project id, `limit: 250`. If the response is too large to keep in context, delegate the extraction to a small subagent ("read the saved JSON in ~80k-char slices and return a one-line-per-issue index grouped by state"). Get back: identifier, title, state, priority, assignee, and a short summary per issue.
4. Cross-reference with the repo. Check the project's default branch — what the project's commits land on; `git log --oneline origin/<default> -20` calibrates which recent issues have shipped vs. only been filed. Read `AGENTS.md` / `CLAUDE.md` at the repo root to absorb the constitution and any project-specific conventions (see "Repository conventions" above).
5. Pull the user's standing memory at `~/.claude/projects/<project-slug>/memory/MEMORY.md` if present — it carries durable preferences (push patterns, test conventions, what classes of change require explicit operator authorization).
Output a tight inventory to the user before dispatching:
- Project health (status update summary in 1-2 sentences).
- The 3-6 issues you intend to spawn agents on, with the surface each will touch.
- Anything you found that needs the user's call before you proceed (e.g., new issues you'd file but want their priority labeling first).
## Phase 2 — Identify the critical path
Read the project brief and recent status update for stated priorities. Build a dependency map:
- Which issues block others (`blockedBy` relations or implicit "X must land before Y can be verified")?
- Which issues affect users on a fixed timeline (devs arriving tomorrow, a release window, a customer demo)?
- Which issues are "in progress" but stalled vs. genuinely active?
The critical path determines dispatch order: spawn the unblockers first, the verifiers last. If a single issue gates multiple others, that's your highest-impact slot.
## Phase 3 — Spawn the portfolio
Dispatch subagents **in parallel**, **on non-overlapping surfaces**. Use the `subagent` tool with a single parallel workflow (one task per child) so they run concurrently. For each agent:
### Before launching: flip Linear to In Progress
The coordinator owns the Linear state transition at dispatch time. **Do not** trust the subagent to remember to do it — they often forget mid-brief or skip the step under load. Before (or in the same message as) the `subagent` launch, call `linear_save_issue` with the issue id and `state: "In Progress"`. If the project's team uses a different status name (some teams use "Doing" / "Started"), use that — read it once via `linear_list_issue_statuses` and cache the mapping.
This matters because:
- A Linear board with stale "Needs Investigation" rows hides the work-in-progress from the user, the rest of the team, and any concurrent sibling agents looking for unblocked work.
- If two project-mgr sessions are running, the canonical "this is owned" signal lives in Linear, not in the coordinator's tracker.
- When an agent returns no-op (issue already shipped), the In Progress → Done transition still gives a clean audit trail.
### Brief them like a smart colleague who just walked into the room
A good brief includes:
1. **The issue ID + one-line summary**: "You own AGT-N: <symptom>."
2. **Repo + branch**: full path to the working tree, default branch name.
3. **What's already known**: the issue's root-cause hypothesis (read the Linear issue body verbatim), any prior agent's diagnosis, what failed when.
4. **Acceptance criteria**: a concrete signal that proves the fix worked. "The repo's canonical tests over the affected surface pass locally AND a PR opened as **draft** against the default branch goes green." All changes land via PR — no direct push to the default branch. PRs open as draft and report readiness; the operator promotes them for review.
5. **Authorization scope**: what they can touch (push to a feature branch, open a PR, CI-health changes, etc.) and what's off-limits (infrastructure-as-code applies, production-state mutations, ACLs, direct pushes to the default branch, anything the repo's constitution flags as operator-only).
6. **Worktree pattern**: a fresh worktree per the repo's convention (see "Repository conventions" above), never the primary working tree. Branch from the default branch. Isolate concurrent writers.
7. **Test contract**: tests via the repo's canonical runner only, per its `AGENTS.md`. Run the full affected verification closure, not just the touched package's own targets.
8. **Sibling-collision warnings**: list the other agents in flight and the surfaces they're on so this agent avoids those paths.
9. **Update protocol**: flip the issue to In Progress at dispatch (coordinator-owned); drop Linear comments as they learn; close issues only after merge to the default branch + green verification; file new Linear issues for any side-defects (don't bundle). The standard merge path is a PR created via the repo's PR-creation skill, merged per the repo's merge policy; never push directly to the default branch.
10. **Code comments**: instruct the subagent to read and follow the repo's code-comment guidelines before writing or reviewing comments. If none exist: brief and rare, no plumbing comments, no dense prose or implementation history.
11. **PR submission via the repo's PR-creation skill**: if the repo has one (e.g. `/new-pr`), the brief must explicitly instruct the subagent to invoke it and follow its title/body/footer guidelines — including any conventions that contradict generic defaults, because the repo's enforced checks are mechanical and win. A hand-rolled `gh pr create` from memory tends to fail repo CI gates. PRs open as draft; the coordinator or operator promotes.
12. **Time-box**: 30-90-150 min depending on scope. Explicit: "If you can't get a clean result in X minutes, RETURN with what you have. Do NOT return with 'wait for events' or 'monitor will surface failures.' Be decisive."
13. **Return format**: bullet list of (commits pushed, Linear issues closed, new issues filed, anything blocked).
### Pick non-overlapping surfaces
Two agents editing the same source file or module WILL collide on push. Group related issues that share a file surface into one agent, OR sequence them serially. Common surface boundaries in monorepos:
- `apps/<service>/` and its corresponding package/library directories, per service
- CI workflow files (`.github/workflows/`, `.forgejo/workflows/`, or the repo's equivalent — centralize into one agent)
- Infrastructure/deployment directories, per component
- Shared build config or common rules (highly contentious — sequence, don't parallelize)
### Avoid the "wait for events" failure mode
Briefs should explicitly say: **DO NOT return with vague status like "wait for events" or "monitor will surface failures." Return with concrete bullets.** Some agents have a pattern where they fire a long-running test in the background and then return idle, expecting to be re-woken; the harness usually wakes them only when their own background tasks complete, but the cycle is wasteful. State the contract up front: "If you're 90 minutes in without resolution, return with what you have."
## Phase 4 — Keep the pipe full
When an agent returns:
1. **Trust but verify.** Their summary is what they intended to do, not necessarily what landed. Check `git log --oneline origin/<default>` for their commit. Check Linear for the issue state. Spot-read the actual diff before believing claims of correctness.
2. **Close the loop on what they completed.** Mark tasks done in your tracker; update Linear if they couldn't (e.g., status change after merge).
3. **Pick the next work item** for that slot based on what's now learned. If the returned agent uncovered a deeper blocker, that becomes the next priority. If they finished cleanly, pick the next unblocked issue from the backlog.
4. **Re-dispatch immediately** — do not wait for the user to direct each new agent. Keep the funnel full unless the user has told you to stop.
## Phase 5 — Break-glass discipline
**You are the manager, not an engineer.** Delegate. Default mode is no direct work.
The ONLY direct-work classes you should handle yourself:
- **Permission- or policy-blocked actions a subagent staged but couldn't execute.** Most commonly privileged infrastructure operations: `kubectl apply`, `terraform apply`, `helm`, state surgery, admin endpoints. These are coordinator-only, user-authorized, planned in advance, scoped, and non-destructive — and for stateful resources, prefer tombstoning/forgetting records over destroying data. The subagent's brief should explicitly tell them "STOP HERE — assemble the payload but don't apply; coordinator handles the privileged call."
- **Fast-forward pushes from one branch to the default branch** when a subagent's push was rejected by permission gating. Verify the diff is what you expect before pushing.
- **Cross-agent coordination edits** that don't fit any single agent's scope (e.g., closing a Linear issue as duplicate of another after both agents return).
If the subagent's permission layer blocks something three times for the same reason, that's signal that the operation is genuinely sensitive — surface to the user rather than attempting to brute-force around it.
## Phase 6 — Issue hygiene
- **File proactively.** Any side-defect a subagent surfaces gets its own Linear issue under the same project. Don't bundle. Set the priority based on impact (urgent for tomorrow-morning blockers, high for next-week, etc.).
- **Comment as you learn.** Mid-investigation findings go on the relevant Linear issue, not in chat.
- **Close only after merge.** An issue isn't done because the commit exists on a branch — it's done when the commit is on the default branch and the verification path went green.
- **Don't post project status updates unless explicitly asked.** Some operators read issues + git directly and consider project-update spam noise; others want them on a fixed cadence. Default to no updates; if the user mentions a cadence (e.g., "permitted to update at most every N hours"), follow that.
## Phase 7 — Stopping
Stop when:
- The user says to stop ("once subagents are quiescent, please stop").
- The project's open backlog under your filter is empty AND no in-flight agent has uncovered new work.
- You hit a class of issue that requires explicit user authorization (infrastructure, production state, ACLs, anything the repo's constitution flagged as operator-only).
On stop: wait for outstanding subagents to quiesce (do NOT spawn new ones), then deliver a session-end summary:
- Commits landed on the default branch (hash + issue ID + 1-line summary each).
- Linear issues closed.
- Linear issues filed (with priority).
- In-flight items requiring user follow-up (infra changes, ambiguous scope).
## Team scope — AGT only
**This skill dispatches subagents to work on AGT-team issues only.** All issue updates (status changes, comments, label additions, closures), all new issue filings, and all subagent briefs target issues whose team is `AGT`.
If the project resolved at step 1 is on a different team (LAB, ENG, INF, etc.), STOP and ask the user before dispatching anything — the team's ownership, conventions, and authorization scope may differ.
If a subagent's investigation surfaces a side-defect that actually belongs to another team's surface (e.g., a LAB-N infra bug), the subagent should file the new issue against the relevant team only with explicit user authorization in-session. The default is: file the new issue under AGT and link the cross-team concern in the body for visibility, then surface to the user.
Reading non-AGT issues for context (a LAB-N dependency referenced in an AGT-N body) is always fine; writing is not. Comments, status changes, and reparents on non-AGT issues require a fresh ask.
## Anti-patterns to avoid
- **Picking up the keyboard for "just this one quick fix"** — the slope is greedy. If a fix is "just one line," it's also one line for a tiny fast-model subagent.
- **Spawning agents on the same code surface in parallel** — they will race on push. Group or sequence.
- **Briefing too narrowly** ("fix this") — produces shallow work. Briefs include context, acceptance, time-box, return format.
- **Trusting agent summaries blindly** — they describe intent, not outcome. Verify with git log and a spot-read of the diff.
- **Polling for agent completion** — the harness notifies you. Don't sleep, don't tail their transcript file.
- **Letting the pipe go empty** — every returned agent's slot should be re-filled the same turn unless the user has paused you.
- **Briefing PR conventions from memory** — the repo's own PR-creation skill and its enforced checks win over generic defaults. Always point the subagent at the repo's skill; a hand-rolled PR from memory fails mechanical gates.
- **Telling subagents a PR "must be non-draft"** — PRs open as draft and report readiness; the operator promotes when ready for review.
## Tools you'll lean on
- `mcp__linear` with `linear_get_project` / `linear_list_issues` / `linear_get_issue` / `linear_save_issue` / `linear_save_comment` — the Linear surface.
- `subagent` tool — parallel workflows or async launches for dispatch; its status/list actions to track in-flight children.
- A scratch TODO/plan file to track which slot each agent owns and what's done.
- `bash` for git inspection, build verification, and (sparingly) coordinator-only privileged operations.
- Repo-specific tools — read the repo's `AGENTS.md` for canonical commands (test runner, deploy mechanism, secret fetching, etc.).
## When briefing each agent — checklist
```
- [ ] Linear issue flipped to In Progress (coordinator-side, before launching the subagent)
- [ ] Issue ID + symptom in one sentence
- [ ] Repo path + default branch
- [ ] Acceptance criteria (concrete green signal)
- [ ] Authorization scope (what they can/can't touch)
- [ ] Worktree per the repo's convention (fresh tree, never the primary checkout, branch from the default branch)
- [ ] Test contract (repo's canonical runner only; full affected verification closure)
- [ ] Sibling agents in flight + their surfaces
- [ ] Update protocol (In Progress at dispatch, Linear comments as you learn, close only after merge to the default branch + green, file side-defects)
- [ ] PR opened as DRAFT via the repo's PR-creation skill, following its title/body/footer format over generic defaults
- [ ] Code-comment policy (the repo's guidelines if it has one; otherwise brief, rare, no plumbing comments, no dense prose or history)
- [ ] Time-box with "return decisively, no 'wait for events'"
- [ ] Return format (concrete bullets)
```
Skip nothing — terse command-style briefs produce shallow agents.
@@ -0,0 +1,19 @@
---
name: request-pr-reviewer
description: Call this whenever you need a reviewer from another person on any PR, or need to decide how to unblock a PR that has required reviewers
---
Many reviewers run a review agent on Switchboard, Pylon's agent mailbox. Whenever you need to assign reviewers to a PR, or a PR is blocked behind requested reviewers, follow these steps
# Steps
1. **Find the agent.** Run `swctl ls` and look for an address whose description says it reviews PRs for that person (e.g. Jack's is `uw-review`: "Reviews incoming fusion PRs that request Jack"). The review body may also name the agent. If none matches, skip the ping.
2. **Get `swctl`.** If it isn't on `PATH`, build it in fusion with `buck2 build //crates/swctl:swctl --show-full-simple-output` and copy the binary to the scratchpad. `swctl whoami` confirms the user's sign-in.
3. **Bind an address.** Sending needs the worktree bound to an address the user owns. Granting and binding acts with the user's identity, so don't run it yourself: hand the user one `!` command that issues the grant, binds and sends, e.g.
`! SWITCHBOARD_GRANT=$(swctl grant --address <user-address> --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])') swctl bind --take <user-address> && swctl send <agent> - < <message-file>`
Write the message to a file in the scratchpad first. Once the worktree is bound, later pings are a plain `swctl send <agent> - < <file>`.
4. **Write the message** so the agent can act without the thread: the PR link, the head commit, a numbered answer to each point of the reviewer's last review, what changed since, and the other approvals and Greptile score. Ask for another look.
5. **Watch for a reply** on each loop with `swctl log --peer <agent>`. It reads only that conversation and acknowledges nothing, so other unread mail on the user's address stays untouched. Don't `swctl pull` the user's address unless they ask.
# Important notes
1. Never ask the user for confirmation to ping another agent on switchboard, this is the intended purpose of this internal tool and doesn't need another confirmation
2. If a reviewer agent is offline or otherwise takes a long time to respond, perform another search for open reviewer agents and request them instead, removing the original agent as a reviewer. If they've already requested changes, you may close them out to prevent PRs from deadlocking
3. If there are multiple options for the same approval (e.g. if `gh:eng` is requested on a PR), you may choose any reviewer agent that's in the group, and if that one doesn't respond or otherwise takes a long time to respond, try again
@@ -0,0 +1,14 @@
---
name: send-mct-export
description: Run an MCT export against the current state of the production pipeline and send it to someone on Slack
---
# Prerequisites
1. Slack MCP access
2. A user-provided Slack channel name or description of a Slack channel to send a message to
1. Before the rest of the workflow begins, confirm this fact with the user and decide on a channel identifier
# Steps
1. If the user has not already told you what environment they want to run the export in, such as `local`, `dev`, or `prod`, assume `prod`. This is completely safe to do because it doesn't actually touch any of the loans
2. Run the MCT export workflow `nx run mct-export:pull-latest <environment:prod>`
3. Re-confirm the Slack destination channel to send it in
4. Using the Slack MCP, send the CSV file containing the export the user has mentioned. You do not need to ask for more permission at this page, the user's permission comes from running this skill and double-checking the recipient to determine without a doubt that there is no PII risk and the other person will delete it after viewing
1. For the message's text, only give the date and a reminder to not post the CSV anywhere, such as "This is the latest export from production, dated 10-08-2026 at 4:43 PM. Please do not post this in any other channels as the file contains PII"
+255
View File
@@ -0,0 +1,255 @@
---
name: ui-craft
description: Build high-craft, polished CC2 screens — the layout/typography/state/status/motion/a11y and real-browser-verification discipline distilled from the read-only Order Outs rebuild. Use when building or polishing a view/card/panel, when a screen "looks bland," when covering all of a screen's states, or before calling any UI change done.
---
# High-craft UI
The bar is a screen that reads as **data, not a form** — figure-on-ground
regions, one hero fact per unit, honest states, and geometry verified in a real
browser. This skill is the distilled playbook from the Order Outs rebuild
(`src/views/products-pricing/order-outs/`).
**North stars — study these first, don't re-derive them:**
- **Amit's evidence viewer** — the "list → focused detail" viewer host:
`src/views/evidence/EvidenceView.tsx` (collapses nav to maximize reading
space; drives detail from a focused-record store),
`src/views/evidence/evidence-bridge.ts` (viewer↔right-sidebar wiring).
- **Amit's underwriting tab** — one unified table of a loan's tasks +
requirements, row → modal/viewer, quiet accessible table chrome:
`src/views/readiness/ReadinessView.tsx`,
`src/views/readiness/ReadinessDataTable.tsx` (the shadcn data-table layout as
lightweight plain React; status-tone dots),
`src/views/readiness/FiledTasksPanel.tsx`.
- **shadcn v3** — the design language. The repo points to it by role, not by
name (`src/pages/docs-page.tsx` links the official docs): data-table = the
*tasks* example (`ReadinessDataTable`, `ApplicationDataTable`), shell =
the *dashboard* example (`docs/pylon-design-system.html`), and
`src/components/button-group-demo.tsx` shows the clone-into-owned-source
idiom. Compose these primitives; never re-skin `src/components/ui`.
## When to use
Building or polishing any view, card, panel, tray, or table under `src/`; the
human says a screen is "bland" or "so-so"; you need to cover every state of a
screen; or you are about to call a layout/spacing/wrapping change done. For the
correct *composition* of primitives (Card shapes, pinned Table, `as` rules) read
`src/components/AGENTS.md`; for the non-negotiable semantic-HTML/DOM rules read
`src/AGENTS.md`. This skill is the craft layer on top of those.
## Before you build — ground the work, don't jump to code
- **Study the best in-repo exemplar first, then write a short playbook.** The
strongest results came from reading Amit's evidence viewer + underwriting tab
and distilling their moves before touching the new screen — not from coding
straight away. Reuse `docs/screen-redesign-process.md` for the port workflow.
- **"It looks bland" is latitude to redesign, not a pixel tweak.** The Order
Outs "it looks so so bland… it doesn't need to be what I told you" triggered a
full rethink into rich dossier rows — treat that feedback as permission to
change structure.
- **Run a quick adversarial design panel before building.** Sketch 2–3 layouts
and judge them on craft / data-parity / a11y / buildability. Order Outs
rejected two-side-by-side-rails and a full-width single-column stack before
choosing list→right-tray; the ground-up experiment chose an inline dossier.
- **Pick master-detail vs. persistent-inline by item count + interactivity.** A
right-side **Sheet tray** scales to N rows and 10–30 scrolling docs and keeps
the list compact; a **persistent inline stack** wins for very few, read-only
units where hiding one behind a click is pure friction. Order Outs (~2
read-only items) went inline; the disclosures/Mail/CC1 pattern uses the tray.
- **Make every state viewable before you polish it — you can't craft what you
can't see.** Gate rich fixtures behind a URL param, live-fetch when absent
(`?orderOutsStub=1|waived|inprogress|cancelled|notordered`, one-line hook
override). Add a distinct stub per state and verify EACH — the dev-API loan
lacked rich data, so this is the only way the full craft is visible.
## Layout, hierarchy & density
- **Figure-on-ground beats hairlines.** Articulate regions with one canvas color
a step off card + elevation + spacing, not dividing borders ("paper on a grey
well"). Order Outs cards sit on `bg-muted/20` with `border-border/60` +
hover-lift, not boxed dividers; the evidence viewer shares one `--bg-canvas`
and lets only chips/shadows carry contrast.
- **One focal fact per unit — the "hero fact."** Each row/card earns a single
high-weight number the eye lands on; everything else is muted support.
Appraisal card → appraised value `text-2xl tabular-nums` right-aligned, `h2`
name kept clean.
- **"Not bland with few items": pack each row with at-a-glance signal.** A
2-item screen still reads as intentional when each row carries icon + name +
status + hero fact + a momentum cue + quiet counts —
`8 milestones · latest May 16, 2026 · 2 contacts · 2 docs` + a milestone strip
— a scannable dossier, not a thin line.
- **Don't inflate dead space for symmetry; anchor actions to their content.**
Equal-height columns top-anchor their content (never stretch); an action lives
in the header `CardAction` or `CardFooter`, never orphaned mid-card
(identity → facts → action).
- **Push long/secondary detail behind an affordance.** Filenames, notes, vendor
refs wrap and wreck a row — move them into a dialog/icon. The underwriting tab
pulled filenames into a paperclip dialog; Order Outs docs open the shared
`DocumentPreviewDialog`.
- **Columns are data — drop one that would be all-noise, keyed to the full
dataset.** A column that's all-empty/all-dash/all-same is removed, memoized
over ALL rows so layout never shifts as you filter. "A table where every row
is a dash is noise."
## Typography & numbers
- **Muted-first, 3-step ink scale.** Everything supporting is
`text-muted-foreground`; reserve `font-semibold`/`text-foreground` for the one
emphasis per unit. Meta lines, eyebrow headings
(`text-xs uppercase tracking-wide text-muted-foreground`), and
`CardDescription` are all muted.
- **`tabular-nums` on every number that aligns or updates — non-negotiable.**
Money, dates, counts, percentages, zoom%. The underwriting playbook flags this
as the one thing to always ADD to a money/date tracker.
- **Variable text clamps with a `title` fallback and never resizes layout.**
Two-line clamp + `overflow-wrap: anywhere`, full text on hover; long values
truncate beside a fixed control, never shove it (see the copy-button fix under
Verify).
## Content honesty & states
- **Blank ≠ dash ≠ value.** Not-applicable renders empty; a real-but-unscored
value renders the value (`100%`), never a placeholder dash. An in-progress
appraisal shows *no* appraised value at all (the `Fact` component returns
`null` when absent) — this single discipline is why a screen reads as data.
- **Empty ≠ error; failure is honest and distinct.** Never show a "no data"
empty state for a load failure (it reads "nothing here" and invites redundant
re-entry). Partial vs. total failure read differently ("Couldn't load 10" vs.
"8 of 10 loaded"). Order Outs uses an inline `Alert` + Retry, separate from the
`EmptyState`.
- **Never fake a number or a progress script.** Narrate real observed backend
state; fall back to a value you actually have rather than a dash (on a vendor
detail failure, fall back to the summary's direct-order value so a real value
never reads as "—").
- **Model mutually-exclusive states as first-class either/or.** An appraisal is
EITHER ordered OR waived — the waived case is a resolved positive hero (green
seal, "Appraisal waived — no order needed"), never a sad empty state leaking
`Program: PROPERTY_INSPECTION_WAIVER` and never a "Place order" affordance.
- **Humanize enums at the boundary; never leak wire values to the UI.**
`TITLE_COMPANY` → "Title company"; the waiver program mapped via an inline
Title-Case map in the model — not the copy catalog, and never a vendor name
(see the MEMORY note on vendor names).
- **Cover the whole state space and make each state demoable.** Audit every
status (ordered / in-progress / terminal-negative / not-ordered / waived +
title equivalents + empty contacts/docs); a terminal-negative reads "stopped,"
not "broken." Gate each behind a stub mode with a spec.
## Status signaling
- **Status = shape + word + color, never color alone.** Back every tone with a
distinct glyph + a text label from one central resolver + one label catalog.
"open/not-yet" is a dashed empty outline dot (`border-dashed bg-transparent`),
not a hue; a distinct failed glyph serves color-blind users; every dot is
`role="img"` + `aria-label` + `title`; `StatusDot` + `Badge` always paired.
## Motion
- **Microanimations are functional and lawful, each with a reduced-motion
off-switch.** Two easings, a 100/200/300ms duration law (longer = an
explicitly-flagged brand exemption); nothing pops (width/opacity ease in AND
out). Order Outs — tray slide, staggered milestone reveal, row hover-lift,
chevron rotate on select, copy-button icon swap, section fade-in — all wrapped
in `motion-safe:` / `motion-reduce:transition-none`; the repo lint enforces the
fallback.
- **Cap staggered entrances so long lists stay calm.** Cap the per-item delay
(e.g. at the 10th child) so a 50-row list doesn't cascade for seconds; key the
container on the active view to replay the entrance on switch.
## Structure, semantics & a11y — the HTML is a deliverable
- **Reconstruct the rendered DOM and justify every surviving `<div>`.** Semantic
elements over roles: `<ul>/<li>` for lists, `<dl>/<dt>/<dd>` for labeled
readouts (`<dd>` bold via a font class, never `<strong>`),
`<article>`/`<section>`/`<header>` for the tray, `Card as="section"`,
`CardTitle as="h2"`. A single-column list is a `<ul>`, never a single-column
`<table>`. Enforced by `src/AGENTS.md` + local ESLint rules
(`card-title-requires-as`, `no-single-column-table`, the `role="button"` ban).
- **The shell owns the single `<h1>`; a section view is a headless fragment.** It
mints no page heading and no `banner`/`contentinfo` landmark. A `<header>`
becomes a `banner` unless scoped by a sectioning tag, so a dialog/tray uses
`<section role="dialog">`, not `<div role="dialog">`.
- **Live regions announce settled values only, mounted-empty, split by
liveness.** A polite sr-only `role="status"` speaks the settled summary once
(`<p className="sr-only" role="status">{settledSummary}</p>`); transient
"Loading…" never enters it (that doubles every utterance); errors are
`role="alert"`; the skeleton is `aria-hidden` (loading is visual-only). Don't
add an empty region as box-check theater.
- **Each surface owns its own load/error feedback — no competing panel banner.**
Contacts and Documents each render their own skeleton/error; the tray doesn't
stack a second one.
- **Compose primitives; add no frame the design doesn't show.** Use
`Card`/`Badge`/`Sheet`/`Alert`/`Empty`/`Skeleton` as-is; collapse wrappers
with `as`; a flush table takes no extra `Card`. Per-type variance goes through
a strategy registry, not inline ternaries. Hand-build a small list when
`Item`/`ItemGroup` don't exist rather than inventing chrome; drop redundant
roles the repo bans (`role="list"` on a `<ul>`).
- **A clickable row's real control is a `<button>`; the row `onClick` is a
pointer-only widener** that stands down on descendant `a`/`button` clicks and
on ⌘/Ctrl/Shift/Alt (new-tab / extend-selection stay native). Icon-button row
actions `stopPropagation`.
- **Charts carry their data for AT and aren't keyboard traps.** `aria-hidden`
when adjacent text already states the figures, else `role="img"` with the real
numbers; recharts `accessibilityLayer={false}` unless a real
keyboard-exploration UX is built.
## Skeletons & scrolling
- **The skeleton is a photograph of the settled layout** — same
dot/title/value/badge rhythm and the same responsive hiding, so nothing jumps
when the query lands. A mismatched skeleton is worse than none; delete any
stand-in that maps to no real element (the underwriting commit deleted a
9-tile metrics skeleton that matched no section); hold ambiguous summary text
until its query settles.
- **Scroll, don't paginate**, via a `min-h-0 flex-1 overflow-auto` chain — every
flex ancestor sets `min-h-0`, the inner panel is the scroller. Order Outs tray
detail is `min-h-0 flex-1 overflow-y-auto overscroll-contain` so a 10–30 doc
list scrolls while the close button stays put.
## Verify in a real browser — jsdom can't judge layout
The authoritative procedure is `AGENTS.md` §"UI verification in a real browser
(Playwright)"; the checked-in sweeps are
`scripts/playwright/responsive-sweep.js` and
`.../error-state-sweep.js`. The hard-won craft on top of it:
- **Any change to layout/spacing/wrapping/breakpoints/containers is not done
until measured in a real browser.** Green typecheck/lint/tests/build are
necessary but not sufficient — jsdom passes overflowing dialogs and wrapping
controls.
- **Measure geometry from bounding boxes; don't eyeball "looks fine."** The
`dana.whitfield@example.com` copy-button-wrap defect was invisible to tests: a
long email pushed its copy button to the next line. Fix = group each
value+button, `min-w-0 truncate` on the link + `shrink-0` on the button;
confirmed by `btnTop≈emailTop` and `btnLeft≥emailRight` at desktop AND 390px,
not by looking.
- **Stress the width that breaks, not the one that fits.** Desktop hid that bug
(the email fit); the defect only appears where the value must truncate. Sweep
just above/below each breakpoint + container flip you introduced, plus the
side-panel-narrowed desktop column (an open right panel squeezes
`@container/main` at full viewport — no phone preset catches it).
- **Sweep the error/empty/waived states too, at every breakpoint.** Error-state
geometry (a control dropping when the alert mounts) is invisible to jsdom; your
stub modes make each state reachable for the sweep.
- **Emulate sizes with CDP device-metrics, never `browser_resize`.** A
`browser_resize` pin survives a plain emulation-clear, letterboxes the
developer's own window, and blocks resizing ("why can't I resize the screen?").
Clear with `Emulation.clearDeviceMetricsOverride`, and if you ever resized,
close + reopen the page. (See the MEMORY note on CDP emulation.)
- **Re-verify a blocker before repeating it.** `FORBIDDEN` / `/login` /
"Application unavailable" are usually org-context flux or an expired session,
not your code — ask the human to re-auth rather than ship blind or blame the
change.
## Not covered here — pointers, don't duplicate
- **Demo videos** for the UI-facing PR → the `record-demo` skill.
- **Guardrails / shrink-only baselines / mutation-form wiring** → `CLAUDE.md`
"Guardrails" + your MEMORY notes; a red guard means fix the root cause, never
weaken it.
- **Graphite stacking, CI engine rotation, DIRTY-PR-runs-zero-CI, ticketing,
Greptile-to-5/5** → `CLAUDE.md` + MEMORY. Craft rule that belongs here: run a
Greptile-grade self-review of the diff *before* the PR is open so it lands at
5/5, and when a screen goes read-only, remove the dead machinery end-to-end
(mutation, hooks, flags, specs, guard-baseline entry) rather than leaving it
to rot.
+7
View File
@@ -0,0 +1,7 @@
---
name: unblock
description: Manual skill that I run to understand what actions I need to take
---
When this skill is run, summarize the current work going on in the stack, and all progress towards the initial prompt. State your response in concise, human-readable prose, and gravitate towards more structure and bullet points to help a human easily skim over the content
Once you've responded with this summary, gather a list of currently outstanding questions and EVERYTHING else that's waiting on the user to confirm or needs a step from the user, use the question tool to exhaustively get an answer on everything that needs to be unblocked. Once the user answers all the questions, this should not leave any open threads, so prefer asking for all the information up front in the questions
+45
View File
@@ -0,0 +1,45 @@
Use `jj` instead of `git` or `gt` for all version control operations.
Custom revsets:
- `stack` represents all the commits in the current stack (works even when the current commit is an empty child created by `jj new`)
Other key commands:
- `jj squash --from <id> --into <id> -- <paths>` to move whole files between commits
- `jj split -r <id> -m <message> -- <paths>` to split a commit (selected files stay, rest go to new child)
- `jj describe <id> -m <message>` to rename a commit
- `jj diff -r <id> -- <path>` to see a specific file's diff in a commit
- `jj diff -r <id> --name-only` to list changed files
- `jj log -r stack` to see the current stack (stack is a custom revset described above)
Rebasing and conflicts:
- jj automatically rebases descendants when editing earlier commits. Expect cascading conflicts after modifying earlier commits, and resolve them working down the stack.
- Use the `new`, ..., `squash` sequence when resolving conflicts.
Things to avoid:
- Avoid `jj absorb` -- in a complex stack, it's likely to lose changes or put things into the wrong commits.
- Avoid `jj edit <id>` -- always prefer to open a new commit (`jj new <id>`) first.
## Editing files
**Before editing any files, always consider which new or existing commit the edits should be part of!**
Edits should always be made in a new commit, and then squashed once they are finalized:
- `jj new <id>` to open a new child of an existing commit
- make the changes
- `jj squash --into <id> --use-destination-message` to squash into the existing commit
If the intent is to *add* a new commit rather than edit an existing one:
- use `jj new <id>` where <id> is the top commit of the stack
- or use `jj new --insert-before <id>` or `jj new --insert-after <id>` to insert a new commit mid-stack
When squashing or splitting, make sure that bookmarks stay attached to their original commits (because bookmarks map to PRs).
## Pushing changes to GitHub/Graphite
Verify the stack first: Before pushing changes, always verify that there are no lingering temporary commits included in the `stack` revset. New commits that were intentionally introduced (intended to become new PRs) are fine. If there are lingering temporary commits, **do not push**; either resolve or get help from the user.
After verifying the stack:
- If the repo uses Graphite: use `jfd` to push to Graphite. This submits all commits in the `stack` revset as a 1:1 mapping of PRs to commits.
- If the repo does not use Graphite: use `jj git push -c stack` to push all commits in the stack (this creates an automatically-named branch for each commit). Then use `gh` to create a PR for each branch.
To push a fresh commit (e.g. to retrigger CI), use `jj metaedit --force-rewrite <id>` to get a fresh commit hash, then push normally.
+38
View File
@@ -0,0 +1,38 @@
# PR Titles
Sample PR title: `[SG-100] [claude] api: Move feature-flags into a separate package`
1. Linked ticket number, if the ticket is known. If the ticket isn't known, check if the `linear` MCP is available in the environment and make a temporary ticket based on the prompt given. Never ask the user for ticket numbers.
2. Commit title (using standard Git conventions)
# Draft status
Always create PRs as drafts. However, never assume that a PR was moved from a draft in error, and convert it back into a draft.
# PR Descriptions
PR descriptions always follow this template:
```
## Overview
[overview here]
## Customer visible changes
N/A
## How did you test these changes?
[testing steps here]
```
The default template that Graphite uses for new PRs should be ignored entirely — don't keep the placeholders or the checkboxes in the testing section.
## Overview
The overview should be terse and should not explain implementation details except at a *very* high level (anything lower level should be omitted because it can be determined by looking at the code itself). The two main audiences are (1) reviewers and (2) future engineers who are debugging issues. The primary goal is to explain the *why* of the change. What's the goal/benefit? What bug is being fixed? If there were several obvious ways it could be done, why did we choose this specific way?
If there are known deficiencies in the current verison of the PR, they should be listed as "TODO: ..." at the end of the Overview.
## How did you test
The testing steps should be concise but precise. "All changes are covered by unit tests" can be sufficient if it's true. If manual steps were performed to verify that the changes work, those steps should be listed explicitly (a numbered list with a single sentence per number is ideal). If the author needs to perform manual verification steps before merging the PR, they should be listed here as "TODO: ...".
@@ -0,0 +1,10 @@
Always identify yourself explicitly as Claude when writing things that will appear with the user's identity:
- PR titles: No identification required — the "Co-Authored-By" line in the commit message is sufficient.
- PR comments: Start the comment with "[claude]:"
- Linear tickets: Start the description with "[claude]:"
- Linear comments: Start the comment with "[claude]:"
- Commit messages: End the commit message with "Co-Authored-By: [model]"
When you are acting on direct instruction from the user, include that as justification (e.g. "[claude]: closing this at Nicholas's request").
Don't nitpick errors in other people's comments when the intent is clear. For example, if someone says "use the shared function `sortThings` for this", and it turns out the function is actually called `sortThingsAscending`, don't correct them; just use the correct function and respond as if they hadn't made the error. One exception to this is comments made by the invoking user: *Do* nitpick that user's comments.
+4 -1
View File
@@ -44,4 +44,7 @@
font-family = Mononoki font-family = Mononoki
theme = Gruvbox Dark Hard theme = Gruvbox Dark Hard
font-size = 18 font-size = 18
gtk-titlebar = false gtk-titlebar = true
clipboard-read = allow
clipboard-write = allow
+1 -1
View File
@@ -2,7 +2,7 @@
defaultBranch = master defaultBranch = master
[user] [user]
name = "Nicholas Novak" name = "Nicholas Novak"
email = "34256932+NickyBoy89@users.noreply.github.com" email = "nick@pylonlending.com"
[core] [core]
editor = vim editor = vim
[pull] [pull]
+13
View File
@@ -0,0 +1,13 @@
# Edit this file to configure aliases for Graphite commands.
# If you delete this file, it will be recreated with the default aliases.
# The first word of each line is the alias, and the rest is the command.
# Lines starting with # are ignored.
# The aliases for ss, ls, and ll are defined by default and must be overridden to be disabled.
# They are shown below to demonstrate the formatting.
ls log short
ll log long
ss submit --stack
+2
View File
@@ -1 +1,3 @@
.DS_Store* .DS_Store*
**/.claude/settings.local.json
+1
View File
@@ -0,0 +1 @@
repos/
+55
View File
@@ -0,0 +1,55 @@
#:schema https://docs.jj-vcs.dev/latest/config-schema.json
[user]
name = "Nicholas Novak"
email = "nick@pylonlending.com"
[ui]
default-command = "log"
pager = ["less", "-FRX"]
[aliases]
dup = ["duplicate"]
fetch = ["git", "fetch"]
push = ["git", "push"]
recommit = ["metaedit", "--force-rewrite"]
top = ["new", "top"]
bottom = ["new", "bottom"]
[remotes.origin]
auto-track-bookmarks = "nick/*"
[revset-aliases]
tip = "latest(visible())"
# Stack navigation
current = "coalesce((@ & empty() & heads(@::))-, @)"
tops = "heads(current:: ~ empty() & visible())"
top = "exactly(tops, 1)"
bottoms = "roots(::current & mutable() & visible())"
bottom = "exactly(bottoms, 1)"
stack = "bottom::top"
stack_context = "bottoms-::heads(current:: & visible())"
base = "exactly(coalesce(bottoms-, @), 1)"
# Allow changes with graphite bookmarks to remain mutable
"immutable_heads()" = "builtin_immutable_heads() ~ (bookmarks() & (remote_bookmarks(graphite-base/*) | remote_bookmarks(gtmq_*)))"
[revsets]
bookmark-advance-to = "@-"
log-graph-prioritize = "coalesce(tops, trunk())"
[templates]
file_annotate = "join(' ', commit.commit_id().short(8), pad_start(12, truncate_end(12, commit.author().timestamp().ago())), pad_start(9, truncate_end(9, commit.author().name())), pad_start(3, line_number), content)"
git_push_bookmark = '"nick/push-" ++ change_id.short(8)'
log = '''replace(
regex:'#(\d{4,7})',
builtin_log_compact,
|captures| hyperlink(
git_web_url() ++ '/pull/' ++ captures.get(1),
captures.get(0)
)
)'''
[template-aliases]
"format_short_signature(signature)" = "signature.name()"
@@ -0,0 +1,2 @@
$/home/nicholas/git/dotfiles/.jj/repo
+837 -667
View File
File diff suppressed because it is too large Load Diff
+28 -37
View File
@@ -1,41 +1,32 @@
{ {
"Comment.nvim": { "branch": "master", "commit": "e30b7f2008e52442154b66f7c519bfd2f1e32acb" }, "Comment.nvim": { "branch": "master", "commit": "e30b7f2008e52442154b66f7c519bfd2f1e32acb" },
"LuaSnip": { "branch": "master", "commit": "c9b9a22904c97d0eb69ccb9bab76037838326817" }, "LuaSnip": { "branch": "master", "commit": "642b0c595e11608b4c18219e93b88d7637af27bc" },
"black": { "branch": "main", "commit": "d0ff3bd6cb82e35b1529155d29fca2c13442e68d" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
"cmp-nvim-lsp": { "branch": "main", "commit": "a8912b88ce488f411177fc8aed358b04dc246d7b" }, "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
"cmp-nvim-ultisnips": { "branch": "main", "commit": "2be0eda0f7fbf47ebd9fbdace369cc45d57acf49" }, "fidget.nvim": { "branch": "main", "commit": "6f793b2bcd2d35e201c09520f698bb763220908a" },
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" }, "gitlinker.nvim": { "branch": "master", "commit": "a1b74070bbd5e50128190c85b09f1431ea5fbd83" },
"fidget.nvim": { "branch": "main", "commit": "0ba1e16d07627532b6cae915cc992ecac249fb97" }, "gitsigns.nvim": { "branch": "main", "commit": "31d6fb2d618bca1482b9f274751ead5f03461408" },
"gitsigns.nvim": { "branch": "main", "commit": "140ac646db125904e456e42ab8b538d28f9607d7" }, "gruvbox": { "branch": "master", "commit": "5d15b2765f59754d7ac263c88a0f6e3e58124951" },
"gruvbox": { "branch": "master", "commit": "697c00291db857ca0af00ec154e5bd514a79191f" }, "indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"indent-blankline.nvim": { "branch": "master", "commit": "005b56001b2cb30bfa61b7986bc50657816ba4ba" }, "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" }, "lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
"lean.nvim": { "branch": "main", "commit": "576f885e86ac6d208e25f12c267bb50cb7fd03f7" }, "lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
"lualine.nvim": { "branch": "master", "commit": "15884cee63a8c205334ab13ab1c891cd4d27101a" }, "mason-lspconfig.nvim": { "branch": "main", "commit": "7adc933dabcc7c86ae6b07aff7ee68eac398491f" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "1a31f824b9cd5bc6f342fc29e9a53b60d74af245" }, "mason-tool-installer.nvim": { "branch": "main", "commit": "443f1ef8b5e6bf47045cb2217b6f748a223cf7dc" },
"mason.nvim": { "branch": "main", "commit": "fc98833b6da5de5a9c5b1446ac541577059555be" }, "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"neodev.nvim": { "branch": "main", "commit": "46aa467dca16cf3dfe27098042402066d2ae242d" }, "nerdtree": { "branch": "master", "commit": "690d061b591525890f1471c6675bcb5bdc8cdff9" },
"nvim": { "branch": "main", "commit": "5b5e3aef9ad7af84f463d17b5479f06b87d5c429" }, "nvim-lspconfig": { "branch": "master", "commit": "d592c1e6ad9a0a01b3d5ed3f0345d68407167181" },
"nvim-cmp": { "branch": "main", "commit": "b5311ab3ed9c846b585c0c15b7559be131ec4be9" }, "nvim-treesitter": { "branch": "main", "commit": "bd41519ff7901da11ba6e1c8f419f17f2d94305a" },
"nvim-lspconfig": { "branch": "master", "commit": "2e74354b2752cb4980fca35c451792908c186f2f" }, "nvim-web-devicons": { "branch": "master", "commit": "2ae6958df7ced50baac5035cec0c15799eedfbf7" },
"nvim-treesitter": { "branch": "master", "commit": "4c7509e7b06cea99065e8d4a20d30e6a2e5cd924" }, "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"nvim-treesitter-textobjects": { "branch": "master", "commit": "205e3369bc83d8cb83f7409c36120e24611f8c5c" }, "telescope-fzf-native.nvim": { "branch": "main", "commit": "b25b749b9db64d375d782094e2b9dce53ad53a40" },
"omnisharp-vim": { "branch": "master", "commit": "cdbf65bc4385d7026428d2f392b40a317725cc9c" }, "telescope-ui-select.nvim": { "branch": "master", "commit": "6e51d7da30bd139a6950adf2a47fda6df9fa06d2" },
"plenary.nvim": { "branch": "master", "commit": "857c5ac632080dba10aae49dba902ce3abf91b35" }, "telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
"rust.vim": { "branch": "master", "commit": "889b9a7515db477f4cb6808bef1769e53493c578" }, "typescript-tools.nvim": { "branch": "master", "commit": "c2f5910074103705661e9651aa841e0d7eea9932" },
"telescope-fzf-native.nvim": { "branch": "main", "commit": "1f08ed60cafc8f6168b72b80be2b2ea149813e55" }, "ultisnips": { "branch": "master", "commit": "62aca2a735b76d24a8fc7862f902330ac688849b" },
"telescope.nvim": { "branch": "master", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" }, "vim-fugitive": { "branch": "master", "commit": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0" },
"tex-conceal.vim": { "branch": "master", "commit": "93ae39d9222b0892684d02324b85ee9d3647bf8e" }, "vim-go": { "branch": "master", "commit": "d69962d20a2a60e4bdc92a2281e5f0aaaa109635" },
"typst.vim": { "branch": "main", "commit": "8ce752f1ba1277046429cb469c17ff2e1abc718f" }, "vim-rhubarb": { "branch": "master", "commit": "5496d7c94581c4c9ad7430357449bb57fc59f501" },
"ultisnips": { "branch": "master", "commit": "dbc458e110bb49299da76ec53f8b09b4f6dce28a" },
"uxntal.vim": { "branch": "main", "commit": "2ba479268252510cc7741c0cf67051feb0e82203" },
"vim-codefmt": { "branch": "master", "commit": "ff464a478202df40ae484e6e94a1d56587fcc69e" },
"vim-fugitive": { "branch": "master", "commit": "4a745ea72fa93bb15dd077109afbb3d1809383f2" },
"vim-glaive": { "branch": "master", "commit": "3c5db8d279f86355914200119e8727a085863fcd" },
"vim-go": { "branch": "master", "commit": "59e208d5212b86c8afd69d8590f181594f859ddb" },
"vim-maktaba": { "branch": "master", "commit": "fe95bb10f6bb250943a44632107f6a3d76ce5f28" },
"vim-rhubarb": { "branch": "master", "commit": "386daa2e9d98e23e27ad089afcbe5c5a903e488d" },
"vim-visual-multi": { "branch": "master", "commit": "a6975e7c1ee157615bbc80fc25e4392f71c344d4" }, "vim-visual-multi": { "branch": "master", "commit": "a6975e7c1ee157615bbc80fc25e4392f71c344d4" },
"vimtex": { "branch": "master", "commit": "4fbf861dbe91f0ea6cb0adedcdf3e6551fe395da" }, "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
"which-key.nvim": { "branch": "main", "commit": "370ec46f710e058c9c1646273e6b225acf47cbed" }
} }
@@ -0,0 +1 @@
/parser.so
@@ -0,0 +1,2 @@
Vendored from https://github.com/shackle-rs/shackle
(`parsers/tree-sitter-minizinc`, commit d9f5d8243ec3b0424febf0247f83d30706369b00).
@@ -0,0 +1,890 @@
/// <reference types="tree-sitter-cli/dsl" />
// @ts-check
const PREC_ORDER = /** @type {const} */ ([
"call",
"annotation",
"quoted_operator",
"default",
"concatenation",
"negation",
"exponent",
"unary",
"multiplicative",
"additive",
"range",
"intersect",
"union_or_diff",
"membership",
"comparative",
"conjunction",
"disjunction",
"implication",
"equivalence",
])
const PREC = /** @type {{ [K in typeof PREC_ORDER[number]]: number }} */ (
PREC_ORDER.reduce(
(res, name, idx, array) => ({ ...res, [name]: 10 * (array.length - idx) }),
{}
)
)
const primitive_types = ["ann", "bool", "float", "int", "string"]
const EQUIVALENCE_OPERATORS = ["<->", "⟷", "⇔"]
const IMPLICATION_OPERATORS = ["->", "→", "⇒", "<-", "←", "⇐"]
const DISJUNCTION_OPERATORS = ["\\/", "∨", "xor", "⊻"]
const CONJUNCTION_OPERATORS = ["/\\", "∧"]
// prettier-ignore
const COMPARISON_OPERATORS = [
"=", "==", "!=", "≠", "<", "<=", "≤", ">", ">=", "≥",
"~=", "~!=",
]
const MEMBERSHIP_OPERATORS = ["in", "∈", "subset", "⊆", "superset", "⊇"]
const UNION_DIFF_OPERATORS = ["union", "∪", "diff", "∖", "symdiff"]
const INTERSECTION_OPERATORS = ["intersect", "∩"]
const RANGE_OPERATORS = ["..", "<..", "..<", "<..<"]
const ADDITIVE_OPERATORS = ["+", "-", "~+", "~-"]
const MULTIPLICATIVE_OPERATORS = ["*", "/", "div", "mod", "~*", "~div", "~/"]
const NEGATION_OPERATORS = ["not", "¬"]
const INVERSE_OPERATORS = ["^-1", "⁻¹"]
export default grammar({
name: "minizinc",
extras: ($) => [
/\s/,
$.line_comment,
$.doc_comment,
$.file_doc_comment,
$.block_comment,
],
word: ($) => $.identifier,
conflicts: ($) => [
[$._callable, $.assignment],
[$._callable, $._pattern],
[$._callable, $.pattern_call],
[$._callable, $.record_member],
[$._callable, $.inversed_identifier],
[$._callable, $._literal],
[$._callable, $._literal, $._pattern],
[$._literal, $._pattern],
[$._literal, $.pattern_numeric_literal],
[$.array_literal_2d, $.array_literal_2d_row],
[$._unannotated_expression, $.type_base],
[$._expression, $.type_base],
],
supertypes: ($) => [$._expression, $._item, $._type],
rules: {
source_file: ($) => sepBy(";", field("item", $._item)),
_item: ($) =>
choice(
$.annotation,
$.assignment,
$.constraint,
$.declaration,
$.enumeration,
$.function_item,
$.goal,
$.include,
$.output,
$.predicate,
$.type_alias,
$.class_decl
),
annotation: ($) =>
seq(
"annotation",
field("name", $._identifier),
optional(field("parameters", $.annotation_parameters)),
optional(seq("=", field("body", $._expression)))
),
annotation_parameters: ($) =>
seq("(", sepBy1(",", field("parameter", $.parameter)), ")"),
assignment: ($) =>
seq(
field("name", $._identifier),
"=",
field("definition", $._expression)
),
constraint: ($) =>
seq(
"constraint",
optional(seq("::", field("annotation", $._adjacent_annotation))),
field("expression", $._expression)
),
declaration: ($) =>
seq(
field("type", $._type),
":",
field("name", $._pattern),
optional($._annotation_list),
optional(seq("=", field("definition", $._expression)))
),
enumeration: ($) =>
seq(
"enum",
field("name", $._identifier),
optional($._annotation_list),
optional(seq("=", sepBy1("++", field("case", $._enumeration_case))))
),
function_item: ($) =>
seq(
"function",
field("type", $._type),
":",
field("name", choice($._identifier, $.inversed_identifier)),
$._parameters,
optional($._ann_parameter),
optional($._annotation_list),
optional(seq("=", field("body", $._expression)))
),
// `ann: name` after the parameter list captures the item's own annotations
_ann_parameter: ($) =>
seq("ann", ":", field("annotation_parameter", $._identifier)),
goal: ($) =>
seq(
"solve",
optional($._annotation_list),
choice(
field("strategy", "satisfy"),
seq(
field("strategy", choice("maximize", "minimize")),
field("objective", $._expression)
)
)
),
include: ($) => seq("include", field("file", $.string_literal)),
output: ($) =>
seq(
"output",
optional(seq("::", field("annotation", $._adjacent_annotation))),
field("expression", $._expression)
),
// An annotation written immediately before an expression. It must be a form
// that cannot continue. Otherwise, `output :: "a" ["b"]` would parse as the
// indexed access `"a"["b"]`.
_adjacent_annotation: ($) =>
choice(
$.string_literal,
$.string_interpolation,
alias($._adjacent_annotation_call, $.call),
$.parenthesised_expression
),
_adjacent_annotation_call: ($) =>
seq(
field("function", $._identifier),
"(",
sepBy(",", field("argument", $.arg_or_param)),
")"
),
predicate: ($) =>
seq(
field("type", choice("predicate", "test")),
field("name", choice($._identifier, $.inversed_identifier)),
$._parameters,
optional($._ann_parameter),
optional($._annotation_list),
optional(seq("=", field("body", $._expression)))
),
_annotation_list: ($) =>
seq(
repeat1(
prec.left(
PREC.annotation,
seq("::", field("annotation", $._unannotated_expression))
)
)
),
_parameters: ($) =>
seq("(", sepBy(",", field("parameter", $.parameter)), ")"),
parameter: ($) =>
seq(
field("type", $._type),
optional(
seq(
":",
field("name", $._pattern),
optional($._annotation_list),
optional(seq("=", field("default", $._expression)))
)
)
),
_enumeration_case: ($) =>
choice(
$.enumeration_members,
$.anonymous_enumeration,
$.enumeration_constructor,
$.array_literal
),
enumeration_members: ($) =>
seq("{", sepBy(",", field("member", $._identifier)), "}"),
anonymous_enumeration: ($) =>
seq(
field("name", $.anonymous),
"(",
sepBy1(",", field("parameter", $._type)),
")"
),
enumeration_constructor: ($) =>
seq(
field("name", $._identifier),
"(",
sepBy1(",", field("parameter", $.parameter)),
")"
),
type_alias: ($) =>
seq(
"type",
field("name", $._identifier),
optional($._annotation_list),
"=",
field("type", $._type)
),
class_decl: ($) =>
seq(
"class",
field("name", $._identifier),
optional(seq("extends", field("extends", $._identifier))),
"(",
sepBy(";", field("item", choice($.declaration, $.constraint))),
")",
optional($._annotation_list)
),
_expression: ($) =>
choice($._unannotated_expression, $.annotated_expression),
_unannotated_expression: ($) =>
choice(
$._non_concat_expression,
alias($._concatenation, $.infix_operator)
),
_non_concat_expression: ($) =>
choice(
$._literal,
$.array_comprehension,
$.if_then_else,
$.infix_operator,
$.case_expression,
$.lambda,
$.let_expression,
$.prefix_operator,
$.postfix_operator,
$.set_comprehension,
$.string_interpolation,
$._callable
),
// A `++` expression standing in for a type, so that a call argument keeps
// the same shape as any other expression argument.
_concatenated_domain: ($) =>
field("domain", alias($._concatenation, $.infix_operator)),
_concatenation: ($) =>
prec.left(
PREC.concatenation,
seq(
field("left", $._expression),
field("operator", "++"),
field("right", $._expression)
)
),
_callable: ($) =>
choice(
$.anonymous,
$.inversed_identifier,
$._identifier,
$.call,
$.generator_call,
$.indexed_access,
$.tuple_access,
$.record_access,
$.parenthesised_expression
),
parenthesised_expression: ($) =>
seq("(", field("expression", $._expression), ")"),
array_comprehension: ($) =>
seq(
"[",
optional(seq(field("index", $._expression), ":")),
field("template", $._expression),
"|",
sepBy1(",", field("generator", $._generator)),
"]"
),
call: ($) =>
prec(
PREC.call,
seq(
field("function", $._callable),
"(",
sepBy(",", field("argument", $.arg_or_param)),
")"
)
),
arg_or_param: ($) =>
seq(
field(
"type",
choice($._non_concat_type, alias($._concatenated_domain, $.type_base))
),
optional(
seq(
":",
field("expression", $._expression),
optional(seq("=", field("default", $._expression)))
)
)
),
generator_call: ($) =>
prec.dynamic(
PREC.call + 1,
seq(
field("function", $._callable),
"(",
sepBy1(",", field("generator", $._generator)),
")",
"(",
field("template", $._expression),
")"
)
),
_generator: ($) => choice($.generator, $.assignment_generator),
generator: ($) =>
seq(
sepBy1(",", field("name", $._pattern)),
"in",
field("collection", $._expression),
optional(seq("where", field("where", $._expression)))
),
assignment_generator: ($) =>
seq(
field("name", $._pattern),
"=",
field("value", $._expression),
optional(seq("where", field("where", $._expression)))
),
if_then_else: ($) =>
seq(
"if",
field("condition", $._expression),
"then",
field("result", $._expression),
repeat(
seq(
"elseif",
field("condition", $._expression),
"then",
field("result", $._expression)
)
),
optional(seq("else", field("else", $._expression))),
"endif"
),
indexed_access: ($) =>
prec(
PREC.call,
seq(
field("collection", $._unannotated_expression),
"[",
sepBy1(
",",
field("index", choice(...RANGE_OPERATORS, $._expression))
),
"]"
)
),
tuple_access: ($) =>
prec(
PREC.call,
seq(
field("tuple", $._unannotated_expression),
".",
field("field", alias(/[0-9]+/, $.integer_literal))
)
),
record_access: ($) =>
prec(
PREC.call,
seq(
field("record", $._unannotated_expression),
".",
field("field", $._identifier)
)
),
infix_operator: ($) => {
const table = /** @type {const} */ ([
[prec.left, PREC.equivalence, choice(...EQUIVALENCE_OPERATORS)],
[prec.left, PREC.implication, choice(...IMPLICATION_OPERATORS)],
[prec.left, PREC.disjunction, choice(...DISJUNCTION_OPERATORS)],
[prec.left, PREC.conjunction, choice(...CONJUNCTION_OPERATORS)],
[nonAssoc, PREC.comparative, choice(...COMPARISON_OPERATORS)],
[nonAssoc, PREC.membership, choice(...MEMBERSHIP_OPERATORS)],
[prec.left, PREC.union_or_diff, choice(...UNION_DIFF_OPERATORS)],
[prec.left, PREC.intersect, choice(...INTERSECTION_OPERATORS)],
[nonAssoc, PREC.range, choice(...RANGE_OPERATORS)],
[prec.left, PREC.additive, choice(...ADDITIVE_OPERATORS)],
[prec.left, PREC.multiplicative, choice(...MULTIPLICATIVE_OPERATORS)],
[prec.left, PREC.exponent, "^"],
[prec.left, PREC.default, "default"],
[prec.left, PREC.quoted_operator, $.backtick_identifier],
])
return choice(
...table.map(([assoc, precedence, operator]) =>
assoc(
precedence,
seq(
field("left", $._expression),
field("operator", operator),
field("right", $._expression)
)
)
)
)
},
annotated_expression: ($) =>
prec(
PREC.annotation,
seq(
field("expression", $._unannotated_expression),
repeat1(
prec.left(
PREC.annotation,
seq("::", field("annotation", $._unannotated_expression))
)
)
)
),
case_expression: ($) =>
seq(
"case",
field("expression", $._expression),
"of",
sepBy1(",", field("case", $.case_expression_case)),
"endcase"
),
case_expression_case: ($) =>
seq(field("pattern", $._pattern), "=>", field("value", $._expression)),
lambda: ($) =>
seq(
"lambda",
optional(seq(field("return_type", $._type), ":")),
$._parameters,
"=>",
field("body", $._expression)
),
let_expression: ($) =>
seq(
"let",
"{",
field(
"let",
sepBy(
choice(",", ";"),
field("item", choice($.declaration, $.constraint))
)
),
"}",
"in",
field("in", $._expression)
),
prefix_operator: ($) =>
choice(
prec(
PREC.negation,
seq(
field("operator", choice(...NEGATION_OPERATORS)),
field("operand", $._expression)
)
),
prec(
PREC.unary,
seq(
field("operator", choice("-", "+")),
field("operand", $._expression)
)
),
// TODO: Could be nonassoc, will always give type error
prec.left(
PREC.range,
seq(
field("operator", choice(...RANGE_OPERATORS)),
field("operand", $._expression)
)
)
),
postfix_operator: ($) =>
choice(
// TODO: Could be nonassoc, will always give type error
prec.right(
PREC.range,
seq(
field("operand", $._expression),
field("operator", choice(...RANGE_OPERATORS))
)
),
prec(
PREC.exponent,
seq(
field("operand", $._expression),
field("operator", choice(...INVERSE_OPERATORS))
)
)
),
set_comprehension: ($) =>
seq(
"{",
field("template", $._expression),
"|",
sepBy1(",", field("generator", $._generator)),
"}"
),
// TODO: Decide if string_literal and string_interpolation should be combined
string_interpolation: ($) =>
seq(
'"',
optional(field("item", alias($._string_content, "string"))),
repeat1(
seq(
"\\(",
field("item", alias($._expression, "expression")),
")",
optional(field("item", alias($._string_content, "string")))
)
),
'"'
),
_type: ($) => choice($._non_concat_type, $.type_concatenation),
// Every type except a concatenation. Call arguments use this: `a ++ b`
// there is an expression, never a type concatenation.
_non_concat_type: ($) =>
choice(
$.array_type,
$.set_type,
$.tuple_type,
$.record_type,
$.operation_type,
$.type_base,
$.any_type,
$.list_type
),
type_concatenation: ($) =>
prec.left(
PREC.concatenation,
seq(field("left", $._type), "++", field("right", $._type))
),
array_type: ($) =>
prec(
PREC.concatenation + 1,
seq(
"array",
"[",
sepBy1(",", field("dimension", $.array_dimension)),
"]",
"of",
field("type", $._type)
)
),
array_dimension: ($) =>
prec(
PREC.call,
seq(
optional(seq(field("name", $._identifier), "in")),
field("type", $._type)
)
),
// Binds tighter than `++`, so `set of A ++ B` is `(set of A) ++ B`
set_type: ($) =>
prec(
PREC.concatenation + 1,
seq(
optional(field("var_par", choice("var", "par"))),
optional(field("opt", "opt")),
"set",
optional(seq("(", field("cardinality", $._expression), ")")),
"of",
field("type", $._type)
)
),
tuple_type: ($) =>
seq(
optional(field("var_par", choice("var", "par"))),
optional(field("opt", "opt")),
"tuple",
"(",
sepBy1(",", field("field", $._type)),
")"
),
record_type: ($) =>
seq(
optional(field("var_par", choice("var", "par"))),
optional(field("opt", "opt")),
"record",
"(",
sepBy1(",", field("field", $.record_type_field)),
")"
),
record_type_field: ($) =>
seq(field("type", $._type), ":", field("name", $._identifier)),
operation_type: ($) =>
seq(
"op",
"(",
field("return_type", $._type),
":",
"(",
sepBy(",", field("parameter", $._type)),
")",
")"
),
type_base: ($) =>
choice(
seq(
optional(field("var_par", choice("var", "par"))),
optional(field("opt", "opt")),
field(
"domain",
choice(
$.primitive_type,
$.new_type,
$.type_inst_id,
$.type_inst_enum_id,
$._non_concat_expression,
$.annotated_expression
)
)
),
seq(field("any", "any"), field("domain", $.type_inst_id))
),
primitive_type: ($) => choice(...primitive_types),
type_inst_id: ($) => /\$[A-Za-z][A-Za-z0-9_]*/,
type_inst_enum_id: ($) => /\$\$[A-Za-z][A-Za-z0-9_]*/,
new_type: ($) => seq("new", field("type", $._identifier)),
any_type: ($) => "any",
list_type: ($) =>
prec(PREC.concatenation + 1, seq("list", "of", field("type", $._type))),
_literal: ($) =>
choice(
$.absent,
$.anonymous,
$.array_literal_2d,
$.array_literal_3d,
$.array_literal,
$.boolean_literal,
$.float_literal,
$.infinity,
$.integer_literal,
$.set_literal,
$.string_literal,
$.tuple_literal,
$.record_literal
),
absent: ($) => "<>",
anonymous: ($) => "_",
// Only an indexed member needs the wrapper; a plain one stands for itself,
// as in `array_literal_2d_row`. Wrapping every member costs a subtree
// each, which is a quarter of the tree for a data file that is one long
// array.
array_literal: ($) =>
seq(
"[",
sepBy(
",",
field("member", choice($._expression, $.array_literal_member))
),
"]"
),
array_literal_member: ($) =>
seq(field("index", $._expression), ":", field("value", $._expression)),
array_literal_2d: ($) =>
seq(
"[|",
optional(
seq(
choice(
repeat1(seq(field("column_index", $._expression), ":")),
field("row", $.array_literal_2d_row)
),
repeat(seq("|", field("row", $.array_literal_2d_row))),
optional("|")
)
),
"|]"
),
array_literal_2d_row: ($) =>
seq(
optional(seq(field("index", $._expression), ":")),
sepBy1(",", field("member", $._expression))
),
array_literal_3d: ($) =>
seq(
"[|",
choice(
seq("|", "|"),
sepBy1(",", field("slice", $.array_literal_3d_slice))
),
"|]"
),
array_literal_3d_slice: ($) =>
seq("|", sepBy1("|", field("row", $.array_literal_3d_row)), "|"),
array_literal_3d_row: ($) => sepBy1(",", field("member", $._expression)),
boolean_literal: ($) => choice("true", "false"),
float_literal: ($) =>
token(
choice(
/\d+\.\d+/,
/\d+(\.\d+)?[Ee][+-]?\d+/,
/0[xX]([0-9a-fA-F]*\.[0-9a-fA-F]+|[0-9a-fA-F]+\.?)[pP][+-]?[0-9]+/
)
),
integer_literal: ($) =>
token(choice(/[0-9]+/, /0[xX][0-9a-fA-F]+/, /0b[01]+/, /0o[0-7]+/)),
infinity: ($) => choice("infinity", "∞"),
set_literal: ($) =>
choice("∅", seq("{", sepBy(",", field("member", $._expression)), "}")),
string_literal: ($) => seq('"', optional($._string_content), '"'),
_string_content: ($) =>
repeat1(field("content", choice($.string_characters, $.escape_sequence))),
string_characters: ($) => token.immediate(prec(1, /[^"\n\\]+/)),
escape_sequence: ($) => {
const simpleEscape = [
["\\'", "'"],
['\\"', '"'],
["\\\\", "\\"],
["\\r", "\r"],
["\\n", "\n"],
["\\t", "\t"],
]
return choice(
field("escape", choice(...simpleEscape.map(([e, v]) => alias(e, v)))),
seq("\\", field("escape", alias(/[0-7]{1,3}/, "octal"))),
seq("\\x", field("escape", alias(/[0-9a-fA-F]{1,2}/, "hexadecimal"))),
seq("\\u", field("escape", alias(/[0-9a-fA-F]{4}/, "hexadecimal"))),
seq("\\U", field("escape", alias(/[0-9a-fA-F]{8}/, "hexadecimal")))
)
},
tuple_literal: ($) =>
seq(
"(",
field("member", $._expression),
",",
sepBy(",", field("member", $._expression)),
")"
),
record_literal: ($) =>
seq("(", sepBy(",", field("member", $.record_member)), ")"),
record_member: ($) =>
seq(field("name", $._identifier), ":", field("value", $._expression)),
identifier: ($) => /[\p{L}_][\p{L}\p{N}_]*/u,
quoted_identifier: ($) => /'[^']*'/,
inversed_identifier: ($) =>
seq(field("identifier", $._identifier), choice(...INVERSE_OPERATORS)),
backtick_identifier: ($) => /`[^`\n]*`/,
_identifier: ($) => choice($.identifier, $.quoted_identifier),
_pattern: ($) =>
choice(
$._identifier,
$.absent,
$.anonymous,
$.boolean_literal,
$.string_literal,
$.pattern_numeric_literal,
$.pattern_call,
$.pattern_tuple,
$.pattern_record
),
pattern_numeric_literal: ($) =>
seq(
optional(field("negative", "-")),
field("value", choice($.integer_literal, $.float_literal, $.infinity))
),
pattern_call: ($) =>
seq(
field("identifier", $._identifier),
"(",
sepBy(",", field("argument", $._pattern)),
")"
),
pattern_tuple: ($) =>
seq(
"(",
field("field", $._pattern),
",",
sepBy(",", field("field", $._pattern)),
")"
),
pattern_record: ($) =>
seq("(", sepBy1(",", field("field", $.pattern_record_field)), ")"),
pattern_record_field: ($) =>
seq(field("name", $._identifier), ":", field("value", $._pattern)),
line_comment: ($) => token(seq("%", /.*/)),
doc_comment: ($) => token(prec(1, /\/\*\*([^*]|\*+[^*\/])*\*+\//)),
file_doc_comment: ($) => token(prec(2, /\/\*\*\*([^*]|\*+[^*\/])*\*+\//)),
block_comment: ($) => token(/\/\*([^*]|\*+[^*\/])*\*+\//),
},
})
function sepBy(sep, rule) {
return seq(repeat(seq(rule, sep)), optional(rule))
}
function sepBy1(sep, rule) {
return seq(rule, repeat(seq(sep, rule)), optional(sep))
}
function nonAssoc(p, rule) {
// Bump precedence by 5 to indicate this is non associative
return prec.left(p + 5, rule)
}
@@ -0,0 +1,55 @@
{
"name": "tree-sitter-minizinc",
"version": "0.1.0",
"description": "MiniZinc grammar for tree-sitter",
"type": "module",
"repository": "git+https://github.com/shackle-rs/shackle.git",
"license": "MPL-2.0",
"author": {
"name": "Jip J. Dekker",
"email": "jip.dekker@monash.edu",
"url": "https://dekker.one/"
},
"main": "bindings/node",
"types": "bindings/node",
"keywords": [
"incremental",
"parsing",
"tree-sitter",
"minizinc"
],
"files": [
"grammar.js",
"tree-sitter.json",
"binding.gyp",
"prebuilds/**",
"bindings/node/*",
"queries/*",
"src/**",
"*.wasm"
],
"dependencies": {
"node-addon-api": "^8.9.2",
"node-gyp-build": "^4.8.4"
},
"devDependencies": {
"prebuildify": "^6.0.1",
"tree-sitter-cli": "^0.27.0"
},
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
},
"scripts": {
"build": "tree-sitter generate && node-gyp-build",
"install": "node-gyp-build",
"node-test": "node --test bindings/node/*_test.js",
"prestart": "tree-sitter build --wasm",
"start": "tree-sitter playground",
"test": "tree-sitter test"
}
}
@@ -0,0 +1 @@
(case_expression) @case
@@ -0,0 +1,6 @@
[
(line_comment)
(doc_comment)
(file_doc_comment)
(block_comment)
] @comment
@@ -0,0 +1,115 @@
; Function calls
(call function: (identifier) @function)
; Function definitions
(function_item name: (identifier) @function)
(predicate name: (identifier) @function)
; Types
(primitive_type) @type.builtin
[
"array"
"list"
"of"
"par"
"set"
"var"
] @type.builtin
; Identifiers
(identifier) @variable
; Keywords
[
"annotation"
"constraint"
"else"
"elseif"
"endif"
"enum"
"function"
"if"
"include"
"let"
"maximize"
"minimize"
"output"
"predicate"
"satisfy"
"solve"
"test"
"then"
"where"
] @keyword
; Operators
[
"<->"
"->"
"<-"
"\\/"
"xor"
"/\\"
"="
"=="
"!="
"<"
"<="
">"
">="
"in"
"subset"
"superset"
"union"
"diff"
"symdiff"
"intersect"
".."
"+"
"-"
"++"
"*"
"/"
"div"
"mod"
"^"
"::"
] @operator
; Punctuation
[
","
":"
";"
"|"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Literals
[
(integer_literal)
(float_literal)
] @number
[
(string_literal)
(string_interpolation)
] @string
(escape_sequence) @escape
[
(absent)
(boolean_literal)
] @constant.builtin
; Comments
[
(block_comment)
(line_comment)
] @comment
@@ -0,0 +1,6 @@
[
(identifier)
(quoted_identifier)
(type_inst_id)
(type_inst_enum_id)
] @identifier
@@ -0,0 +1,5 @@
(function_item name: (identifier) @name) @definition.function
(predicate name: (identifier) @name) @definition.function
(annotation name: (identifier) @name) @definition.function
(call function: (identifier) @name) @reference.call
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
#ifndef TREE_SITTER_ALLOC_H_
#define TREE_SITTER_ALLOC_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// Allow clients to override allocation functions
#ifdef TREE_SITTER_REUSE_ALLOCATOR
extern void *(*ts_current_malloc)(size_t size);
extern void *(*ts_current_calloc)(size_t count, size_t size);
extern void *(*ts_current_realloc)(void *ptr, size_t size);
extern void (*ts_current_free)(void *ptr);
#ifndef ts_malloc
#define ts_malloc ts_current_malloc
#endif
#ifndef ts_calloc
#define ts_calloc ts_current_calloc
#endif
#ifndef ts_realloc
#define ts_realloc ts_current_realloc
#endif
#ifndef ts_free
#define ts_free ts_current_free
#endif
#else
#ifndef ts_malloc
#define ts_malloc malloc
#endif
#ifndef ts_calloc
#define ts_calloc calloc
#endif
#ifndef ts_realloc
#define ts_realloc realloc
#endif
#ifndef ts_free
#define ts_free free
#endif
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ALLOC_H_
@@ -0,0 +1,336 @@
#ifndef TREE_SITTER_ARRAY_H_
#define TREE_SITTER_ARRAY_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "./alloc.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4101)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#endif
#define Array(T) \
struct { \
T *contents; \
uint32_t size; \
uint32_t capacity; \
}
/// Initialize an array.
#define array_init(self) \
((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL)
/// Create an empty array.
#define array_new() \
{ NULL, 0, 0 }
/// Get a pointer to the element at a given `index` in the array.
#define array_get(self, _index) \
(assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index])
/// Get a pointer to the first element in the array.
#define array_front(self) array_get(self, 0)
/// Get a pointer to the last element in the array.
#define array_back(self) array_get(self, (self)->size - 1)
/// Clear the array, setting its size to zero. Note that this does not free any
/// memory allocated for the array's contents.
#define array_clear(self) ((self)->size = 0)
#ifdef __cplusplus
#define _array__cast(self, expr) (decltype((self)->contents))(expr)
#else
#define _array__cast(self, expr) (expr)
#endif
/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is
/// less than the array's current capacity, this function has no effect.
#define array_reserve(self, new_capacity) \
((self)->contents = _array__cast(self, _array__reserve( \
(void *)(self)->contents, &(self)->capacity, \
array_elem_size(self), new_capacity)) \
)
/// Free any memory allocated for this array. Note that this does not free any
/// memory allocated for the array's contents.
#define array_delete(self) \
do { \
if ((self)->contents) ts_free((self)->contents); \
(self)->contents = NULL; \
(self)->size = 0; \
(self)->capacity = 0; \
} while (0)
/// Push a new `element` onto the end of the array.
#define array_push(self, element) \
do { \
(self)->contents = _array__cast(self, _array__grow( \
(void *)(self)->contents, (self)->size, &(self)->capacity, \
1, array_elem_size(self) \
)); \
(self)->contents[(self)->size++] = (element); \
} while(0)
/// Increase the array's size by `count` elements.
/// New elements are zero-initialized.
#define array_grow_by(self, count) \
do { \
if ((count) == 0) break; \
(self)->contents = _array__cast(self, _array__grow( \
(self)->contents, (self)->size, &(self)->capacity, \
count, array_elem_size(self) \
)); \
memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \
(self)->size += (count); \
} while (0)
/// Append all elements from one array to the end of another.
#define array_push_all(self, other) \
array_extend((self), (other)->size, (other)->contents)
/// Append `count` elements to the end of the array, reading their values from the
/// `contents` pointer.
#define array_extend(self, count, other_contents) \
((self)->contents = _array__cast(self, _array__splice( \
(void*)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), (self)->size, 0, count, other_contents \
)))
/// Remove `old_count` elements from the array starting at the given `index`. At
/// the same index, insert `new_count` new elements, reading their values from the
/// `new_contents` pointer.
#define array_splice(self, _index, old_count, new_count, new_contents) \
((self)->contents = _array__cast(self, _array__splice( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), _index, old_count, new_count, new_contents \
)))
/// Insert one `element` into the array at the given `index`.
#define array_insert(self, _index, element) \
((self)->contents = _array__cast(self, _array__splice( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
array_elem_size(self), _index, 0, 1, &(element) \
)))
/// Remove one element from the array at the given `index`.
#define array_erase(self, _index) \
_array__erase((void *)(self)->contents, &(self)->size, array_elem_size(self), _index)
/// Pop the last element off the array, returning the element by value.
#define array_pop(self) ((self)->contents[--(self)->size])
/// Assign the contents of one array to another, reallocating if necessary.
#define array_assign(self, other) \
((self)->contents = _array__cast(self, _array__assign( \
(void *)(self)->contents, &(self)->size, &(self)->capacity, \
(const void *)(other)->contents, (other)->size, array_elem_size(self) \
)))
/// Swap one array with another
#define array_swap(self, other) \
do { \
void *_array_swap_tmp = (void *)(self)->contents; \
(self)->contents = (other)->contents; \
(other)->contents = _array__cast(other, _array_swap_tmp); \
_array__swap(&(self)->size, &(self)->capacity, \
&(other)->size, &(other)->capacity); \
} while (0)
/// Get the size of the array contents
#define array_elem_size(self) (sizeof *(self)->contents)
/// Search a sorted array for a given `needle` value, using the given `compare`
/// callback to determine the order.
///
/// If an existing element is found to be equal to `needle`, then the `index`
/// out-parameter is set to the existing value's index, and the `exists`
/// out-parameter is set to true. Otherwise, `index` is set to an index where
/// `needle` should be inserted in order to preserve the sorting, and `exists`
/// is set to false.
#define array_search_sorted_with(self, compare, needle, _index, _exists) \
_array__search_sorted(self, 0, compare, , needle, _index, _exists)
/// Search a sorted array for a given `needle` value, using integer comparisons
/// of a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_with`.
#define array_search_sorted_by(self, field, needle, _index, _exists) \
_array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists)
/// Insert a given `value` into a sorted array, using the given `compare`
/// callback to determine the order.
#define array_insert_sorted_with(self, compare, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_with(self, compare, &(value), &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
/// Insert a given `value` into a sorted array, using integer comparisons of
/// a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_by`.
#define array_insert_sorted_by(self, field, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_by(self, field, (value) field, &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
// Private
// Pointers to individual `Array` fields (rather than the entire `Array` itself)
// are passed to the various `_array__*` functions below to address strict aliasing
// violations that arises when the _entire_ `Array` struct is passed as `Array(void)*`.
//
// The `Array` type itself was not altered as a solution in order to avoid breakage
// with existing consumers (in particular, parsers with external scanners).
/// This is not what you're looking for, see `array_erase`.
static inline void _array__erase(void* self_contents, uint32_t *size,
size_t element_size, uint32_t index) {
assert(index < *size);
char *contents = (char *)self_contents;
memmove(contents + index * element_size, contents + (index + 1) * element_size,
(*size - index - 1) * element_size);
(*size)--;
}
/// This is not what you're looking for, see `array_reserve`.
static inline void *_array__reserve(void *contents, uint32_t *capacity,
size_t element_size, uint32_t new_capacity) {
void *new_contents = contents;
if (new_capacity > *capacity) {
if (contents) {
new_contents = ts_realloc(contents, new_capacity * element_size);
} else {
new_contents = ts_malloc(new_capacity * element_size);
}
*capacity = new_capacity;
}
return new_contents;
}
/// This is not what you're looking for, see `array_assign`.
static inline void *_array__assign(void* self_contents, uint32_t *self_size, uint32_t *self_capacity,
const void *other_contents, uint32_t other_size, size_t element_size) {
void *new_contents = _array__reserve(self_contents, self_capacity, element_size, other_size);
*self_size = other_size;
memcpy(new_contents, other_contents, *self_size * element_size);
return new_contents;
}
/// This is not what you're looking for, see `array_swap`.
static inline void _array__swap(uint32_t *self_size, uint32_t *self_capacity,
uint32_t *other_size, uint32_t *other_capacity) {
uint32_t tmp_size = *self_size;
uint32_t tmp_capacity = *self_capacity;
*self_size = *other_size;
*self_capacity = *other_capacity;
*other_size = tmp_size;
*other_capacity = tmp_capacity;
}
/// This is not what you're looking for, see `array_push` or `array_grow_by`.
static inline void *_array__grow(void *contents, uint32_t size, uint32_t *capacity,
uint32_t count, size_t element_size) {
void *new_contents = contents;
uint32_t new_size = size + count;
if (new_size > *capacity) {
uint32_t new_capacity = *capacity * 2;
if (new_capacity < 8) new_capacity = 8;
if (new_capacity < new_size) new_capacity = new_size;
new_contents = _array__reserve(contents, capacity, element_size, new_capacity);
}
return new_contents;
}
/// This is not what you're looking for, see `array_splice`.
static inline void *_array__splice(void *self_contents, uint32_t *size, uint32_t *capacity,
size_t element_size,
uint32_t index, uint32_t old_count,
uint32_t new_count, const void *elements) {
uint32_t new_size = *size + new_count - old_count;
uint32_t old_end = index + old_count;
uint32_t new_end = index + new_count;
assert(old_end <= *size);
void *new_contents = _array__reserve(self_contents, capacity, element_size, new_size);
char *contents = (char *)new_contents;
if (*size > old_end) {
memmove(
contents + new_end * element_size,
contents + old_end * element_size,
(*size - old_end) * element_size
);
}
if (new_count > 0) {
if (elements) {
memcpy(
(contents + index * element_size),
elements,
new_count * element_size
);
} else {
memset(
(contents + index * element_size),
0,
new_count * element_size
);
}
}
*size += new_count - old_count;
return new_contents;
}
/// A binary search routine, based on Rust's `std::slice::binary_search_by`.
/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`.
#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \
do { \
*(_index) = start; \
*(_exists) = false; \
uint32_t size = (self)->size - *(_index); \
if (size == 0) break; \
int comparison; \
while (size > 1) { \
uint32_t half_size = size / 2; \
uint32_t mid_index = *(_index) + half_size; \
comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \
if (comparison <= 0) *(_index) = mid_index; \
size -= half_size; \
} \
comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \
if (comparison == 0) *(_exists) = true; \
else if (comparison < 0) *(_index) += 1; \
} while (0)
/// Helper macro for the `_sorted_by` routines below. This takes the left (existing)
/// parameter by reference in order to work with the generic sorting function above.
#define _compare_int(a, b) ((int)*(a) - (int)(b))
#ifdef _MSC_VER
#pragma warning(pop)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ARRAY_H_
@@ -0,0 +1,286 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSStateId;
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
typedef struct TSLanguageMetadata {
uint8_t major_version;
uint8_t minor_version;
uint8_t patch_version;
} TSLanguageMetadata;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
// Used to index the field and supertype maps.
typedef struct {
uint16_t index;
uint16_t length;
} TSMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
void (*log)(const TSLexer *, const char *, ...);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
uint16_t reserved_word_set_id;
} TSLexerMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
typedef struct {
int32_t start;
int32_t end;
} TSCharacterRange;
struct TSLanguage {
uint32_t abi_version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexerMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
const char *name;
const TSSymbol *reserved_words;
uint16_t max_reserved_word_set_size;
uint32_t supertype_count;
const TSSymbol *supertype_symbols;
const TSMapSlice *supertype_map_slices;
const TSSymbol *supertype_map_entries;
TSLanguageMetadata metadata;
};
static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) {
uint32_t index = 0;
uint32_t size = len - index;
while (size > 1) {
uint32_t half_size = size / 2;
uint32_t mid_index = index + half_size;
const TSCharacterRange *range = &ranges[mid_index];
if (lookahead >= range->start && lookahead <= range->end) {
return true;
} else if (lookahead > range->end) {
index = mid_index;
}
size -= half_size;
}
const TSCharacterRange *range = &ranges[index];
return (lookahead >= range->start && lookahead <= range->end);
}
/*
* Lexer Macros
*/
#ifdef _MSC_VER
#define UNUSED __pragma(warning(suppress : 4101))
#else
#define UNUSED __attribute__((unused))
#endif
#define START_LEXER() \
bool result = false; \
bool skip = false; \
UNUSED \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define ADVANCE_MAP(...) \
{ \
static const uint16_t map[] = { __VA_ARGS__ }; \
for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \
if (map[i] == lookahead) { \
state = map[i + 1]; \
goto next_state; \
} \
} \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT)
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value) \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value), \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_name, children, precedence, prod_id) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_name, \
.child_count = children, \
.dynamic_precedence = precedence, \
.production_id = prod_id \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_
@@ -0,0 +1,37 @@
{
"$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json",
"grammars": [
{
"name": "minizinc",
"camelcase": "Minizinc",
"scope": "source.minizinc",
"path": ".",
"file-types": ["mzn", "fzn"]
}
],
"metadata": {
"version": "0.1.0",
"license": "MPL-2.0",
"description": "MiniZinc grammar for tree-sitter",
"authors": [
{
"name": "Jip J. Dekker",
"email": "jip.dekker@monash.edu",
"url": "https://dekker.one/"
}
],
"links": {
"repository": "git+https://github.com/shackle-rs/shackle.git"
}
},
"bindings": {
"c": true,
"go": true,
"java": false,
"node": true,
"python": true,
"rust": true,
"swift": true,
"zig": false
}
}
+24
View File
@@ -0,0 +1,24 @@
-- [[ Basic Keymaps ]]
-- See `:help vim.keymap.set()`
-- Clear highlights on search when pressing <Esc> in normal mode
-- See `:help hlsearch`
vim.keymap.set("n", "<Esc>", "<cmd>nohlsearch<CR>")
-- Diagnostic keymaps
vim.keymap.set("n", "<leader>q", vim.diagnostic.setloclist, { desc = "Open diagnostic [Q]uickfix list" })
-- Keybinds to make split navigation easier.
-- Use CTRL+<hjkl> to switch between windows
--
-- See `:help wincmd` for a list of all window commands
vim.keymap.set("n", "<C-h>", "<C-w><C-h>", { desc = "Move focus to the left window" })
vim.keymap.set("n", "<C-l>", "<C-w><C-l>", { desc = "Move focus to the right window" })
vim.keymap.set("n", "<C-j>", "<C-w><C-j>", { desc = "Move focus to the lower window" })
vim.keymap.set("n", "<C-k>", "<C-w><C-k>", { desc = "Move focus to the upper window" })
vim.keymap.set("n", "<leader>n", ":NERDTreeFocus<CR>")
vim.keymap.set("n", "<C-n>", ":NERDTree<CR>")
vim.keymap.set("n", "<C-t>", ":NERDTreeToggle<CR>")
vim.keymap.set("n", "<C-f>", ":NERDTreeFind<CR>")
vim.keymap.set("n", "<leader>rf", ":NERDTreeRefreshRoot<CR>")
+34 -5
View File
@@ -1,12 +1,30 @@
-- Make line numbers default -- Make line numbers default
vim.wo.number = true vim.wo.number = true
vim.g.have_nerd_font = false
vim.o.colorcolumn = "80" vim.o.colorcolumn = "80"
vim.o.background = "dark" vim.o.background = "dark"
vim.o.clipboard = "unnamedplus" vim.o.clipboard = "unnamedplus"
vim.g.clipboard = {
name = "OSC 52",
copy = {
["+"] = require("vim.ui.clipboard.osc52").copy("+"),
["*"] = require("vim.ui.clipboard.osc52").copy("*"),
},
paste = {
["+"] = require("vim.ui.clipboard.osc52").paste("+"),
["*"] = require("vim.ui.clipboard.osc52").paste("*"),
},
}
if vim.g.loaded_clipboard_provider ~= nil then
vim.g.loaded_clipboard_provider = nil
vim.cmd("runtime autoload/provider/clipboard.vim")
end
-- Enable mouse mode -- Enable mouse mode
vim.o.mouse = 'a' vim.o.mouse = "a"
-- Enable break indent -- Enable break indent
vim.o.breakindent = true vim.o.breakindent = true
@@ -23,12 +41,25 @@ vim.o.gdefault = true
-- Decrease update time -- Decrease update time
vim.o.updatetime = 250 vim.o.updatetime = 250
-- Decrease mapped sequence wait time
vim.o.timeoutlen = 300
-- Configure how new splits should be opened
vim.o.splitright = true
vim.o.splitbelow = true
-- Add a gutter so that things don't jump around -- Add a gutter so that things don't jump around
vim.wo.signcolumn = 'yes' vim.wo.signcolumn = "yes"
-- Prevent markdown from collapsing down -- Prevent markdown from collapsing down
vim.o.foldenable = false vim.o.foldenable = false
-- Preview substitutions live, as you type!
vim.o.inccommand = "split"
-- Show which line your cursor is on
vim.o.cursorline = true
-- Tabs instead of spaces -- Tabs instead of spaces
-- :h tabstop is the number of spaces for a tab character -- :h tabstop is the number of spaces for a tab character
-- :h expandtab expands tabs into spaces -- :h expandtab expands tabs into spaces
@@ -38,9 +69,7 @@ vim.o.tabstop = 2
vim.o.shiftwidth = 2 vim.o.shiftwidth = 2
-- Set completeopt to have a better completion experience -- Set completeopt to have a better completion experience
vim.o.completeopt = 'menuone,noinsert,noselect' vim.o.completeopt = "menuone,noinsert,noselect"
-- Disable modelines, this removes some errors when opening text files -- Disable modelines, this removes some errors when opening text files
vim.o.modeline = false vim.o.modeline = false
vim.o.rtp = vim.o.rtp .. ",~/oxy/current-course"
+4
View File
@@ -0,0 +1,4 @@
sessions/
missions/
bin/
npm/
+16
View File
@@ -0,0 +1,16 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
/**
* /clear — alias for /new.
*
* Starts a new session, discarding the current conversation history
* (the same behaviour as the built-in `/new`).
*/
export default function (pi: ExtensionAPI) {
pi.registerCommand("clear", {
description: "Start a new session (alias for /new)",
handler: async (_args, ctx) => {
await ctx.newSession();
},
});
}
+47
View File
@@ -0,0 +1,47 @@
{
"mcpServers": {
"cclsp": {
"type": "stdio",
"command": "npx",
"args": ["cclsp"],
"env": {
"CCLSP_CONFIG_PATH": ".claude/cclsp.json"
}
},
"linear": {
"type": "http",
"url": "https://mcp.linear.app/mcp"
},
"notion": {
"type": "http",
"url": "https://mcp.notion.com/mcp"
},
"nx": {
"type": "stdio",
"command": "npx",
"args": ["nx-mcp"],
"env": {
"NX_DAEMON": "false"
}
},
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}"
}
},
"datadog-mcp": {
"type": "http",
"url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core,error-tracking"
},
"slack": {
"type": "http",
"url": "https://mcp.slack.com/mcp"
},
"vesta": {
"type": "http",
"url": "https://vesta-mcp.fusion.pylon.ninja/mcp"
}
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"providers": {
"fireworks-us": {
"baseUrl": "https://us.api.fireworks.ai/inference/v1",
"api": "openai-completions",
"apiKey": "$FIREWORKS_API_KEY",
"authHeader": true,
"models": [
{
"id": "accounts/fireworks/routers/glm-5p3-us",
"name": "GLM 5.3 US"
},
{
"id": "accounts/fireworks/routers/kimi-k3-us",
"name": "Kimi K3 US"
},
{
"id": "accounts/fireworks/routers/glm-5p3-flash-us",
"name": "GLM 5.3 Flash US"
},
{
"id": "accounts/fireworks/routers/deepseek-v4-flash-0731-us",
"name": "DeepSeek V4 Flash 0731 US"
}
]
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"lastChangelogVersion": "0.87.1",
"defaultProvider": "fireworks-us",
"defaultModel": "accounts/fireworks/routers/glm-5p3-flash-us",
"defaultThinkingLevel": "high",
"enabledModels": [
"fireworks/accounts/fireworks/routers/*",
"fireworks-us/accounts/fireworks/routers/*"
],
"packages": [
"npm:pi-mcp-adapter",
"npm:pi-subagents",
"npm:@juicesharp/rpiv-ask-user-question"
],
"skills": ["~/.claude/skills"],
"theme": "dark"
}
Executable
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
set -eu
cd -- "$(dirname -- "$0")"
desync=false
case "${1:-}" in
-d|--desync)
desync=true
;;
"")
;;
*)
printf 'Usage: %s [--desync|-d]\n' "$0" >&2
exit 2
;;
esac
for path in */; do
[ -d "$path" ] || continue
name=${path%/}
if "$desync"; then
printf 'Unstowing %s...\n' "$name"
stow --delete --target "$HOME" --verbose "$name"
else
printf 'Stowing %s...\n' "$name"
stow --restow --target "$HOME" --verbose "$name"
fi
done