Compare commits
6
Commits
b183a97a56
...
6b848c9454
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b848c9454 | ||
|
|
f1b5077aa1 | ||
|
|
8ccb3dfc43 | ||
|
|
0d1fddb2f2 | ||
|
|
5916bf9581 | ||
|
|
4220d6d143 |
@@ -6,3 +6,7 @@
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# 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 `exec` 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.
|
||||
|
||||
## Two 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. `command` cannot be used in the fallback
|
||||
|
||||
The natural fallback is `set -l launch command claude`, but fish rejects it:
|
||||
|
||||
```
|
||||
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. `/usr/bin/env claude` is used in both branches instead — same guarantee, and it expands cleanly.
|
||||
|
||||
## 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, and it must show a **bare** `claude`, not an absolute path:
|
||||
|
||||
```bash
|
||||
ps -ao ppid,args | grep "^<pane_pid>"
|
||||
# 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
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
||||
- **Status `Wont Do`** — `isCompleted: true` so it drops off the frontier, `excludeFromCycle: true` so `tn toggle` can't land on it, `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.
|
||||
- **`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.
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: dev-tickets
|
||||
description: Storage model for development tickets, specs, and planning maps, which live in the Obsidian vault as TaskNotes tasks. 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 at `/home/tgrosinger/Documents/Atrium`, as TaskNotes tasks. This skill owns the storage model — the shape on disk, the metadata, and the commands 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.
|
||||
|
||||
General `tn` usage — filter syntax, JSON shapes, gotchas — is the `tasknotes-cli` skill. This skill covers only what is specific to dev tickets.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
|
||||
**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]]` attaches every ticket in that repo to a personal `Obsidian.md` sitting anywhere in the vault, which then lists them in its Subtasks view. Nothing looks broken — the frontmatter still reads `[[obsidian]]` and every `tn` query still returns the right tasks — so this is found by looking, not by failing. Check a title before adopting it, with `-iname`, not `-name`:
|
||||
|
||||
```sh
|
||||
find /home/tgrosinger/Documents/Atrium -iname '<title>.md' -not -path '*/.obsidian/*'
|
||||
```
|
||||
|
||||
## Frontmatter
|
||||
|
||||
`tn create` writes every one of these in a single call. The middle column names the flag.
|
||||
|
||||
| Field | Set with | Values |
|
||||
|---|---|---|
|
||||
| `status` | `--status` | `Open` · `In Progress` (claimed) · `Done` (complete, resolved) · `Wont Do` (abandoned, out of scope) |
|
||||
| `contexts` | `--contexts` | from the repo's `CLAUDE.local.md` |
|
||||
| `projects` | `--projects` | effort → `[[repo]]` · ticket → `[[effort]]` |
|
||||
| actor tag | `--tags` | `agent-step` · `human-step` — exactly one, durable, survives completion |
|
||||
| `needs-info` | `tn update --add-tags` | tag, present only while stalled on an unanswered question |
|
||||
| `blockedBy` | `--blocked-by` | list of `{uid, reltype}`; `reltype` is `FINISHTOSTART`. The flag is not repeatable — pass it once with every blocker in one comma-separated list |
|
||||
| `ticket_type` | `--prop` | `research` · `prototype` · `grilling` · `setup` · `implementation` |
|
||||
| `git_branch` | `--prop` | effort tasks only; omit entirely when on main/master |
|
||||
| `git_worktree` | `--prop` | effort tasks only; omit entirely when the main worktree |
|
||||
| `agent_session` | `obsidian property:set` | `$CLAUDE_CODE_SESSION_ID`, overwritten each session that works the ticket |
|
||||
|
||||
The `task` tag is added automatically; `--tags` carries only the actor tag.
|
||||
|
||||
A ticket on disk reads:
|
||||
|
||||
```yaml
|
||||
---
|
||||
tags:
|
||||
- task
|
||||
- agent-step
|
||||
status: Open
|
||||
contexts:
|
||||
- Obsidian
|
||||
projects:
|
||||
- "[[Network isolation]]"
|
||||
ticket_type: implementation
|
||||
blockedBy:
|
||||
- uid: "[[Verify internal network publish]]"
|
||||
reltype: FINISHTOSTART
|
||||
agent_session: 444e4dcb-de76-4f6b-8c39-7d08d6aa9f5f
|
||||
---
|
||||
```
|
||||
|
||||
`blocked` is not a status. TaskNotes derives `isBlocked` from `blockedBy` and the live status of each blocker, so blocking is recorded once, in `blockedBy`, and read back from `isBlocked`.
|
||||
|
||||
`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:
|
||||
|
||||
```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**, under a sub-heading wikilinking the current date.
|
||||
|
||||
`## As Built` is written at completion, 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
|
||||
|
||||
`tn create` writes frontmatter, custom fields, and body in one call. `--folder` is the effort directory; the note is filed there after creation.
|
||||
|
||||
```sh
|
||||
tn create --title 'Split obsync into its own container' \
|
||||
--folder 'TaskNotes/Dev/atribot/Network isolation' \
|
||||
--contexts Obsidian \
|
||||
--projects '[[Network isolation]]' \
|
||||
--tags agent-step \
|
||||
--prop ticket_type=implementation \
|
||||
--blocked-by '[[Verify internal network publish]],[[Provision the registry mirror]]' \
|
||||
--details-file - <<'EOF'
|
||||
## What to build
|
||||
|
||||
- [ ] Move the compose service into its own namespace
|
||||
|
||||
## Notes
|
||||
|
||||
## As Built
|
||||
EOF
|
||||
```
|
||||
|
||||
An effort task is the same call with `--projects '[[atribot]]'`, no actor tag, and `--prop git_branch=…` when the work is not on main.
|
||||
|
||||
Create in dependency order so a blocker's title exists before the ticket naming it. Flag reference: `tasknotes-cli`.
|
||||
|
||||
A second `--blocked-by` does not add a second blocker — the flag is read once, so every blocker goes in one comma-separated value as above. Commas inside `[[...]]` are literal, so titles containing commas are safe.
|
||||
|
||||
Run the case-insensitive, vault-wide title check above before creating every effort or ticket. If `tn create --folder` nevertheless reports that the destination already exists and leaves the new task in `TaskNotes/Tasks`, treat the creation as failed: report both paths and do not move, rename, delete, or continue using the stranded task. Resolving the collision requires the user's choice because dev-ticket titles become immutable as soon as they are created and deletion requires explicit authorization.
|
||||
|
||||
### 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 — `tn` matches 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`
|
||||
```
|
||||
|
||||
`tn create` cannot produce one, since it 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` |
|
||||
|---|---|---|---|
|
||||
| Created | `Open` | set, exactly one | absent |
|
||||
| Claimed | → `In Progress` | unchanged | unchanged |
|
||||
| Stalled on a question | unchanged | unchanged | **add** |
|
||||
| Question answered | unchanged | unchanged | **remove** |
|
||||
| Reassigned to the other actor | unchanged | **swap** | unchanged |
|
||||
| Completed | → `Done` | **swap if the other actor did the work** | must be absent |
|
||||
| Abandoned | → `Wont Do` | unchanged | remove if present |
|
||||
|
||||
Two rules are easy to forget, and both corrupt a query when missed:
|
||||
|
||||
- **Correct the actor tag at close.** A ticket scoped `agent-step` that the human finished by hand closes as `human-step`. This is what keeps `agent-step AND status:Done` a truthful record of what agents actually shipped, rather than what was merely hoped for.
|
||||
- **Clear `needs-info` before closing.** A closed ticket carrying it pollutes the human's inbox permanently.
|
||||
|
||||
Tag writes go through `tn`:
|
||||
|
||||
```sh
|
||||
tn update '<path>' --add-tags needs-info
|
||||
tn update '<path>' --remove-tags needs-info
|
||||
tn update '<path>' --add-tags human-step --remove-tags agent-step
|
||||
```
|
||||
|
||||
## Working a ticket
|
||||
|
||||
1. **Claim it** before any work, so a concurrent session skips it:
|
||||
```sh
|
||||
tn update 'TaskNotes/Dev/atribot/Network isolation/Split obsync into its own container.md' --status 'In Progress'
|
||||
obsidian property:set name=agent_session value="$CLAUDE_CODE_SESSION_ID" path='TaskNotes/Dev/atribot/Network isolation/Split obsync into its own container.md'
|
||||
```
|
||||
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
|
||||
obsidian property:set name=git_branch value='feat/network-isolation' path='TaskNotes/Dev/atribot/Network isolation/Network isolation.md'
|
||||
obsidian property:set name=git_worktree value='/home/tgrosinger/code/atribot-ni' path='TaskNotes/Dev/atribot/Network isolation/Network isolation.md'
|
||||
```
|
||||
Omit `git_branch` on `main`/`master`, and omit `git_worktree` in the main worktree. If the checkout changes later, update both fields to describe the checkout currently used for the effort.
|
||||
3. **Append notes as you go**, each under a `### [[YYYY-MM-DD]]` sub-heading. `obsidian append` writes at end of file, which files the entry under `## As Built`; insert it above that heading instead:
|
||||
```sh
|
||||
obsidian eval code='const note = "\n### [[2026-08-10]]\n\nSquid rejects CONNECT to the registry on first run.\n";
|
||||
const f = app.vault.getAbstractFileByPath("<ticket path>");
|
||||
app.vault.process(f, d => d.replace("\n## As Built", note + "\n## As Built"));'
|
||||
```
|
||||
For a note long enough to fight the shell quoting, put the JS in a file and pass `code="$(cat note.js)"` — command-substitution output is not re-expanded, so backticks and `$` in the JS are safe. Wrap any `await` in an async IIFE; `eval` runs the code as a plain script.
|
||||
4. **Stall it** if you hit a question only the user can answer: `tn update '<path>' --add-tags needs-info`, and log the question under `## Notes`. Leave `status` at `In Progress` — the ticket is still yours, it just cannot proceed. Remove the tag once answered.
|
||||
5. **Close it** — `tn update '<path>' --status Done`, or `--status 'Wont Do'` when abandoned. Before closing: add `## As Built` if the implementation deviated, remove `needs-info` if present, and swap the actor tag if the other actor did the work.
|
||||
|
||||
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
|
||||
|
||||
The frontier of one effort:
|
||||
|
||||
```sh
|
||||
tn list --filter 'projects:contains:"[[Network isolation]]"' --json \
|
||||
| jq '[.data.tasks[] | select((.isBlocked | not) and .status == "Open" and (.archived | not))]'
|
||||
```
|
||||
|
||||
The **agent frontier** — what an agent may pick up right now. Open, unblocked, agent work, and not stalled:
|
||||
|
||||
```sh
|
||||
tn list --filter 'projects:contains:"[[Network isolation]]" AND tags:contains:agent-step AND status:is:Open AND archived:false AND dependencies.isBlocked:false AND tags:does-not-contain:needs-info' --json
|
||||
```
|
||||
|
||||
Every effort in a repo: `tn list --filter 'projects:contains:"[[atribot]]"' --json`
|
||||
|
||||
**Awaiting a human** — both axes reach the human, so the inbox is their union:
|
||||
|
||||
```sh
|
||||
tn list --filter '(tags:contains:human-step OR tags:contains:needs-info) AND status:is-not:Done' --json
|
||||
```
|
||||
|
||||
The `status` filter is not optional there. Actor tags are durable, so without it the query returns every human step ever completed.
|
||||
|
||||
`--filter` returns completed and archived tasks too, so filter on status when you want live work.
|
||||
|
||||
The dependency fields are spelled differently in the JSON than in a filter expression, which is the easy thing to get wrong here:
|
||||
|
||||
- **In the JSON**, every task carries top-level `isBlocked` and `isBlocking` booleans — that is why the frontier query above reads `.isBlocked`, not `.dependencies.isBlocked`. There is no `dependencies` object in the response. The `blockedBy` and `blocking` arrays appear only on tasks that actually have them.
|
||||
- **In a `--filter` expression**, those same booleans are `dependencies.isBlocked` and `dependencies.isBlocking`. Bare `isBlocked` is not a filter property; it fails outright with `Filter parsing error: Unknown property: isBlocked`.
|
||||
|
||||
So the frontier can be narrowed server-side instead of in jq:
|
||||
|
||||
```sh
|
||||
tn list --filter 'projects:contains:"[[Network isolation]]" AND dependencies.isBlocked:false AND status:is:Open AND archived:false' --json
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
`tn create` covers creation entirely, custom properties and body included. The split only matters afterwards:
|
||||
|
||||
- **`tn update`** for everything TaskNotes models — status, priority, dates, estimate, tags, contexts, projects, `blockedBy` — and all queries. It goes through the update service, which maintains `date_modified` and recurring-instance bookkeeping.
|
||||
- **`obsidian`** for what `tn update` has no flag for: custom properties after creation (`agent_session`, `git_branch`, `git_worktree`) and body text. `obsidian property:set` writes YAML directly and skips the update service.
|
||||
|
||||
Mid-flight dependency edits are rare, since blockers are written at creation. When one is needed:
|
||||
|
||||
```sh
|
||||
tn update '<ticket path>' --add-blocked-by '[[Verify internal network publish]]'
|
||||
tn update '<ticket path>' --remove-blocked-by 'Verify internal network publish'
|
||||
```
|
||||
|
||||
Targets compare by resolved note path, so the bracketed and bare forms name the same ticket, and removing the last blocker deletes the `blockedBy` key.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Create a new ticket rather than renaming a published one.** Every inbound `projects` and `blockedBy` edge points at the title.
|
||||
- **Change an indexed note through `tn` or `obsidian`,** never by editing the file after it is created. Obsidian holds the live index and rewrites the file underneath a direct edit.
|
||||
- `obsidian task` and `obsidian tasks` operate on markdown checkboxes and have nothing to do with TaskNotes.
|
||||
|
||||
Why the model is shaped this way, and what was rejected on the way here: [README.md](./README.md).
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 420 B |
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: tasknotes-cli
|
||||
description: Query, create, and update TaskNotes tasks and projects from the command line with `tn`, the TaskNotes CLI.
|
||||
---
|
||||
|
||||
# TaskNotes CLI (`tn`)
|
||||
|
||||
`tn` is a thin **client**. It does not read the vault's markdown — it talks to an HTTP API served by the TaskNotes plugin running **inside Obsidian** (`localhost:9898`, bearer token). Obsidian must be open. In sandboxed environments, (such as Codex) `localhost` may refer to the sandbox and cannot reach the host. In this case, request permission to run the command from outside the sandbox.
|
||||
|
||||
Handle `Cannot connect to TaskNotes API` according to where it occurred:
|
||||
|
||||
- In Claude Code's Bash sandbox, a `tn` call inside `$(command substitution)` or a `for` loop can fail even while a plain pipeline works. Re-run the same query once as a bare `tn ... | jq ...` pipeline.
|
||||
- If the bare command is blocked because sandboxed `localhost` cannot reach the host, request permission to run it outside the sandbox.
|
||||
- If a bare command outside that limitation still cannot connect, no server is listening: Obsidian is not running or the API is disabled in plugin settings. Report that result; do not retry command variants or attempt to start or reconfigure Obsidian.
|
||||
|
||||
## Vocabulary comes from config, not from this file
|
||||
|
||||
Statuses and priorities are user-defined and matched **exactly, capitalization included**. Run `tn filter-options` to see the live set along with the tags, contexts, and projects actually in use. You can provide `--json` for a structured output.
|
||||
|
||||
Current vault values:
|
||||
|
||||
- Statuses: `Open`, `In Progress`, `Done`, `Wont Do`
|
||||
- Priorities: `0-None`, `1-Low`, `2-Normal`, `3-High`, `4-Urgent`
|
||||
- Contexts: @Coding, @Obsidian, @Tiltshift
|
||||
|
||||
## Task IDs
|
||||
|
||||
A task ID is its vault-relative path: `TaskNotes/Tasks/Check for eggs.md`. Read it from the `ID:` line in text output, or:
|
||||
|
||||
```sh
|
||||
tn list --json | jq -r '.data.tasks[].path'
|
||||
```
|
||||
|
||||
Always quote an ID — the paths contain spaces.
|
||||
|
||||
## Querying
|
||||
|
||||
```sh
|
||||
tn list # incomplete tasks only, 20 max
|
||||
tn list --today # scheduled today
|
||||
tn list --overdue # due before now and not complete
|
||||
tn list --completed
|
||||
tn list --limit 50 --json
|
||||
```
|
||||
|
||||
`--filter` takes an expression and **cannot be combined with `--today`, `--overdue`, or `--completed`** — the CLI errors out.
|
||||
|
||||
```sh
|
||||
tn list --filter 'priority:4-Urgent AND tags:home'
|
||||
tn list --filter '(priority:4-Urgent OR priority:3-High) AND archived:false'
|
||||
tn list --filter 'due:before:2026-09-01 AND status:is-not:Done'
|
||||
tn list --filter 'title:contains:"west wall"'
|
||||
tn list --filter 'dependencies.isBlocked:false AND status:is-not:Done'
|
||||
tn list --filter 'blockedBy:contains:"[[Pour the footings]]"'
|
||||
```
|
||||
|
||||
Two things about `--filter` that will bite otherwise:
|
||||
|
||||
- It applies **no default exclusions**. Plain `tn list` hides completed tasks; `--filter` does not. Add `status:is-not:Done` and `archived:false` when you mean active tasks.
|
||||
- `--limit` is applied client-side *after* every match is fetched, so it caps display, not work.
|
||||
|
||||
`--json` returns `{ success, data: { tasks, total, filtered, count, vault }, meta: {...} }` — tasks live under `.data.tasks`. Progress lines (`- Fetching tasks...`, `✔ Found 111 tasks`) go to **stderr**, so `tn ... --json | jq` works without redirecting; `2>&1 | jq` is what breaks it.
|
||||
|
||||
### Dependency fields: JSON vs. filter
|
||||
|
||||
The blocked/blocking booleans are named one way in the response and another way in a filter expression. Both spellings are correct in their own place:
|
||||
|
||||
| | JSON (`jq`) | `--filter` expression |
|
||||
|---|---|---|
|
||||
| blocked | `.isBlocked` | `dependencies.isBlocked` |
|
||||
| blocking | `.isBlocking` | `dependencies.isBlocking` |
|
||||
|
||||
There is **no `dependencies` object in the JSON** — `.dependencies.isBlocked` is always null. Conversely, bare `isBlocked` is not a filter property and errors with `Unknown property: isBlocked`.
|
||||
|
||||
`isBlocked` and `isBlocking` are on every task. The related arrays are present only when non-empty: `blocking` is a list of task paths, and `blockedBy` is a list of objects, not strings:
|
||||
|
||||
```json
|
||||
"blockedBy": [ { "uid": "Pour the footings", "reltype": "FINISHTOSTART" } ],
|
||||
"blocking": [ "TaskNotes/Tasks/Paint the trim.md" ],
|
||||
"isBlocked": true,
|
||||
"isBlocking": false
|
||||
```
|
||||
|
||||
`blockedBy` and `blocking` are also filter properties in their own right (`blocking:not-empty`, `blockedBy:contains:"[[Pour the footings]]"`).
|
||||
|
||||
### Filter syntax
|
||||
|
||||
`property:value` (operator inferred) or `property:operator:value`, combined with `AND` / `OR` and parentheses.
|
||||
|
||||
Properties: `title`, `path`, `status`, `priority`, `tags`, `contexts`, `projects`, `blockedBy`, `blocking`, `due`, `scheduled`, `completedDate`, `dateCreated`, `dateModified`, `archived`, `hasSubtasks`, `dependencies.isBlocked`, `dependencies.isBlocking`, `timeEstimate`, `recurrence`, `status.isCompleted`. These aliases are rewritten for you: `tag`, `context`, `project`, `created` (→ `dateCreated`), `modified` (→ `dateModified`), `completed` (→ `completedDate`), `estimate` (→ `timeEstimate`).
|
||||
|
||||
Operators: `is`, `is-not`, `contains`, `does-not-contain`, `is-before`, `is-after`, `is-on-or-before`, `is-on-or-after`, `is-empty`, `is-not-empty`, `is-greater-than`, `is-less-than`, `is-greater-than-or-equal`, `is-less-than-or-equal`, `is-checked`, `is-not-checked`. The short forms `before`, `after`, `on-or-before`, `on-or-after`, `greater-than`, `less-than`, `greater-than-or-equal`, `less-than-or-equal`, `empty`, `not-empty`, `checked`, `not-checked` all map onto those.
|
||||
|
||||
Gotchas:
|
||||
|
||||
- Unquoted values may only contain letters, digits, `_`, `.`, `-`, and `/`. Anything with a space, `[`, or `]` must be double-quoted *inside* the expression: `--filter 'projects:contains:"Barn painting"'`.
|
||||
- Booleans (`archived`, `status.isCompleted`, `hasSubtasks`, `dependencies.isBlocked`, `dependencies.isBlocking`): `archived:true` matches archived; any other value matches not-archived.
|
||||
- A bare `property:empty`, `not-empty`, `checked`, or `not-checked` parses as that operator with no value — `blockedBy:not-empty` is complete as written.
|
||||
- A misspelled **property** is caught by the CLI whitelist and fails loudly — `Filter parsing error: Unknown property: foo`, non-zero exit, no JSON. A bogus **value** does not: `status:is:Bogus` returns `success: true` with zero tasks, so a typo there looks like "no matches".
|
||||
|
||||
### Search and stats
|
||||
|
||||
`tn search <query>` is a client-side substring match over title, details, tags, contexts, projects, and path. It only sees non-archived, incomplete tasks. `--json` returns the same envelope as `tn list`, and `--limit` (default 20) caps what is shown; the message below the results counts the matches it left out.
|
||||
|
||||
`tn stats` gives counts and insights. `tn filter-options` lists the available filter values.
|
||||
|
||||
## Creating
|
||||
|
||||
Two forms. Natural language for quick capture; `--title` and flags when the task needs a body, custom frontmatter, or a specific folder. Give one or the other — text plus `--title` is not a combination.
|
||||
|
||||
### From natural language
|
||||
|
||||
```sh
|
||||
tn 'Replace planer belt tomorrow @shop +Shop maintenance'
|
||||
tn create 'Call the vet about the goats due friday high priority'
|
||||
```
|
||||
|
||||
The text is parsed as natural language server-side. Triggers enabled in this vault: `#tag`, `@context`, `+project`, `*status`.
|
||||
|
||||
- **`!priority` is disabled here.** Write "high priority" in the text, or set it afterwards with `tn update --priority 3-High`.
|
||||
- Bare dates become **scheduled**, not due (`nlpDefaultToScheduled` is on). Say "due friday" explicitly for a due date.
|
||||
- Also parsed: `tomorrow`, `friday`, `next week`, `2026-12-25`; estimates `2h`, `30min`; recurrence `daily`, `every monday`.
|
||||
- **Single-quote the whole task text.** Unquoted `#` starts a shell comment and `!` triggers history expansion.
|
||||
|
||||
The command prints the parsed fields — check them rather than assuming the phrasing landed.
|
||||
|
||||
### From flags
|
||||
|
||||
```sh
|
||||
tn create --title 'Split obsync into its own container' \
|
||||
--folder 'TaskNotes/Dev/atribot/Network isolation' \
|
||||
--scheduled 2026-08-12 \
|
||||
--contexts Obsidian \
|
||||
--projects '[[Network isolation]]' \
|
||||
--tags agent-step \
|
||||
--blocked-by '[[Verify internal network publish]]' \
|
||||
--prop ticket_type=implementation \
|
||||
--details-file - <<'EOF'
|
||||
## What to build
|
||||
|
||||
- [ ] Move the compose service into its own namespace
|
||||
EOF
|
||||
```
|
||||
|
||||
| Flag | Notes |
|
||||
|---|---|
|
||||
| `--details <text>` / `--details-file <path>` | Note body. `--details-file -` reads stdin. |
|
||||
| `--prop <key=value>` | Custom frontmatter property. Repeatable. |
|
||||
| `--contexts` `--projects` `--tags` `--blocked-by` | Comma-separated, splitting the same way as `tn update` — commas inside `[[...]]` are literal. |
|
||||
| `--status` `--priority` `--due` `--scheduled` `--estimate` | Same values as `tn update`. |
|
||||
| `--folder <dir>` | Vault-relative destination. See below. |
|
||||
| `--json` | Emit the created task, including its final path. |
|
||||
|
||||
- The `task` tag is added automatically; `--tags` merges with it, so pass only the tags beyond it.
|
||||
- `--blocked-by` writes the full `{uid, reltype: FINISHTOSTART}` shape.
|
||||
- Date keys are mapped for you: `--scheduled` lands as `date_scheduled` in the frontmatter.
|
||||
- Omitting `--scheduled` leaves the plugin's default, which is **today**, not empty.
|
||||
|
||||
#### `--folder`
|
||||
|
||||
The plugin ignores any folder in the create payload and always files new notes into its configured tasks folder (`TaskNotes/Tasks`). `--folder` works around that server-side gap: `tn` creates the task, then moves the file and reports the final path.
|
||||
|
||||
- The folder is validated **before** the task is created — absolute paths and `..` are rejected — so a bad `--folder` cannot strand a note.
|
||||
- Missing directories are created.
|
||||
- When a note already exists at the destination, `tn` **aborts without overwriting** and reports that the new task is sitting in the tasks folder instead. Both notes survive. Treat creation as incomplete and report both paths; do not move, rename, or delete either note unless the caller's storage model explicitly defines a recovery. In particular, dev-ticket titles are immutable once created, so follow the dev-ticket collision procedure rather than renaming the stranded task.
|
||||
- `--folder` applies to the natural-language form too.
|
||||
|
||||
## Updating
|
||||
|
||||
```sh
|
||||
tn update 'TaskNotes/Tasks/Water nut trees.md' --status 'In Progress'
|
||||
tn update <id> --priority 3-High --due 2026-08-20 --estimate 90
|
||||
tn update <id> --title 'Water the nut trees' --scheduled 2026-08-21
|
||||
```
|
||||
|
||||
`--status`, `--priority`, `--due`, `--scheduled`, `--title`, and `--estimate` travel in one `PUT`. `--estimate` is in minutes, and at least one flag is required.
|
||||
|
||||
The list flags each go to their own endpoint, after that `PUT` and one request per field:
|
||||
|
||||
```sh
|
||||
tn update <id> --add-tags 'urgent,bug' --remove-tags 'later'
|
||||
tn update <id> --add-contexts Coding --add-projects '[[Barn painting]]'
|
||||
tn update <id> --add-blocked-by '[[Pour the footings]]' --remove-blocked-by '[[Old blocker]]'
|
||||
```
|
||||
|
||||
- Values split on commas **except inside `[[...]]`**, so a wikilink may contain a comma.
|
||||
- Projects and dependencies compare by resolved note path: `[[Barn painting]]` and `Barn painting` are one target, and removing the last entry deletes the frontmatter key.
|
||||
- Adds are a set union — re-adding an existing value is a no-op, not a duplicate.
|
||||
- Removing the `task` identification tag returns 400 rather than de-tasking the note. An unresolvable `--add-blocked-by` target returns 400 too.
|
||||
- These endpoints arrived after plugin 4.12.3. Against an older plugin `tn` reports *"This version of the TaskNotes plugin has no list-modification endpoints"* — that means update the plugin, not that the task is missing.
|
||||
|
||||
State changes:
|
||||
|
||||
- `tn complete <id>` — marks complete; warns instead of acting if it already is.
|
||||
- `tn toggle <id>` — cycles status. `In Progress` is excluded from the cycle here, so this goes `Open` ⇄ `Done`. Reach `In Progress` with `tn update --status 'In Progress'`.
|
||||
- `tn archive <id>` — toggles the archive flag. Archived tasks **move to `TaskNotes/Archive/`, so the task ID changes**. Re-query before acting on it again.
|
||||
- `tn recurring-complete <id> <YYYY-MM-DD>` — completes one occurrence of a recurring task. Use this, not `tn complete`, for anything with a `recurrence`.
|
||||
- `tn delete <id> --force` — `--force` is mandatory (there is no interactive prompt).
|
||||
|
||||
## Projects
|
||||
|
||||
Projects are **not first-class objects**. They exist only as `projects:` frontmatter wikilinks on tasks (`- "[[Barn painting]]"`), and a project "exists" as long as some task references it.
|
||||
|
||||
**`tn projects create` always fails** — it is hard-coded to error out. Create a project by assigning it:
|
||||
|
||||
```sh
|
||||
tn 'Paint the trim +[[Barn painting]]'
|
||||
tn update '<task path>' --add-projects '[[Barn painting]]'
|
||||
```
|
||||
|
||||
Use `+[[Wikilink]]` for any multi-word project — bare `+Barn painting` parses as project `Barn` and leaves `painting` in the title.
|
||||
|
||||
`--period` is accepted by the parser but does nothing.
|
||||
|
||||
```sh
|
||||
tn projects list # derived from up to 1000 active tasks
|
||||
tn projects show 'Barn painting'
|
||||
tn projects stats 'Barn painting'
|
||||
```
|
||||
|
||||
All three compare names with `[[ ]]` stripped, so a name copied out of `list` matches whether or not you bracket it. Both `show` and `stats` skip archived tasks, and under `--json` `stats` returns a zeroed stats object rather than prose when nothing matches.
|
||||
|
||||
## Never do
|
||||
|
||||
- **Never run bare `tn` or `tn interactive`** — they launch a full-screen TUI and hang a non-interactive shell. Always pass a subcommand or quoted task text.
|
||||
- Never `tn delete` without an explicit request.
|
||||
- Don't retry variants on a connection error; Obsidian isn't running.
|
||||
- Don't hand-edit `TaskNotes/*.md` frontmatter to do something the CLI can do — Obsidian holds the live index and will overwrite you.
|
||||
- Time tracking (`tn timer`) and Pomodoro (`tn pomodoro`) are out of scope for this skill.
|
||||
@@ -19,3 +19,8 @@ history_filter = [
|
||||
## Default filter mode can be overridden with the filter_mode setting.
|
||||
#filters = [ "global", "host", "session", "session-preload", "workspace", "directory" ]
|
||||
filters = [ "global", "workspace", "directory" ]
|
||||
|
||||
[tmux]
|
||||
enabled = true
|
||||
width = "80%"
|
||||
height = "60%"
|
||||
|
||||
@@ -34,7 +34,7 @@ function __dev_create_session -a session dir
|
||||
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" 'claude' Enter
|
||||
tmux send-keys -t "$right" 'dev-claude' Enter
|
||||
|
||||
tmux select-pane -t "$bottom_right"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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" used to do here, so a fish
|
||||
# function or alias named claude still can't shadow the real binary.
|
||||
# "command" itself cannot be used: fish rejects "exec $launch ..." when the
|
||||
# expansion starts with a builtin ("The expanded command is a keyword").
|
||||
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
|
||||
|
||||
# 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"
|
||||
exec $launch --resume $id $argv[3..]
|
||||
end
|
||||
exec $launch --session-id $id $argv[3..]
|
||||
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
|
||||
exec $launch $argv
|
||||
end
|
||||
end
|
||||
|
||||
exec $launch --session-id (uuidgen) $argv
|
||||
end
|
||||
@@ -70,7 +70,7 @@ function dev-wt-pr -d "Open a GitHub PR in a worktree in a new tmux window (Octo
|
||||
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" 'claude' Enter
|
||||
tmux send-keys -t "$right" 'dev-claude' Enter
|
||||
|
||||
tmux select-window -t "$win"
|
||||
end
|
||||
|
||||
@@ -7,9 +7,9 @@ promptToReturnFromSubprocess: false # removes "press enter to return to lazygit"
|
||||
notARepository: 'skip'
|
||||
git:
|
||||
autoForwardBranches: "none"
|
||||
pagers:
|
||||
- pager: delta --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"
|
||||
- pager: delta --paging=never --line-numbers --no-gitconfig --light
|
||||
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:
|
||||
editPreset: "nvim"
|
||||
gui:
|
||||
|
||||
@@ -19,7 +19,5 @@ pnpm = "latest"
|
||||
"npm:typescript-language-server" = "latest"
|
||||
"npm:typescript" = "latest"
|
||||
"github:Satty-org/Satty" = "0.20.1"
|
||||
"github:DarthSim/overmind" = "latest"
|
||||
"github:F1bonacc1/process-compose" = "latest"
|
||||
"github:modem-dev/hunk" = "latest"
|
||||
"npm:@earendil-works/pi-coding-agent" = "latest"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Resource ceiling for Claude Code sessions, applied via dev-claude.fish.
|
||||
#
|
||||
# Why: a runaway subprocess tree under Claude (parallel vitest/tsc/eslint) could
|
||||
# exhaust RAM and drive the machine into zram thrash, which locked up the desktop
|
||||
# hard enough that rebooting was the only recovery. Capping the tree here means it
|
||||
# gets OOM-killed inside this cgroup while sway and everything else stay alive.
|
||||
#
|
||||
# Limits are on the SLICE, not on each scope, so all concurrent sessions share one
|
||||
# budget. Per-scope limits would let four sessions claim 4x the ceiling and defeat
|
||||
# the point.
|
||||
#
|
||||
# Full write-up: ~/.dotfiles/docs/claude-resource-limits.md
|
||||
|
||||
[Unit]
|
||||
Description=Claude Code sessions (resource-capped)
|
||||
|
||||
[Slice]
|
||||
# Soft ceiling: throttle and reclaim first, so there is back pressure before
|
||||
# anything is killed.
|
||||
MemoryHigh=10G
|
||||
|
||||
# Hard ceiling: OOM-kill happens inside this cgroup. Leaves ~16G of the 30G total
|
||||
# for the rest of the system, which normally sits around 13G.
|
||||
MemoryMax=14G
|
||||
|
||||
# Swap is 8G of zram ONLY. Uncapped, this slice can push pages into zram, which
|
||||
# itself occupies physical RAM -- that feedback loop is the thrash spiral. Keep
|
||||
# Claude out of most of the pool.
|
||||
MemorySwapMax=2G
|
||||
|
||||
# Only bites under contention, so nothing is slowed down when the box is idle.
|
||||
CPUWeight=50
|
||||
|
||||
# Reserve roughly 4 of 16 cores for sway and the compositor unconditionally.
|
||||
CPUQuota=1200%
|
||||
|
||||
# Bound pid exhaustion from a runaway worker pool.
|
||||
TasksMax=2048
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Run tn under the global (lts) node, bypassing the mise shim which
|
||||
# would otherwise resolve node from the current directory's pin.
|
||||
# Also handles tn-fzf: symlink it to this script; dispatches on $0.
|
||||
bin="$HOME/.local/share/mise/installs/node/lts/bin"
|
||||
PATH="$bin:$PATH" exec "$bin/${0##*/}" "$@"
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
tn
|
||||
+4
-1
@@ -89,9 +89,12 @@ set -g @plugin 'tmux-plugins/tmux-continuum'
|
||||
|
||||
# Session persistence (resurrect + continuum)
|
||||
set -g @continuum-restore 'on'
|
||||
set -g @continuum-save-interval '10'
|
||||
set -g @continuum-save-interval '5'
|
||||
set -g @resurrect-capture-pane-contents 'on'
|
||||
set -g @resurrect-strategy-nvim 'session'
|
||||
# Claude panes are saved as `claude --session-id <uuid>`; dev-claude turns that
|
||||
# saved command line back into a resume of that exact conversation.
|
||||
set -g @resurrect-processes '"claude->dev-claude *"'
|
||||
|
||||
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
|
||||
Reference in New Issue
Block a user