Compare commits

..
2 Commits
Author SHA1 Message Date
tgrosinger fa680ff2e4 Atuin: Exclude youtube-dl from history 2025-12-08 08:41:53 -08:00
tgrosinger 72c20701b7 Sway: Switch from workrave to custom break timer 2025-12-08 08:41:37 -08:00
151 changed files with 424 additions and 9499 deletions
-3
View File
@@ -6,6 +6,3 @@
# Python # Python
*.pyc *.pyc
# dev-tickets CLI dependencies, installed by install-packages.sh (npm ci)
home/.local/lib/dev-tickets/node_modules/
-3
View File
@@ -1,3 +0,0 @@
[submodule "mattpocock-skills"]
path = mattpocock-skills
url = ssh://git@git.grosinger.net:22322/tgrosinger/mattpocock-skills.git
-4
View File
@@ -6,7 +6,3 @@
`run-stow.sh` just links the dotfiles from this repo into their correct locations. `run-stow.sh` just links the dotfiles from this repo into their correct locations.
## Docs
[`docs/claude-resource-limits.md`](docs/claude-resource-limits.md) — why `dev-claude` launches through `systemd-run` into `claude.slice`, and what breaks if that wrapper is simplified.
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
podman run --rm -i \
-v ${HOME}/Music:/downloads:z \
--userns keep-id:uid=1000,gid=1000 \
--entrypoint scdl \
yt-dlp:latest $@
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
podman run --rm -i \
-v ${HOME}/Music:/downloads:z \
--userns keep-id:uid=1000,gid=1000 \
--entrypoint spotdl \
yt-dlp:latest $@
+7
View File
@@ -0,0 +1,7 @@
#/bin/bash
podman run --rm \
-v ${HOME}/Videos/youtube:/downloads:z \
--userns keep-id:uid=1000,gid=1000 \
yt-dlp:latest -S 'res:1080' $@
+7
View File
@@ -0,0 +1,7 @@
#/bin/bash
podman run --rm \
-v ${HOME}/Videos/youtube:/downloads:z \
--userns keep-id:uid=1000,gid=1000 \
yt-dlp:latest --embed-metadata -o "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s" -S 'res:1080' $@
-208
View File
@@ -1,208 +0,0 @@
# Claude Code resource limits
## The problem this solves
Runaway subprocess trees under Claude Code — parallel vitest workers, `tsc`, eslint — periodically locked this machine up hard enough that rebooting was the only way out.
The cause is **not** CPU starvation. With 16 cores the scheduler timeslices through a busy load fine. It is memory exhaustion driving **zram thrash**: swap on this machine is 8 GiB of zram *only*, so swapping consumes physical RAM and burns CPU compressing, which frees less memory, which swaps harder. The kernel OOM killer fires far too late to save the interactive session, and sway stops being scheduled long enough to accept input.
At the time this was set up, nothing on the box caught that spiral:
| Mechanism | State |
| --- | --- |
| `kernel.sysrq` | `16` — only `sync` permitted, so `Alt+SysRq+F` was blocked |
| `systemd-oomd` | inactive and disabled |
| `earlyoom` | not installed |
| `sshd` | inactive — no remote rescue path |
There was no escape hatch, which is why rebooting was the only recovery.
## The fix
Every Claude session launched through `dev-claude` runs inside a transient systemd scope placed in **`claude.slice`**, which carries a hard memory ceiling. A runaway tree now hits that ceiling and is OOM-killed *inside its own cgroup*, while sway and everything else keep running.
This needs no root: `cpu memory pids` are already delegated to the user manager (`Delegate=yes` on `user@1000.service`).
### Files involved
| File | Role |
| --- | --- |
| `home/.config/systemd/user/claude.slice` | The limits themselves |
| `home/.config/fish/functions/dev-claude.fish` | Places each session into the slice |
### The launcher
`dev-claude.fish` builds a launch prefix once and uses it at all four launch sites:
```fish
set -l launch /usr/bin/env claude
if command -q systemd-run
set launch systemd-run --user --scope --quiet --collect --slice=claude.slice -- /usr/bin/env claude
end
```
Because `systemd-run --scope` **execs** rather than forks, the resulting process tree and command line are byte-identical to running `claude` directly. The only difference is which cgroup it lands in.
## Three non-obvious things
These are the parts that will look wrong later and get "cleaned up". Don't.
### 1. `/usr/bin/env` is load-bearing
`systemd-run` resolves its target through `PATH` and **rewrites `argv[0]` to an absolute path**:
```
$ systemd-run --user --scope --quiet -- argvprobe.sh
argv0=[/tmp/argvprobe.sh] # not "argvprobe.sh"
```
`~/.tmux.conf` sets `@resurrect-processes '"claude->dev-claude *"'`, and tmux-resurrect matches that with an **anchored** regex — `scripts/process_restore_helpers.sh`:
```bash
if [[ "$pane_full_command" =~ (^${match} ) ]] || [[ "$pane_full_command" =~ (^${match}$) ]]; then
```
So a bare `systemd-run … -- claude` makes the saved command `/home/tgrosinger/.local/bin/claude --session-id …`, which fails `^claude ` and **silently stops restoring Claude panes** — breaking the exact thing `dev-claude` exists to provide. There is no error; restore just quietly does nothing.
Interposing `env` fixes it, because `env` re-execs its target with `argv[0]` set to the bare name:
```
$ systemd-run --user --scope --quiet -- /usr/bin/env sleep 137
cmdline: [sleep 137] # bare argv[0], as if run directly
ppid: bash # systemd-run exec'd; no extra process in the tree
```
`env` also does the PATH lookup that `command claude` used to do here, so a fish function or alias named `claude` still cannot shadow the real binary.
### 2. `dev-claude` must not `exec`
This one bit once already: for months every Claude pane was saved, matched nothing, and restored nothing — silently.
tmux-resurrect's `ps` save strategy does not read the pane process. It reads the pane process's **children** (`save_command_strategies/ps.sh`):
```bash
ps -ao "ppid,args" | sed "s/^ *//" | grep "^${PANE_PID}" | cut -d' ' -f2-
```
`exec` makes Claude *become* the pane pid, so that grep returns Claude's own subprocesses instead of Claude. The saved `pane_full_command` came out as `socat UNIX-LISTEN:/tmp/claude-http-….sock …` — or empty, when nothing happened to be running — never `claude --session-id …`. Both fail `^claude `, so `@resurrect-processes` never fires.
Running Claude as a plain foreground child keeps fish at the pane pid, which is exactly where resurrect looks:
```
$ fish -c 'echo $fish_pid; /usr/bin/env sleep 3' # no exec
286 285 fish -c …
317 286 sleep 3
-> resurrect saves: [sleep 3]
$ fish -c 'echo $fish_pid; exec /usr/bin/env sleep 3'
328 285 sleep 3
-> resurrect saves: []
```
The cost is that the pane returns to a fish prompt when Claude exits rather than closing. That is the intended trade — a `dev` layout does not collapse when a session ends.
### 3. `command` cannot be used in the fallback
The natural fallback is `set -l launch command claude`. While `dev-claude` still used `exec`, fish rejected it outright:
```
fish: The expanded command is a keyword.
set -l launch command echo; exec $launch ok
^~~~~~^
```
`exec` will not accept an expansion that begins with a builtin. That specific constraint is gone with `exec`, but `/usr/bin/env claude` stays in both branches: it gives the same shadowing guarantee, and keeping the two branches identical means the saved command line does not depend on whether `systemd-run` was found.
## Limits, and why each one
Set on the **slice**, not on each scope, so all concurrent sessions share one budget. Per-scope limits would let four sessions claim 4 × 14 GiB and defeat the point.
| Setting | Value | Reason |
| --- | --- | --- |
| `MemoryHigh` | `10G` | Soft ceiling — throttle and reclaim first, so there's back pressure before anything dies |
| `MemoryMax` | `14G` | Hard ceiling — OOM-kill inside the cgroup. Leaves ~16 GiB of 30 GiB for everything else, which normally sits near 13 GiB |
| `MemorySwapMax` | `2G` | The zram-specific piece. Uncapped, the slice can push 8 GiB into zram, which itself occupies physical RAM — that's the thrash spiral |
| `CPUWeight` | `50` | Only bites under contention, so nothing slows down when the box is idle |
| `CPUQuota` | `1200%` | Reserves ~4 of 16 cores for sway and the compositor unconditionally |
| `TasksMax` | `2048` | Bounds pid exhaustion from a runaway worker pool |
## Operating it
Kill every Claude session at once, without touching anything else:
```bash
systemctl --user kill claude.slice
```
Watch live usage against the caps:
```bash
systemd-cgtop --user
systemctl --user status claude.slice
```
Tune without restarting running sessions (edit the unit file to persist):
```bash
systemctl --user set-property claude.slice MemoryMax=16G
```
After editing the unit file:
```bash
systemctl --user daemon-reload
```
## Verifying it actually works
**The check that matters.** If the unit file is missing or `daemon-reload` was skipped, systemd **silently auto-creates `claude.slice` with no limits** — sessions launch fine, appear healthy, and are completely unbounded. There is no warning. Always confirm the real value:
```bash
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/claude.slice/memory.max
# must print 15032385536, NOT "max"
```
Confirm a running session is actually inside the slice:
```bash
systemd-cgls --user-unit claude.slice
```
Confirm tmux-resurrect is unaffected — this is the string its `ps` save strategy reads. It must be non-empty, must name `claude` and not one of its subprocesses, and must show a **bare** `claude`, not an absolute path:
```bash
ps -ao ppid,args | grep "^$(tmux display -p '#{pane_pid}') "
# claude --session-id <uuid>
```
Then check what actually landed on disk, which is the thing restore reads:
```bash
grep -a claude ~/.local/share/tmux/resurrect/last | cut -f11
# :claude --session-id <uuid>
```
### Testing containment safely
Do **not** force an OOM inside `claude.slice` to test it. The kernel picks the largest consumer in the cgroup, which is very likely to be a real session rather than the test process. Use an unrelated top-level slice instead — note that systemd treats `-` as a hierarchy separator, so a name like `claude-test.slice` would nest *inside* `claude.slice` and hit the same problem:
```bash
systemd-run --user --scope --quiet --collect --slice=capcheck.slice \
-p MemoryMax=512M -p MemorySwapMax=0 \
-- python3 -c "b=bytearray()
while True: b.extend(bytes(1<<20))"
```
Expect `Killed`, with the desktop unaffected.
## Known gap
Typing plain `claude` instead of `dev-claude` **bypasses the cap entirely**. Closing that would mean adding a `claude` fish function delegating to `dev-claude` — safe from recursion, since `env` resolves the binary through `PATH` and never re-enters fish.
## Deliberately not done here
Independent of this, and all requiring root:
- `kernel.sysrq=1`, restoring `Alt+SysRq+F` as an in-kernel escape hatch that works even when userspace is fully starved
- `earlyoom` (packaged in Fedora), which would also catch non-Claude runaways
- enabling `sshd` for remote rescue from another device
-143
View File
@@ -1,143 +0,0 @@
---
name: brainstorming
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
source: Modified from https://github.com/obra/superpowers/blob/main/skills/brainstorming/SKILL.md
---
# Brainstorming Ideas Into Designs
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>
## Anti-Pattern: "This Is Too Simple To Need A Design"
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
## Checklist
You MUST create a task for each of these items and complete them in order:
1. **Explore project context** — check files, docs, recent commits
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke skills such as grill-with-docs or to-prd to create implementation plan
## Process Flow
```dot
digraph brainstorming {
"Explore project context" [shape=box];
"Visual questions ahead?" [shape=diamond];
"Ask clarifying questions" [shape=box];
"Propose 2-3 approaches" [shape=box];
"Present design sections" [shape=box];
"User approves design?" [shape=diamond];
"Write design doc" [shape=box];
"Spec self-review\n(fix inline)" [shape=box];
"User reviews spec?" [shape=diamond];
"Transition to implementation" [shape=doublecircle];
"Explore project context" -> "Visual questions ahead?";
"Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
"Ask clarifying questions" -> "Propose 2-3 approaches";
"Propose 2-3 approaches" -> "Present design sections";
"Present design sections" -> "User approves design?";
"User approves design?" -> "Present design sections" [label="no, revise"];
"User approves design?" -> "Write design doc" [label="yes"];
"Write design doc" -> "Spec self-review\n(fix inline)";
"Spec self-review\n(fix inline)" -> "User reviews spec?";
"User reviews spec?" -> "Write design doc" [label="changes requested"];
"User reviews spec?" -> "Transition to implementation" [label="approved"];
}
```
**The terminal state is writing a plan.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skills you invoke after brainstorming are grill-with-docs or to-prd.
## The Process
**Understanding the idea:**
- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria
**Exploring approaches:**
- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why
**Presenting the design:**
- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense
**Design for isolation and clarity:**
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
**Working in existing codebases:**
- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
## After the Design
**Documentation:**
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
Fix any issues inline. No need to re-review — just fix and move on.
**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
**Implementation:**
- Invoke the grill-with-docs skill to create a detailed implementation plan
- Do NOT invoke any other skill. grill-with-docs is the next step.
## Key Principles
- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/codebase-design
@@ -1,122 +0,0 @@
---
name: daily-note-cleanup
description: Tidy a day's Obsidian daily note — create that day's note and the next day's from the template when missing, fill in the weather, and other cleanup tasks. Use when asked to clean up, tidy, or process yesterday's or a given day's daily note, or on a scheduled end-of-day run.
---
# daily-note-cleanup — tidy a day's daily note
The subject is one daily note: **yesterday's by default**, or a date the user names. Every step below reads and edits that note, plus the **next day's** note as the one place carried-forward work lands. Nothing here touches the vault's other notes.
The notes live at `<vault>/daily/<YYYY-MM-DD>.md`. The template is `<vault>/templates/daily.md`; its only placeholder is `{{date}}`.
Do the steps in order. Each edits the note in place — touch only what a step names and leave prose, ordering, and embeds exactly as they are. The vault syncs live from other devices, so a wholesale rewrite is a sync conflict waiting to happen: make targeted edits.
## 1. Resolve the two dates
```
date -d "yesterday" +%F # subject day, when the user names none
date -d "<subject> +1 day" +%F # the next day
```
## 2. Create either note if it is missing
For a note that does not exist, read `<vault>/templates/daily.md`, replace `{{date}}` with that note's own date, and write it to `<vault>/daily/<date>.md`. A note that already exists is left untouched here — it may already hold a day's writing.
## 3. Carry pending tasks forward
Under the subject note's `## Work`, find the `Pending tasks:` line and the bullet list right under it. The block runs from that label to the next header or the next non-bullet, non-blank line. Move the whole block — label and bullets — into the **next day's** note under its `## Work`:
- Fresh next-day note: drop the block straight under `## Work`.
- Next-day `## Work` already carries a `Pending tasks:` list: append the bullets to it, don't add a second label.
Then delete the block from the subject note. If that empties `## Work`, step 6 removes the header.
No `Pending tasks:` under `## Work` means nothing to carry — skip.
## 4. Fill in the weather
If the subject note's frontmatter has no weather keys (`temp_high` and the rest), invoke the **weather** skill for the subject date; it owns reading the station and writing those keys. Already present — the scheduled run often fills yesterday — and you skip it. If weather refuses the day (a station gap, partial coverage, or a date that hasn't ended), leave the keys absent and move on.
## 5. Remove empty frontmatter fields
Delete any frontmatter line whose value is blank — `work_mood:` with nothing after the colon. Keep every field that has a value. If no frontmatter fields remain, remove the `---` fences.
## 6. Remove empty headers
Work from the deepest headers outward. Remove a header when nothing lives under it — everything from it to the next same-or-higher header is blank — and no sub-header of it survives. Removing an empty leaf can empty its parent, so re-check the parent.
Two kinds of header stay even when they look bare:
- **Embeds are content.** A header whose body is only `![[...]]` embed links stays (`## Articles`, `## Tasks`).
- **Structure is content.** A header with a surviving sub-header stays, even when its own lines are blank (`## Notes` over a `### ...` that has bullets).
Never remove the `# <date>` title. Collapse the blank lines a removed section leaves behind to a single blank between the headers that remain.
## What it looks like
A subject note the morning after, with empty template sections, blank moods, no weather yet, and a pending list under Work:
```markdown
---
work_mood:
general_mood:
weight:
---
# 2026-08-27
## Articles
![[Articles.base#Added Today]]
## Timeline
## Journal
## Notes
## Work
Pending tasks:
- Deploy the account server again.
- Refactor ReviewRequestTypes.
## Tasks
![[Daily Note Tasks.base#Completed Today]]
```
After — moods removed, weather filled (step 4 succeeded here), the pending block carried into 2026-08-28, and Timeline/Journal/Notes/Work gone (Work emptied by the carry) while Articles and Tasks stay on their embeds:
```markdown
---
temp_high: 78.1
temp_low: 55.6
gdd_base50: 14.4
solar_energy_kwh_m2: 7.02
---
# 2026-08-27
## Articles
![[Articles.base#Added Today]]
## Tasks
![[Daily Note Tasks.base#Completed Today]]
```
The next day's note gains, under its `## Work`:
```markdown
## Work
Pending tasks:
- Deploy the account server again.
- Refactor ReviewRequestTypes.
```
-125
View File
@@ -1,125 +0,0 @@
# Why dev-tickets is shaped this way
`SKILL.md` is the what and the how. This is the why — the constraints that forced each choice, the alternatives rejected, and the vault settings the model quietly depends on. Read it before changing the model; most of the obvious simplifications have already been tried on paper and don't survive contact with the tooling.
Designed 2026-08-10, replacing per-repo `.scratch/` directories. Revised 2026-08-28: the model is unchanged, but the mechanics moved from `tn` + `obsidian` to the file-based `ticket` CLI — see [2026-08-28: file-based on every host](#2026-08-28-file-based-on-every-host). The sections between here and there describe the constraints as they stood on the desktop with Obsidian running; where they talk about `tn` or `obsidian`, that is history.
## The goal
Everything about a piece of work — the idea, the planning, the spec, and the implementation tickets — in one place, queryable, with real dependency edges instead of prose cross-references like "that is ticket 03".
## The constraints that shaped it
These are properties of the tools, not preferences. Most of the design falls out of them.
**Grouping rides on `projects`, not on folders.** `path` is filterable, so directory layout *could* scope a query — but archiving moves a note to `TaskNotes/Archive/` and changes its path, while its `projects` edges survive. Folders organise notes for a human; `projects` is what a query can trust.
**`tn update` cannot write a task's body.** There is no `details` flag on update, so a body can only be set at creation — which `tn create --details-file` now does. Editing an existing body still goes through `obsidian`.
**Three CLI gaps were closed rather than worked around** (2026-08-11, effort `tn API feature gaps`): `--add-tags`/`--add-contexts`/`--add-projects` were silent no-ops that returned 200 with the task unchanged; `blockedBy` was invisible to both `tn update` and `--filter`; and `PUT {"blockedBy": []}` could not clear the field, because the deletion pass fired only on a literal `undefined` that JSON cannot express. The plugin gained `POST /api/tasks/:id/{tags,contexts,projects,dependencies}` and the CLI was wired to them. The two-CLI split below survives this, but shrank to what `tn` genuinely does not model.
**Subtasks are `projects`.** There is no parent field anywhere in TaskNotes. A task is a subtask of whatever its `projects` array wikilinks to, and the parent can be any note — including one that doesn't exist. The vault's own `TaskNotes/Views/relationships.base` defines its Subtasks view exactly this way.
**`isBlocked` is derived.** The plugin maintains a dependency index and computes `isBlocked` from `blockedBy` plus the live status of each blocker, exposing it as a read-only field on every task. Storing a `blocked` status alongside it would be a second source of truth that drifts.
## Choices, and what lost
**Vault over `.scratch/`.** `.scratch/` worked from a plain filesystem with no daemon; the vault needs Obsidian running. That's a real availability regression, accepted because a single location for idea → planning → implementation was the whole point.
Moving creation to `tn create` (2026-08-11) deepened it. Tickets used to be written straight to disk, so a stopped Obsidian cost only status updates; now it costs creation as well. Reading a ticket never depended on Obsidian and still doesn't — the notes are plain markdown on disk. The trade was accepted because hand-authoring had to reproduce the plugin's `fieldMapping` (`scheduled``date_scheduled`) by hand, and silently produced a note the plugin then rewrote.
**The spec and the planning map are one note.** They were briefly separate — a plain "effort note" holding the wayfinder map, with the spec as a task beneath it — because a map isn't naturally a task. That split existed only to give the map a non-task home, and it cost a query: with tickets two levels down, no single `projects:contains` reached the whole effort. Collapsing them is also closer to the truth, since the map's Destination is what graduates into the spec. The cost is that decision and implementation tickets become siblings with no structural distinction, which `ticket_type` now carries instead.
**Rejected: dual-parenting.** Giving implementation tickets both `[[Spec]]` and `[[Effort]]` would make one query span everything, but it overloads `projects` to mean both "hierarchy parent" and "effort membership" — the effort's Subtasks view would flatten the spec and every ticket into one list.
**Rejected: a tag per repo and per effort.** `tags:contains:"effort/network-isolation"` scopes at any depth and would have worked, but it creates unbounded tag vocabulary that duplicates the directory path. The `[[repo]]` wikilink gets the same result with the mechanism already in use.
**Repo project notes are optional** (revised 2026-08-11). They were originally specified as one plain note per repo. Two findings demoted them. Grouping never needed the note: `projects` matches on the frontmatter value, so a dangling `[[repo]]` queries identically to a resolved one — `[[tasknotes-cli]]` had been running that way unnoticed. And the documented `projects: []` frontmatter was inert, since the Subtasks filter opens with `file.hasTag("task")` and a project note deliberately has no such tag, so nothing ever read it.
What survives is narrower and worth keeping: the note's *path* is what `relationships.base` compares `this.file` against, so a repo with no note has no page to view its efforts on. That makes the note a landing page, created on demand, with a free-form body. It still must not be a task — one there would show up in `tn list` as something to do.
**The link binds without a note.** Making the note optional does not make an unmatched `[[repo]]` inert: Obsidian resolves case-insensitively across the whole vault, so `[[obsidian]]` attaches the repo's tickets to a personal `Obsidian.md` at the vault root and lists them in *its* Subtasks view. Queries stay correct throughout, which is why it goes unnoticed. Title collisions are checked by looking, with `-iname`.
**Tags for routing, a field for type.** The routing tags sit on the note as tags; `ticket_type` is a registered user field instead, because TaskNotes rewrites frontmatter on update and an unregistered key isn't guaranteed to survive the round-trip.
**Actor and stall are two axes, not one** (revised 2026-08-11). The original vocabulary was `needs-info` / `ready-for-agent` / `ready-for-human` — three mutually exclusive values answering *who acts next*. That framing made every value transient: claiming or closing a ticket should logically have cleared or rewritten the tag, and none of the transitions were written down. The first six real tickets all reached `Done` still carrying `ready-for-agent`, which under that reading is drift.
Renaming the actor values to `agent-step` / `human-step` changes the question the tag answers from "who is queued next" to "whose step is this", which is **durable** — true at creation and still true after completion, where it becomes a record of who did the work. That makes the observed behaviour correct rather than sloppy, and it costs nothing to maintain.
`needs-info` does not belong on that axis: it is not an actor, it is a stall. It became an orthogonal flag, so `agent-step` + `needs-info` reads as "agent work, currently stalled on a question" — a state the old mutually-exclusive vocabulary could not express at all without lying about who owned the ticket.
The one maintained rule is that the actor tag is **corrected at close** if the other actor did the work. Pure write-once was considered and rejected: it makes `agent-step AND status:Done` mean "was scoped for an agent" rather than "an agent shipped it", and the second is the question worth being able to ask.
The full transition table is in `SKILL.md`; it exists because its absence was the single largest source of uncertainty in the model.
**`setup` instead of wayfinder's `task`.** Upstream's fourth type is `task`, which collides twice here: with `implementation` (both mean "do work") and with the `task` tag that makes a note a task at all. `setup` names what the type is for — groundwork whose result later tickets depend on.
**Statuses collapse, nothing is archived.** Upstream ran two deliberately separate vocabularies: `blocked`/`needs-info`/`ready-for-agent`/`ready-for-human`/`wont-do`/`complete` for implementation tickets, and `claimed`/`resolved`/`out-of-scope` for decision tickets. Both fold onto the same four TaskNotes statuses. Archiving would have been the natural home for `wont-do`, but it moves the file to `TaskNotes/Archive/` and breaks the directory layout — hence the `Wont Do` status.
**Rejected: `curl`.** `PUT /api/tasks/:id` has no validation and accepts `blockedBy` and `projects` directly, which made it the obvious escape hatch until the Obsidian CLI turned out to cover the same ground with no auth token and no JSON assembly.
**Rejected: MCP.** The plugin ships an MCP server, currently disabled. Its `create_task` and `update_task` tools validate through a zod schema that has no `blockedBy` key, so an extra field is *stripped* before the handler sees it — strictly worse than the unvalidated HTTP route.
## Vault settings this depends on
Invisible from `SKILL.md`, and the model breaks quietly without them.
- **mdbase export enabled** — TaskNotes maintains `mdbase.yaml` and `_types/task.md` in the vault root. `ticket` opens the vault through that schema and refuses to run without a `task` type; the completed statuses (`tn_completed_values`) come from it, not from code.
- **Status `Wont Do`**`isCompleted: true` so it drops off the frontier and `ticket` treats it as completed, `excludeFromCycle: true`, `autoArchive: false`.
- **Status `Done` with `autoArchive: false`** — changed from `true` for this system. Auto-archiving would move completed tickets out of their effort directory. Note this is global: personal completed tasks no longer move to `TaskNotes/Archive/` either.
- **User fields** `ticket_type`, `git_worktree`, `git_branch`, `agent_session` — all `text`, hidden from the creation and edit modals via `modalFieldsConfig`, since they're agent-written. Registering them is what puts them in `_types/task.md`, which is the only reason a file-based writer can be schema-faithful.
- **`taskIdentificationMethod: tag`** with `taskTag: task` and empty `excludedFolders` — this is what lets tickets live under `TaskNotes/Dev/` instead of `tasksFolder`. Switching identification to folder-based would orphan every dev ticket at once.
## Confirmed against a running vault
The three behaviours derived from source at design time were observed on 2026-08-11:
1. `tn list --json` does include `isBlocked` on every task, so the frontier query works as written.
2. `tn update --add-tags` was a silent no-op, exactly as suspected — and is now fixed.
3. `obsidian property:set` and `obsidian eval` behave as the app bundle suggested.
One behaviour was *not* anticipated: `obsidian append` writes only at end of file, so a `## Notes` entry lands under the trailing `## As Built` heading instead. `SKILL.md` gives the working form.
Probing `POST /api/tasks` the same day settled how creation works. The endpoint accepts `details` and `customProperties` — which is what made `tn create` able to replace hand-authoring — but **silently ignores** `folder`, `folderPath`, and `path`, filing every new note into `tasksFolder` regardless. `tn create --folder` therefore creates and then moves. Obsidian's watcher re-indexes the move immediately, and wikilinks resolve by basename, so nothing breaks; the note is seconds old and has no inbound links yet.
## Deferred
- The four live `.scratch/` directories (`atribot`, `claude-review`, `gnucash-visualizations`, `pinball-datasette`) stay where they are; new efforts use this system. Completed work isn't worth migrating, and the in-flight `atribot` tickets carry prose cross-references that need rewriting by hand.
- Making `POST /api/tasks` honour a folder would let `tn create --folder` drop its post-create move. The move is a workaround for a server-side gap, and it is the one step that touches the vault outside the plugin.
- `tn update` has no `--prop`, so custom properties still need `obsidian property:set` after creation. Adding it would retire the second CLI from the ticket workflow entirely.
## 2026-08-28: file-based on every host
### The constraint that changed
The skill assumed the desktop: a hardcoded vault path under `~/Documents`, `tn` (which needs the TaskNotes HTTP API inside a running Obsidian) and `obsidian` (which needs the desktop app). On tachi none of that exists — the vault is the obsync container's headless-sync mirror at another path, with no Obsidian process — so a session there could not create, claim, annotate, or close a ticket at all. The "vault over `.scratch/`" trade above had accepted needing Obsidian; a second host made that unacceptable.
### What replaced it
One file-based tool, `ticket`, over `@callumalpass/mdbase` — the same library and `_types/task.md` schema that TaskNotes' mdbase export already maintains in the vault. It reads and writes the markdown directly, so it behaves identically wherever the vault is on disk and whether or not Obsidian is running. The only per-host fact is the vault path, recorded once with `ticket vault --set` in `~/.config/mdbase-tasknotes/config.json` — the same file and key `mtn` would use, so the two could never disagree — and resolved as flag → `MDBASE_TASKNOTES_PATH` → that file, with no current-directory fallback. The model, the shape, the routing tags and the transition table are unchanged; only the mechanics moved, and the verbs enforce the rules the prose used to ask agents to remember (needs-info cleared at close, actor corrected at close, blockers must exist, efforts refuse to close over open tickets).
The package lives in dotfiles at `~/.local/lib/dev-tickets` (plain ESM JavaScript, `node --test`, mdbase the only dependency). There is no shell shim: the entry point's shebang is `#!/usr/bin/env -S MISE_NODE_VERSION=lts node`, which runs it under mise's lts Node from inside any repo regardless of that repo's pin, needs only mise's shims directory on PATH, and resolves `node_modules` from the file's real path so the stow symlink is transparent.
### Facts measured against mdbase 0.2.2 and the live vault
- mdbase's `update` rewrites the entire frontmatter in its own style: single-quoted wikilinks *and* dates where TaskNotes double-quotes and leaves them bare, trailing blank lines dropped, type defaults applied on read but never written. All valid YAML; TaskNotes rewrites in its own style when it next touches the note. `priority` is therefore written explicitly, and keys are emitted in TaskNotes' order, so tool-written notes look native.
- mdbase does not stamp the type's `generated:` fields for a v0.2 type. The tool stamps `date_created` / `date_modified` in TaskNotes' local-offset shape itself.
- mdbase's concurrency check covers only its own stat→write window inside `update()`. The tool records the mtime when it reads, compares in mdbase's `preWriteHook`, and refuses the write (exit 3) if the file changed — no retry; every verb is idempotent and re-running is the recovery. On tachi that guard is the only coordination with the atribot agent container sharing the vault copy, deliberately.
- `in` in `--where` errors with `invalid_expression` through the library; the earlier belief that it "silently returns nothing" was `mtn` swallowing the error.
- On the live vault (3,859 notes, 247 tasks): open ~50 ms, all tasks ~0.6 s, one effort ~20 ms.
- `.mdbase/cache.sqlite` in the vault root is a local artifact; it never appears in the sync log, while `mdbase.yaml` does.
- Vault-wide, case-insensitive title uniqueness is now enforced by `create`, not checked by hand; an ambiguous blocker (two notes with the same title) is treated like an unresolvable one — warned about and ignored — which surfaces the collision instead of guessing which note Obsidian would pick.
### What lost
- **A second host-side sync device, writes through the container, and ACLs.** The mirror at `/tachi/docker/atribot/vault` became user-owned instead; ZFS there is mounted `noacl`, so ACLs were never on the table.
- **`mtn` (mdbase-tasknotes) as the tool.** Its `create` is natural-language only and files into the default tasks folder; its `update` cannot touch projects, blockers, custom properties, or the body; its JSON carries no `isBlocked`. A wrapper was needed regardless, so the wrapper is the whole tool. It is not installed even as the writer of the config file — `ticket vault --set` covers that — and nothing is contributed upstream.
- **A shell shim** (the `tn` pattern). The shebang does the same job with no file to maintain; `tn` already ran off-pin (`installs/node/lts` was not the version pinned in `config.toml`), so the shim bought nothing the shebang does not.
- **`--folder` and `--project` on create.** The tool owns `TaskNotes/Dev/<repo>/<effort>/`, so `--repo` / `--effort` derive both, plus the effort's contexts for a ticket, and the effort/ticket flag validation falls out of that.
- **Auto-retry on a concurrent modification.** A retry that silently re-applied `close` over someone else's edit is exactly the clobber the guard exists to prevent.
- **Cascading effort close.** Refusing while children are open makes the human decide what to abandon.
- **An effort-agnostic frontier.** An agent always works one effort; the wider question is a `list --where`.
- **A `CONTEXT.md` and a `docs/adr/`.** `SKILL.md`'s Vocabulary is the glossary and this README is the decision log; a second copy of either would drift.
- **Detecting stale `In Progress` claims** from crashed sessions. Still a `list --where` question, not a model change.
Acceptance was dogfooding: this effort's own tickets were hand-written (nothing existed yet to write them), and each lifecycle step moved to `ticket` the moment the verb existed.
-304
View File
@@ -1,304 +0,0 @@
---
name: dev-tickets
description: Storage model for development tickets, specs, and planning maps, which live in the Obsidian vault as TaskNotes tasks and are read and written with the `ticket` CLI. Use when creating an effort or spec, writing implementation or decision tickets, finding the frontier, or claiming, annotating, and closing a ticket — and whenever /to-spec, /to-tickets, /wayfinder, or an implementor reads or writes them.
---
# Dev tickets
Tickets, specs, and planning maps live in the Obsidian vault as TaskNotes tasks. They are read and written with **`ticket`**, a file-based CLI that works on the markdown directly — no running Obsidian, no HTTP API — so it behaves the same on every host. This skill owns the storage model: the shape on disk, the metadata, and the verbs that read and write it. The skills that produce tickets (`/to-spec`, `/to-tickets`, `/wayfinder`) own their own process and defer here for everything below.
**Where the vault is** is what `ticket vault` prints. It is configured once per host, and the skill never names a path:
```sh
ticket vault --set <vault path> # once per host; refuses a directory that is not the vault
```
If `ticket` says no vault is configured, ask the user for the path and run that. If `ticket` itself is missing, the dotfiles install script installs it (`npm ci` inside `~/.local/lib/dev-tickets`); the launcher prints the exact command when its dependencies are absent.
## Vocabulary
- **Vault** — the Obsidian vault, wherever it is on this host. The user-facing word for where tickets live.
- **Collection** — the vault as mdbase sees it: `mdbase.yaml` plus the `_types/task.md` schema that TaskNotes maintains. Used only when talking about the tool's mechanics.
- **Repo project** — the `[[repo]]` wikilink every effort in a repo shares. A note at that title is **optional**; see [Repo project notes](#repo-project-notes).
- **Effort** — one feature or change. A task whose body is the planning map, which graduates into the spec.
- **Decision ticket** — a subtask that answers a question. `/wayfinder`'s unit.
- **Implementation ticket** — a subtask that builds something. `/to-tickets`' unit.
- **Frontier** — the tickets that are open and not blocked.
- **Title** — a note's filename stem. It is the note's identity (every edge is a wikilink to it) and is never a frontmatter key.
- **Record** — what `--json` returns for a note: `path`, `title`, the derived `isBlocked`, then the frontmatter as it is on disk. `show --json` adds `body`.
## Shape
```
TaskNotes/Dev/atribot/
├── atribot.md ← repo project note; optional, NOT a task
└── Network isolation/
├── Network isolation.md ← effort task
├── Should obsync share a namespace.md ← decision ticket
└── Split obsync into its own container.md ← implementation ticket
```
One directory per repo, one per effort. Every note in an effort directory sits directly in it — the hierarchy is expressed by `projects`, not by nesting. `ticket create` owns this shape: it derives the directory and the parent link from `--repo` or `--effort`, so a note cannot be filed in the wrong place.
**A title is an identity.** Every parent and blocking edge is a `[[wikilink]]` to a title, so titles are globally unique across the vault and a published note is never renamed. Give each one a name that reads on its own — `Split obsync into its own container`, not `02-split-container`.
Uniqueness is **case-insensitive and vault-wide**, and it binds whether or not you created a note for the title: `projects: [[obsidian]]` would attach every ticket in that repo to a personal `Obsidian.md` anywhere in the vault. `ticket create` refuses a title that any note already carries, and `ticket show <title>` is the pre-check when choosing one (exit 2 means the title is free).
## Frontmatter
`ticket create` writes every one of these in a single call. The middle column names what sets it.
| Field | Set with | Values |
|---|---|---|
| `status` | `--status` at creation, then `claim` / `close` | `Open` · `In Progress` (claimed) · `Done` (complete, resolved) · `Wont Do` (abandoned, out of scope) |
| `contexts` | `--context` on an effort; tickets inherit the effort's | from the repo's `CLAUDE.local.md` |
| `projects` | derived: `--repo R``[[R]]`, `--effort E``[[E]]` | effort → `[[repo]]` · ticket → `[[effort]]` |
| actor tag | `--actor agent\|human` at creation; `close --actor` to correct it | `agent-step` · `human-step` — exactly one, durable, survives completion |
| `needs-info` | `stall` / `unstall` | tag, present only while stalled on an unanswered question |
| `blockedBy` | `--blocked-by` (repeatable) at creation; `set blockedBy+=` / `-=` later | list of `{uid, reltype}`; `reltype` is `FINISHTOSTART`; every blocker must resolve to an existing task |
| `ticket_type` | `--type` | `research` · `prototype` · `grilling` · `setup` · `implementation` |
| `git_branch` | `set git_branch=…` on the effort | effort tasks only; omit entirely when on main/master |
| `git_worktree` | `set git_worktree=…` on the effort | effort tasks only; omit entirely when the main worktree |
| `agent_session` | `claim` | `$CLAUDE_CODE_SESSION_ID`, overwritten each session that works the ticket |
The tool writes the rest itself: the `task` tag, `priority` (the vault's default), `date_created` and `date_modified` (bumped on every write), and `date_completed` at close. It never writes `date_scheduled` or a `title` key.
A ticket on disk reads:
```yaml
---
status: Open
priority: 2-Normal
contexts:
- Obsidian
projects:
- "[[Network isolation]]"
date_created: 2026-08-10T09:12:44.118-07:00
date_modified: 2026-08-10T09:12:44.118-07:00
blockedBy:
- uid: "[[Verify internal network publish]]"
reltype: FINISHTOSTART
tags:
- task
- agent-step
ticket_type: implementation
agent_session: 444e4dcb-de76-4f6b-8c39-7d08d6aa9f5f
---
```
Quoting varies by writer — `ticket` single-quotes wikilinks and dates, TaskNotes double-quotes and leaves them bare — and both are valid YAML; do not "fix" it.
`blocked` is not a status. `isBlocked` is derived from `blockedBy` and the live status of each blocker (TaskNotes does the same on the desktop), so blocking is recorded once, in `blockedBy`, and read back from `isBlocked` in every record. A blocker that does not resolve warns and does not block.
`ticket_type: setup` is groundwork that unblocks a decision — provisioning access, moving data — whose *result* later tickets depend on. It corresponds to `/wayfinder`'s "task" type.
## Body
Effort task — the map first, and the spec replacing `## Destination` once the way is clear. `/wayfinder` and `/to-spec` each own their own section content.
Ticket — `## Question` for a decision ticket, `## What to build` plus acceptance checkboxes for an implementation ticket. A resolved decision ticket gains `## Answer`.
Every effort and ticket task takes the same two trailing sections, which `ticket create` appends when the body lacks them:
```markdown
## Notes
### [[2026-08-10]]
Squid rejects CONNECT to the registry on first run; the allowlist needs the CDN host too, not just the API host.
## As Built
```
`## Notes` is a running log — hurdles, surprises, design changes — appended **while the work happens** with `ticket note`, under a sub-heading wikilinking the current date.
`## As Built` is written at completion with `ticket as-built`, and only when the implementation materially deviated from what the ticket asked for.
## Repo configuration
Each repo's `CLAUDE.local.md` carries the context to stamp on its notes:
```markdown
## Dev tickets
- context: Obsidian
- repo: atribot # optional; defaults to the directory name
```
When a repo has no such block, ask the user for the context and offer to write the block. A guessed context files the effort under the wrong area and stays wrong.
## Creating
An effort names its repo and context; the tool files it at `TaskNotes/Dev/<repo>/<title>/<title>.md` with `projects: [[repo]]` and no actor tag:
```sh
ticket create --repo atribot --title 'Network isolation' --context Obsidian \
--set git_branch=feat/network-isolation \
--body-file - <<'BODY'
## Destination
...
BODY
```
A ticket names its effort; the tool files it beside the effort note with `projects: [[effort]]`, the effort's contexts, the actor tag, and `ticket_type`:
```sh
ticket create --effort 'Network isolation' --title 'Split obsync into its own container' \
--type implementation --actor agent \
--blocked-by 'Verify internal network publish' --blocked-by 'Provision the registry mirror' \
--body-file - <<'BODY'
## What to build
- [ ] Move the compose service into its own namespace
BODY
```
`--type` and `--actor` are required for a ticket and refused for an effort; `--context` is the reverse. `--blocked-by` accepts bare or `[[bracketed]]` titles and every one must exist — a typo'd blocker would never block, so it is an error instead. Create in dependency order. `--set k=v` adds any other field (`git_worktree`, `timeEstimate`); `--json` prints the record.
Creation refuses — and writes nothing — when the target file exists or any note in the vault has the same title case-insensitively (exit 2). Resolving a collision is the user's choice.
### Repo project notes
Optional, and there is no need to create one. `projects: [[atribot]]` groups the repo's efforts whether or not a note by that name exists — queries match on the frontmatter value, so a dangling link queries exactly like a resolved one.
Create one only to give a repo a landing page. Its single job is to be the note Obsidian's Subtasks view renders on: `TaskNotes/Views/relationships.base` matches a task's `projects` against `this.file`, so with no note there is no page to see the effort list on. Nothing reads the note's own frontmatter — it is not a task, so TaskNotes skips it — which makes the body free-form. A description and the repo path is enough:
```markdown
Matrix bot and its companion services, deployed as compose stacks.
Repo: `/home/tgrosinger/code/atribot`
```
`ticket` only makes tasks. Write the file directly at `TaskNotes/Dev/<repo>/<repo>.md`.
## Routing tags
Two independent axes, neither of which is a status. `status` carries the lifecycle; these carry *who does this step* and *whether it can proceed*.
**Actor — `agent-step` or `human-step`.** Exactly one on every ticket, from creation onward. On an open ticket it names the intended next actor; on a closed ticket it records the actor who actually completed the work. It is durable: claiming and closing do not remove it, so a `Done` ticket still records whose step it was. Effort tasks carry no actor tag; only tickets do.
**Stall — `needs-info`.** Transient and orthogonal to the actor. It means the assigned actor cannot proceed without information from someone else; it does not reassign the ticket. Thus `agent-step` + `needs-info` means *agent work awaiting a human answer*, while `human-step` + `needs-info` means *human work awaiting information from another person or source*. Add it the moment work stalls, remove it when the question is answered.
The actor usually follows from `ticket_type`, and `/wayfinder` calls the same distinction HITL vs AFK:
| `ticket_type` | actor | why |
|---|---|---|
| `research` · `implementation` | `agent-step` | an agent drives it alone |
| `grilling` · `prototype` | `human-step` | resolves only through live exchange with the user |
| `setup` | either | depends on whether the agent can do the work itself |
`setup` is the case that must be decided per ticket — which is exactly why the actor is a tag and not derived from `ticket_type` at query time.
| Transition | `status` | actor tag | `needs-info` | verb |
|---|---|---|---|---|
| Created | `Open` | set, exactly one | absent | `create --actor` |
| Claimed | → `In Progress` | unchanged | unchanged | `claim` |
| Stalled on a question | unchanged | unchanged | **add** | `stall` |
| Question answered | unchanged | unchanged | **remove** | `unstall` |
| Reassigned to the other actor | unchanged | **swap** | unchanged | `set tags+=… tags-=…` |
| Completed | → `Done` | **swap if the other actor did the work** | must be absent | `close [--actor]` |
| Abandoned | → `Wont Do` | unchanged | remove if present | `close --wont-do` |
The verbs enforce the two rules that used to be easy to forget: `close` always clears `needs-info`, and `close --actor human` (or `agent`) corrects the actor tag when the other actor finished the work — which is what keeps `agent-step AND status:Done` a truthful record of what agents actually shipped. `set tags+=needs-info` and friends still work as an escape hatch, with a warning naming the verb that would have done it properly.
## Working a ticket
1. **Claim it** before any work, so a concurrent session skips it:
```sh
ticket claim 'Split obsync into its own container'
```
The session id comes from `$CLAUDE_CODE_SESSION_ID` (or `--session`). `claim` refuses a ticket that is blocked or already `In Progress` under another session; `--force` overrides both.
2. **Record the checkout** on the *effort* task. When the branch is not `main` or `master`, set `git_branch`. When working in a linked worktree rather than the repository's main worktree, also set `git_worktree` to the absolute root returned by `git rev-parse --show-toplevel`:
```sh
ticket set 'Network isolation' git_branch=feat/network-isolation git_worktree=/home/tgrosinger/code/atribot-ni
```
Omit `git_branch` on `main`/`master`, and omit `git_worktree` in the main worktree (`ticket set '<effort>' git_worktree=` removes it). If the checkout changes later, update both fields.
3. **Append notes as you go**; each lands under today's `### [[YYYY-MM-DD]]` heading (reused within a day), above `## As Built`:
```sh
ticket note 'Split obsync into its own container' 'Squid rejects CONNECT to the registry on first run.'
ticket note 'Split obsync into its own container' --file - # longer text on stdin
```
4. **Stall it** if you hit a question only the user can answer — the tag and the logged question are one write:
```sh
ticket stall 'Split obsync into its own container' 'Should the registry mirror share the proxy namespace?'
ticket unstall 'Split obsync into its own container' 'Yes — same namespace.'
```
Leave `status` at `In Progress` — the ticket is still yours, it just cannot proceed.
5. **Close it.** Write `## As Built` first if the implementation deviated, then:
```sh
ticket as-built 'Split obsync into its own container' 'Kept the socket; a namespace split was not needed.'
ticket close 'Split obsync into its own container' # Done
ticket close 'Split obsync into its own container' --actor human # the human finished it
ticket close 'Split obsync into its own container' --wont-do # abandoned
```
`close` stamps `date_completed` and clears `needs-info`. Closing an **effort** is refused while any of its tickets is still open — close or `--wont-do` them first; it never cascades.
Closing a ticket unblocks its dependents automatically; there is nothing to update on them.
To resume a ticket: `cd` to its effort's `git_worktree`, then `claude --resume <agent_session>`. When `git_worktree` is absent, use the repository's main worktree: the repo project note records its path when that note exists; otherwise resume from an existing checkout of that repo and use its root from `git rev-parse --show-toplevel`.
## Querying
Every reference (`REF`, `EFFORT`) is a vault-relative path or a title — bare or `[[bracketed]]`, case-insensitive. An ambiguous title is refused with the candidates listed.
The **frontier** of one effort — open and unblocked — and the **agent frontier**, what an agent may pick up right now (also `agent-step` and not stalled):
```sh
ticket frontier 'Network isolation'
ticket frontier 'Network isolation' --actor agent --json
```
**Awaiting a human** — both axes reach the human, so the inbox is their union, minus completed work, across the whole vault:
```sh
ticket inbox
```
Everything else is `list`, scoped to `TaskNotes/Dev/` unless `--all`; the filters AND together:
```sh
ticket list --effort 'Network isolation' # every ticket of the effort, any status
ticket list --repo atribot --open # the repo's open efforts
ticket list --open --where 'status == "In Progress"'
ticket list --all --where 'tags.contains("human-step") && exists("date_completed")'
```
`--where` is mdbase's expression language, not CEL: `==`, `!=`, `&&`, `||`, `!`, `.contains("x")`, `exists("field")`, `file.name`, indexing. `in` and CEL macros (`exists(t, …)`) are rejected as invalid expressions — the tool reports the error rather than returning nothing.
Text output is one row per note — status (with `blocked` / `needs-info` flags), actor, title, path. `--json` prints records; pipe to `jq`:
```sh
ticket frontier 'Network isolation' --actor agent --json | jq -r '.[0].path'
```
## Cheatsheet
```
ticket vault [--set PATH] print the vault path, or record it for this host
ticket show REF print a note verbatim (--json: its record with body)
ticket list [--effort E] [--repo R] [--open] [--where EXPR] [--all] tasks under TaskNotes/Dev (or the whole vault with --all)
ticket frontier EFFORT [--actor agent|human] the effort's tickets that are Open and unblocked
ticket inbox human-step or needs-info, not completed, vault-wide
ticket create --repo R --title T --context C [--set k=v] [--body-file F|-]
ticket create --effort E --title T --type TYPE --actor agent|human [--blocked-by B]... [--body-file F|-]
ticket claim REF [--session ID] [--force] In Progress under this session
ticket note REF TEXT|--file F [--date YYYY-MM-DD] append to ## Notes, above ## As Built
ticket as-built REF TEXT|--file F append under ## As Built
ticket stall REF TEXT|--file F add needs-info and log the question
ticket unstall REF [TEXT|--file F] clear needs-info, optionally logging the answer
ticket close REF [--wont-do] [--actor agent|human] Done (or Wont Do), date_completed, needs-info cleared
ticket set REF k=v k= k+=v k-=v ... edit frontmatter; += / -= on tags, contexts, projects, blockedBy
global: --vault PATH --json --help
```
Exit codes: `1` usage or error, `2` refused (collision, blocked, ambiguous, not found, open children, not a task), `3` the file changed between read and write — nothing was written; re-run the command.
## Guardrails
- **Create a new ticket rather than renaming a published one.** Every inbound `projects` and `blockedBy` edge points at the title.
- **Body by hand is fine; frontmatter goes through `ticket`.** `set` and the verbs stamp `date_modified`, refuse non-tasks, keep the schema's types, and refuse to clobber a file another writer changed underneath them. A direct frontmatter edit does none of that.
- **There is no delete.** `ticket` never removes a note, and neither should you without the user's explicit say-so.
- `ticket` does not talk to Obsidian. When Obsidian is running on the same host it picks the change up like any external edit; when it is not, sync carries it.
Why the model is shaped this way, and what was rejected on the way here: [README.md](./README.md).
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/diagnosing-bugs
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/domain-modeling
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/grill-with-docs
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/grilling
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/handoff
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/implement
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/improve-codebase-architecture
-644
View File
@@ -1,644 +0,0 @@
---
name: json-canvas
description: Create and edit JSON Canvas files (.canvas) with nodes, edges, groups, and connections. Use when working with .canvas files, creating visual canvases, mind maps, flowcharts, or when the user mentions Canvas files in Obsidian.
source: https://github.com/kepano/obsidian-skills/blob/main/skills/json-canvas/SKILL.md
---
# JSON Canvas Skill
This skill enables Claude Code to create and edit valid JSON Canvas files (`.canvas`) used in Obsidian and other applications.
## Overview
JSON Canvas is an open file format for infinite canvas data. Canvas files use the `.canvas` extension and contain valid JSON following the [JSON Canvas Spec 1.0](https://jsoncanvas.org/spec/1.0/).
## File Structure
A canvas file contains two top-level arrays:
```json
{
"nodes": [],
"edges": []
}
```
- `nodes` (optional): Array of node objects
- `edges` (optional): Array of edge objects connecting nodes
## Nodes
Nodes are objects placed on the canvas. There are four node types:
- `text` - Text content with Markdown
- `file` - Reference to files/attachments
- `link` - External URL
- `group` - Visual container for other nodes
### Z-Index Ordering
Nodes are ordered by z-index in the array:
- First node = bottom layer (displayed below others)
- Last node = top layer (displayed above others)
### Generic Node Attributes
All nodes share these attributes:
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `id` | Yes | string | Unique identifier for the node |
| `type` | Yes | string | Node type: `text`, `file`, `link`, or `group` |
| `x` | Yes | integer | X position in pixels |
| `y` | Yes | integer | Y position in pixels |
| `width` | Yes | integer | Width in pixels |
| `height` | Yes | integer | Height in pixels |
| `color` | No | canvasColor | Node color (see Color section) |
### Text Nodes
Text nodes contain Markdown content.
```json
{
"id": "6f0ad84f44ce9c17",
"type": "text",
"x": 0,
"y": 0,
"width": 400,
"height": 200,
"text": "# Hello World\n\nThis is **Markdown** content."
}
```
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `text` | Yes | string | Plain text with Markdown syntax |
### File Nodes
File nodes reference files or attachments (images, videos, PDFs, notes, etc.).
```json
{
"id": "a1b2c3d4e5f67890",
"type": "file",
"x": 500,
"y": 0,
"width": 400,
"height": 300,
"file": "Attachments/diagram.png"
}
```
```json
{
"id": "b2c3d4e5f6789012",
"type": "file",
"x": 500,
"y": 400,
"width": 400,
"height": 300,
"file": "Notes/Project Overview.md",
"subpath": "#Implementation"
}
```
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `file` | Yes | string | Path to file within the system |
| `subpath` | No | string | Link to heading or block (starts with `#`) |
### Link Nodes
Link nodes display external URLs.
```json
{
"id": "c3d4e5f678901234",
"type": "link",
"x": 1000,
"y": 0,
"width": 400,
"height": 200,
"url": "https://obsidian.md"
}
```
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `url` | Yes | string | External URL |
### Group Nodes
Group nodes are visual containers for organizing other nodes.
```json
{
"id": "d4e5f6789012345a",
"type": "group",
"x": -50,
"y": -50,
"width": 1000,
"height": 600,
"label": "Project Overview",
"color": "4"
}
```
```json
{
"id": "e5f67890123456ab",
"type": "group",
"x": 0,
"y": 700,
"width": 800,
"height": 500,
"label": "Resources",
"background": "Attachments/background.png",
"backgroundStyle": "cover"
}
```
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `label` | No | string | Text label for the group |
| `background` | No | string | Path to background image |
| `backgroundStyle` | No | string | Background rendering style |
#### Background Styles
| Value | Description |
|-------|-------------|
| `cover` | Fills entire width and height of node |
| `ratio` | Maintains aspect ratio of background image |
| `repeat` | Repeats image as pattern in both directions |
## Edges
Edges are lines connecting nodes.
```json
{
"id": "f67890123456789a",
"fromNode": "6f0ad84f44ce9c17",
"toNode": "a1b2c3d4e5f67890"
}
```
```json
{
"id": "0123456789abcdef",
"fromNode": "6f0ad84f44ce9c17",
"fromSide": "right",
"fromEnd": "none",
"toNode": "b2c3d4e5f6789012",
"toSide": "left",
"toEnd": "arrow",
"color": "1",
"label": "leads to"
}
```
| Attribute | Required | Type | Default | Description |
|-----------|----------|------|---------|-------------|
| `id` | Yes | string | - | Unique identifier for the edge |
| `fromNode` | Yes | string | - | Node ID where connection starts |
| `fromSide` | No | string | - | Side where edge starts |
| `fromEnd` | No | string | `none` | Shape at edge start |
| `toNode` | Yes | string | - | Node ID where connection ends |
| `toSide` | No | string | - | Side where edge ends |
| `toEnd` | No | string | `arrow` | Shape at edge end |
| `color` | No | canvasColor | - | Line color |
| `label` | No | string | - | Text label for the edge |
### Side Values
| Value | Description |
|-------|-------------|
| `top` | Top edge of node |
| `right` | Right edge of node |
| `bottom` | Bottom edge of node |
| `left` | Left edge of node |
### End Shapes
| Value | Description |
|-------|-------------|
| `none` | No endpoint shape |
| `arrow` | Arrow endpoint |
## Colors
The `canvasColor` type can be specified in two ways:
### Hex Colors
```json
{
"color": "#FF0000"
}
```
### Preset Colors
```json
{
"color": "1"
}
```
| Preset | Color |
|--------|-------|
| `"1"` | Red |
| `"2"` | Orange |
| `"3"` | Yellow |
| `"4"` | Green |
| `"5"` | Cyan |
| `"6"` | Purple |
Note: Specific color values for presets are intentionally undefined, allowing applications to use their own brand colors.
## Complete Examples
### Simple Canvas with Text and Connections
```json
{
"nodes": [
{
"id": "8a9b0c1d2e3f4a5b",
"type": "text",
"x": 0,
"y": 0,
"width": 300,
"height": 150,
"text": "# Main Idea\n\nThis is the central concept."
},
{
"id": "1a2b3c4d5e6f7a8b",
"type": "text",
"x": 400,
"y": -100,
"width": 250,
"height": 100,
"text": "## Supporting Point A\n\nDetails here."
},
{
"id": "2b3c4d5e6f7a8b9c",
"type": "text",
"x": 400,
"y": 100,
"width": 250,
"height": 100,
"text": "## Supporting Point B\n\nMore details."
}
],
"edges": [
{
"id": "3c4d5e6f7a8b9c0d",
"fromNode": "8a9b0c1d2e3f4a5b",
"fromSide": "right",
"toNode": "1a2b3c4d5e6f7a8b",
"toSide": "left"
},
{
"id": "4d5e6f7a8b9c0d1e",
"fromNode": "8a9b0c1d2e3f4a5b",
"fromSide": "right",
"toNode": "2b3c4d5e6f7a8b9c",
"toSide": "left"
}
]
}
```
### Project Board with Groups
```json
{
"nodes": [
{
"id": "5e6f7a8b9c0d1e2f",
"type": "group",
"x": 0,
"y": 0,
"width": 300,
"height": 500,
"label": "To Do",
"color": "1"
},
{
"id": "6f7a8b9c0d1e2f3a",
"type": "group",
"x": 350,
"y": 0,
"width": 300,
"height": 500,
"label": "In Progress",
"color": "3"
},
{
"id": "7a8b9c0d1e2f3a4b",
"type": "group",
"x": 700,
"y": 0,
"width": 300,
"height": 500,
"label": "Done",
"color": "4"
},
{
"id": "8b9c0d1e2f3a4b5c",
"type": "text",
"x": 20,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 1\n\nImplement feature X"
},
{
"id": "9c0d1e2f3a4b5c6d",
"type": "text",
"x": 370,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 2\n\nReview PR #123",
"color": "2"
},
{
"id": "0d1e2f3a4b5c6d7e",
"type": "text",
"x": 720,
"y": 50,
"width": 260,
"height": 80,
"text": "## Task 3\n\n~~Setup CI/CD~~"
}
],
"edges": []
}
```
### Research Canvas with Files and Links
```json
{
"nodes": [
{
"id": "1e2f3a4b5c6d7e8f",
"type": "text",
"x": 300,
"y": 200,
"width": 400,
"height": 200,
"text": "# Research Topic\n\n## Key Questions\n\n- How does X affect Y?\n- What are the implications?",
"color": "5"
},
{
"id": "2f3a4b5c6d7e8f9a",
"type": "file",
"x": 0,
"y": 0,
"width": 250,
"height": 150,
"file": "Literature/Paper A.pdf"
},
{
"id": "3a4b5c6d7e8f9a0b",
"type": "file",
"x": 0,
"y": 200,
"width": 250,
"height": 150,
"file": "Notes/Meeting Notes.md",
"subpath": "#Key Insights"
},
{
"id": "4b5c6d7e8f9a0b1c",
"type": "link",
"x": 0,
"y": 400,
"width": 250,
"height": 100,
"url": "https://example.com/research"
},
{
"id": "5c6d7e8f9a0b1c2d",
"type": "file",
"x": 750,
"y": 150,
"width": 300,
"height": 250,
"file": "Attachments/diagram.png"
}
],
"edges": [
{
"id": "6d7e8f9a0b1c2d3e",
"fromNode": "2f3a4b5c6d7e8f9a",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"label": "supports"
},
{
"id": "7e8f9a0b1c2d3e4f",
"fromNode": "3a4b5c6d7e8f9a0b",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"label": "informs"
},
{
"id": "8f9a0b1c2d3e4f5a",
"fromNode": "4b5c6d7e8f9a0b1c",
"fromSide": "right",
"toNode": "1e2f3a4b5c6d7e8f",
"toSide": "left",
"toEnd": "arrow",
"color": "6"
},
{
"id": "9a0b1c2d3e4f5a6b",
"fromNode": "1e2f3a4b5c6d7e8f",
"fromSide": "right",
"toNode": "5c6d7e8f9a0b1c2d",
"toSide": "left",
"label": "visualized by"
}
]
}
```
### Flowchart
```json
{
"nodes": [
{
"id": "a0b1c2d3e4f5a6b7",
"type": "text",
"x": 200,
"y": 0,
"width": 150,
"height": 60,
"text": "**Start**",
"color": "4"
},
{
"id": "b1c2d3e4f5a6b7c8",
"type": "text",
"x": 200,
"y": 100,
"width": 150,
"height": 60,
"text": "Step 1:\nGather data"
},
{
"id": "c2d3e4f5a6b7c8d9",
"type": "text",
"x": 200,
"y": 200,
"width": 150,
"height": 80,
"text": "**Decision**\n\nIs data valid?",
"color": "3"
},
{
"id": "d3e4f5a6b7c8d9e0",
"type": "text",
"x": 400,
"y": 200,
"width": 150,
"height": 60,
"text": "Process data"
},
{
"id": "e4f5a6b7c8d9e0f1",
"type": "text",
"x": 0,
"y": 200,
"width": 150,
"height": 60,
"text": "Request new data",
"color": "1"
},
{
"id": "f5a6b7c8d9e0f1a2",
"type": "text",
"x": 400,
"y": 320,
"width": 150,
"height": 60,
"text": "**End**",
"color": "4"
}
],
"edges": [
{
"id": "a6b7c8d9e0f1a2b3",
"fromNode": "a0b1c2d3e4f5a6b7",
"fromSide": "bottom",
"toNode": "b1c2d3e4f5a6b7c8",
"toSide": "top"
},
{
"id": "b7c8d9e0f1a2b3c4",
"fromNode": "b1c2d3e4f5a6b7c8",
"fromSide": "bottom",
"toNode": "c2d3e4f5a6b7c8d9",
"toSide": "top"
},
{
"id": "c8d9e0f1a2b3c4d5",
"fromNode": "c2d3e4f5a6b7c8d9",
"fromSide": "right",
"toNode": "d3e4f5a6b7c8d9e0",
"toSide": "left",
"label": "Yes",
"color": "4"
},
{
"id": "d9e0f1a2b3c4d5e6",
"fromNode": "c2d3e4f5a6b7c8d9",
"fromSide": "left",
"toNode": "e4f5a6b7c8d9e0f1",
"toSide": "right",
"label": "No",
"color": "1"
},
{
"id": "e0f1a2b3c4d5e6f7",
"fromNode": "e4f5a6b7c8d9e0f1",
"fromSide": "top",
"fromEnd": "none",
"toNode": "b1c2d3e4f5a6b7c8",
"toSide": "left",
"toEnd": "arrow"
},
{
"id": "f1a2b3c4d5e6f7a8",
"fromNode": "d3e4f5a6b7c8d9e0",
"fromSide": "bottom",
"toNode": "f5a6b7c8d9e0f1a2",
"toSide": "top"
}
]
}
```
## ID Generation
Node and edge IDs must be unique strings. Obsidian generates 16-character hexadecimal IDs:
```json
"id": "6f0ad84f44ce9c17"
"id": "a3b2c1d0e9f8g7h6"
"id": "1234567890abcdef"
```
This format is a 16-character lowercase hex string (64-bit random value).
## Layout Guidelines
### Positioning
- Coordinates can be negative (canvas extends infinitely)
- `x` increases to the right
- `y` increases downward
- Position refers to top-left corner of node
### Recommended Sizes
| Node Type | Suggested Width | Suggested Height |
|-----------|-----------------|------------------|
| Small text | 200-300 | 80-150 |
| Medium text | 300-450 | 150-300 |
| Large text | 400-600 | 300-500 |
| File preview | 300-500 | 200-400 |
| Link preview | 250-400 | 100-200 |
| Group | Varies | Varies |
### Spacing
- Leave 20-50px padding inside groups
- Space nodes 50-100px apart for readability
- Align nodes to grid (multiples of 10 or 20) for cleaner layouts
## Validation Rules
1. All `id` values must be unique across nodes and edges
2. `fromNode` and `toNode` must reference existing node IDs
3. Required fields must be present for each node type
4. `type` must be one of: `text`, `file`, `link`, `group`
5. `backgroundStyle` must be one of: `cover`, `ratio`, `repeat`
6. `fromSide`, `toSide` must be one of: `top`, `right`, `bottom`, `left`
7. `fromEnd`, `toEnd` must be one of: `none`, `arrow`
8. Color presets must be `"1"` through `"6"` or valid hex color
## References
- [JSON Canvas Spec 1.0](https://jsoncanvas.org/spec/1.0/)
- [JSON Canvas GitHub](https://github.com/obsidianmd/jsoncanvas)
-121
View File
@@ -1,121 +0,0 @@
---
name: magpie-review
description: Run a multi-AI adversarial code review using the `magpie` CLI. Multiple AI models independently review the changes, debate findings, and a verifier audits each issue against the actual code. Use whenever the user asks for a "magpie review", a "multi-AI review", an "adversarial review", a "second opinion review", or wants magpie to look at local uncommitted changes, the current branch, or a GitHub PR. Also use when the user wants to `magpie discuss` a topic or do a whole-repo review.
---
# magpie-review
Wraps the `magpie` CLI to run adversarial multi-AI code reviews. Magpie spawns several reviewer models (Claude Code, Codex, Gemini, etc.) that independently review a change, debate across rounds, then a verifier audits each reported issue against the actual code.
## When to pick which mode
Magpie supports three review targets — pick based on what the user is reviewing:
| User's intent | Command |
|---|---|
| Review work I haven't committed yet (staged + unstaged) | `magpie review --local` |
| Review the commits on my current branch vs a base | `magpie review --branch [base]` (base defaults to `main`) |
| Review a GitHub PR | `magpie review <pr-number>` or `magpie review <pr-url>` |
| Review specific files only | `magpie review --files <path> [path...]` |
| Review the entire repository | `magpie review --repo` |
| Discuss a topic / design question | `magpie discuss "<topic>"` or `magpie discuss <path-to-file.md>` |
If the user is ambiguous (e.g. "review my changes"), check `git status` and `git log @{u}..HEAD` to figure out whether they mean uncommitted, committed-but-unpushed, or already-pushed work — then pick the matching mode rather than guessing.
## Running a review
Always run `magpie` from inside the target repo's working tree. For PR mode it uses the `origin` remote to find the repo by default.
### Common flags
These apply to both `review` and `discuss` unless noted:
- `-i, --interactive` — Pause between turns for Q&A. Use when the user wants to drive the review themselves.
- `-a, --all` — Use every configured reviewer without an interactive picker. Good for non-interactive/batch runs.
- `--reviewers <ids>` — Comma-separated reviewer IDs (e.g. `claude-code,gemini-cli`) when you want a specific subset.
- `-r, --rounds <n>` — Cap the debate rounds (default 5).
- `--no-converge` — Disable early-stop on consensus. Use when you want the full debate even if reviewers agree quickly.
- `-o, --output <file>` and `-f, --format <markdown|json>` — Save results to a file.
- `--fail-fast` — Abort the whole flow if any reviewer fails. Default is resilient (continues with surviving reviewers). Use fail-fast when debugging provider/auth issues or when the user wants a guarantee every reviewer participated.
- `--plan-only` — Generate the review plan without running reviewers. Useful for a quick preview of what magpie *would* do.
### `review`-only flags worth knowing
- `--skip-context` — Skip the context-gathering phase (call chains, related PRs). Faster, less informed.
- `--no-post` — Skip the post-debate GitHub comment-posting flow. Use in non-interactive contexts where you just want the review output, not the interactive post-each-issue loop.
- `--no-conclusion` — Skip the final summarizer. Useful for bot/CI use.
- `--git-remote <remote>` — Override the remote used for PR-URL detection (default `origin`).
- `--reanalyze` — Bypass the analyzer cache and re-analyze from scratch.
### Repo-mode flags (with `--repo`)
- `--path <subdir>` — Limit the repo review to a subdirectory.
- `--ignore <patterns...>` — Skip matching paths.
- `--quick` — Architecture overview only.
- `--deep` — Full analysis, no prompts.
- `--list-sessions` / `--session <id>` / `--export <file>` — Manage long-running repo review sessions (they persist so you can pause and resume).
## Interactive vs non-interactive
Magpie's default flow includes interactive prompts (reviewer selection, per-issue post/edit/skip after the debate). When you (Claude) are invoking magpie programmatically on the user's behalf:
- Prefer `-a` (or `--reviewers`) to skip the reviewer-selection prompt.
- Prefer `--no-post` to skip the per-issue posting loop — the user can still read the review output.
- If you want a clean machine-readable result, add `-f json -o <file>`.
When the user wants to drive the review themselves, hand the command back to them to run (e.g. via `! magpie review ...`) so they get the interactive UX rather than running it through a tool call.
## Running it as a long task (it takes minutes)
A full review runs several models across multiple debate rounds, so it takes minutes. Run it as a background `Bash` task with `run_in_background: true` and save the output (`-f markdown -o <file>`). The background task auto-notifies you when it completes — that completion notification is all you need; read the saved output file then.
- **Do not add a separate `Monitor` on the same output file.** The background task already notifies on completion, so a monitor watching the same file is redundant. Only add a `Monitor` if you genuinely need streamed interim progress, and even then it is usually unnecessary for a fire-and-forget review.
- **Never copy the output path by hand.** Pass the same explicit `-o <file>` path you chose (e.g. `/tmp/magpie-review.md`) to your follow-up `Read` — don't transcribe the long auto-generated task-output path from the completion notification, which is easy to typo.
- **Don't suppress stderr** (`2>/dev/null`) on magpie or on any watcher command. If something fails — bad path, auth error, missing reviewer — you want to see why, not a bare non-zero exit. Use `2>&1 | tee <log>` if you want both a saved log and visible errors.
## Examples
**User: "Have magpie look at what I'm working on right now."**
They likely mean uncommitted work. Run:
```
magpie review --local
```
**User: "Get a magpie review on this branch before I push."**
Current branch vs main:
```
magpie review --branch
```
**User: "Run magpie on PR 4521."**
```
magpie review 4521
```
**User: "Use magpie to review just the changes to `src/auth/`."**
Pick the files mode:
```
magpie review --files src/auth/login.ts src/auth/session.ts
```
**User: "Get a fast magpie sanity check on PR 4521 — I just want the output, don't post anything."**
```
magpie review 4521 -a --no-post --skip-context
```
**User: "Have magpie debate whether we should adopt tRPC."**
```
magpie discuss "Should we adopt tRPC for our internal APIs?"
```
## Configuration notes
- Magpie reads `~/.magpie/config.yaml` for providers, reviewers, analyzer, summarizer, and the context-gatherer config.
- CLI providers (`claude-code`, `codex-cli`, `gemini-cli`, `qwen-code`, `opencode-cli`) use the user's existing subscriptions/logins — no API keys needed and they're the recommended choice.
- If the user hasn't run `magpie init` yet, suggest `magpie init` (interactive) or `magpie init -y` (defaults) before the first review.
- If `magpie` is not on PATH, the project at `/home/tgrosinger/code/magpie` may need `npm install && npm run build && npm link` from its root.
## When *not* to use this skill
- The user is asking how magpie itself is implemented or wants to modify magpie's source — that's a normal code task in the magpie repo, not an invocation of this skill.
- The user wants a single-model review (just Claude reviewing the diff). Use the built-in `/code-review` skill instead.
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/code-review
-620
View File
@@ -1,620 +0,0 @@
---
name: obsidian-bases
description: Create and edit Obsidian Bases (.base files) with views, filters, formulas, and summaries. Use when working with .base files, creating database-like views of notes, or when the user mentions Bases, table views, card views, filters, or formulas in Obsidian.
source: https://github.com/kepano/obsidian-skills/blob/main/skills/obsidian-bases/SKILL.md
---
# Obsidian Bases Skill
This skill enables Claude Code to create and edit valid Obsidian Bases (`.base` files) including views, filters, formulas, and all related configurations.
## Overview
Obsidian Bases are YAML-based files that define dynamic views of notes in an Obsidian vault. A Base file can contain multiple views, global filters, formulas, property configurations, and custom summaries.
## File Format
Base files use the `.base` extension and contain valid YAML. They can also be embedded in Markdown code blocks.
## Complete Schema
```yaml
# Global filters apply to ALL views in the base
filters:
# Can be a single filter string
# OR a recursive filter object with and/or/not
and: []
or: []
not: []
# Define formula properties that can be used across all views
formulas:
formula_name: 'expression'
# Configure display names and settings for properties
properties:
property_name:
displayName: "Display Name"
formula.formula_name:
displayName: "Formula Display Name"
file.ext:
displayName: "Extension"
# Define custom summary formulas
summaries:
custom_summary_name: 'values.mean().round(3)'
# Define one or more views
views:
- type: table | cards | list | map
name: "View Name"
limit: 10 # Optional: limit results
groupBy: # Optional: group results
property: property_name
direction: ASC | DESC
filters: # View-specific filters
and: []
order: # Properties to display in order
- file.name
- property_name
- formula.formula_name
summaries: # Map properties to summary formulas
property_name: Average
```
## Filter Syntax
Filters narrow down results. They can be applied globally or per-view.
### Filter Structure
```yaml
# Single filter
filters: 'status == "done"'
# AND - all conditions must be true
filters:
and:
- 'status == "done"'
- 'priority > 3'
# OR - any condition can be true
filters:
or:
- 'file.hasTag("book")'
- 'file.hasTag("article")'
# NOT - exclude matching items
filters:
not:
- 'file.hasTag("archived")'
# Nested filters
filters:
or:
- file.hasTag("tag")
- and:
- file.hasTag("book")
- file.hasLink("Textbook")
- not:
- file.hasTag("book")
- file.inFolder("Required Reading")
```
### Filter Operators
| Operator | Description |
|----------|-------------|
| `==` | equals |
| `!=` | not equal |
| `>` | greater than |
| `<` | less than |
| `>=` | greater than or equal |
| `<=` | less than or equal |
| `&&` | logical and |
| `\|\|` | logical or |
| <code>!</code> | logical not |
## Properties
### Three Types of Properties
1. **Note properties** - From frontmatter: `note.author` or just `author`
2. **File properties** - File metadata: `file.name`, `file.mtime`, etc.
3. **Formula properties** - Computed values: `formula.my_formula`
### File Properties Reference
| Property | Type | Description |
|----------|------|-------------|
| `file.name` | String | File name |
| `file.basename` | String | File name without extension |
| `file.path` | String | Full path to file |
| `file.folder` | String | Parent folder path |
| `file.ext` | String | File extension |
| `file.size` | Number | File size in bytes |
| `file.ctime` | Date | Created time |
| `file.mtime` | Date | Modified time |
| `file.tags` | List | All tags in file |
| `file.links` | List | Internal links in file |
| `file.backlinks` | List | Files linking to this file |
| `file.embeds` | List | Embeds in the note |
| `file.properties` | Object | All frontmatter properties |
### The `this` Keyword
- In main content area: refers to the base file itself
- When embedded: refers to the embedding file
- In sidebar: refers to the active file in main content
## Formula Syntax
Formulas compute values from properties. Defined in the `formulas` section.
```yaml
formulas:
# Simple arithmetic
total: "price * quantity"
# Conditional logic
status_icon: 'if(done, "✅", "⏳")'
# String formatting
formatted_price: 'if(price, price.toFixed(2) + " dollars")'
# Date formatting
created: 'file.ctime.format("YYYY-MM-DD")'
# Complex expressions
days_old: '((now() - file.ctime) / 86400000).round(0)'
```
## Functions Reference
### Global Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `date()` | `date(string): date` | Parse string to date. Format: `YYYY-MM-DD HH:mm:ss` |
| `duration()` | `duration(string): duration` | Parse duration string |
| `now()` | `now(): date` | Current date and time |
| `today()` | `today(): date` | Current date (time = 00:00:00) |
| `if()` | `if(condition, trueResult, falseResult?)` | Conditional |
| `min()` | `min(n1, n2, ...): number` | Smallest number |
| `max()` | `max(n1, n2, ...): number` | Largest number |
| `number()` | `number(any): number` | Convert to number |
| `link()` | `link(path, display?): Link` | Create a link |
| `list()` | `list(element): List` | Wrap in list if not already |
| `file()` | `file(path): file` | Get file object |
| `image()` | `image(path): image` | Create image for rendering |
| `icon()` | `icon(name): icon` | Lucide icon by name |
| `html()` | `html(string): html` | Render as HTML |
| `escapeHTML()` | `escapeHTML(string): string` | Escape HTML characters |
### Any Type Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `isTruthy()` | `any.isTruthy(): boolean` | Coerce to boolean |
| `isType()` | `any.isType(type): boolean` | Check type |
| `toString()` | `any.toString(): string` | Convert to string |
### Date Functions & Fields
**Fields:** `date.year`, `date.month`, `date.day`, `date.hour`, `date.minute`, `date.second`, `date.millisecond`
| Function | Signature | Description |
|----------|-----------|-------------|
| `date()` | `date.date(): date` | Remove time portion |
| `format()` | `date.format(string): string` | Format with Moment.js pattern |
| `time()` | `date.time(): string` | Get time as string |
| `relative()` | `date.relative(): string` | Human-readable relative time |
| `isEmpty()` | `date.isEmpty(): boolean` | Always false for dates |
### Date Arithmetic
```yaml
# Duration units: y/year/years, M/month/months, d/day/days,
# w/week/weeks, h/hour/hours, m/minute/minutes, s/second/seconds
# Add/subtract durations
"date + \"1M\"" # Add 1 month
"date - \"2h\"" # Subtract 2 hours
"now() + \"1 day\"" # Tomorrow
"today() + \"7d\"" # A week from today
# Subtract dates for millisecond difference
"now() - file.ctime"
# Complex duration arithmetic
"now() + (duration('1d') * 2)"
```
### String Functions
**Field:** `string.length`
| Function | Signature | Description |
|----------|-----------|-------------|
| `contains()` | `string.contains(value): boolean` | Check substring |
| `containsAll()` | `string.containsAll(...values): boolean` | All substrings present |
| `containsAny()` | `string.containsAny(...values): boolean` | Any substring present |
| `startsWith()` | `string.startsWith(query): boolean` | Starts with query |
| `endsWith()` | `string.endsWith(query): boolean` | Ends with query |
| `isEmpty()` | `string.isEmpty(): boolean` | Empty or not present |
| `lower()` | `string.lower(): string` | To lowercase |
| `title()` | `string.title(): string` | To Title Case |
| `trim()` | `string.trim(): string` | Remove whitespace |
| `replace()` | `string.replace(pattern, replacement): string` | Replace pattern |
| `repeat()` | `string.repeat(count): string` | Repeat string |
| `reverse()` | `string.reverse(): string` | Reverse string |
| `slice()` | `string.slice(start, end?): string` | Substring |
| `split()` | `string.split(separator, n?): list` | Split to list |
### Number Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `abs()` | `number.abs(): number` | Absolute value |
| `ceil()` | `number.ceil(): number` | Round up |
| `floor()` | `number.floor(): number` | Round down |
| `round()` | `number.round(digits?): number` | Round to digits |
| `toFixed()` | `number.toFixed(precision): string` | Fixed-point notation |
| `isEmpty()` | `number.isEmpty(): boolean` | Not present |
### List Functions
**Field:** `list.length`
| Function | Signature | Description |
|----------|-----------|-------------|
| `contains()` | `list.contains(value): boolean` | Element exists |
| `containsAll()` | `list.containsAll(...values): boolean` | All elements exist |
| `containsAny()` | `list.containsAny(...values): boolean` | Any element exists |
| `filter()` | `list.filter(expression): list` | Filter by condition (uses `value`, `index`) |
| `map()` | `list.map(expression): list` | Transform elements (uses `value`, `index`) |
| `reduce()` | `list.reduce(expression, initial): any` | Reduce to single value (uses `value`, `index`, `acc`) |
| `flat()` | `list.flat(): list` | Flatten nested lists |
| `join()` | `list.join(separator): string` | Join to string |
| `reverse()` | `list.reverse(): list` | Reverse order |
| `slice()` | `list.slice(start, end?): list` | Sublist |
| `sort()` | `list.sort(): list` | Sort ascending |
| `unique()` | `list.unique(): list` | Remove duplicates |
| `isEmpty()` | `list.isEmpty(): boolean` | No elements |
### File Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `asLink()` | `file.asLink(display?): Link` | Convert to link |
| `hasLink()` | `file.hasLink(otherFile): boolean` | Has link to file |
| `hasTag()` | `file.hasTag(...tags): boolean` | Has any of the tags |
| `hasProperty()` | `file.hasProperty(name): boolean` | Has property |
| `inFolder()` | `file.inFolder(folder): boolean` | In folder or subfolder |
### Link Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `asFile()` | `link.asFile(): file` | Get file object |
| `linksTo()` | `link.linksTo(file): boolean` | Links to file |
### Object Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `isEmpty()` | `object.isEmpty(): boolean` | No properties |
| `keys()` | `object.keys(): list` | List of keys |
| `values()` | `object.values(): list` | List of values |
### Regular Expression Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `matches()` | `regexp.matches(string): boolean` | Test if matches |
## View Types
### Table View
```yaml
views:
- type: table
name: "My Table"
order:
- file.name
- status
- due_date
summaries:
price: Sum
count: Average
```
### Cards View
```yaml
views:
- type: cards
name: "Gallery"
order:
- file.name
- cover_image
- description
```
### List View
```yaml
views:
- type: list
name: "Simple List"
order:
- file.name
- status
```
### Map View
Requires latitude/longitude properties and the Maps plugin.
```yaml
views:
- type: map
name: "Locations"
# Map-specific settings for lat/lng properties
```
## Default Summary Formulas
| Name | Input Type | Description |
|------|------------|-------------|
| `Average` | Number | Mathematical mean |
| `Min` | Number | Smallest number |
| `Max` | Number | Largest number |
| `Sum` | Number | Sum of all numbers |
| `Range` | Number | Max - Min |
| `Median` | Number | Mathematical median |
| `Stddev` | Number | Standard deviation |
| `Earliest` | Date | Earliest date |
| `Latest` | Date | Latest date |
| `Range` | Date | Latest - Earliest |
| `Checked` | Boolean | Count of true values |
| `Unchecked` | Boolean | Count of false values |
| `Empty` | Any | Count of empty values |
| `Filled` | Any | Count of non-empty values |
| `Unique` | Any | Count of unique values |
## Complete Examples
### Task Tracker Base
```yaml
filters:
and:
- file.hasTag("task")
- 'file.ext == "md"'
formulas:
days_until_due: 'if(due, ((date(due) - today()) / 86400000).round(0), "")'
is_overdue: 'if(due, date(due) < today() && status != "done", false)'
priority_label: 'if(priority == 1, "🔴 High", if(priority == 2, "🟡 Medium", "🟢 Low"))'
properties:
status:
displayName: Status
formula.days_until_due:
displayName: "Days Until Due"
formula.priority_label:
displayName: Priority
views:
- type: table
name: "Active Tasks"
filters:
and:
- 'status != "done"'
order:
- file.name
- status
- formula.priority_label
- due
- formula.days_until_due
groupBy:
property: status
direction: ASC
summaries:
formula.days_until_due: Average
- type: table
name: "Completed"
filters:
and:
- 'status == "done"'
order:
- file.name
- completed_date
```
### Reading List Base
```yaml
filters:
or:
- file.hasTag("book")
- file.hasTag("article")
formulas:
reading_time: 'if(pages, (pages * 2).toString() + " min", "")'
status_icon: 'if(status == "reading", "📖", if(status == "done", "✅", "📚"))'
year_read: 'if(finished_date, date(finished_date).year, "")'
properties:
author:
displayName: Author
formula.status_icon:
displayName: ""
formula.reading_time:
displayName: "Est. Time"
views:
- type: cards
name: "Library"
order:
- cover
- file.name
- author
- formula.status_icon
filters:
not:
- 'status == "dropped"'
- type: table
name: "Reading List"
filters:
and:
- 'status == "to-read"'
order:
- file.name
- author
- pages
- formula.reading_time
```
### Project Notes Base
```yaml
filters:
and:
- file.inFolder("Projects")
- 'file.ext == "md"'
formulas:
last_updated: 'file.mtime.relative()'
link_count: 'file.links.length'
summaries:
avgLinks: 'values.filter(value.isType("number")).mean().round(1)'
properties:
formula.last_updated:
displayName: "Updated"
formula.link_count:
displayName: "Links"
views:
- type: table
name: "All Projects"
order:
- file.name
- status
- formula.last_updated
- formula.link_count
summaries:
formula.link_count: avgLinks
groupBy:
property: status
direction: ASC
- type: list
name: "Quick List"
order:
- file.name
- status
```
### Daily Notes Index
```yaml
filters:
and:
- file.inFolder("Daily Notes")
- '/^\d{4}-\d{2}-\d{2}$/.matches(file.basename)'
formulas:
word_estimate: '(file.size / 5).round(0)'
day_of_week: 'date(file.basename).format("dddd")'
properties:
formula.day_of_week:
displayName: "Day"
formula.word_estimate:
displayName: "~Words"
views:
- type: table
name: "Recent Notes"
limit: 30
order:
- file.name
- formula.day_of_week
- formula.word_estimate
- file.mtime
```
## Embedding Bases
Embed in Markdown files:
```markdown
![[MyBase.base]]
<!-- Specific view -->
![[MyBase.base#View Name]]
```
## YAML Quoting Rules
- Use single quotes for formulas containing double quotes: `'if(done, "Yes", "No")'`
- Use double quotes for simple strings: `"My View Name"`
- Escape nested quotes properly in complex expressions
## Common Patterns
### Filter by Tag
```yaml
filters:
and:
- file.hasTag("project")
```
### Filter by Folder
```yaml
filters:
and:
- file.inFolder("Notes")
```
### Filter by Date Range
```yaml
filters:
and:
- 'file.mtime > now() - "7d"'
```
### Filter by Property Value
```yaml
filters:
and:
- 'status == "active"'
- 'priority >= 3'
```
### Combine Multiple Conditions
```yaml
filters:
or:
- and:
- file.hasTag("important")
- 'status != "done"'
- and:
- 'priority == 1'
- 'due != ""'
```
## References
- [Bases Syntax](https://help.obsidian.md/bases/syntax)
- [Functions](https://help.obsidian.md/bases/functions)
- [Views](https://help.obsidian.md/bases/views)
- [Formulas](https://help.obsidian.md/formulas)
@@ -1,622 +0,0 @@
---
name: obsidian-markdown
description: Create and edit Obsidian Flavored Markdown with wikilinks, embeds, callouts, properties, and other Obsidian-specific syntax. Use when working with .md files in Obsidian, or when the user mentions wikilinks, callouts, frontmatter, tags, embeds, or Obsidian notes.
source: https://github.com/kepano/obsidian-skills/blob/main/skills/obsidian-markdown/SKILL.md
---
# Obsidian Flavored Markdown Skill
This skill enables Claude Code to create and edit valid Obsidian Flavored Markdown, including all Obsidian-specific syntax extensions.
## Overview
Obsidian uses a combination of Markdown flavors:
- [CommonMark](https://commonmark.org/)
- [GitHub Flavored Markdown](https://github.github.com/gfm/)
- [LaTeX](https://www.latex-project.org/) for math
- Obsidian-specific extensions (wikilinks, callouts, embeds, etc.)
## Basic Formatting
### Paragraphs and Line Breaks
```markdown
This is a paragraph.
This is another paragraph (blank line between creates separate paragraphs).
For a line break within a paragraph, add two spaces at the end
or use Shift+Enter.
```
### Headings
```markdown
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```
### Text Formatting
| Style | Syntax | Example | Output |
|-------|--------|---------|--------|
| Bold | `**text**` or `__text__` | `**Bold**` | **Bold** |
| Italic | `*text*` or `_text_` | `*Italic*` | *Italic* |
| Bold + Italic | `***text***` | `***Both***` | ***Both*** |
| Strikethrough | `~~text~~` | `~~Striked~~` | ~~Striked~~ |
| Highlight | `==text==` | `==Highlighted==` | ==Highlighted== |
| Inline code | `` `code` `` | `` `code` `` | `code` |
### Escaping Formatting
Use backslash to escape special characters:
```markdown
\*This won't be italic\*
\#This won't be a heading
1\. This won't be a list item
```
Common characters to escape: `\*`, `\_`, `\#`, `` \` ``, `\|`, `\~`
## Internal Links (Wikilinks)
### Basic Links
```markdown
[[Note Name]]
[[Note Name.md]]
[[Note Name|Display Text]]
```
### Link to Headings
```markdown
[[Note Name#Heading]]
[[Note Name#Heading|Custom Text]]
[[#Heading in same note]]
[[##Search all headings in vault]]
```
### Link to Blocks
```markdown
[[Note Name#^block-id]]
[[Note Name#^block-id|Custom Text]]
```
Define a block ID by adding `^block-id` at the end of a paragraph:
```markdown
This is a paragraph that can be linked to. ^my-block-id
```
For lists and quotes, add the block ID on a separate line:
```markdown
> This is a quote
> With multiple lines
^quote-id
```
### Search Links
```markdown
[[##heading]] Search for headings containing "heading"
[[^^block]] Search for blocks containing "block"
```
## Markdown-Style Links
```markdown
[Display Text](Note%20Name.md)
[Display Text](Note%20Name.md#Heading)
[Display Text](https://example.com)
[Note](obsidian://open?vault=VaultName&file=Note.md)
```
Note: Spaces must be URL-encoded as `%20` in Markdown links.
## Embeds
### Embed Notes
```markdown
![[Note Name]]
![[Note Name#Heading]]
![[Note Name#^block-id]]
```
### Embed Images
```markdown
![[image.png]]
![[image.png|640x480]] Width x Height
![[image.png|300]] Width only (maintains aspect ratio)
```
### External Images
```markdown
![Alt text](https://example.com/image.png)
![Alt text|300](https://example.com/image.png)
```
### Embed Audio
```markdown
![[audio.mp3]]
![[audio.ogg]]
```
### Embed PDF
```markdown
![[document.pdf]]
![[document.pdf#page=3]]
![[document.pdf#height=400]]
```
### Embed Lists
```markdown
![[Note#^list-id]]
```
Where the list has been defined with a block ID:
```markdown
- Item 1
- Item 2
- Item 3
^list-id
```
### Embed Search Results
````markdown
```query
tag:#project status:done
```
````
## Callouts
### Basic Callout
```markdown
> [!note]
> This is a note callout.
> [!info] Custom Title
> This callout has a custom title.
> [!tip] Title Only
```
### Foldable Callouts
```markdown
> [!faq]- Collapsed by default
> This content is hidden until expanded.
> [!faq]+ Expanded by default
> This content is visible but can be collapsed.
```
### Nested Callouts
```markdown
> [!question] Outer callout
> > [!note] Inner callout
> > Nested content
```
### Supported Callout Types
| Type | Aliases | Description |
|------|---------|-------------|
| `note` | - | Blue, pencil icon |
| `abstract` | `summary`, `tldr` | Teal, clipboard icon |
| `info` | - | Blue, info icon |
| `todo` | - | Blue, checkbox icon |
| `tip` | `hint`, `important` | Cyan, flame icon |
| `success` | `check`, `done` | Green, checkmark icon |
| `question` | `help`, `faq` | Yellow, question mark |
| `warning` | `caution`, `attention` | Orange, warning icon |
| `failure` | `fail`, `missing` | Red, X icon |
| `danger` | `error` | Red, zap icon |
| `bug` | - | Red, bug icon |
| `example` | - | Purple, list icon |
| `quote` | `cite` | Gray, quote icon |
### Custom Callouts (CSS)
```css
.callout[data-callout="custom-type"] {
--callout-color: 255, 0, 0;
--callout-icon: lucide-alert-circle;
}
```
## Lists
### Unordered Lists
```markdown
- Item 1
- Item 2
- Nested item
- Another nested
- Item 3
* Also works with asterisks
+ Or plus signs
```
### Ordered Lists
```markdown
1. First item
2. Second item
1. Nested numbered
2. Another nested
3. Third item
1) Alternative syntax
2) With parentheses
```
### Task Lists
```markdown
- [ ] Incomplete task
- [x] Completed task
- [ ] Task with sub-tasks
- [ ] Subtask 1
- [x] Subtask 2
```
## Quotes
```markdown
> This is a blockquote.
> It can span multiple lines.
>
> And include multiple paragraphs.
>
> > Nested quotes work too.
```
## Code
### Inline Code
```markdown
Use `backticks` for inline code.
Use double backticks for ``code with a ` backtick inside``.
```
### Code Blocks
````markdown
```
Plain code block
```
```javascript
// Syntax highlighted code block
function hello() {
console.log("Hello, world!");
}
```
```python
# Python example
def greet(name):
print(f"Hello, {name}!")
```
````
### Nesting Code Blocks
Use more backticks or tildes for the outer block:
`````markdown
````markdown
Here's how to create a code block:
```js
console.log("Hello")
```
````
`````
## Tables
```markdown
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
```
### Alignment
```markdown
| Left | Center | Right |
|:---------|:--------:|---------:|
| Left | Center | Right |
```
### Using Pipes in Tables
Escape pipes with backslash:
```markdown
| Column 1 | Column 2 |
|----------|----------|
| [[Link\|Display]] | ![[Image\|100]] |
```
## Math (LaTeX)
### Inline Math
```markdown
This is inline math: $e^{i\pi} + 1 = 0$
```
### Block Math
```markdown
$$
\begin{vmatrix}
a & b \\
c & d
\end{vmatrix} = ad - bc
$$
```
### Common Math Syntax
```markdown
$x^2$ Superscript
$x_i$ Subscript
$\frac{a}{b}$ Fraction
$\sqrt{x}$ Square root
$\sum_{i=1}^{n}$ Summation
$\int_a^b$ Integral
$\alpha, \beta$ Greek letters
```
## Diagrams (Mermaid)
````markdown
```mermaid
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Do this]
B -->|No| D[Do that]
C --> E[End]
D --> E
```
````
### Sequence Diagrams
````markdown
```mermaid
sequenceDiagram
Alice->>Bob: Hello Bob
Bob-->>Alice: Hi Alice
```
````
### Linking in Diagrams
````markdown
```mermaid
graph TD
A[Biology]
B[Chemistry]
A --> B
class A,B internal-link;
```
````
## Footnotes
```markdown
This sentence has a footnote[^1].
[^1]: This is the footnote content.
You can also use named footnotes[^note].
[^note]: Named footnotes still appear as numbers.
Inline footnotes are also supported.^[This is an inline footnote.]
```
## Comments
```markdown
This is visible %%but this is hidden%% text.
%%
This entire block is hidden.
It won't appear in reading view.
%%
```
## Horizontal Rules
```markdown
---
***
___
- - -
* * *
```
## Properties (Frontmatter)
Properties use YAML frontmatter at the start of a note:
```yaml
---
title: My Note Title
date: 2024-01-15
tags:
- project
- important
aliases:
- My Note
- Alternative Name
cssclasses:
- custom-class
status: in-progress
rating: 4.5
completed: false
due: 2024-02-01T14:30:00
---
```
### Property Types
| Type | Example |
|------|---------|
| Text | `title: My Title` |
| Number | `rating: 4.5` |
| Checkbox | `completed: true` |
| Date | `date: 2024-01-15` |
| Date & Time | `due: 2024-01-15T14:30:00` |
| List | `tags: [one, two]` or YAML list |
| Links | `related: "[[Other Note]]"` |
### Default Properties
- `tags` - Note tags
- `aliases` - Alternative names for the note
- `cssclasses` - CSS classes applied to the note
## Tags
```markdown
#tag
#nested/tag
#tag-with-dashes
#tag_with_underscores
In frontmatter:
---
tags:
- tag1
- nested/tag2
---
```
Tags can contain:
- Letters (any language)
- Numbers (not as first character)
- Underscores `_`
- Hyphens `-`
- Forward slashes `/` (for nesting)
## HTML Content
Obsidian supports HTML within Markdown:
```markdown
<div class="custom-container">
<span style="color: red;">Colored text</span>
</div>
<details>
<summary>Click to expand</summary>
Hidden content here.
</details>
<kbd>Ctrl</kbd> + <kbd>C</kbd>
```
## Complete Example
````markdown
---
title: Project Alpha
date: 2024-01-15
tags:
- project
- active
status: in-progress
priority: high
---
# Project Alpha
## Overview
This project aims to [[improve workflow]] using modern techniques.
> [!important] Key Deadline
> The first milestone is due on ==January 30th==.
## Tasks
- [x] Initial planning
- [x] Resource allocation
- [ ] Development phase
- [ ] Backend implementation
- [ ] Frontend design
- [ ] Testing
- [ ] Deployment
## Technical Notes
The main algorithm uses the formula $O(n \log n)$ for sorting.
```python
def process_data(items):
return sorted(items, key=lambda x: x.priority)
```
## Architecture
```mermaid
graph LR
A[Input] --> B[Process]
B --> C[Output]
B --> D[Cache]
```
## Related Documents
- ![[Meeting Notes 2024-01-10#Decisions]]
- [[Budget Allocation|Budget]]
- [[Team Members]]
## References
For more details, see the official documentation[^1].
[^1]: https://example.com/docs
%%
Internal notes:
- Review with team on Friday
- Consider alternative approaches
%%
````
## References
- [Basic formatting syntax](https://help.obsidian.md/syntax)
- [Advanced formatting syntax](https://help.obsidian.md/advanced-syntax)
- [Obsidian Flavored Markdown](https://help.obsidian.md/obsidian-flavored-markdown)
- [Internal links](https://help.obsidian.md/links)
- [Embed files](https://help.obsidian.md/embeds)
- [Callouts](https://help.obsidian.md/callouts)
- [Properties](https://help.obsidian.md/properties)
@@ -1,64 +0,0 @@
---
name: process-compose
description: Inspect a running process-compose project read-only. Use when the user asks what services/processes are running, whether a service is up or healthy, what state a process is in, or to read a service's recent log output — or mentions process-compose (pc) by name.
---
# Inspecting process-compose
The `process-compose` CLI is a thin **client**. It does not read your config or
processes directly — it queries a process-compose **server** that is already
running (the one started by `process-compose up`). Every command below talks to
that server over TCP, default `localhost:8080`.
This skill is **read-only**: never start, stop, restart, or scale processes.
## Connecting
- Default target is `localhost:8080`. If a project runs on another port, pass
`-p PORT` (or set `PC_PORT_NUM`); the port is whatever that project's
`process-compose.yaml` / launch command set.
- A "connection refused" error means **no server is running on that port**, not
a bad command. Report that the project isn't up rather than retrying variants.
- `pc` is a fish abbreviation for `process-compose` and exists only in an
interactive fish shell. In scripts and Bash calls use the full `process-compose`.
## Read commands
List every process with its status (one line each):
```
process-compose process list -o wide
```
Add `-o json` when you need to parse fields (status, health, pid, restarts, exit
code) rather than display them.
Full state of one process:
```
process-compose process get NAME -o json
```
Recent log lines for a process (tail the last N — adjust the number to the need):
```
process-compose process logs NAME -n 100
```
Multiple processes: comma-separate them (`proc1,proc2`). A whole namespace:
`-N NAMESPACE`.
Whole-project state (is everything ready):
```
process-compose project state
```
## Never do
- **No TUI.** Bare `process-compose`, `up`, and `attach` launch the interactive
full-screen TUI and hang a non-interactive shell. Always use a subcommand.
- **No `-f` / `--follow`** on `logs` — it streams forever and blocks. Use `-n` to
pull a finite tail instead.
- **No mutations**`start`, `stop`, `restart`, `scale`, `down` are out of scope
for this skill.
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/prototype
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/research
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/resolving-merge-conflicts
@@ -1,71 +0,0 @@
---
name: rewrite-comments
description: Inspect and rewrite unclear source-code comments after tracing the surrounding behavior and contracts. Use when asked to review, improve, clean up, or rewrite comments selected by a Git commit, the current working tree, or a named function or method; function requests may target comments on the declaration, inside its body, or both.
---
# Rewrite Comments
Rewrite comments only after understanding the code well enough to preserve its meaning. Make edits in the current working tree; never rewrite Git history unless explicitly requested.
## Resolve the target
Determine the scope from the request:
- **Commit:** Inspect the commit diff and the complete current versions of the code regions it changes. Treat comments added or modified by the commit, plus comments attached to or inside changed declarations, as candidates. If the relevant code no longer exists in the current tree, report that instead of editing an older revision or history.
- **Working tree:** Inspect staged, unstaged, and untracked changes. Treat comments in or attached to changed declarations as candidates, prioritizing comments changed in the diff.
- **Function:** Locate the named function or method. Include comments documenting the declaration and comments within its body by default. Honor `on`, `above`, or `documentation` as declaration-only, and `within`, `inside`, or `body` as body-only.
Use repository-aware search and language structure where available. Account for overloads, methods with the same name, generated code, and renamed files. Ask for clarification only when multiple plausible targets remain and choosing one would materially change the edits.
Do not broaden the edit scope merely because nearby comments could also be improved. Read outside the scope freely to establish context.
## Build context before editing
Read the full enclosing declaration and enough neighboring code to understand it. Trace relevant definitions and call sites until the important behavior is supported by evidence. Establish, as applicable:
- why the code exists and which callers depend on it;
- accepted inputs, returned values, mutations, side effects, and failure behavior;
- ordering, lifecycle, concurrency, caching, security, and performance constraints;
- invariants, edge cases, compatibility requirements, and intentionally surprising choices;
- types, tests, interfaces, domain documentation, and architectural decisions that define the contract.
Prefer direct evidence from code, tests, and repository documentation. Do not invent intent. If a comment makes a claim that cannot be verified, either make it narrower and factual or remove it.
Stop exploring when the comment's purpose and every retained behavioral claim can be explained from evidence. Avoid tracing unrelated parts of the system.
## Judge each candidate
Keep an accurate comment when it already adds durable information. Rewrite or remove a comment when it is unclear, redundant, misleading, stale, speculative, or coupled to incidental implementation details.
Prioritize comments that capture:
- hidden contracts and invariants;
- non-obvious reasons and tradeoffs;
- caller-visible edge cases or failure behavior;
- constraints imposed by another subsystem, API, format, or compatibility promise;
- deliberate deviations from the obvious implementation.
Avoid comments that:
- narrate syntax or restate names and types;
- describe mechanics that are immediately apparent from the next few lines;
- duplicate the type system or stable API shape without adding a contract;
- predict future work without an actionable, repository-standard marker;
- mention transient details, line positions, counts, or internal steps likely to drift;
- preserve a confident explanation unsupported by the code.
Retain required legal notices, tool directives, generated-file markers, suppression comments, and structured documentation tags unless the task explicitly includes them and changing them is safe.
## Rewrite
Edit only comments unless the user explicitly requests code changes. Preserve behavior, public API, formatting conventions, comment style, and documentation syntax.
Write concise, direct comments at the narrowest useful location. Explain `why`, `must`, `unless`, or `despite` when those ideas matter. State contracts in terms of observable behavior rather than current implementation. Include inputs and outputs only when their semantics, ownership, units, normalization, sentinel values, or failure modes are not already obvious from code and types.
Delete a comment when removal is clearer than a rewrite. Do not add comments merely to replace every removed one, and do not churn wording without a meaningful clarity or correctness improvement.
## Verify and report
Review the final diff and confirm that edits are limited to the resolved scope and do not alter executable code. Re-read each edited comment against its code and callers. Run repository-required checks for changed files and any broader checks explicitly required by repository instructions; treat failures as blocking.
Summarize which comments changed and the hidden behavior or contract they now clarify. Mention comments intentionally removed, unresolved ambiguity, unverifiable claims, and checks that could not be run.
@@ -1,4 +0,0 @@
interface:
display_name: "Rewrite Comments"
short_description: "Rewrite code comments around selected changes"
default_prompt: "Use $rewrite-comments to inspect and improve comments in my working tree."
@@ -1,121 +0,0 @@
---
name: senior-code-review
description: Perform a thorough, read-only review of a pull request, branch, commit, working-tree diff, or named files. Use when asked to review code.
---
# Senior Code Review
Perform a high-recall review followed by a high-precision verification pass. Behave like a senior engineer who must understand the local system before judging the change.
## Operating contract
- Treat the review as read-only. Do not edit files, apply fixes, change git state, commit, or post comments unless the user explicitly asks in a separate instruction.
- Review the change, not the author's process. Do not excuse a defect because tests pass or because the implementation was difficult.
- Prefer concrete, falsifiable findings over generic advice. Do not report style preferences unless they violate repository conventions or materially harm comprehension.
- Keep distinct findings separate; combine repeated instances of one root cause.
- Investigate enough surrounding code to determine whether a suspected problem is real. The diff is an index, not the full review surface.
- Report no findings when none survive verification. Never invent issues to make a review look thorough.
Read [references/review-lenses.md](references/review-lenses.md) before reviewing. When independent subagents are available, also read [references/reviewer-briefs.md](references/reviewer-briefs.md).
## 1. Establish scope
Use the user's explicit target when supplied. Otherwise choose the first applicable scope:
1. Staged, unstaged, and relevant untracked changes when the working tree has changes.
2. The current branch against its merge base with the default or upstream branch.
3. The most recent commit when no other change exists.
Resolve the base and head before reviewing. Inspect the diff summary, changed paths, commits, and rename/binary status. Stop with a concise explanation if the target is invalid or the diff is empty.
Exclude generated, vendored, lock, and snapshot files from line-by-line review when appropriate, but inspect their semantic effects when dependency, schema, API, or snapshot changes are relevant.
## 2. Build codebase context
Before judging implementation details:
1. Read the applicable repository instructions, including root and path-scoped `AGENTS.md`, `CLAUDE.md`, `REVIEW.md`, `CONTRIBUTING.md`, and relevant engineering or architecture documents.
2. Determine the intended behavior from the user's request, PR or issue text when available, commit messages, specifications, and tests. State any important requirement that remains unavailable.
3. Read each changed source and test file as a whole when feasible, not only the edited hunks.
4. Trace affected entry points, callers, callees, types, data flows, configuration, and persistence boundaries far enough to understand observable behavior.
5. Search for sibling implementations, established helpers, test conventions, error-handling patterns, and architectural precedents.
6. Use focused git history or blame when a surprising invariant, compatibility shim, or architectural choice may be intentional.
Produce a short internal context map: change intent, affected behavior, relevant invariants, architectural boundaries, and verification mechanisms. Share it with review subagents; do not clutter the final response with it unless it explains a finding.
## 3. Inspect tests before implementation
Read changed and nearby tests first. Infer the promised behavior, then challenge the tests:
- Would a plausible broken implementation still pass?
- Do assertions observe externally meaningful behavior or merely repeat mocks, fixtures, implementation details, or values constructed by the test itself?
- Do mocks preserve the real contract, ordering, failure modes, and data shape?
- Does a regression test fail without the fix for the intended reason?
- Are negative, boundary, authorization, error, concurrency, and integration paths covered where risk warrants them?
- Can the test pass vacuously because code was not called, an async result was not awaited, an exception was swallowed, or the assertion is too broad?
Do not equate coverage with correctness. Treat missing tests as a finding only when you can name the meaningful behavior or regression that remains unprotected.
## 4. Run independent review passes
For a non-trivial change, use four independent review seats in parallel when the host supports subagents:
1. Correctness and unintended behavior.
2. Test validity and verification gaps.
3. Security and defensive coding.
4. Architecture, abstractions, and maintainability.
Give every seat the same scope, intent, context map, changed-file list, and read-only constraint. Keep their reasoning isolated. Use the exact contracts in `references/reviewer-briefs.md` and allow them to inspect the repository rather than pasting an enormous diff into each prompt.
If subagents are unavailable or the change is very small, perform the four passes sequentially yourself. Finish one lens before starting the next so concerns do not collapse into a shallow general scan.
Add focused passes for performance, concurrency, migrations, compatibility, observability, accessibility, or dependency risk when the changed surfaces make them relevant.
## 5. Verify candidate findings
Collect candidate findings from every pass, deduplicate by root cause, then challenge them with a fresh verification pass. Use an independent verifier subagent when available.
Keep a bug, security, or test finding only when all applicable statements are true:
- The finding identifies a precise changed or directly affected location.
- The reviewer read the surrounding implementation, callers, guards, types, and relevant tests.
- A concrete input, state, or execution path leads to a specific bad outcome.
- Existing validation, framework behavior, or another layer does not already prevent it.
- The issue is introduced by the change or is directly relevant to safely merging it. Clearly label significant pre-existing issues.
- The severity matches realistic impact and likelihood.
- The proposed direction addresses the root cause without creating a larger problem.
Keep an architecture or maintainability finding only when it identifies a concrete cost such as an invalid dependency direction, duplicated source of truth, leaky boundary, misleading abstraction, excessive coupling, hidden invariant, or materially harder future change. Explain the tradeoff and label non-blocking improvements as suggestions.
Require at least 80% confidence for reported defects. Drop refuted, purely speculative, or unactionable candidates. When uncertainty is important and cannot be resolved, state the missing evidence rather than asserting a defect.
## 6. Report findings
Lead with findings ordered by severity and then by confidence. Use these levels:
- `P0 Critical`: immediate security compromise, data loss, corruption, or broad outage.
- `P1 High`: concrete bug, security vulnerability, broken contract, or invalid test that should block merge.
- `P2 Medium`: real but narrower defect, meaningful defensive-coding gap, or structural problem likely to cause defects.
- `P3 Suggestion`: non-blocking architectural or maintainability improvement with a concrete benefit.
Format each finding as:
```markdown
### [P1][correctness] Short imperative title
`path/to/file.ext:line`
- Evidence: What the code does and the surrounding fact that makes it wrong.
- Failure scenario: The triggering input/state and observable outcome.
- Recommendation: The smallest sound direction for fixing the root cause.
- Confidence: 92%
```
For test findings, name the mutation or broken implementation that would still pass. For security findings, name the trust boundary, attacker-controlled input or capability, and impact. For architecture findings, name the boundary or invariant and compare the proposed structure with the current cost.
After findings, include:
- `Verdict`: `Do not merge`, `Merge after fixes`, or `Ready to merge`.
- `Review coverage`: the scope and lenses actually inspected.
- `Verification gaps`: commands, runtime checks, specifications, or environment access that were unavailable.
If no finding survives verification, say `No verified findings.` and still list material verification gaps. Do not add praise, a diff summary, or generic best-practice advice unless it helps the user act on the review.
@@ -1,14 +0,0 @@
interface:
display_name: Senior Code Review
short_description: Thorough, evidence-backed code review
default_prompt: Use $senior-code-review to perform a thorough, read-only review
of the current changes.
icon_small: assets/icon.svg
icon_large: assets/icon.svg
policy:
products:
- chatgpt
- codex
- api
- atlas
allow_implicit_invocation: true
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.5C3 12.1193 4.11929 11 5.5 11H9V21H5.5C4.11929 21 3 19.8807 3 18.5V13.5Z" fill="#FFD400"/>
<path d="M15 7H18.5C19.8807 7 21 8.11929 21 9.5V18.5C21 19.8807 19.8807 21 18.5 21H15V7Z" fill="#F75858"/>
<path d="M9 5.5C9 4.11929 10.1193 3 11.5 3H12.5C13.8807 3 15 4.11929 15 5.5V21H9V5.5Z" fill="#FFA43D"/>
</svg>

Before

Width:  |  Height:  |  Size: 420 B

@@ -1,73 +0,0 @@
# Review lenses
Use every core lens. Expand only the checks relevant to the changed surfaces.
## Contents
- Correctness and unintended behavior
- Test validity
- Security and defensive coding
- Architecture and maintainability
- Conditional lenses
## Correctness and unintended behavior
- Trace happy, error, empty, null, boundary, retry, cancellation, and partial-failure paths.
- Check state transitions, ordering, idempotency, caching, invalidation, and cleanup.
- Look for off-by-one errors, stale state, wrong defaults, lossy conversions, timezone or locale mistakes, and mismatched units.
- Check async ownership, missing awaits, races, deadlocks, double execution, resource leaks, and work after cancellation.
- Compare types and runtime values across boundaries; do not assume static types validate external data.
- Check compatibility with existing callers, serialized formats, database rows, events, CLI flags, environment variables, and public APIs.
- Follow error values and exceptions. Flag swallowed failures, misleading fallbacks, partial writes, and success reported before durable completion.
- Verify deletions and refactors leave no live caller, stale branch, duplicate path, or behavior split between old and new implementations.
- Distinguish behavior newly broken by the change from a nearby pre-existing issue.
## Test validity
- Identify the behavior each test claims to protect and the production branch that implements it.
- Mentally mutate or remove the production behavior. Determine whether the test fails for the intended reason.
- Detect tautological assertions, assertions derived from the same value under test, snapshots accepted without semantic inspection, and tests with no meaningful assertion.
- Check that the exercised path runs: awaited promises, consumed generators, subscribed streams, flushed transactions, rendered components, and invoked callbacks.
- Treat mocks as contracts. Compare mock methods, data, ordering, errors, and side effects with the real dependency.
- Flag tests that verify implementation details while missing observable outcomes.
- Check false positives from broad exception assertions, loose matchers, unconditional waits, retries that mask failure, or fixtures that bypass the behavior.
- For bug fixes, require a regression test that would fail on the old behavior when practical.
- Seek high-risk gaps: permissions, tenant isolation, malformed input, boundaries, failure recovery, concurrency, migrations, and cross-component integration.
- Do not demand a test for trivial wiring already proven by stronger coverage.
## Security and defensive coding
- Identify trust boundaries and attacker-controlled input before applying a checklist.
- Check authentication, authorization, object ownership, tenant isolation, and confused-deputy paths at the point of action.
- Validate and canonicalize input at boundaries; encode output for its destination context.
- Look for injection into SQL, commands, templates, HTML, URLs, headers, logs, paths, deserializers, and dynamic code.
- Check secrets, credentials, tokens, personal data, and sensitive metadata in source, logs, errors, caches, telemetry, URLs, and client-visible responses.
- Review cryptography, randomness, signature verification, token lifetime, replay resistance, secure comparison, and key handling when touched.
- Check filesystem traversal, unsafe temporary files, archive extraction, symlink behavior, permissions, and cleanup.
- Review SSRF, redirect validation, request smuggling surfaces, unbounded downloads, parser limits, and denial-of-service opportunities for network-facing changes.
- Check dependency provenance, pinned integrity, install or build scripts, dangerous configuration defaults, and privilege expansion.
- Prefer fail-closed behavior where authorization or integrity is uncertain, without turning recoverable availability failures into outages.
- Consider abuse cases and defense in depth; report only issues supported by a realistic capability and impact.
## Architecture and maintainability
- Compare the change with existing architectural boundaries and dependency direction.
- Check separation of orchestration, domain logic, persistence, transport, and presentation.
- Identify feature logic leaking into shared infrastructure or transport details leaking into the domain.
- Look for duplicated sources of truth, parallel implementations, inconsistent policy enforcement, and abstractions that conceal rather than remove complexity.
- Challenge both under-abstraction and premature abstraction. Require a named invariant or repeated variation before adding a general layer.
- Check cohesion, coupling, fan-in/fan-out, circular dependencies, global state, hidden temporal coupling, and unclear resource ownership.
- Prefer explicit invariants, narrow interfaces, meaningful types, and canonical helpers over casts, optional fields, catch-all objects, and silent fallbacks.
- Judge readability through control flow, naming, locality, concept count, and how much unrelated context a maintainer must retain.
- Check whether the design supports likely change without optimizing for hypothetical futures.
- When suggesting restructuring, name the move and why it lowers risk: split responsibility, invert dependency, centralize policy, remove pass-through abstraction, model state explicitly, or reuse a canonical path.
- Avoid requesting large refactors unless the current change creates or materially worsens the problem.
## Conditional lenses
- Performance: complexity, repeated I/O, N+1 access, allocations, hot-path blocking, pagination, cache correctness, and backpressure.
- Data changes: forward/backward compatibility, transactionality, rollbacks, backfills, mixed-version deployment, and irreversible loss.
- Distributed systems: retries, duplicate delivery, ordering, idempotency, clock assumptions, split brain, and partial availability.
- UI and accessibility: loading/error/empty states, keyboard and focus behavior, semantics, contrast, responsive behavior, and hydration.
- Observability: actionable errors, stable structured fields, correlation, sensitive-data hygiene, metrics on failure paths, and diagnosable fallbacks.
- Dependencies and configuration: default changes, environment parity, least privilege, reproducibility, version compatibility, and safe rollout.
@@ -1,57 +0,0 @@
# Reviewer briefs
Use these briefs for isolated review seats. Replace placeholders with the established scope and context. Every reviewer must remain read-only and inspect repository files directly.
## Shared preamble
```text
Act as an independent senior code reviewer. Do not edit files or change git state.
Review target: {TARGET}
Intent and requirements: {INTENT}
Changed files: {CHANGED_FILES}
Relevant repository guidance: {GUIDANCE}
Context map and invariants: {CONTEXT_MAP}
Read the relevant diff, complete changed files, surrounding code, and tests. Trace callers or dependencies when needed. Return candidate findings only. For every candidate include file and line, concrete evidence, failure or maintenance scenario, severity, recommendation, and confidence. Do not report generic advice or repeat repository tooling. Mark pre-existing issues explicitly.
```
## Correctness seat
```text
Focus on runtime correctness and unintended behavior. Trace affected flows through callers, boundaries, state transitions, error paths, compatibility contracts, async behavior, cleanup, and edge cases. Find concrete cases where the implementation produces the wrong observable outcome. Challenge assumptions made by types, frameworks, and happy-path tests. Ignore cosmetic style and unproven hypotheticals.
```
## Test seat
```text
Focus on whether the tests genuinely prove the promised behavior. Read tests before implementation. For each suspicious test, name a realistic broken production mutation that would still pass. Check vacuous execution, weak or tautological assertions, unrealistic mocks, missing awaits, swallowed failures, implementation-detail coupling, and high-risk behavioral gaps. Do not equate coverage with correctness or demand low-value tests.
```
## Security seat
```text
Focus on security and defensive coding. Identify trust boundaries, attacker-controlled inputs, sensitive assets, and privileges first. Examine authentication, authorization, isolation, injection, data exposure, unsafe parsing or filesystem/network behavior, cryptography, dependency risk, resource limits, and failure posture. Report only findings with a realistic attacker capability, path, and impact; include relevant defense-in-depth gaps when concrete.
```
## Architecture seat
```text
Focus on architecture, abstractions, readability, and maintainability in the context of this repository. Compare with existing patterns and boundaries. Look for invalid dependency direction, duplicated truth, leaky layers, hidden invariants, excessive coupling, unclear ownership, premature or missing abstractions, and complexity merely relocated. Suggest a specific structural move and explain its benefit and tradeoff. Separate merge-blocking structural regressions from optional improvements.
```
## Verification seat
```text
Act as an adversarial verifier, not another finder. Given the repository, review target, requirements, and candidate findings, verify each candidate independently.
For each candidate:
1. Read the cited code and enough surrounding callers, guards, types, tests, and framework behavior to understand it.
2. Reproduce or reason through the exact input/state and outcome.
3. Decide confirmed, uncertain, or refuted.
4. Check whether the change introduced it or merely exposed a pre-existing issue.
5. Correct severity and recommendation.
6. Merge duplicates that share a root cause.
Return only confirmed findings at 80% confidence or higher, plus unresolved uncertainties where missing evidence is itself important. Preserve concrete non-blocking architecture suggestions, clearly labeled as suggestions. Do not accept a finding because multiple reviewers repeated it.
```
-128
View File
@@ -1,128 +0,0 @@
---
name: show-me
description: Help the user understand the current topic visually with concise diagrams, code-shape sketches, and focused HTML artifacts.
source: https://github.com/humanlayer/skills/blob/main/plugins/show-me/skills/show-me/SKILL.md
---
Help the user understand the current topic of conversation visually. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear.
- Show logic or an algorithm as pseudocode:
```text
on(save)
if content is unchanged
return cached result
write new content
return fresh result
```
- Show runtime control flow as a call tree:
```text
submitForm
createSession
persistPrompt
launchAgent
navigateToSession
```
- Show UI structure as a component tree, including state and module boundaries that matter:
```tsx
<SessionPage> (apps/example/src/routes/session.tsx)
useSessionEvents()
<SessionToolbar>
<RunSkillButton> (packages/ui)
```
- Show file responsibility or a broad refactor as a shallow file tree:
```text
src/
├── commands/ # parses user actions
├── sessions/ # owns session state
└── transport/ # sends API requests
```
- Show component interaction, control flow, or data flow with Mermaid:
```mermaid
sequenceDiagram
participant User
participant UI
participant Daemon
User->>UI: choose command
UI->>Daemon: send expanded prompt
Daemon-->>UI: stream result
```
- Use `diff` when the point is what changes and the surrounding shape already exists. Match the diff shape to the topic.
For a component change:
```diff
<SessionPage>
useSessionEvents()
<SessionToolbar>
+ <RunSkillButton />
<SessionTimeline>
+ <SkillResultCard />
```
For a file-layout change:
```diff
src/
├── commands/
+│ └── show-me.ts # expands the slash command
├── sessions/
-└── transport.ts
+└── transport/
+ ├── client.ts
+ └── stream.ts
```
For a call-tree or call-stack change:
```diff
submitForm
createSession
persistPrompt
+ expandSkillMention
launchAgent
- navigateToSession
+ navigateToSession
+ subscribeToEvents
```
For a state or control-flow change:
```diff
on(save)
- write content
+ if content is unchanged
+ return cached result
+ write new content
+ invalidate cache
```
- Show the whole block when most of it is new, when omitted context would hide ownership or order, or when the user needs a copyable target shape:
```ts
function expandSkill(command: string): string {
const skillName = command.slice(1)
return `use the ${skillName} skill`
}
```
- For a visual UI, layout, state comparison, or concept too dense for Mermaid, write one focused HTML file — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile. Then open it for the user:
```
Bash(open path/to/show-me-{description}.html)
```
### guidance
Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point.
You may use one of these, you may use several, it is unlikely you will use all of them. Use your judgement and don't overwhelm the user.
@@ -1,64 +0,0 @@
---
name: simplify-code
description: Simplify recently changed code without changing behavior. Use when asked to clean up, clarify, streamline, refactor, or review an uncommitted diff, staged changes, changes against a Git ref or branch, the last commit, or specified changed files. Restrict edits to changed lines, follow repository guidance, preserve public APIs and semantics, and verify the result with the project's tests and checks.
---
# Simplify Code
Improve the readability and maintainability of a Git-scoped change while preserving its behavior. Treat the existing diff as the edit boundary, not as permission to refactor the surrounding code.
## Resolve the scope
1. Find the repository root with `git rev-parse --show-toplevel` and work from it.
2. Read applicable repository instructions such as `AGENTS.md` and `CLAUDE.md`, including any nearer files that govern the changed paths.
3. Preserve unrelated worktree changes. Never reset, restore, discard, stage, or commit changes unless the user explicitly asks.
4. Select one base scope from the request:
- Default, including staged and unstaged tracked changes: `git diff HEAD`
- Staged only: `git diff --cached`
- Against a ref or branch: `git diff <ref>`
- Last commit: `git diff HEAD~1..HEAD`
5. If the user names files, append `-- <paths...>` and exclude every other path.
6. List statuses with the matching `git diff --name-status` command. For renames or copies, use the destination path.
7. If the default scope is empty and the request refers generally to recent changes, fall back to `HEAD~1..HEAD`. State in the final summary that the last commit was used. Do not use this fallback for an explicitly staged, ref, or file-scoped request.
8. If the resulting scope is empty, report that there is nothing to simplify and stop.
Use `git diff --unified=0 --no-ext-diff <scope> -- <path>` to identify current-file line ranges from each hunk's `+start,count` coordinates. Treat an omitted count as one line and a zero count as a deletion-only hunk with no current lines. Treat an added file as fully in scope. If ranges touch or overlap, treat them as one range.
Do not hand-edit binary files, generated output, minified files, vendored code, lockfiles, or snapshots unless the user specifically includes them and the repository expects manual edits. Report skipped files briefly.
## Simplify within the boundary
Inspect surrounding code for context, but modify only the changed current-file ranges. Work one file at a time so line movement remains manageable. Recalculate that file's diff after editing and confirm no simplification edit escaped the original changed regions.
Prioritize concrete improvements:
- Reduce unnecessary nesting, branching, duplication, and intermediate state.
- Replace unclear names when every required reference can be changed inside the allowed scope.
- Consolidate logic that is already one concern; keep distinct concerns separate.
- Remove dead or redundant code and comments that merely narrate syntax.
- Retain comments that explain intent, constraints, business rules, or non-obvious behavior.
- Prefer straightforward control flow over clever expressions; avoid nested ternaries.
- Follow established project patterns instead of introducing a new abstraction or style.
Preserve observable behavior, error handling, side effects, ordering, concurrency behavior, types, serialization formats, and public APIs. Do not add features or broaden the task. Do not remove a useful abstraction merely to reduce line count.
If an improvement requires touching unchanged code, leave the code alone and mention the opportunity in the final summary. If no worthwhile in-scope improvement exists, make no edit to that region.
## Verify
1. Review the final diff for every edited file and run `git diff --check` with the same scope where applicable.
2. Run the narrowest relevant formatter, linter, type checker, and tests first, using repository-provided commands.
3. Run the broader project verification suite when practical.
4. If verification fails, determine whether the failure was caused by the simplification. Fix in-scope regressions; otherwise preserve the failure output and report it accurately.
5. Reconfirm that behavior and public interfaces did not change and that unrelated files remain untouched.
## Report
Summarize:
- the Git scope reviewed;
- the simplifications made and why they improve clarity;
- checks and tests run, with their results;
- skipped files, pre-existing failures, or worthwhile out-of-scope improvements.
Do not claim a simplification when the code was already clear enough to leave unchanged.
@@ -1,14 +0,0 @@
interface:
display_name: Simplify Code
short_description: Simplify changed code without changing behavior
default_prompt: Use $simplify-code to simplify my recent code changes while preserving
behavior.
icon_small: assets/icon.svg
icon_large: assets/icon.svg
policy:
products:
- chatgpt
- codex
- api
- atlas
allow_implicit_invocation: true
@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.58 4.38996C18.05 2.86996 15.68 2.88996 14.18 4.38996L5.26999 13.33C4.25999 14.34 3.84999 14.92 3.59999 16.26L3.06999 19.56C2.88999 20.47 3.50999 21.09 4.43999 20.93L7.74999 20.39C9.06999 20.18 9.64999 19.76 10.66 18.75L19.58 9.82996C21.1 8.30996 21.12 5.93996 19.58 4.40996V4.38996Z" fill="#FEBD08"/>
<path d="M19.58 9.81997C21.1 8.29997 21.12 5.92997 19.58 4.39997C18.05 2.87997 15.68 2.89997 14.18 4.39997L13.77 4.80997L19.18 10.22L19.58 9.81997Z" fill="#FF928C"/>
<path d="M13.7694 4.81308L12.3552 6.22729L17.7646 11.6367L19.1788 10.2224L13.7694 4.81308Z" fill="#D9D9D9"/>
<path opacity="0.5" d="M12.36 6.22998L5.26998 13.34C4.25998 14.35 3.84998 14.93 3.59998 16.27L3.06998 19.57C2.97998 20.02 3.08998 20.4 3.32998 20.65L15.05 8.92998L12.36 6.22998Z" fill="white"/>
<path d="M4.60001 14.05C4.06001 14.69 3.79001 15.27 3.60001 16.26L3.51001 16.81L7.18001 20.48L7.75001 20.39C8.74001 20.23 9.31001 19.96 9.95001 19.41L4.60001 14.05Z" fill="#FFDDBC"/>
<path d="M3.50999 16.8101L3.06999 19.5601C2.88999 20.4701 3.50999 21.0901 4.43999 20.9301L7.17999 20.4801L3.50999 16.8101Z" fill="#4D4D4D"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/tdd
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/teach
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/to-questionnaire
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/to-spec
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/to-tickets
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/wait-what
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/wayfinder
-105
View File
@@ -1,105 +0,0 @@
---
name: weather
description: Record a day's high temperature, low temperature, rainfall, growing degree days, solar energy, and freezing exposure in that day's daily note, read from the home weather station. Use whenever the user asks to log, add, backfill, or look up the weather for a date — including when they say only "yesterday's weather" or "add the weather to my daily note", when they ask about growing degree days, GDD, accumulated heat, solar energy, frost or freezing hours, and including the scheduled run that fills in the day that just ended.
---
# weather — a day's weather in the daily note
Shell the `weather-day` CLI from in this skill's `scripts` directory with the bash tool, then write its values into the daily note's frontmatter. Nothing here needs a key or a login — the station is public.
## Getting the numbers
```
weather-day [YYYY-MM-DD] # no argument: yesterday
```
One JSON object on stdout:
```json
{ "date": "2026-08-18", "station_id": 173994,
"temp_high": 72.3, "temp_low": 54.1, "rainfall": 0.02,
"gdd_base50": 11.4, "solar_energy_kwh_m2": 6.83,
"observations": 287, "coverage_hours": 23.9 }
```
On a day that went below freezing, two more keys appear between `solar_energy_kwh_m2` and `observations`:
```json
"hours_below_freezing": 7.2, "freezing_degree_hours": 31.8,
```
| key | units | what it is |
|---|---|---|
| `temp_high`, `temp_low` | °F | the day's extremes |
| `rainfall` | inches | total for the day |
| `gdd_base50` | °F-days | growing degree days, base 50°F |
| `solar_energy_kwh_m2` | kWh/m² | total solar energy on a horizontal surface |
| `hours_below_freezing` | hours | time spent under 32°F |
| `freezing_degree_hours` | °F-hours | how far under 32°F, integrated over that time |
**Everything from `date` through `freezing_degree_hours` is a frontmatter key** — copy the values across unchanged rather than reformatting or re-rounding them. `observations` and `coverage_hours` are diagnostics; they stay out of the note.
Three of them are written only when they have something to say. **`rainfall` is omitted from the note when it is `0`** — most days here are dry, and a `rainfall: 0` on four notes in five is noise that makes the days it did rain harder to spot. `hours_below_freezing` and `freezing_degree_hours` are omitted by `weather-day` itself on a day that never went below 32°F, so they are simply not there to copy. Either way the key's absence is the record: a dry day, a day with no frost.
Note the difference in where the omission happens. `weather-day` always prints `rainfall`, `0` included — that is a measurement, and the CLI reports it. Dropping it is this skill's decision about the note, so do not expect the JSON to be missing it.
**`gdd_base50` is one day's accumulation, not a running total.** It is the number you *sum* to answer a question — days from planting to harvest, from transplanting to first bloom, from fruit set to ripe. A single day's value on its own says very little. When the user asks "how many GDD since I planted the tomatoes", add up the `gdd_base50` of each daily note from that date forward; say how many days you summed and flag any in the range that have no value rather than treating a missing day as zero.
Any failure exits non-zero with a message on stderr. Report it and stop; do not write a partial or guessed value into the note. That includes the station going quiet mid-day: the totals are integrals over the whole day, so rather than draw a straight line across an hour or more of missing readings, `weather-day` refuses the date outright. There is no partial answer to salvage and nothing to write — say which stretch is missing and leave the note alone.
## Check the coverage before writing
`coverage_hours` is the span from the first observation to the last. A full day is ~24 (23 or 25 across a DST change).
`observations` is a count, not a duration, and a low-looking one is not by itself a problem: the API answers a day-long range on a 5-minute grid, so a complete day is around 288, not the ~1440 a per-minute feed would give. Judge the day by `coverage_hours`.
**If `coverage_hours` is under 20, do not write to the note.** Tell the user the day is only partly covered and give them the numbers to judge. A high or low computed from part of a day looks entirely normal in the frontmatter and stays wrong forever. It usually means the station was offline for a stretch, or the date asked for is today and the day hasn't finished.
A hole *inside* the day is a hard failure rather than a low `coverage_hours`, so a number that gets here has no outage longer than an hour in it. What `coverage_hours` still catches is a day the station started or stopped reporting partway through — and for the totals that is worse than for the extremes: `gdd_base50` and `solar_energy_kwh_m2` are missing the hours that never arrived, so they come out low, plausibly, and stay that way in every season sum afterwards.
## Writing it into the note
The note is `<vault>/daily/<date>.md` — the date exactly as `weather-day` echoed it back, and the absolute path, always.
**If the note exists:** add or update only the keys you are writing. Every other key keeps its value and its position, and the body is untouched — the vault syncs live from other devices, so a rewritten note is a sync conflict waiting to happen. Add new keys at the end of the frontmatter block.
**If the note does not exist:** create it with just those keys and the date as an H1.
```markdown
---
temp_high: 72.3
temp_low: 54.1
rainfall: 0.02
gdd_base50: 11.4
solar_energy_kwh_m2: 6.83
---
# 2026-08-18
```
A dry day with no frost is the common shape, and has neither `rainfall` nor the freezing pair:
```markdown
---
temp_high: 78.1
temp_low: 55.6
gdd_base50: 14.4
solar_energy_kwh_m2: 7.02
---
# 2026-08-19
```
**Any of those keys you are not writing, delete from the note if it is already there** — `rainfall` when the day was dry, `hours_below_freezing` and `freezing_degree_hours` when it never froze. Absence is the record, so a value left behind from an earlier run reports rain or a frost that did not happen, in exactly the format a real one would take. This is the one case where this skill removes rather than only adds, and it reaches only those three keys — never anything else in the frontmatter.
Re-running a date is otherwise safe: the same keys get the same values again.
## Reading it back
To answer "what was the weather on X" for a day already recorded, read the note's frontmatter — that is faster than a station call and it is the recorded value. Call `weather-day` when the note has no weather keys yet, or when the user wants the station's answer rather than the note's.
**A missing `rainfall` on a note that carries the other weather keys means zero, not unknown.** The same goes for the freezing pair. So when totalling rain over a month or frost hours over a winter, a note with `temp_high` but no `rainfall` is a recorded dry day: count it as `0` and as a day you have data for. Reading it as a gap gets the total right by luck and everything around it wrong — it reports most of a dry July as unmeasured, and sends you re-running `weather-day` over days that were never missing. A note with *none* of the weather keys is the genuinely unrecorded case, and that one is worth saying out loud or filling in before answering.
`gdd_base50` is different — it is written on every recorded day, including a `0`. A daily note missing it has not been filled in, and should be reported as a hole rather than summed as zero.
## A different station
`STATION_ID` is a constant at the top of the `weather-day` script, in this skill's directory. Changing it needs an image rebuild; say so rather than trying to override it at the command line.
@@ -1,290 +0,0 @@
#!/usr/bin/env bash
# High/low temperature, total rainfall, growing degree days, solar energy, and
# freezing exposure for one local day, read from a public Tempest (WeatherFlow)
# station. Prints one JSON object on stdout.
# Usage: weather-day [YYYY-MM-DD] (default: yesterday)
#
# The station is public, so the key below is WeatherFlow's own web-app key rather
# than a secret — it is what tempestwx.com sends, works for any public station, and
# is documented as the default in the weather-stats repo's .env.example. Nothing
# here belongs in .env: swap the two constants and rebuild to point at a different
# station.
#
# Reaches swd.weatherflow.com, which must be on the `acl allowed` line in
# config/squid.conf or every call here fails with a 403 from the proxy. That
# coupling cannot be asserted at build time — squid.conf is mounted into the squid
# sidecar and never exists inside this image.
set -euo pipefail
STATION_ID=173994
API_KEY=6bff2f89-84ab-463c-886e-fc0f443da4cf
API_BASE=https://swd.weatherflow.com/swd/rest
BUILD=169
# Everything below the high/low is an integral, and an integral fails quietly: a
# wrong high looks wrong, a wrong degree-day total looks like a number. So this
# one constant guards both ways a day's samples can misrepresent the day.
#
# As the *fetch pad*, it is how far either side of midnight the observations query
# reaches, so the samples bracketing each boundary exist and the day can be
# interpolated to its true edges instead of truncated to the first and last
# reading inside it.
#
# As the *gap limit*, it is the widest hole between consecutive samples the
# integrals will cross. This endpoint answers a day-long range on a 5-minute grid
# — 2026-08-18 came back as 287 observations spanning 23.8 h, which is 300.0 s
# apart — so an hour-long hole is a dozen consecutive reports missing, an outage
# rather than a few dropped ones. A straight line drawn across one, through a
# summer afternoon especially, moves the day's totals a long way while looking
# entirely ordinary in the note. Better to refuse the day.
#
# Measured rather than assumed, and worth re-measuring before leaning on it: the
# station itself reports far more often than this, so the 5 minutes is the API's
# resolution for a range this wide, not the hardware's.
#
# One number for both on purpose: a neighbouring observation further out than the
# pad is one the gap limit would refuse to interpolate across anyway, so widening
# either alone buys nothing.
MAX_GAP_SECONDS=3600
# Both headers are load-bearing, not politeness, and the key is why: it was lifted
# from the tempestwx.com JavaScript bundle, and WeatherFlow gates it on the request
# resembling that web app. Sending neither returns 401, observed — these are the
# two values the working weather-stats client sends on every call
# (src/lib/server/collectors/tempest-api.ts), and `build` is part of the same act.
# Drop any of the three and expect the 401 back.
ORIGIN=https://tempestwx.com
USER_AGENT='Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0'
if [ $# -gt 1 ]; then
echo 'usage: weather-day [YYYY-MM-DD] (default: yesterday)' >&2
exit 2
fi
day=${1:-$(date -d yesterday +%F)}
# Rejected here rather than passed through: `date -d` accepts a great deal that is
# not a calendar date ("now", "next friday", "1 day ago"), and any of them would
# produce a plausible-looking result filed under a nonsense `date` key.
if ! [[ $day =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "weather-day: '$day' is not a date in YYYY-MM-DD form" >&2
exit 2
fi
if ! date -d "$day" >/dev/null 2>&1; then
echo "weather-day: '$day' is not a real calendar date" >&2
exit 2
fi
# The container's TZ decides where the day starts, which is what makes this the
# user's day rather than UTC's. `+ 1 day` is calendar arithmetic rather than
# +86400, so a DST transition gives a 23- or 25-hour day instead of one shifted by
# an hour — verified at both 2026 transitions.
#
# The bare date, with no `00:00:00`, is load-bearing. GNU date parses a `+N` that
# follows a *time* as a numeric timezone, so `"$day 00:00:00 + 1 day"` yields
# 16:00 the same day and silently loses a third of it.
start=$(date -d "$day" +%s)
end=$(date -d "$day + 1 day" +%s)
# Shared by both calls: check the HTTP status before handing the body to jq, so a
# proxy denial or an API outage reports itself instead of surfacing as "no
# observations" for a day the station was up.
tempest_get() {
local endpoint=$1 resp code json
resp=$(curl -sS -w $'\n%{http_code}' -G "${API_BASE}/${endpoint}" \
-A "${USER_AGENT}" \
-H "Origin: ${ORIGIN}" \
--data-urlencode "api_key=${API_KEY}" \
--data-urlencode "build=${BUILD}" \
"${@:2}")
code=${resp##*$'\n'}
json=${resp%$'\n'*}
if [ "$code" != "200" ]; then
echo "weather-day: ${endpoint} failed (HTTP $code)" >&2
echo " $json" >&2
exit 1
fi
printf '%s' "$json"
}
# A station has one ST (Tempest) device and usually an HB (hub) alongside it; only
# the ST reports weather.
locations=$(tempest_get "locations/${STATION_ID}" --data-urlencode 'include_arbitrary_locations=true')
device_id=$(printf '%s' "$locations" | jq -r '
if .status.status_code != 0 then
"ERR:\(.status.status_message)"
else
(.locations[0].devices[]? | select(.device_type == "ST") | .device_id) // "ERR:no ST device"
end' | head -1)
case "$device_id" in
ERR:*)
echo "weather-day: station ${STATION_ID}: ${device_id#ERR:}" >&2
exit 1
;;
''|*[!0-9]*)
echo "weather-day: station ${STATION_ID}: could not resolve a Tempest device id" >&2
exit 1
;;
esac
# Deliberately wider than the day on both sides — MAX_GAP_SECONDS of padding — so
# the observations either side of each midnight come back and the boundaries can
# be interpolated. Without them the first integral segment would start at whenever
# the station happened to report after 00:00 and the day would be quietly short.
#
# The padding is *only* for interpolation. The jq filter below re-asserts the
# half-open window itself, so temp_high, temp_low, rainfall, observations and
# coverage_hours still describe the requested day and nothing else — which also
# makes time_end's inclusivity on the API side stop mattering here.
observations=$(tempest_get "observations/device/${device_id}" \
--data-urlencode "time_start=$((start - MAX_GAP_SECONDS))" \
--data-urlencode "time_end=$((end + MAX_GAP_SECONDS))")
# Positional obs_st layout, per the OBS_IDX table in weather-stats
# (src/lib/server/collectors/tempest-api.ts): [0] epoch, [7] air temperature in C,
# [11] solar radiation in W/m², [12] precipitation in mm accumulated over the
# report interval.
#
# Rainfall is the sum of [12] rather than the station's own
# precip_accum_local_day, which resets at the station's midnight — summing keeps
# the total tied to the window actually requested.
#
# The four derived numbers are integrals over the day rather than statistics over
# the samples, and the difference is not academic. The spacing between reports is
# nothing this code should assume — it is whatever the API returns for the range
# asked for, and reports go missing — so a per-sample average silently weights a
# stretch the station was chatty the same as a stretch it was quiet; and clipping
# each *sample* at a threshold charges a whole interval to whichever side its
# endpoints landed on. Both errors are invisible in the output and both compound
# when a season of these gets summed. So: piecewise-linear between real
# timestamps, thresholds crossed at the exact instant the line crosses them.
printf '%s' "$observations" | jq \
--arg date "$day" \
--argjson station "$STATION_ID" \
--argjson start "$start" \
--argjson end "$end" \
--argjson maxgap "$MAX_GAP_SECONDS" '
def mag: if . < 0 then - . else . end;
def fahrenheit: . * 9 / 5 + 32;
def r1: . * 10 | round / 10;
def r2: . * 100 | round / 100;
# Every def below takes a series: [epoch, value] pairs, ascending, one per
# observation that actually carried the field.
# The value of the segment [$a,$b] at time $t.
def at($a; $b; $t): $a[1] + ($b[1] - $a[1]) * ($t - $a[0]) / ($b[0] - $a[0]);
# $p clipped to the day, with the two boundary values interpolated from the
# observations just outside it — which is what the padded fetch above is for.
# A neighbour further out than $maxgap is not used: that is a hole, not a
# boundary, so the series just starts (or ends) at the nearest real sample and
# coverage_hours is left to report the short day.
def day_series($p):
[$p[] | select(.[0] >= $start and .[0] < $end)] as $in
| if ($in | length) == 0 then []
else
([$p[] | select(.[0] < $start)] | last) as $pre
| ([$p[] | select(.[0] >= $end)] | first) as $post
| (if $pre != null and $in[0][0] > $start and ($in[0][0] - $pre[0]) <= $maxgap
then [[$start, at($pre; $in[0]; $start)]] else [] end)
+ $in
+ (if $post != null and $in[-1][0] < $end and ($post[0] - $in[-1][0]) <= $maxgap
then [[$end, at($in[-1]; $post; $end)]] else [] end)
end;
# ∫max(v,0)·dt in value-hours. A segment whose endpoints straddle zero
# contributes only the triangle on the positive side of the crossing, and
# solving for that crossing is the entire reason this is not a trapezoid sum:
# a trapezoid of the clipped endpoints charges the whole interval to the
# positive side, which is how an hour at 31°F becomes an hour of thaw.
def positive_area($s):
reduce range(1; $s | length) as $i (0;
$s[$i - 1][1] as $a
| $s[$i][1] as $b
| (($s[$i][0] - $s[$i - 1][0]) / 3600) as $h
| . + (if $a >= 0 and $b >= 0 then ($a + $b) / 2 * $h
elif $a <= 0 and $b <= 0 then 0
else (([$a, 0] | max) as $pa
| ([$b, 0] | max) as $pb
| ($pa * $pa + $pb * $pb) / (2 * (($a - $b) | mag)) * $h)
end));
# Hours for which the interpolated value is strictly positive, same crossing.
def positive_hours($s):
reduce range(1; $s | length) as $i (0;
$s[$i - 1][1] as $a
| $s[$i][1] as $b
| (($s[$i][0] - $s[$i - 1][0]) / 3600) as $h
| . + (if $a > 0 and $b > 0 then $h
elif $a <= 0 and $b <= 0 then 0
else $h * (([$a, 0] | max) + ([$b, 0] | max)) / (($a - $b) | mag)
end));
# The widest interval between consecutive samples, as [from, to, seconds].
def widest($s):
[range(1; $s | length) | [$s[. - 1][0], $s[.][0], ($s[.][0] - $s[. - 1][0])]]
| max_by(.[2]);
# Refuse rather than draw a straight line across an outage. See MAX_GAP_SECONDS.
def no_gaps($s; $what):
widest($s) as $g
| if $g != null and $g[2] > $maxgap then
"weather-day: \($what) for \($date) stops for \($g[2] / 3600 | r1) h (\($g[0] | localtime | strftime("%H:%M")) to \($g[1] | localtime | strftime("%H:%M"))) — a day total interpolated across a hole that size would look entirely normal and be wrong\n" | halt_error(1)
else . end;
if .status.status_code != 0 then
"weather-day: device observations: \(.status.status_message)\n" | halt_error(1)
else . end
# Sorted and one-per-timestamp because the integrals below walk consecutive
# pairs; the API answers in order, but nothing here should depend on that.
| ([.obs[]? | select(type == "array" and (.[0] | type) == "number")] | unique_by(.[0])) as $all
| [$all[] | select(.[0] >= $start and .[0] < $end)] as $o
| if ($o | length) == 0 then
"weather-day: no observations for \($date) — station offline, or the date is outside its history\n" | halt_error(1)
else . end
| [$o[] | .[7] | numbers] as $temps
| [$o[] | .[12] | numbers] as $precip
| (if ($temps | length) == 0 then
"weather-day: observations for \($date) carry no temperature readings\n" | halt_error(1)
else . end)
| day_series([$all[] | select((.[7] | type) == "number") | [.[0], (.[7] | fahrenheit)]]) as $tempF
| day_series([$all[] | select((.[11] | type) == "number") | [.[0], .[11]]]) as $solar
# A dead pyranometer integrates to a perfectly plausible 0.0 that would be
# written into a note and summed forever, so it is an error, not a zero. Zero
# readings are different, and stay zero.
| (if ($solar | length) == 0 then
"weather-day: observations for \($date) carry no solar radiation readings\n" | halt_error(1)
else . end)
| no_gaps($tempF; "temperature")
| no_gaps($solar; "solar radiation")
# Omitted, not zeroed, on a day that never froze: absent says "no exposure",
# whereas 0.0 is also what a broken calculation says. Tested on the series
# minimum rather than the rounded temp_low, so a midnight boundary that
# interpolates just under 32°F counts — it is inside the day.
| (if ([$tempF[] | .[1]] | min) < 32 then
([$tempF[] | [.[0], (32 - .[1])]] as $freezing
| { hours_below_freezing: (positive_hours($freezing) | r1),
freezing_degree_hours: (positive_area($freezing) | r1) })
else {} end) as $cold
| {
date: $date,
station_id: $station,
temp_high: (($temps | max) | fahrenheit | r1),
temp_low: (($temps | min) | fahrenheit | r1),
rainfall: (($precip | add // 0) / 25.4 | r2),
# Degree-days, so the °F-hours above the base divided by 24 — one day at a
# steady 60°F is 10, not 240. A daily figure, meant to be summed.
gdd_base50: (positive_area([$tempF[] | [.[0], (.[1] - 50)]]) / 24 | r1),
# W/m² integrated over hours is Wh/m². positive_area rather than a plain
# trapezoid because irradiance cannot be negative, and a pyranometer reading
# a little below zero on a clear night should contribute nothing, not debt.
solar_energy_kwh_m2: (positive_area($solar) / 1000 | r2)
}
+ $cold
+ {
observations: ($o | length),
coverage_hours: ((($o | last | .[0]) - ($o | first | .[0])) / 3600 | r1)
}'
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/engineering/wizard
-1
View File
@@ -1 +0,0 @@
../../../mattpocock-skills/skills/productivity/writing-for-agents
+4 -11
View File
@@ -64,11 +64,7 @@ fi
alias tmux="tmux -2" alias tmux="tmux -2"
alias grep="grep --color=auto" alias grep="grep --color=auto"
if command -v brew >/dev/null 2>&1; then eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
eval "$(brew shellenv)"
elif [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
# Fix gpg signing # Fix gpg signing
# https://github.com/keybase/keybase-issues/issues/2798 # https://github.com/keybase/keybase-issues/issues/2798
@@ -82,20 +78,17 @@ export TERM="xterm-256color"
export EDITOR=$(which nvim) export EDITOR=$(which nvim)
# Adding applications to path # Adding applications to path
if [[ -d ${HOME}/.local/bin ]] && [[ ":${PATH}:" != *":${HOME}/.local/bin:"* ]]; then if [[ -d ${HOME}/.dotfiles/bin/linux ]]; then
export PATH=${HOME}/.local/bin:${PATH} export PATH=$PATH:${HOME}/.dotfiles/bin/linux
fi fi
if [[ -d ${HOME}/bin ]]; then if [[ -d ${HOME}/bin ]]; then
export PATH=$PATH:${HOME}/bin export PATH=$PATH:${HOME}/bin
fi fi
eval "$(mise activate bash)"
# Enable atuin # Enable atuin
# https://docs.atuin.sh # https://docs.atuin.sh
if command -v atuin >/dev/null 2>&1; then eval "$(atuin init bash)"
eval "$(atuin init bash)"
fi
# fzf # fzf
#eval "$(fzf --bash)" #eval "$(fzf --bash)"
-27
View File
@@ -1,27 +0,0 @@
#!/bin/bash
# Source: https://github.com/mattpocock/skills
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
DANGEROUS_PATTERNS=(
"git push"
"git reset --hard"
"git clean -fd"
"git clean -f"
"git branch -D"
"git checkout \."
"git restore \."
"push --force"
"reset --hard"
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$pattern"; then
echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2
exit 2
fi
done
exit 0
-98
View File
@@ -1,98 +0,0 @@
#!/bin/bash
# Blocks Bash commands that delete or destroy files.
# Companion to CLAUDE.md's "never delete files" rule.
block() {
printf 'BLOCKED: %s\n' "$1" >&2
exit 2
}
command -v jq >/dev/null 2>&1 || block "jq is required to validate Bash commands."
command -v realpath >/dev/null 2>&1 || block "realpath is required to validate deletion paths."
command -v stat >/dev/null 2>&1 || block "stat is required to validate the temporary directory."
INPUT=$(cat) || block "could not read hook input."
COMMAND=$(printf '%s' "$INPUT" | jq -er '.tool_input.command | select(type == "string")') ||
block "hook input does not contain a valid tool_input.command string."
# Do not trust TMPDIR: when it is unset, `$TMPDIR/path` expands to `/path`.
# This fixed per-user directory is the only place where deletion is permitted.
TMP_ROOT="/tmp/claude-$(id -u)"
# True only for one command in one of these exact forms:
# rm -- /tmp/claude-UID/path [...]
# rm -f|-r|-rf|-fr -- /tmp/claude-UID/path [...]
#
# Paths are deliberately restricted to an unquoted, expansion-free character
# set. The filesystem checks reject an escaping canonical path and any symlink
# that already exists in the path. This is defense against accidental deletion,
# not a race-free security boundary against a process changing paths concurrently.
confined_to_tmp() {
local cmd="$1" option_part path_part path canonical component partial mode uid
local -a paths components
uid=$(id -u) || return 1
[[ -d "$TMP_ROOT" && ! -L "$TMP_ROOT" ]] || return 1
[[ "$(realpath -e -- "$TMP_ROOT" 2>/dev/null)" == "$TMP_ROOT" ]] || return 1
[[ "$(stat -c '%u' -- "$TMP_ROOT" 2>/dev/null)" == "$uid" ]] || return 1
mode=$(stat -c '%a' -- "$TMP_ROOT" 2>/dev/null) || return 1
(( (8#$mode & 0022) == 0 )) || return 1
if [[ "$cmd" =~ ^rm\ (--|-f\ --|-r\ --|-rf\ --|-fr\ --)\ (/tmp/claude-[0-9]+/[A-Za-z0-9._/+,=:@%-]+(\ /tmp/claude-[0-9]+/[A-Za-z0-9._/+,=:@%-]+)*)$ ]]; then
option_part="${BASH_REMATCH[1]}"
path_part="${BASH_REMATCH[2]}"
else
return 1
fi
# The regex excludes shell syntax and whitespace within paths, so this split
# does not attempt to interpret arbitrary Bash source.
read -r -a paths <<< "$path_part"
((${#paths[@]} > 0)) || return 1
for path in "${paths[@]}"; do
[[ "$path" == "$TMP_ROOT"/?* ]] || return 1
[[ "$path" != */./* && "$path" != */../* && "$path" != */. && "$path" != */.. ]] || return 1
canonical=$(realpath -m -- "$path" 2>/dev/null) || return 1
[[ "$canonical" == "$TMP_ROOT"/?* ]] || return 1
# realpath catches symlink escapes. Reject in-tree symlinks too, since rm
# must not traverse a link whose target happens to currently be in the tree.
partial=""
IFS='/' read -r -a components <<< "$path"
for component in "${components[@]}"; do
[[ -n "$component" ]] || continue
partial="$partial/$component"
[[ ! -L "$partial" ]] || return 1
done
done
# Keep the variable used so shellcheck documents that only allowlisted option
# spellings can reach this point.
[[ -n "$option_part" ]]
}
confined_to_tmp "$COMMAND" && exit 0
# These patterns are a backstop for ordinary Bash spellings, not a parser.
# Include quotes and backslashes between letters to catch forms such as r""m
# and r\m that Bash resolves to rm.
DANGEROUS_PATTERNS=(
'\br["'"'"'\\]*m\b'
'\bu["'"'"'\\]*n["'"'"'\\]*l["'"'"'\\]*i["'"'"'\\]*n["'"'"'\\]*k\b'
'\br["'"'"'\\]*m["'"'"'\\]*d["'"'"'\\]*i["'"'"'\\]*r\b'
'\bshred\b'
'\btruncate\b'
'\bfind\b.*-delete\b'
'\btrash\b'
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if printf '%s\n' "$COMMAND" | grep -qE "$pattern"; then
block "'$COMMAND' is a deletion command. Only literal 'rm [-f|-r|-rf|-fr] -- $TMP_ROOT/path' commands without variables, quotes, globs, shell operators, or symlinks are allowed."
fi
done
exit 0
-163
View File
@@ -1,163 +0,0 @@
{
"attribution": {
"commit": "",
"pr": ""
},
"permissions": {
"allow": [
"Bash(npx tsc:*)",
"Bash(pnpm run build:*)",
"Bash(pnpm tsc:*)",
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(pnpm install:*)",
"Bash(git diff:*)"
],
"deny": [
"Bash(git push *)",
"Bash(ssh *)",
"Bash(ssh)",
"Bash(scp *)",
"Bash(shred:*)",
"Bash(truncate:*)",
"Bash(trash:*)",
"CronCreate",
"CronDelete",
"CronList",
"PushNotification",
"NotebookEdit",
"DesignSync"
],
"defaultMode": "auto"
},
"model": "claude-opus-4-8",
"disableClaudeAiConnectors": true,
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "atuin hook claude-code"
}
]
}
],
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "atuin hook claude-code"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/block-dangerous-git.sh"
},
{
"type": "command",
"command": "~/.claude/hooks/block-file-deletion.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "atuin hook claude-code"
}
]
}
]
},
"disableWorkflows": true,
"disableArtifact": true,
"statusLine": {
"type": "command",
"command": "bash /home/tgrosinger/.claude/statusline-command.sh"
},
"enabledPlugins": {
"claude-md-management@claude-plugins-official": true,
"code-simplifier@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true,
"gopls-lsp@claude-plugins-official": true,
"pr-review-toolkit@claude-plugins-official": true,
"security-guidance@claude-plugins-official": true,
"skill-creator@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": [
"github.com",
"*.github.com",
"*.githubusercontent.com",
"registry.npmjs.org",
"proxy.golang.org",
"sum.golang.org",
"cache.nixos.org",
"nodejs.org",
"go.dev",
"dl.google.com",
"mise.en.dev"
]
},
"filesystem": {
"allowWrite": [
"~/.local/share/pnpm",
"~/.cache/pnpm",
"~/Documents/Atrium",
"/tachi/docker/atribot/vault"
],
"denyRead": [
"~/.ssh",
"~/.config/Signal",
"~/Documents",
"/tachi/backups",
"/tachi/documents",
"/tachi/docker"
],
"allowRead": [
"~/Documents/Atrium",
"/tachi/docker/atribot/vault"
]
},
"excludedCommands": [
"git push *",
"brew *"
]
},
"spinnerVerbs": {
"mode": "replace",
"verbs": [
"Thinking"
]
},
"effortLevel": "xhigh",
"tui": "fullscreen",
"voice": {
"enabled": false,
"mode": "hold"
},
"prefersReducedMotion": true,
"autoMemoryEnabled": true,
"autoDreamEnabled": true,
"theme": "light",
"editorMode": "vim",
"remoteControlAtStartup": false,
"agentPushNotifEnabled": true,
"skipAutoPermissionPrompt": true
}
-1
View File
@@ -1 +0,0 @@
../.agents/skills
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env bash
input=$(cat)
# Context-window fill from the latest message's usage in the transcript.
transcript=$(echo "$input" | jq -r '.transcript_path // ""')
model_id=$(echo "$input" | jq -r '.model.id // ""')
case "$model_id" in
*1m*|*"[1m]"*) limit=1000000 ;;
*) limit=200000 ;;
esac
used=""
if [ -n "$transcript" ] && [ -f "$transcript" ]; then
used=$(jq -rs '[.[] | select(.message.usage != null) | .message.usage
| (.input_tokens // 0) + (.cache_read_input_tokens // 0)
+ (.cache_creation_input_tokens // 0) + (.output_tokens // 0)]
| last // ""' "$transcript" 2>/dev/null)
fi
# No usage recorded yet (e.g. fresh session before the first turn).
if [ -z "$used" ]; then
exit 0
fi
pct=$(awk -v u="$used" -v l="$limit" 'BEGIN{ printf "%d", (l>0? u*100/l : 0) }')
used_h=$(awk -v n="$used" 'BEGIN{ if(n>=1000000) printf "%.1fM", n/1000000; else printf "%.1fk", n/1000 }')
limit_h=$(awk -v n="$limit" 'BEGIN{ if(n>=1000000) printf "%gM", n/1000000; else printf "%gk", n/1000 }')
if [ "$used" -ge 200000 ]; then ctx_color='1;31'
elif [ "$used" -ge 100000 ]; then ctx_color='1;33'
else ctx_color='1;90'; fi
printf '\033[%sm%s/%s (%d%%)\033[0m' "$ctx_color" "$used_h" "$limit_h" "$pct"
+1 -2
View File
@@ -282,6 +282,5 @@
"editor.inlayHints.fontFamily": "Sriracha", "editor.inlayHints.fontFamily": "Sriracha",
"github.copilot.nextEditSuggestions.enabled": true, "github.copilot.nextEditSuggestions.enabled": true,
"gitlens.ai.model": "vscode", "gitlens.ai.model": "vscode",
"gitlens.ai.vscode.model": "copilot:gpt-4.1", "gitlens.ai.vscode.model": "copilot:gpt-4.1"
"workbench.startupEditor": "none"
} }
-5
View File
@@ -8,8 +8,3 @@ normal = { family = "JetBrainsMonoNerdFontMono", style = "Regular" }
bold = { family = "JetBrainsMonoNerdFontMono", style = "Bold" } bold = { family = "JetBrainsMonoNerdFontMono", style = "Bold" }
italic = { family = "JetBrainsMonoNerdFontMono", style = "Italic" } italic = { family = "JetBrainsMonoNerdFontMono", style = "Italic" }
bold_italic = { family = "JetBrainsMonoNerdFontMono", style = "BoldItalic" } bold_italic = { family = "JetBrainsMonoNerdFontMono", style = "BoldItalic" }
[[keyboard.bindings]]
key = "Return"
mods = "Shift"
chars = "\u001b\r"
+1 -8
View File
@@ -4,8 +4,6 @@ workspaces = true
keymap_mode = "auto" keymap_mode = "auto"
enter_accept = true enter_accept = true
sync_address = "https://atuin.i.grosinger.net"
history_filter = [ history_filter = [
"^cd$", "^cd$",
"^lg$", "^lg$",
@@ -20,9 +18,4 @@ history_filter = [
## The "workspace" mode is skipped when not in a workspace or workspaces = false. ## The "workspace" mode is skipped when not in a workspace or workspaces = false.
## Default filter mode can be overridden with the filter_mode setting. ## Default filter mode can be overridden with the filter_mode setting.
#filters = [ "global", "host", "session", "session-preload", "workspace", "directory" ] #filters = [ "global", "host", "session", "session-preload", "workspace", "directory" ]
filters = [ "global", "host", "workspace", "directory" ] filters = [ "global", "workspace", "directory" ]
[tmux]
enabled = true
width = "80%"
height = "60%"
-286
View File
@@ -1,286 +0,0 @@
#? Config file for btop v.1.4.7
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
color_theme = "/home/tgrosinger/.config/btop/themes/catppuccin_latte.theme"
#* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = true
#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
truecolor = true
#* Set to true to force tty mode regardless if a real tty has been detected or not.
#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
force_tty = false
#* Option to disable presets. Either the default preset, custom presets, or all presets.
#* "Off" All presets are enabled.
#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled.
disable_presets = "Off"
#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
#* Use whitespace " " as separator between different presets.
#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
vim_keys = false
#* Disable all mouse events.
disable_mouse = false
#* Rounded corners on boxes, is ignored if TTY mode is ON.
rounded_corners = true
#* Use terminal synchronized output sequences to reduce flickering on supported terminals.
terminal_sync = true
#* Default symbols to use for graph creation, "braille", "block" or "tty".
#* "braille" offers the highest resolution but might not be included in all fonts.
#* "block" has half the resolution of braille but uses more common characters.
#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
graph_symbol = "braille"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_cpu = "default"
# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
graph_symbol_gpu = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_mem = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_net = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_proc = "default"
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
shown_boxes = "cpu mem net proc"
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
update_ms = 2000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "cpu lazy"
#* Reverse sorting order, True or False.
proc_reversed = false
#* Show processes as a tree.
proc_tree = true
#* Use the cpu graph colors in the process list.
proc_colors = true
#* Use a darkening gradient in the process list.
proc_gradient = true
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
proc_per_core = true
#* Show process memory as bytes instead of percent.
proc_mem_bytes = true
#* Show cpu graph for each process.
proc_cpu_graphs = true
#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
proc_info_smaps = false
#* Show proc box on left side of screen instead of right.
proc_left = false
#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
proc_filter_kernel = false
#* Should the process list follow the selected process when detailed view is open.
proc_follow_detailed = true
#* In tree-view, always accumulate child process resources in the parent process.
proc_aggregate = false
#* Should cpu and memory usage display be preserved for dead processes when paused.
keep_dead_proc_usage = false
#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_upper = "Auto"
#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_lower = "Auto"
#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
show_gpu_info = "Auto"
#* Toggles if the lower CPU graph should be inverted.
cpu_invert_lower = true
#* Set to True to completely disable the lower CPU graph.
cpu_single_graph = false
#* Show cpu box at bottom of screen instead of top.
cpu_bottom = false
#* Shows the system uptime in the CPU box.
show_uptime = true
#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
show_cpu_watts = true
#* Show cpu temperature.
check_temp = true
#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
cpu_sensor = "Auto"
#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
show_coretemp = true
#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
#* Example: "4:0 5:1 6:3"
cpu_core_map = ""
#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
temp_scale = "celsius"
#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
base_10_sizes = false
#* Show CPU frequency.
show_cpu_freq = true
#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
freq_mode = "first"
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
clock_format = "%X"
#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
background_update = true
#* Custom cpu model name, empty string to disable.
custom_cpu_name = ""
#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
disks_filter = ""
#* Show graphs instead of meters for memory values.
mem_graphs = true
#* Show mem box below net box instead of above.
mem_below_net = false
#* Count ZFS ARC in cached and available memory.
zfs_arc_cached = true
#* If swap memory should be shown in memory box.
show_swap = true
#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
swap_disk = true
#* If mem box should be split to also show disks info.
show_disks = true
#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
only_physical = true
#* Read disks list from /etc/fstab. This also disables only_physical.
use_fstab = true
#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
zfs_hide_datasets = false
#* Set to true to show available disk space for privileged users.
disk_free_priv = false
#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
show_io_stat = true
#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
io_mode = false
#* Set to True to show combined read/write io graphs in io mode.
io_graph_combined = false
#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
#* Example: "/mnt/media:100 /:20 /boot:1".
io_graph_speeds = ""
#* Swap the positions of the upload and download speed graphs. When true, upload will be on top.
swap_upload_download = false
#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
net_download = 100
net_upload = 100
#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
net_auto = true
#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
net_sync = true
#* Starts with the Network Interface specified here.
net_iface = ""
#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
base_10_bitrate = "Auto"
#* Show battery stats in top right if battery is present.
show_battery = true
#* Which battery to use if multiple are present. "Auto" for auto detection.
selected_battery = "Auto"
#* Show power stats of battery next to charge indicator.
show_battery_watts = true
#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
log_level = "WARNING"
#* Automatically save current settings to config file on exit.
save_config_on_exit = true
#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
nvml_measure_pcie_speeds = true
#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
rsmi_measure_pcie_speeds = true
#* Horizontally mirror the GPU graph.
gpu_mirror_graph = true
#* Set which GPU vendors to show. Available values are "nvidia amd intel apple"
shown_gpus = "nvidia amd intel"
#* Custom gpu0 model name, empty string to disable.
custom_gpu_name0 = ""
#* Custom gpu1 model name, empty string to disable.
custom_gpu_name1 = ""
#* Custom gpu2 model name, empty string to disable.
custom_gpu_name2 = ""
#* Custom gpu3 model name, empty string to disable.
custom_gpu_name3 = ""
#* Custom gpu4 model name, empty string to disable.
custom_gpu_name4 = ""
#* Custom gpu5 model name, empty string to disable.
custom_gpu_name5 = ""
@@ -1,83 +0,0 @@
# Main background, empty for terminal default, need to be empty if you want transparent background
theme[main_bg]="#eff1f5"
# Main text color
theme[main_fg]="#4c4f69"
# Title color for boxes
theme[title]="#4c4f69"
# Highlight color for keyboard shortcuts
theme[hi_fg]="#1e66f5"
# Background color of selected item in processes box
theme[selected_bg]="#bcc0cc"
# Foreground color of selected item in processes box
theme[selected_fg]="#1e66f5"
# Color of inactive/disabled text
theme[inactive_fg]="#8c8fa1"
# Color of text appearing on top of graphs, i.e uptime and current network graph scaling
theme[graph_text]="#dc8a78"
# Background color of the percentage meters
theme[meter_bg]="#bcc0cc"
# Misc colors for processes box including mini cpu graphs, details memory graph and details status text
theme[proc_misc]="#dc8a78"
# CPU, Memory, Network, Proc box outline colors
theme[cpu_box]="#8839ef" #Mauve
theme[mem_box]="#40a02b" #Green
theme[net_box]="#e64553" #Maroon
theme[proc_box]="#1e66f5" #Blue
# Box divider line and small boxes line color
theme[div_line]="#9ca0b0"
# Temperature graph color (Green -> Yellow -> Red)
theme[temp_start]="#40a02b"
theme[temp_mid]="#df8e1d"
theme[temp_end]="#d20f39"
# CPU graph colors (Teal -> Lavender)
theme[cpu_start]="#179299"
theme[cpu_mid]="#209fb5"
theme[cpu_end]="#7287fd"
# Mem/Disk free meter (Mauve -> Lavender -> Blue)
theme[free_start]="#8839ef"
theme[free_mid]="#7287fd"
theme[free_end]="#1e66f5"
# Mem/Disk cached meter (Sapphire -> Lavender)
theme[cached_start]="#209fb5"
theme[cached_mid]="#1e66f5"
theme[cached_end]="#7287fd"
# Mem/Disk available meter (Peach -> Red)
theme[available_start]="#fe640b"
theme[available_mid]="#e64553"
theme[available_end]="#d20f39"
# Mem/Disk used meter (Green -> Sky)
theme[used_start]="#40a02b"
theme[used_mid]="#179299"
theme[used_end]="#04a5e5"
# Download graph colors (Peach -> Red)
theme[download_start]="#fe640b"
theme[download_mid]="#e64553"
theme[download_end]="#d20f39"
# Upload graph colors (Green -> Sky)
theme[upload_start]="#40a02b"
theme[upload_mid]="#179299"
theme[upload_end]="#04a5e5"
# Process box color gradient for threads, mem and cpu usage (Sapphire -> Mauve)
theme[process_start]="#209fb5"
theme[process_mid]="#7287fd"
theme[process_end]="#8839ef"
@@ -1,123 +0,0 @@
[delta "catppuccin-latte"]
blame-palette = "#eff1f5 #e6e9ef #dce0e8 #ccd0da #bcc0cc"
commit-decoration-style = "#9ca0b0" bold box ul
light = true
file-decoration-style = "#9ca0b0"
file-style = "#4c4f69"
hunk-header-decoration-style = "#9ca0b0" box ul
hunk-header-file-style = bold
hunk-header-line-number-style = bold "#6c6f85"
hunk-header-style = file line-number syntax
line-numbers-left-style = "#9ca0b0"
line-numbers-minus-style = bold "#d20f39"
line-numbers-plus-style = bold "#40a02b"
line-numbers-right-style = "#9ca0b0"
line-numbers-zero-style = "#9ca0b0"
# 35% red 65% base
minus-emph-style = bold syntax "#e5a2b3"
# 20% red 80% base
minus-style = syntax "#e9c4cf"
# 35% green 65% base
plus-emph-style = bold syntax "#b2d5ae"
# 20% green 80% base
plus-style = syntax "#cce1cd"
map-styles = \
bold purple => syntax "#cbb1f2", \
bold blue => syntax "#a6c1f5", \
bold cyan => syntax "#9dd7ef", \
bold yellow => syntax "#eacfa9"
# Should match the name of the bat theme
syntax-theme = Catppuccin Latte
[delta "catppuccin-frappe"]
blame-palette = "#303446 #292c3c #232634 #414559 #51576d"
commit-decoration-style = "#737994" bold box ul
dark = true
file-decoration-style = "#737994"
file-style = "#c6d0f5"
hunk-header-decoration-style = "#737994" box ul
hunk-header-file-style = bold
hunk-header-line-number-style = bold "#a5adce"
hunk-header-style = file line-number syntax
line-numbers-left-style = "#737994"
line-numbers-minus-style = bold "#e78284"
line-numbers-plus-style = bold "#a6d189"
line-numbers-right-style = "#737994"
line-numbers-zero-style = "#737994"
# 35% red 65% base
minus-emph-style = bold syntax "#704f5c"
# 20% red 80% base
minus-style = syntax "#544452"
# 35% green 65% base
plus-emph-style = bold syntax "#596b5e"
# 20% green 80% base
plus-style = syntax "#475453"
map-styles = \
bold purple => syntax "#66597e", \
bold blue => syntax "#505d81", \
bold cyan => syntax "#546b7a", \
bold yellow => syntax "#6f6860"
# Should match the name of the bat theme
syntax-theme = Catppuccin Frappe
[delta "catppuccin-macchiato"]
blame-palette = "#24273a #1e2030 #181926 #363a4f #494d64"
commit-decoration-style = "#6e738d" bold box ul
dark = true
file-decoration-style = "#6e738d"
file-style = "#cad3f5"
hunk-header-decoration-style = "#6e738d" box ul
hunk-header-file-style = bold
hunk-header-line-number-style = bold "#a5adcb"
hunk-header-style = file line-number syntax
line-numbers-left-style = "#6e738d"
line-numbers-minus-style = bold "#ed8796"
line-numbers-plus-style = bold "#a6da95"
line-numbers-right-style = "#6e738d"
line-numbers-zero-style = "#6e738d"
# 35% red 65% base
minus-emph-style = bold syntax "#6a485a"
# 20% red 80% base
minus-style = syntax "#4c3a4c"
# 35% green 65% base
plus-emph-style = bold syntax "#51655a"
# 20% green 80% base
plus-style = syntax "#3e4b4c"
map-styles = \
bold purple => syntax "#5c517c", \
bold blue => syntax "#47557b", \
bold cyan => syntax "#4a6475", \
bold yellow => syntax "#6a635d"
# Should match the name of the bat theme
syntax-theme = Catppuccin Macchiato
[delta "catppuccin-mocha"]
blame-palette = "#1e1e2e #181825 #11111b #313244 #45475a"
commit-decoration-style = "#6c7086" bold box ul
dark = true
file-decoration-style = "#6c7086"
file-style = "#cdd6f4"
hunk-header-decoration-style = "#6c7086" box ul
hunk-header-file-style = bold
hunk-header-line-number-style = bold "#a6adc8"
hunk-header-style = file line-number syntax
line-numbers-left-style = "#6c7086"
line-numbers-minus-style = bold "#f38ba8"
line-numbers-plus-style = bold "#a6e3a1"
line-numbers-right-style = "#6c7086"
line-numbers-zero-style = "#6c7086"
# 35% red 65% base
minus-emph-style = bold syntax "#694559"
# 20% red 80% base
minus-style = syntax "#493447"
# 35% green 65% base
plus-emph-style = bold syntax "#4e6356"
# 20% green 80% base
plus-style = syntax "#394545"
map-styles = \
bold purple => syntax "#5b4e74", \
bold blue => syntax "#445375", \
bold cyan => syntax "#446170", \
bold yellow => syntax "#6b635b"
# Should match the name of the bat theme
syntax-theme = Catppuccin Mocha
@@ -1 +0,0 @@
complete -c dev-switch -f -a '(git worktree list --porcelain 2>/dev/null | string match "worktree *" | string replace "worktree " "" | while read -l p; basename $p; end)'
@@ -1,16 +0,0 @@
function __dev_wt_pr_numbers
# Open PRs in the current repo: number<TAB>title. gh resolves the repo from
# the remote, so this only yields anything inside a GitHub checkout.
gh pr list --json number,title --jq '.[] | "\(.number)\t\(.title)"' 2>/dev/null
end
function __dev_wt_pr_branch
# Second arg is the branch; default to the head branch of the PR already typed.
set -l tokens (commandline -opc)
test (count $tokens) -ge 2; or return
gh pr view $tokens[2] --json headRefName --jq .headRefName 2>/dev/null
end
complete -c dev-wt-pr -f -n __fish_is_first_arg -a '(__dev_wt_pr_numbers)'
complete -c dev-wt-pr -f -n 'test (count (commandline -opc)) -eq 2' -a '(__dev_wt_pr_branch)'
complete -c dev-wt-pr -n 'test (count (commandline -opc)) -eq 3' -a '(__fish_complete_directories)' -d 'Repo path'
-27
View File
@@ -1,27 +0,0 @@
function __dev_wt_completions
# Existing worktrees: dev-wt matches by directory basename or branch name,
# so offer both. Track branches that already have a worktree to avoid
# duplicating them in the "create new" list below.
set -l wt_branches
set -l path ""
for line in (git worktree list --porcelain 2>/dev/null)
if string match -q "worktree *" -- $line
set path (string replace "worktree " "" -- $line)
printf '%s\tworktree\n' (basename $path)
else if string match -q "branch *" -- $line
set -l branch (string replace "branch refs/heads/" "" -- $line)
set -a wt_branches $branch
printf '%s\tworktree branch\n' $branch
end
end
# Remaining local branches: selecting one creates a new worktree for it.
for branch in (git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null)
if not contains -- $branch $wt_branches
printf '%s\tbranch\n' $branch
end
end
end
complete -c dev-wt -f -n __fish_is_first_arg -a '(__dev_wt_completions)'
complete -c dev-wt -s C -l repo -r -a '(__fish_complete_directories)' -d 'Operate on the repo at this path'
@@ -1 +0,0 @@
complete -c pandoc -f -a '(string match -r ".*\\.md\$" -- (ls))'
@@ -1,235 +0,0 @@
# fish completion for process-compose -*- shell-script -*-
function __process_compose_debug
set -l file "$BASH_COMP_DEBUG_FILE"
if test -n "$file"
echo "$argv" >> $file
end
end
function __process_compose_perform_completion
__process_compose_debug "Starting __process_compose_perform_completion"
# Extract all args except the last one
set -l args (commandline -opc)
# Extract the last arg and escape it in case it is a space
set -l lastArg (string escape -- (commandline -ct))
__process_compose_debug "args: $args"
__process_compose_debug "last arg: $lastArg"
# Disable ActiveHelp which is not supported for fish shell
set -l requestComp "PROCESS_COMPOSE_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg"
__process_compose_debug "Calling $requestComp"
set -l results (eval $requestComp 2> /dev/null)
# Some programs may output extra empty lines after the directive.
# Let's ignore them or else it will break completion.
# Ref: https://github.com/spf13/cobra/issues/1279
for line in $results[-1..1]
if test (string trim -- $line) = ""
# Found an empty line, remove it
set results $results[1..-2]
else
# Found non-empty line, we have our proper output
break
end
end
set -l comps $results[1..-2]
set -l directiveLine $results[-1]
# For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
# completions must be prefixed with the flag
set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
__process_compose_debug "Comps: $comps"
__process_compose_debug "DirectiveLine: $directiveLine"
__process_compose_debug "flagPrefix: $flagPrefix"
for comp in $comps
printf "%s%s\n" "$flagPrefix" "$comp"
end
printf "%s\n" "$directiveLine"
end
# this function limits calls to __process_compose_perform_completion, by caching the result behind $__process_compose_perform_completion_once_result
function __process_compose_perform_completion_once
__process_compose_debug "Starting __process_compose_perform_completion_once"
if test -n "$__process_compose_perform_completion_once_result"
__process_compose_debug "Seems like a valid result already exists, skipping __process_compose_perform_completion"
return 0
end
set --global __process_compose_perform_completion_once_result (__process_compose_perform_completion)
if test -z "$__process_compose_perform_completion_once_result"
__process_compose_debug "No completions, probably due to a failure"
return 1
end
__process_compose_debug "Performed completions and set __process_compose_perform_completion_once_result"
return 0
end
# this function is used to clear the $__process_compose_perform_completion_once_result variable after completions are run
function __process_compose_clear_perform_completion_once_result
__process_compose_debug ""
__process_compose_debug "========= clearing previously set __process_compose_perform_completion_once_result variable =========="
set --erase __process_compose_perform_completion_once_result
__process_compose_debug "Successfully erased the variable __process_compose_perform_completion_once_result"
end
function __process_compose_requires_order_preservation
__process_compose_debug ""
__process_compose_debug "========= checking if order preservation is required =========="
__process_compose_perform_completion_once
if test -z "$__process_compose_perform_completion_once_result"
__process_compose_debug "Error determining if order preservation is required"
return 1
end
set -l directive (string sub --start 2 $__process_compose_perform_completion_once_result[-1])
__process_compose_debug "Directive is: $directive"
set -l shellCompDirectiveKeepOrder 32
set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
__process_compose_debug "Keeporder is: $keeporder"
if test $keeporder -ne 0
__process_compose_debug "This does require order preservation"
return 0
end
__process_compose_debug "This doesn't require order preservation"
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __process_compose_comp_results
# - Return false if file completion should be performed
function __process_compose_prepare_completions
__process_compose_debug ""
__process_compose_debug "========= starting completion logic =========="
# Start fresh
set --erase __process_compose_comp_results
__process_compose_perform_completion_once
__process_compose_debug "Completion results: $__process_compose_perform_completion_once_result"
if test -z "$__process_compose_perform_completion_once_result"
__process_compose_debug "No completion, probably due to a failure"
# Might as well do file completion, in case it helps
return 1
end
set -l directive (string sub --start 2 $__process_compose_perform_completion_once_result[-1])
set --global __process_compose_comp_results $__process_compose_perform_completion_once_result[1..-2]
__process_compose_debug "Completions are: $__process_compose_comp_results"
__process_compose_debug "Directive is: $directive"
set -l shellCompDirectiveError 1
set -l shellCompDirectiveNoSpace 2
set -l shellCompDirectiveNoFileComp 4
set -l shellCompDirectiveFilterFileExt 8
set -l shellCompDirectiveFilterDirs 16
if test -z "$directive"
set directive 0
end
set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
if test $compErr -eq 1
__process_compose_debug "Received error directive: aborting."
# Might as well do file completion, in case it helps
return 1
end
set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
if test $filefilter -eq 1; or test $dirfilter -eq 1
__process_compose_debug "File extension filtering or directory filtering not supported"
# Do full file completion instead
return 1
end
set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
__process_compose_debug "nospace: $nospace, nofiles: $nofiles"
# If we want to prevent a space, or if file completion is NOT disabled,
# we need to count the number of valid completions.
# To do so, we will filter on prefix as the completions we have received
# may not already be filtered so as to allow fish to match on different
# criteria than the prefix.
if test $nospace -ne 0; or test $nofiles -eq 0
set -l prefix (commandline -t | string escape --style=regex)
__process_compose_debug "prefix: $prefix"
set -l completions (string match -r -- "^$prefix.*" $__process_compose_comp_results)
set --global __process_compose_comp_results $completions
__process_compose_debug "Filtered completions are: $__process_compose_comp_results"
# Important not to quote the variable for count to work
set -l numComps (count $__process_compose_comp_results)
__process_compose_debug "numComps: $numComps"
if test $numComps -eq 1; and test $nospace -ne 0
# We must first split on \t to get rid of the descriptions to be
# able to check what the actual completion will be.
# We don't need descriptions anyway since there is only a single
# real completion which the shell will expand immediately.
set -l split (string split --max 1 \t $__process_compose_comp_results[1])
# Fish won't add a space if the completion ends with any
# of the following characters: @=/:.,
set -l lastChar (string sub -s -1 -- $split)
if not string match -r -q "[@=/:.,]" -- "$lastChar"
# In other cases, to support the "nospace" directive we trick the shell
# by outputting an extra, longer completion.
__process_compose_debug "Adding second completion to perform nospace directive"
set --global __process_compose_comp_results $split[1] $split[1].
__process_compose_debug "Completions are now: $__process_compose_comp_results"
end
end
if test $numComps -eq 0; and test $nofiles -eq 0
# To be consistent with bash and zsh, we only trigger file
# completion when there are no other completions
__process_compose_debug "Requesting file completion"
return 1
end
end
return 0
end
# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "process-compose"
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
complete --do-complete "process-compose " > /dev/null 2>&1
end
# Remove any pre-existing completions for the program since we will be handling all of them.
complete -c process-compose -e
# this will get called after the two calls below and clear the $__process_compose_perform_completion_once_result global
complete -c process-compose -n '__process_compose_clear_perform_completion_once_result'
# The call to __process_compose_prepare_completions will setup __process_compose_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
complete -c process-compose -n 'not __process_compose_requires_order_preservation && __process_compose_prepare_completions' -f -a '$__process_compose_comp_results'
# otherwise we use the -k flag
complete -k -c process-compose -n '__process_compose_requires_order_preservation && __process_compose_prepare_completions' -f -a '$__process_compose_comp_results'
-6
View File
@@ -1,6 +0,0 @@
# Gradle needs a JDK it can actually run on. Fedora 43's default is JDK 25,
# which Gradle 8.13 rejects with "Unsupported class file major version 69".
# $HOME is shared with the host, so scope this to the distrobox via CONTAINER_ID.
if set -q CONTAINER_ID; and test -x /usr/lib/jvm/java-21-openjdk/bin/javac
set -gx JAVA_HOME /usr/lib/jvm/java-21-openjdk
end
-57
View File
@@ -1,57 +0,0 @@
function __dev_repo_name
# Use the main worktree (first entry) for consistent naming across worktrees
set -l lines (git worktree list --porcelain | string replace -rf "^worktree " "")
basename $lines[1]
end
function __dev_clean_shell -d "Build a clean login shell command, unsetting devbox vars so they re-source"
# Print one word per line; callers capture with (__dev_clean_shell).
echo env
for var in (set --names --export | string match 'DEVBOX_*')
echo -- -u
echo -- $var
end
echo -- (command -s fish)
echo -- --login
end
function __dev_create_session -a session dir
if test -z "$dir"
set dir (pwd)
end
if not tmux has-session -t "$session" 2>/dev/null
# Clean login shell so the new directory re-sources its environment.
set -l shell (__dev_clean_shell)
tmux new-session -d -s "$session" -c "$dir" -n dev $shell
# Ensure that new panes in this session also start with the clean environment.
tmux set-option -t "$session" default-command (string join ' ' -- $shell)
set -l left (tmux display-message -t "$session:dev" -p '#{pane_id}')
tmux split-window -h -t "$left" -c "$dir" $shell
set -l right (tmux display-message -t "$session:dev" -p '#{pane_id}')
tmux split-window -v -t "$right" -c "$dir" $shell
set -l bottom_right (tmux display-message -t "$session:dev" -p '#{pane_id}')
tmux send-keys -t "$left" 'vim' Enter
tmux send-keys -t "$right" 'dev-claude' Enter
tmux select-pane -t "$bottom_right"
end
end
function __dev_attach_session -a session
if test -z "$TMUX"
tmux attach-session -t "$session"
else
set -l current_session (tmux display-message -p '#S')
if test "$current_session" != "$session"
tmux switch-client -t "$session"
end
end
end
function __dev_wt_session_name -a repo branch
set -l name "$repo-"(string replace -a '/' '-' -- $branch | string replace -a '\\' '-')
string replace -a '.' '-' -- $name
end
@@ -1,14 +0,0 @@
# This file was created by fish when upgrading to version 4.3, to migrate
# the 'fish_key_bindings' variable from its old default scope (universal)
# to its new default scope (global). We recommend you delete this file
# and configure key bindings in ~/.config/fish/config.fish if needed.
set --global fish_key_bindings fish_vi_key_bindings
# Prior to version 4.3, fish shipped an event handler that runs
# `set --universal fish_key_bindings fish_default_key_bindings`
# whenever the fish_key_bindings variable is erased.
# This means that as long as any fish < 4.3 is still running on this system,
# we cannot complete the migration.
# As a workaround, erase the universal variable at every shell startup.
set --erase --universal fish_key_bindings
@@ -1,48 +0,0 @@
# This file was created by fish when upgrading to version 4.3, to migrate
# theme variables from universal to global scope.
# Don't edit this file, as it will be written by the web-config tool (`fish_config`).
# To customize your theme, delete this file and see
# help interactive#syntax-highlighting
# or
# man fish-interactive | less +/^SYNTAX.HIGHLIGHTING
# for appropriate commands to add to ~/.config/fish/config.fish instead.
# See also the release notes for fish 4.3.0 (run `help relnotes`).
set --global fish_color_autosuggestion 9ca0b0
set --global fish_color_cancel d20f39
set --global fish_color_command 1e66f5
set --global fish_color_comment 8c8fa1
set --global fish_color_cwd df8e1d
set --global fish_color_cwd_root red
set --global fish_color_end fe640b
set --global fish_color_error d20f39
set --global fish_color_escape e64553
set --global fish_color_gray 9ca0b0
set --global fish_color_history_current --bold
set --global fish_color_host 1e66f5
set --global fish_color_host_remote 40a02b
set --global fish_color_keyword 8839ef
set --global fish_color_normal 4c4f69
set --global fish_color_operator ea76cb
set --global fish_color_option 40a02b
set --global fish_color_param dd7878
set --global fish_color_quote 40a02b
set --global fish_color_redirection ea76cb
set --global fish_color_search_match --background=ccd0da
set --global fish_color_selection --background=ccd0da
set --global fish_color_status d20f39
set --global fish_color_user 179299
set --global fish_color_valid_path --underline
set --global fish_pager_color_background
set --global fish_pager_color_completion 4c4f69
set --global fish_pager_color_description 9ca0b0
set --global fish_pager_color_prefix ea76cb
set --global fish_pager_color_progress 9ca0b0
set --global fish_pager_color_secondary_background
set --global fish_pager_color_secondary_completion
set --global fish_pager_color_secondary_description
set --global fish_pager_color_secondary_prefix
set --global fish_pager_color_selected_background
set --global fish_pager_color_selected_completion
set --global fish_pager_color_selected_description
set --global fish_pager_color_selected_prefix
+9 -24
View File
@@ -1,11 +1,3 @@
# Mise shims so non-interactive shells (scripts, editor spawns) find tools.
# Interactive sessions get exact versions from `mise activate` below.
fish_add_path --global ~/.local/bin ~/.local/share/mise/shims ~/go/bin
if test -d ~/code/yt-dlp
fish_add_path --global ~/code/yt-dlp
end
if status is-interactive if status is-interactive
# Commands to run in interactive sessions can go here # Commands to run in interactive sessions can go here
@@ -13,37 +5,30 @@ if status is-interactive
# https://fishshell.com/docs/current/cmds/fish_greeting.html # https://fishshell.com/docs/current/cmds/fish_greeting.html
set -g fish_greeting set -g fish_greeting
if command -q brew if test -f /home/linuxbrew/.linuxbrew/bin/brew
brew shellenv | source
else if test -x /home/linuxbrew/.linuxbrew/bin/brew
/home/linuxbrew/.linuxbrew/bin/brew shellenv | source /home/linuxbrew/.linuxbrew/bin/brew shellenv | source
end end
if command -q atuin if test -f $(which atuin)
atuin init fish | source atuin init fish | source
end end
# Override globals # Override globals
set -gx EDITOR nvim set -gx EDITOR nvim
# Enable autoenv
source ~/.config/fish/functions/activate.fish
# Aliases # Aliases
alias vim="nvim" alias vim="nvim"
alias dc="podman compose"
alias lg="lazygit" alias lg="lazygit"
alias la="eza --long --header --git --group --time-style long-iso -a" alias la="eza --long --header --git --group --time-style long-iso -a"
alias record='wf-recorder -g "$(slurp)"' alias record='wf-recorder -g "$(slurp)"'
alias claer="clear"
# Set a location to store variables that should not be tracked in git.
if test -f ~/.local/fish_env.fish
source ~/.local/fish_env.fish
end
# Abbreviations # Abbreviations
# https://fishshell.com/docs/current/cmds/abbr.html # https://fishshell.com/docs/current/cmds/abbr.html
abbr --add --position command pc process-compose abbr --add --position command ds devbox services
# Per-directory toolchains and global tools. # Set Path
# https://mise.jdx.dev/getting-started.html fish_add_path -p /home/tgrosinger/.dotfiles/bin/linux
if type -q mise
mise activate fish | source
end
end end
+41 -1
View File
@@ -1,3 +1,43 @@
# This file contains fish universal variable definitions. # This file contains fish universal variable definitions.
# VERSION: 3.0 # VERSION: 3.0
SETUVAR __fish_initialized:4300 SETUVAR __fish_initialized:3800
SETUVAR fish_color_autosuggestion:9ca0b0
SETUVAR fish_color_cancel:d20f39
SETUVAR fish_color_command:1e66f5
SETUVAR fish_color_comment:8c8fa1
SETUVAR fish_color_cwd:df8e1d
SETUVAR fish_color_cwd_root:red
SETUVAR fish_color_end:fe640b
SETUVAR fish_color_error:d20f39
SETUVAR fish_color_escape:e64553
SETUVAR fish_color_gray:9ca0b0
SETUVAR fish_color_history_current:\x2d\x2dbold
SETUVAR fish_color_host:1e66f5
SETUVAR fish_color_host_remote:40a02b
SETUVAR fish_color_keyword:8839ef
SETUVAR fish_color_normal:4c4f69
SETUVAR fish_color_operator:ea76cb
SETUVAR fish_color_option:40a02b
SETUVAR fish_color_param:dd7878
SETUVAR fish_color_quote:40a02b
SETUVAR fish_color_redirection:ea76cb
SETUVAR fish_color_search_match:\x2d\x2dbackground\x3dccd0da
SETUVAR fish_color_selection:\x2d\x2dbackground\x3dccd0da
SETUVAR fish_color_status:d20f39
SETUVAR fish_color_user:179299
SETUVAR fish_color_valid_path:\x2d\x2dunderline
SETUVAR fish_key_bindings:fish_vi_key_bindings
SETUVAR fish_pager_color_background:\x1d
SETUVAR fish_pager_color_completion:4c4f69
SETUVAR fish_pager_color_description:9ca0b0
SETUVAR fish_pager_color_prefix:ea76cb
SETUVAR fish_pager_color_progress:9ca0b0
SETUVAR fish_pager_color_secondary_background:\x1d
SETUVAR fish_pager_color_secondary_completion:\x1d
SETUVAR fish_pager_color_secondary_description:\x1d
SETUVAR fish_pager_color_secondary_prefix:\x1d
SETUVAR fish_pager_color_selected_background:\x1d
SETUVAR fish_pager_color_selected_completion:\x1d
SETUVAR fish_pager_color_selected_description:\x1d
SETUVAR fish_pager_color_selected_prefix:\x1d
SETUVAR fish_user_paths:/home/tgrosinger/\x2edotfiles/bin/linux
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env fish
#
# Autoenv for fish shell.
# Based on, but heavily modified from:
# https://github.com/loopbit/autoenv_fish
#
set AUTOENV_AUTH_FILE ~/.autoenv_authorized
if [ -z "$AUTOENV_ENV_FILENAME" ]
set AUTOENV_ENV_FILENAME ".env"
end
# probe to see if we have access to a shasum command, otherwise disable autoenv
if which gsha1sum 2>/dev/null >&2
# Okay
else if which sha1sum 2>/dev/null >&2
# Okay
else if which shasum 2>/dev/null >&2
# Okay
else
echo "Autoenv cannot locate a compatible shasum binary; not enabling"
exit 1
end
# This function will be automatically called on directory change
# without needing to override the `cd` command.
function autoenv_init --on-variable PWD
set defIFS $IFS
set IFS (echo -en "\n\b")
set target $argv[1]
set home (dirname $HOME)
set search_dir $PWD
while [ $search_dir != / -a $search_dir != "$home" ]
set file "$search_dir/$AUTOENV_ENV_FILENAME"
if [ -e $file ]
set files $files $file
end
set search_dir (dirname $search_dir)
end
set numerator (count $files)
if [ $numerator -gt 0 ]
for x in (seq $numerator)
set envfile $files[$x]
autoenv_check_authz_and_run "$envfile"
end
end
set IFS $defIFS
end
function autoenv_run
set file "(realpath "$argv[1]")"
autoenv_check_authz_and_run "$file"
end
function autoenv_env
builtin echo "autoenv:" "$argv[1]"
end
function autoenv_printf
builtin printf "autoenv: "
builtin printf "$argv[1]"
end
function autoenv_indent
cat -e $argv[1] | sed 's/.*/autoenv: &/'
end
function autoenv_hashline
# typeset envfile hash
set envfile $argv[1]
set hash (shasum "$envfile" | cut -d' ' -f 1)
echo "$envfile:$hash"
end
function autoenv_check_authz
# typeset envfile hash
set envfile $argv[1]
set hash (autoenv_hashline "$envfile")
touch $AUTOENV_AUTH_FILE
grep -Gq "$hash" $AUTOENV_AUTH_FILE
end
function autoenv_check_authz_and_run
set envfile $argv[1]
if autoenv_check_authz "$envfile"
autoenv_source "$envfile"
return 0
end
if [ -z $MC_SID ] #make sure mc is not running
autoenv_env
autoenv_env "WARNING:"
autoenv_env "This is the first time you are about to source $envfile":
autoenv_env
autoenv_env " --- (begin contents) ---------------------------------------"
autoenv_indent "$envfile"
autoenv_env " --- (end contents) -----------------------------------------"
autoenv_env
autoenv_printf "Are you sure you want to allow this? (y/N) \n"
read answer
if [ $answer = y -o $answer = Y ]
autoenv_authorize_env "$envfile"
autoenv_source "$envfile"
end
end
end
function autoenv_deauthorize_env
#typeset envfile
set envfile $argv[1]
cp "$AUTOENV_AUTH_FILE" "$AUTOENV_AUTH_FILE.tmp"
grep -Gv "$envfile:" "$AUTOENV_AUTH_FILE.tmp" >$AUTOENV_AUTH_FILE
end
function autoenv_authorize_env
#typeset envfile
set envfile $argv[1]
autoenv_deauthorize_env "$envfile"
autoenv_hashline "$envfile" >>$AUTOENV_AUTH_FILE
end
function autoenv_source
#TODO: Why are global vars not being passed to sourced script?
set -g AUTOENV_CUR_FILE $argv[1]
set -g AUTOENV_CUR_DIR (dirname $argv[1])
source "$argv[1]"
#set -e AUTOENV_CUR_FILE
#set -e AUTOENV_CUR_DIR
end
-29
View File
@@ -1,29 +0,0 @@
function aiq -d "Ask Codex for a fish shell command"
if test (count $argv) -eq 0
echo "Usage: aiq <what you want to do>" >&2
return 1
end
if not command -q codex
echo "aiq: codex is not installed or is not in PATH" >&2
return 127
end
set -l need (string join ' ' -- $argv)
set -l prompt "You are suggesting a shell command for the fish shell. Do not run the command or modify any files. The user needs to: $need
Output the suggested command first. Follow it with additional details only when they are very important, or when there is relevant optional configuration the user should know about. Keep the response concise and do not wrap the command in backticks."
# Codex writes progress and run metadata to stderr; keep this helper focused
# on the final answer, but surface stderr when the request fails.
set -l errors (mktemp); or return 1
command codex exec --ephemeral --sandbox read-only --skip-git-repo-check "$prompt" 2>$errors
set -l result $status
if test $result -ne 0
command cat $errors >&2
end
command rm -f $errors
return $result
end
-6
View File
@@ -1,6 +0,0 @@
function del-wt -d "Remove current git worktree and cd into the main worktree"
set -l main_wt (git worktree list --porcelain | head -1 | string replace "worktree " "")
set -l current (git rev-parse --show-toplevel 2>/dev/null)
cd "$main_wt"; or return 1
git worktree remove "$current" # git handles dirty/main-worktree guards itself
end
@@ -1,56 +0,0 @@
function dev-claude -d "Start Claude with a durable session id so tmux-resurrect can restore the conversation"
# tmux-resurrect saves each pane's command line verbatim and replays it on
# restore. Pinning the session id at launch is what lets a restored pane
# rejoin its exact conversation; cwd alone can't disambiguate, since several
# Claude sessions often share one worktree.
# Run Claude inside a transient scope in claude.slice so a runaway subprocess
# tree hits that slice's memory cap and is killed there, instead of thrashing
# the whole machine into a lockup. See ~/.dotfiles/docs/claude-resource-limits.md
#
# The /usr/bin/env shim is load-bearing, in two ways:
# 1. systemd-run resolves its target through PATH and rewrites argv[0] to an
# absolute path. tmux-resurrect matches panes with an anchored "^claude "
# regex, so without the shim the saved command becomes an absolute path,
# stops matching, and pane restore silently dies. env re-execs with argv[0]
# as a bare "claude", keeping the saved command byte-identical.
# 2. env does the PATH lookup that "command" would do here, so a fish
# function or alias named claude still can't shadow the real binary.
set -l launch /usr/bin/env claude
if command -q systemd-run
set launch systemd-run --user --scope --quiet --collect --slice=claude.slice -- /usr/bin/env claude
end
# Never exec here. tmux-resurrect's ps save strategy reads the *children* of
# the pane pid (`ps -ao ppid,args | grep "^$pane_pid"`), so exec'ing would make
# claude the pane pid itself and the saved command would be one of claude's own
# subprocesses — restore then silently does nothing. Keeping fish as the pane
# process is what puts "claude --session-id <uuid>" where resurrect looks.
# Claude exiting drops back to a prompt rather than closing the pane.
# Restore path: resurrect replays the id we launched with. Claude rejects
# --session-id for an id that already exists, so switch to --resume once a
# transcript for it is on disk.
if test (count $argv) -ge 2
and contains -- $argv[1] --session-id --resume
and string match -qir '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$' -- $argv[2]
set -l id $argv[2]
set -l transcript (find $HOME/.claude/projects -maxdepth 2 -name "$id.jsonl" -print -quit)
if test -n "$transcript"
$launch --resume $id $argv[3..]
return
end
$launch --session-id $id $argv[3..]
return
end
# An explicit session flag means the caller is choosing the conversation.
for flag in -c --continue -r --resume --session-id
if contains -- $flag $argv
$launch $argv
return
end
end
$launch --session-id (uuidgen) $argv
end
@@ -1,76 +0,0 @@
function dev-wt-pr -d "Open a GitHub PR in a worktree in a new tmux window (Octo review + Claude)" -a pr_number branch repo_path
if test -z "$pr_number"
echo "Usage: dev-wt-pr <pr-number> [branch] [repo-path]"
return 1
end
# Opens the PR in a new window of the current tmux session, so it must be run
# from inside tmux. Running outside tmux is unsupported for now.
if test -z "$TMUX"
echo "dev-wt-pr must be run from inside a tmux session"
return 1
end
# gh-dash passes the repo path; default to the current directory otherwise.
if test -n "$repo_path"
cd "$repo_path"; or return 1
end
set -l repo (__dev_repo_name 2>/dev/null)
if test -z "$repo"
echo "Not in a git repository"
return 1
end
# gh-dash passes the branch to save a lookup; manual callers can omit it and
# we resolve it from the PR number here.
if test -z "$branch"
set branch (gh pr view $pr_number --json headRefName --jq .headRefName 2>/dev/null)
end
set -l main_wt_path (git worktree list --porcelain | head -1 | string replace "worktree " "")
set -l parent_dir (dirname "$main_wt_path")
# Key the worktree on the PR number to stay unique and avoid slashes in
# branch names producing nested directories.
set -l wt_path "$parent_dir/$repo-pr-$pr_number"
# Create the worktree if it doesn't already exist.
if not test -d "$wt_path"
echo "Creating worktree for PR #$pr_number at $wt_path..."
git worktree add --detach "$wt_path"; or return 1
# gh pr checkout handles both same-repo and fork PRs uniformly.
if not fish -c "cd '$wt_path'; and gh pr checkout $pr_number"
echo "Failed to check out PR #$pr_number"
git worktree remove --force "$wt_path"
return 1
end
# Run repo-specific setup hook if present (matches dev-wt).
set -l setup_hook "$main_wt_path/.worktree-setup.sh"
if test -x "$setup_hook"
echo "Running worktree setup hook..."
bash "$setup_hook" "$wt_path" "$main_wt_path" "$pr_number"
end
end
# Window name like "123-fix-login"; flatten any slashes from the branch name.
set -l window_name "$pr_number-"(string replace -a '/' '-' -- $branch)
# Reuse the window if it already exists (e.g. re-pressing the shortcut).
if tmux list-windows -F '#{window_name}' 2>/dev/null | string match -q -- "$window_name"
tmux select-window -t "$window_name"
return
end
# Clean login shell so the worktree re-sources its environment.
set -l shell (__dev_clean_shell)
set -l win (tmux new-window -P -F '#{window_id}' -n "$window_name" -c "$wt_path" $shell)
tmux send-keys -t "$win" 'nvim -c "Octo review start"' Enter
set -l right (tmux split-window -h -P -F '#{pane_id}' -t "$win" -c "$wt_path" $shell)
tmux send-keys -t "$right" 'dev-claude' Enter
tmux select-window -t "$win"
end
-98
View File
@@ -1,98 +0,0 @@
function dev-wt -d "Switch to a tmux dev session for a worktree, creating it if needed"
argparse 'C/repo=' -- $argv
or return 1
set -l name $argv[1]
# With -C/--repo, behave exactly as if invoked from within that directory by
# cd-ing there for the duration and restoring the caller's cwd afterward.
if set -q _flag_repo
if not test -d "$_flag_repo"
echo "Not a directory: $_flag_repo"
return 1
end
set -l prev_pwd $PWD
cd "$_flag_repo"; or return 1
__dev_wt_impl $name
set -l rc $status
cd "$prev_pwd"
return $rc
end
__dev_wt_impl $name
end
function __dev_wt_impl -a name
set -l repo (__dev_repo_name 2>/dev/null)
if test -z "$repo"
echo "Not in a git repository"
return 1
end
# No argument: switch to a session for the main repo (not a worktree)
if test -z "$name"
set -l main_wt_path (git worktree list --porcelain | head -1 | string replace "worktree " "")
set -l session (string replace -a '.' '-' -- $repo)
__dev_create_session $session $main_wt_path
__dev_attach_session $session
return
end
set -l wt_path ""
set -l wt_branch ""
# Search for worktree by directory basename or branch name
set -l current_path ""
set -l current_branch ""
for line in (git worktree list --porcelain)
if string match -q "worktree *" -- $line
set current_path (string replace "worktree " "" -- $line)
set current_branch ""
else if string match -q "branch *" -- $line
set current_branch (string replace "branch refs/heads/" "" -- $line)
if test (basename "$current_path") = "$name"; or test "$current_branch" = "$name"
set wt_path $current_path
set wt_branch $current_branch
break
end
end
end
# If worktree not found, create it
if test -z "$wt_path"
set -l main_wt_path (git worktree list --porcelain | head -1 | string replace "worktree " "")
set -l parent_dir (dirname "$main_wt_path")
set wt_path "$parent_dir/$repo-$name"
set wt_branch "$name"
if test -d "$wt_path"
echo "Error: directory already exists: $wt_path"
return 1
end
# Create branch from main if it doesn't exist
if not git rev-parse --verify "$wt_branch" >/dev/null 2>&1
echo "Creating branch '$wt_branch' from main..."
git branch "$wt_branch" main
end
echo "Creating worktree at $wt_path..."
git worktree add "$wt_path" "$wt_branch"
# Run repo-specific setup hook if present
set -l setup_hook "$main_wt_path/.worktree-setup.sh"
if test -x "$setup_hook"
echo "Running worktree setup hook..."
bash "$setup_hook" "$wt_path" "$main_wt_path" "$wt_branch"
end
end
if test -z "$wt_branch"
echo "Worktree '$name' has no branch (detached HEAD)"
return 1
end
set -l session (__dev_wt_session_name $repo $wt_branch)
__dev_create_session $session $wt_path
__dev_attach_session $session
end
-5
View File
@@ -1,5 +0,0 @@
function dev -d "Create and attach to a tmux dev session for the current directory"
set -l session (basename (pwd) | string replace -a '.' '-')
__dev_create_session $session
__dev_attach_session $session
end
+1 -8
View File
@@ -6,7 +6,6 @@ function fish_prompt
set -l red (set_color -o red) set -l red (set_color -o red)
set -l green (set_color -o green) set -l green (set_color -o green)
set -l blue (set_color -o blue) set -l blue (set_color -o blue)
set -l magenta (set_color -o magenta)
set -l normal (set_color normal) set -l normal (set_color normal)
set -l arrow_color "$green" set -l arrow_color "$green"
@@ -21,11 +20,5 @@ function fish_prompt
set -l cwd $cyan(prompt_pwd | path basename) set -l cwd $cyan(prompt_pwd | path basename)
# distrobox exports CONTAINER_ID inside the container echo -n -s $arrow ' '$cwd $repo_info $normal ' '
set -l box ""
if set -q CONTAINER_ID
set box $magenta"📦$CONTAINER_ID "
end
echo -n -s $arrow ' ' $box $cwd $repo_info $normal ' '
end end
-7
View File
@@ -1,7 +0,0 @@
function pandoc -d "Convert a markdown file to docx using pandoc in podman" -a input
set -l output (string replace -r '\.md$' '.docx' $input)
podman run --rm -v "$(pwd):/data:z" \
--userns keep-id:uid=1000,gid=1000 \
pandoc/core:3.8-alpine --from markdown --to docx -o "/data/$output" "/data/$input"
end
-4
View File
@@ -1,4 +0,0 @@
function record -d "Capture a screen recording with no audio"
wf-recorder -c libopenh264 --no-audio -g "$(slurp)" -f output.mp4
end
@@ -1,7 +0,0 @@
function reencode -d "Re-encode a video to VBR with no audio" -a input
set -l name (string replace -r '\.[^.]+$' '' $input)
set -l ext (string replace -r '.*\.' '' $input)
set -l output "$name-reenc.$ext"
ffmpeg -i $input -c:v libopenh264 -crf 23 -preset slow -an $output
end
-10
View File
@@ -1,10 +0,0 @@
function togif -d "Convert a video to a high-quality gif" -a input
set -l name (string replace -r '\.[^.]+$' '' $input)
set -l output "$name.gif"
set -l palette (mktemp --suffix .png)
ffmpeg -i $input -vf "fps=15,palettegen" -y $palette
ffmpeg -i $input -i $palette -lavfi "fps=15 [x]; [x][1:v] paletteuse" -y $output
rm -f $palette
end
-6
View File
@@ -1,6 +0,0 @@
function tomov -d "Convert a video to a GitHub-uploadable .mov" -a input
set -l name (string replace -r '\.[^.]+$' '' $input)
set -l output "$name.mov"
ffmpeg -i $input -c:v libopenh264 -crf 23 -preset slow -pix_fmt yuv420p -an $output
end
-122
View File
@@ -1,122 +0,0 @@
prSections:
- title: My Pull Requests
filters: is:open author:@me
- title: Needs My Review
filters: is:open review-requested:@me
- title: Involved
filters: is:open involves:@me -author:@me
issuesSections:
- title: My Issues
filters: is:open author:@me
- title: Assigned
filters: is:open assignee:@me
- title: Involved
filters: is:open involves:@me -author:@me
notificationsSections:
- title: All
filters: ""
- title: Created
filters: reason:author
- title: Participating
filters: reason:participating
- title: Mentioned
filters: reason:mention
- title: Review Requested
filters: reason:review-requested
- title: Assigned
filters: reason:assign
- title: Subscribed
filters: reason:subscribed
- title: Team Mentioned
filters: reason:team-mention
repo:
branchesRefetchIntervalSeconds: 30
prsRefetchIntervalSeconds: 60
defaults:
preview:
open: true
width: 0.45
height: 0.6
position: auto
prsLimit: 20
prApproveComment: LGTM
issuesLimit: 20
notificationsLimit: 20
view: prs
layout:
prs:
updatedAt:
width: 5
createdAt:
width: 5
repo:
width: 20
author:
width: 15
authorIcon:
hidden: false
labels:
width: 22
hidden: true
assignees:
width: 20
hidden: true
base:
width: 15
hidden: true
lines:
width: 15
issues:
updatedAt:
width: 5
createdAt:
width: 5
repo:
width: 15
creator:
width: 10
creatorIcon:
hidden: false
assignees:
width: 20
hidden: true
refetchIntervalMinutes: 30
keybindings:
prs:
- key: C
name: review in worktree
command: >
fish -c "dev-wt-pr {{.PrNumber}} {{.HeadRefName}} {{.RepoPath}}"
universal:
- key: g
name: lazygit
command: >
cd {{.RepoPath}}; lazygit
repoPaths: {}
theme:
colors:
text:
primary: "#4c4f69"
secondary: "#179299"
inverted: "#dce0e8"
faint: "#5c5f77"
warning: "#df8e1d"
success: "#40a02b"
error: "#d20f39"
background:
selected: "#ccd0da"
border:
primary: "#179299"
secondary: "#bcc0cc"
faint: "#ccd0da"
ui:
sectionsShowCount: true
table:
showSeparator: true
compact: false
pager:
diff: hunk
confirmQuit: false
showAuthorIcons: true
smartFilteringAtLaunch: true
includeReadNotifications: true
-5
View File
@@ -1,5 +0,0 @@
theme = Catppuccin Latte
font-family = JetBrainsMono Nerd Font Mono
# https://pi.dev/docs/latest/terminal-setup
keybind = alt+backspace=text:\x1b\x7f
-1
View File
@@ -1 +0,0 @@
theme = "catppuccin-latte"
-4
View File
@@ -1,4 +0,0 @@
{
"version": 1,
"lastSeenCliVersion": "0.14.1"
}
-3
View File
@@ -7,9 +7,6 @@ promptToReturnFromSubprocess: false # removes "press enter to return to lazygit"
notARepository: 'skip' notARepository: 'skip'
git: git:
autoForwardBranches: "none" autoForwardBranches: "none"
diffRenderers:
- command: delta --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"
- command: delta --paging=never --line-numbers --no-gitconfig --light
os: os:
editPreset: "nvim" editPreset: "nvim"
gui: gui:
-39
View File
@@ -1,39 +0,0 @@
[settings]
# Pin exact versions, URLs, and checksums in mise.lock so a tampered
# re-release of the same version can't slip in silently.
lockfile = true
# Block the asdf plugin backend (arbitrary community shell scripts) in favor
# of aqua/ubi/core backends, which verify checksums and signatures. Remove if
# a tool you need exists only as an asdf plugin.
disable_backends = ["asdf", "ubi"]
# Never install versions less than 2 weeks old.
minimum_release_age = "14d"
[tools]
# Short names resolve to the aqua backend (checksum + signature verified)
"aqua:F1bonacc1/process-compose" = "1.120.0"
"aqua:neovim/neovim" = "0.12.4"
"github:Satty-org/Satty" = "0.20.1"
"npm:@anthropic-ai/sandbox-runtime" = "0.0.67"
"npm:@earendil-works/pi-coding-agent" = "0.83.0"
"npm:typescript" = "7.0.2"
"npm:typescript-language-server" = "5.3.0"
bat = "0.26.1"
btop = "1.4.7"
delta = "0.19.2"
dive = "0.13.1"
eza = "0.23.5"
fd = "10.4.2"
gh = "2.97.0"
jq = "1.8.2"
lazygit = "0.64.1"
node = "24.18.1"
opencode = "1.15.11"
pnpm = "11.18.0"
restic = "0.19.1"
ripgrep = "15.2.0"
tmux = "3.7b"
uv = "0.12.0"
@@ -1,7 +0,0 @@
{
// markdownlint-cli2 base config used by nvim-lint (see lua/plugins/markdown.lua).
"config": {
// Disable line-length errors; long lines are soft-wrapped on purpose.
"MD013": false
}
}
+29 -36
View File
@@ -1,45 +1,38 @@
{ {
"LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" }, "LazyVim": { "branch": "main", "commit": "28db03f958d58dfff3c647ce28fdc1cb88ac158d" },
"SchemaStore.nvim": { "branch": "main", "commit": "d34c58439271f5e73ed79c2b8d1a09731c4525f1" }, "SchemaStore.nvim": { "branch": "main", "commit": "5f2299987a1937612c910f00db39156bab6a6b35" },
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, "blink.cmp": { "branch": "main", "commit": "b19413d214068f316c78978b08264ed1c41830ec" },
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, "catppuccin": { "branch": "main", "commit": "ce4a8e0d5267e67056f9f4dcf6cb1d0933c8ca00" },
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" }, "conform.nvim": { "branch": "master", "commit": "4993e07fac6679d0a5005aa7499e0bad2bd39f19" },
"claude-review.nvim": { "branch": "main", "commit": "899a33a04d26b6a75384dda176e212eee75a2532" }, "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" },
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" }, "focus.nvim": { "branch": "master", "commit": "26a755c363284547196ceb258a83f92608d7979b" },
"diffview.nvim": { "branch": "main", "commit": "43e60bca414e4991ed10118e59f809fb03bbeddd" }, "friendly-snippets": { "branch": "main", "commit": "572f5660cf05f8cd8834e096d7b4c921ba18e175" },
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" }, "gitsigns.nvim": { "branch": "main", "commit": "5813e4878748805f1518cee7abb50fd7205a3a48" },
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, "grug-far.nvim": { "branch": "main", "commit": "b58b2d65863f4ebad88b10a1ddd519e5380466e0" },
"gitsigns.nvim": { "branch": "main", "commit": "42d6aed4e94e0f0bbced16bbdcc42f57673bd75e" },
"grug-far.nvim": { "branch": "main", "commit": "11595bf747edc270bce2069d1020502ad4ae56cf" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" }, "lazydev.nvim": { "branch": "main", "commit": "5231c62aa83c2f8dc8e7ba957aa77098cda1257d" },
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" }, "lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" },
"markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" }, "markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "40276c4df7e6bdce6801d6c035c6227f9115a855" }, "mason-lspconfig.nvim": { "branch": "main", "commit": "7d527c76c43f46294de9c19d39c5a86317809b4b" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, "mason.nvim": { "branch": "main", "commit": "57e5a8addb8c71fb063ee4acda466c7cf6ad2800" },
"mini.ai": { "branch": "main", "commit": "25248c6aa002391936a6200f12d1466015987133" }, "mini.ai": { "branch": "main", "commit": "bfb26d9072670c3aaefab0f53024b2f3729c8083" },
"mini.diff": { "branch": "main", "commit": "626b8a5b93874c4d05ca25aedec56cfff0b378fb" }, "mini.icons": { "branch": "main", "commit": "ff2e4f1d29f659cc2bad0f9256f2f6195c6b2428" },
"mini.icons": { "branch": "main", "commit": "98faae31e9be1cc054ae63485e58ceb185efcad0" }, "mini.pairs": { "branch": "main", "commit": "472ec50092a3314ec285d2db2baa48602d71fe93" },
"mini.pairs": { "branch": "main", "commit": "b1c5a726921b7a8c9321e9a7a208aa0571de5810" }, "mini.surround": { "branch": "main", "commit": "88c52297ed3e69ecf9f8652837888ecc727a28ee" },
"mini.surround": { "branch": "main", "commit": "8d5d0c5aa92449368ac251e85451d79d8f69d296" },
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
"nvim-ansible": { "branch": "main", "commit": "c7f595d568b588942d4d0c37b5cd6cae3764a148" }, "nvim-lint": { "branch": "master", "commit": "d1118791070d090777398792a73032a0ca5c79ff" },
"nvim-lint": { "branch": "master", "commit": "3d55c8f67c6ae5c15e1042571e107c7a3d5c5f4e" }, "nvim-lspconfig": { "branch": "master", "commit": "b2441c9374699685991959f50e5e6293c509e501" },
"nvim-lspconfig": { "branch": "master", "commit": "16286347bdba1333c7d124d9de9fe6630731b2b2" }, "nvim-treesitter": { "branch": "main", "commit": "17885756e63df73ed90db62e4630f744ceda6514" },
"nvim-treesitter": { "branch": "main", "commit": "19071296d3d643b48615ee574a20e8a03ac40872" }, "nvim-treesitter-textobjects": { "branch": "main", "commit": "63c4dce4a56312ef1bdeafd16bdefa008fcc950a" },
"nvim-treesitter-textobjects": { "branch": "main", "commit": "898ee307df58f854d11cd7edd06472574d48014e" }, "nvim-ts-autotag": { "branch": "main", "commit": "c4ca798ab95b316a768d51eaaaee48f64a4a46bc" },
"nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" },
"octo.nvim": { "branch": "master", "commit": "af2411604b51cb4a0f3e2de50b1b7cacc2581c48" },
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" },
"render-markdown.nvim": { "branch": "main", "commit": "4663eb3ecd538bd5062628fb6d95bbe6bdca78f6" }, "render-markdown.nvim": { "branch": "main", "commit": "6e0e8902dac70fecbdd8ce557d142062a621ec38" },
"sidekick.nvim": { "branch": "main", "commit": "208e1c5b8170c01fd1d07df0139322a76479b235" }, "snacks.nvim": { "branch": "main", "commit": "fe7cfe9800a182274d0f868a74b7263b8c0c020b" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
"ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" }, "ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" },
"vim-tmux-navigator": { "branch": "master", "commit": "e41c431a0c7b7388ae7ba341f01a0d217eb3a432" }, "vim-tmux-navigator": { "branch": "master", "commit": "c45243dc1f32ac6bcf6068e5300f3b2b237e576a" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
} }

Some files were not shown because too many files have changed in this diff Show More