diff options
Diffstat (limited to '.claude')
| -rwxr-xr-x | .claude/hooks/validate-el.sh | 1 | ||||
| -rw-r--r-- | .claude/rules/cross-project.md | 2 | ||||
| -rw-r--r-- | .claude/rules/daily-drivers.md | 49 | ||||
| -rw-r--r-- | .claude/rules/emacs.md | 6 | ||||
| -rw-r--r-- | .claude/rules/interaction.md | 4 | ||||
| -rw-r--r-- | .claude/rules/locating-craig.md | 48 | ||||
| -rw-r--r-- | .claude/rules/todo-format.md | 101 | ||||
| -rw-r--r-- | .claude/rules/triggers.md | 7 | ||||
| -rw-r--r-- | .claude/rules/working-files.md | 2 | ||||
| -rw-r--r-- | .claude/scripts/coverage-summary.el | 10 |
10 files changed, 218 insertions, 12 deletions
diff --git a/.claude/hooks/validate-el.sh b/.claude/hooks/validate-el.sh index 2529fccb8..8e464577a 100755 --- a/.claude/hooks/validate-el.sh +++ b/.claude/hooks/validate-el.sh @@ -104,6 +104,7 @@ if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then -L "$PROJECT_ROOT/tests" \ -L "$PROJECT_ROOT/themes" \ --eval '(package-initialize)' \ + --eval "(cd \"$PROJECT_ROOT/tests\")" \ -l ert "${load_args[@]}" \ --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then # Terminal gets a compact summary (the run tally + the failing test names); diff --git a/.claude/rules/cross-project.md b/.claude/rules/cross-project.md index ed4a19c5e..caceec9bd 100644 --- a/.claude/rules/cross-project.md +++ b/.claude/rules/cross-project.md @@ -35,7 +35,7 @@ Two acceptable outcomes: ``` Output filenames follow `YYYY-MM-DD-HHMM-from-<this-project>-<slug>.<ext>` automatically, so the target's next session sees the source + timestamp at a glance without you having to construct the name. Fall back to `Write`/`Edit` only when the script isn't available (e.g. a freshly-cloned project before the first startup-rsync). -2. **"Switch projects"** — stop. Let the user reopen Claude in the right cwd. +2. **"Switch projects"** — stop. Let the user reopen the agent session in the right cwd. Don't assume which one was meant. Either guess is wrong half the time and the cost of asking once is one short turn. diff --git a/.claude/rules/daily-drivers.md b/.claude/rules/daily-drivers.md new file mode 100644 index 000000000..eeda33fd5 --- /dev/null +++ b/.claude/rules/daily-drivers.md @@ -0,0 +1,49 @@ +# Daily-Driver Machines + +Applies to: `**/*` + +Craig runs exactly two daily-driver machines: **ratio** and **velox**. They are +kept in sync, and an important change made on one usually needs to reach the +other. + +## The Rule + +When you make or notice a change that is **machine-level and important** — +dotfiles, installed tooling, a synced repo's clone or timer setup, a global +config, a systemd unit, a credential, a one-time bootstrap step — consider +whether the *other* daily driver needs the same change, and flag it. Don't +assume a change made on the current machine is live everywhere. + +This is a prompt to think, not a script to run. The agent can't reach the other +machine; the point is to surface "the other daily driver may need this too" at +the moment the change lands, so it doesn't silently drift to one box. + +## How the sync actually happens + +The mechanism depends on what changed: + +- **A tracked repo** (rulesets, dotfiles, a project) — the other machine just + needs a `git pull` (and, for rulesets, a `make install` to relink anything + new). Most changes are this. +- **Dotfiles** — ride the dotfiles repo; the other machine picks them up on its + next stow/pull. +- **A one-time setup** — a new repo clone, a new systemd timer, a freshly + installed tool, a credential — has to be done by hand on each machine. These + are the ones that silently drift, because nothing carries them automatically. + +When the change is the one-time kind, say so explicitly: name the manual step +the other machine still needs. + +## Knowing which machine you're on + +`uname -n` returns the hostname (`ratio` or `velox`). Use it when a reminder is +machine-specific ("on ratio, you still need to …") so the note is actionable +rather than abstract. + +## Current open instance + +The org-roam knowledge-base clone — `git@cjennings.net:roam.git` — plus its +`roam-sync` systemd timer is confirmed set up on **velox**. It still needs +verifying (clone + timer) on **ratio**. This is the last piece before the +"memory sync across machines" work closes (tracked in the rulesets `todo.org`). +Clear this line once ratio is confirmed. diff --git a/.claude/rules/emacs.md b/.claude/rules/emacs.md index 702b40e7b..ae4f7cb2b 100644 --- a/.claude/rules/emacs.md +++ b/.claude/rules/emacs.md @@ -27,3 +27,9 @@ This re-evaluates the file and redefines its `defun`s live. For straight functio 3. Verify: for visual changes, screenshot and read it (the `screenshot.py` tool under `.ai/scripts/` can capture an app off-screen on a headless output); for behavior, eval or exercise it. This replaces the quit → relaunch → re-find-and-load-files cycle for most edits. A real restart stays the gold standard for a guaranteed-clean state — anything touching `:config`, load order, or when in doubt. + +## Don't edit on disk a file the daemon is capturing into + +The reload caveats above are about pushing changes *into* the daemon. The inverse hazard: a tool that edits a file *on disk* while the daemon has an indirect buffer cloned from it. org-capture works through such a buffer, and a disk write (a hand edit, a `git pull` that fast-forwards the file, a `sed`/Write) reverts the base buffer underneath the capture. The capture is left on stale state, can no longer finalize with `C-c C-c`, and a freshly-typed item can be lost or written back against post-edit content. Orphaned `CAPTURE-*` buffers piling up as Craig retries is the visible symptom. + +The roam inbox (`~/org/roam/inbox.org`) is the live case — Craig captures into it constantly, and the inbox workflow's roam mode (Phase D) edits it. Before a disk write to a file the daemon may be capturing into, check first: `.ai/scripts/capture-guard <file>` exits non-zero (and names the buffer) when a live capture is cloned from `<file>`, and exits 0 — safe — when there's no capture or no reachable Emacs. Same principle as the reload rule, one layer out: leave the daemon's live buffers authoritative rather than yanking the file from under them. diff --git a/.claude/rules/interaction.md b/.claude/rules/interaction.md index 1fd0334ff..9148b4ffd 100644 --- a/.claude/rules/interaction.md +++ b/.claude/rules/interaction.md @@ -2,11 +2,11 @@ Applies to: `**/*` -How Claude communicates with the user during a session — choice prompts, status updates, decision points. +How the agent communicates with the user during a session — choice prompts, status updates, decision points. ## No Popup Menus for Choices -When Claude needs the user to pick between options, **do not** use the AskUserQuestion popup. Present the options inline in chat as a numbered list and ask the user to reply with a number. +When the agent needs the user to pick between options, **do not** use the AskUserQuestion popup. Present the options inline in chat as a numbered list and ask the user to reply with a number. **Why:** The popup menu UI sits at the bottom of the chat window and obscures the chat content directly above it — exactly the area the user needs to read to make the choice. Inline numbered options keep the question, the surrounding context, and the proposed text all visible in the same scrollback. diff --git a/.claude/rules/locating-craig.md b/.claude/rules/locating-craig.md new file mode 100644 index 000000000..c327e3fa9 --- /dev/null +++ b/.claude/rules/locating-craig.md @@ -0,0 +1,48 @@ +# Locating Craig + +Applies to: `**/*` + +When a task needs to know where Craig physically is — a local guide, "what's +near me", travel logistics, the timezone or weather for his actual spot, +distance to an appointment — don't ask him. Run the `whereami` command and read +the result. + +## The Rule + +`whereami` scans nearby WiFi access points and geolocates the machine it runs +on. That only tracks *Craig* on the machine that travels with him: **velox**, +his laptop. On any other machine (ratio, the desktop) it reports that machine's +fixed location, not Craig's, so the result is only meaningful on velox. + +The gate is the machine, stated positively: + +1. Check the host with `uname -n`. +2. **On velox** — run `whereami` whenever location matters, instead of asking. + Treat its reverse-geocoded address as Craig's current location. +3. **Any other host** — don't trust `whereami` as Craig's location. Fall back to + asking, or to known context (trip notes, calendar, reminders). + +Prefer running it over asking — Craig confirmed he'd rather the agent just +check. The output gives coordinates, a reverse-geocoded address, and an +OpenStreetMap link; WiFi-centroid drift of a block or two is normal. + +## When whereami can't answer + +If `whereami` fails — no WiFi to scan, no network, the geolocation API or its +BeaconDB fallback unreachable — it can't locate Craig. Fall back to asking or to +known context, exactly as on a non-velox host. Never fabricate or guess a +location from a stale reading. + +## Keep the location out of shared artifacts + +A reverse-geocoded address is personal data. It can drive a task, but it must +not leak into anything team-visible — commit messages, PR descriptions, tickets, +or public docs (see the content-scope rule in `commits.md`). + +## Why + +Craig travels with velox, so on that machine the agent can know his location for +free rather than interrupting to ask. The command and its design (WiFi BSSID +scan → Google Geolocation API with a BeaconDB fallback → OSM reverse-geocode) +were built 2026-06-24 specifically to replace useless cellular-IP lookup that +reported the carrier gateway instead of the device. diff --git a/.claude/rules/todo-format.md b/.claude/rules/todo-format.md index b9e93bb5a..55530de2c 100644 --- a/.claude/rules/todo-format.md +++ b/.claude/rules/todo-format.md @@ -24,8 +24,8 @@ guessing: The section is mandatory. A `todo.org` without it leaves `[#A]` and the tags undefined, so task-audit can't enforce a vocabulary, task-review can't grade -against agreed semantics, and process-inbox can't file new tasks correctly -(its Phase B.1 already checks for this scheme). Each project defines the +against agreed semantics, and the inbox workflow can't file new tasks correctly +(its priority-scheme check already gates on this scheme). Each project defines the scheme its own way; the floor is that priorities and tags are both spelled out under the header. @@ -33,6 +33,52 @@ When a project's `todo.org` lacks the section, add it before filing or grading further tasks — propose the priority semantics and tag set from the project's existing usage, and confirm with Craig. +### Bug priority from severity × frequency (mandatory where a codebase exists) + +Some projects carry a codebase — source the project maintains under version +control — even when the project isn't primarily a "code project." home and work +both have one. Wherever a project has a codebase, a task representing a bug +*against that codebase* does not get its priority argued: it is **dictated** by +the severity × frequency matrix below. This is not opt-in. Features keep their +per-project `[#A]`–`[#D]` roadmap judgment; codebase bugs do not. + +Two facts set a bug's priority: + +- **Severity** — how bad it is when it occurs (service down / data loss / + security or privacy leak at one end; a cosmetic nit at the other). +- **Frequency** — how often a user hits it (every user every time → rare edge + case). + +``` +| Frequency / Severity | Critical | Major | Minor | Cosmetic | +|------------------------+----------+-------+-------+----------| +| Every user, every time | P1 | P1 | P2 | P3 | +| Most users, frequently | P1 | P2 | P3 | P4 | +| Some users, sometimes | P2 | P3 | P3 | P4 | +| Rare edge case | P2 | P3 | P4 | P4 | +``` + +P-level → priority letter (fixed — not a per-project knob): + +- P1 → `[#A]` +- P2 → `[#B]` +- P3 → `[#C]` +- P4 → `[#D]` + +Release vehicle is illustrative and project-dependent: P1 = current +release/patch, P2 = next patch, P3 = next major, P4 = backlog. A project with no +release train maps the letters and skips the vehicle. A "no open `[#A]` bugs" +release gate therefore means "no open P1." + +Each project still defines, in its scheme header, what +Critical/Major/Minor/Cosmetic and the frequency rows mean *for its own +codebase* — the bands are concrete per codebase, but the matrix structure and +the letter mapping are fixed. + +**Severity-alone carve-out:** privacy or security leaks, compliance violations, +and safety issues are graded on severity alone — one occurrence with the right +consequences is a showstopper no matter how rarely it would be hit. + ## The Rule A todo entry has two parts: @@ -263,3 +309,54 @@ are noise that pollute his `cj:` greps. ** DOING [#A] Kostya's contract :admin:kostya: *** 2026-05-15 Fri @ 14:00:00 -0500 Kostya basis — part-time, 20 hr/week Nerses confirmed 5/15 13:30 CDT: Kostya runs at 20 hr/week part-time, mirroring Vrezh's structure. Plugged into Exhibit A § 2 of the contract draft. + +## Cross-Project Dependency Tags + +A task can be blocked by work that has to happen in a *different project* — a rulesets task that can't finish until `.emacs.d` ships a companion function, say. Left unmarked, two things go wrong: the what's-next workflow keeps recommending the blocked task even though it can't move, and the blocker sits at low priority in the other project, so the dependency stalls silently. + +Two plain org tags track it, one on each side, so neither the waiter nor the blocker loses sight of the dependency: `:blocked:` on the task that's waiting, `:blocker:` on the task that owes the work. The cross-project detail — which project, what work — goes in the task *body*, not a property. This applies to *any* project pair; the convention here and the surfacing in `open-tasks.org` live in the shared rule + workflow layer, not in one project. + +### `:blocked:` — the waiting side + +The task that can't proceed carries `:blocked:`. Its body names the project it's waiting on and what that project owes: + +``` +** DOING [#B] Wrap-teardown feature :feature:blocked: +Blocked on emacsd: needs the ai-term companion functions +(cj/ai-term-quit, -live-count) before the manual validation can run. +``` + +`open-tasks.org` reads the `:blocked:` tag to pull the task out of the "do this next" cascade (it can't be worked) and surface it in a dedicated "Blocked on other projects" section, reading the body for which project to name and nudge. + +### Registering with the blocker — the reciprocal handoff (required) + +Setting `:blocked:` is not complete until the blocking project knows it's blocking. The moment you mark a task `:blocked:` on another project's work, send that project a dependency handoff: + +``` +inbox-send <project> --text "Blocking dependency: <this-project>'s task \"<task>\" is blocked on you — it needs <what>. It stays blocked until this lands. Tag the owning task :blocker: on your side so it surfaces as priority work." +``` + +This is what closes the gap: without it, the blocker only learns it's blocking by accident. The handoff lands in `<project>`'s `inbox/` and its normal inbox processing tags the work (below). A `:blocked:` task with no matching reciprocal handoff is half-done — the dependency is invisible to the one project that can clear it. Skip the send only when the blocker demonstrably already tracks the work (e.g. it's the same handoff that spawned the dependency); it dedups against an existing task either way. + +### `:blocker:` — the blocking side + +When a project processes a blocking-dependency handoff (inbox process mode), it tags the owning task `:blocker:` and names the requesting project in the body: + +``` +** TODO [#B] ai-term wrap-teardown companion :feature:blocker: +Rulesets' wrap-teardown feature is blocked on this — it needs the three +ai-term functions. Surface first so rulesets unblocks. +``` + +The blocking task does *not* carry `:blocked:` — it isn't blocked, it's the blocker. `:blocker:` is a priority signal: `open-tasks.org` surfaces a `:blocker:` task *first*, since clearing it unblocks work in another project, so a dependency that would otherwise stall at low priority gets pulled forward. This is the "surface dependencies first" half of the design. + +### Resolving the dependency + +When the blocker delivers: + +1. The blocking project completes its `:blocker:` task, drops the `:blocker:` tag, and notifies the waiter (`inbox-send <waiter> --text "Delivered: <what> — you're unblocked."`). +2. The waiting project drops the `:blocked:` tag; the task is workable again. Either side noticing the delivery can lift its own tag — the notification just makes it prompt. + +### Not the same as VERIFY + +`:blocked:` marks "waiting on another *project's* work"; `VERIFY` marks "waiting on Craig's input." If Craig's input is what's needed, it's a VERIFY, not `:blocked:`. And `:blocker:` only ever sits on the project that *owes* the work, never the one waiting. diff --git a/.claude/rules/triggers.md b/.claude/rules/triggers.md index e45e660a2..3c4ea6d19 100644 --- a/.claude/rules/triggers.md +++ b/.claude/rules/triggers.md @@ -8,21 +8,22 @@ Trigger phrases the user can say from any session to invoke a cross-project acti Synonyms: "Launch X", "Open project X", "Switch to project X". -**Action:** run the `ai` script (the Claude Code session launcher, installed at `~/.local/bin/ai`) in single-project mode targeting the named project. +**Action:** run the `ai` script (the agent session launcher, installed at `~/.local/bin/ai`) in single-project mode targeting the named project. ``` ai <project-path> ``` -The `ai` script handles tmux session creation, window placement, and the per-project Claude opening line — see `~/code/rulesets/claude-templates/bin/ai` for the canonical source. +The `ai` script handles tmux session creation, window placement, and the per-project agent opening line — see `~/code/rulesets/claude-templates/bin/ai` for the canonical source. **Resolving X.** Match against project basenames discoverable by `ai` — directories under `~/code/`, `~/projects/`, and `~/.emacs.d` that contain `.ai/protocols.org`. - Exact basename match (case-insensitive) → invoke `ai <path>` directly. +- Dot-stripped match → a dotted basename is addressed with its dots removed, so `emacsd` matches `.emacs.d` and `dotfiles` matches `.dotfiles`. Strip dots from both the spoken name and each candidate basename when comparing; an exact match still wins over a dot-stripped one. (`inbox-send` resolves the same way, so the spoken name is consistent across both.) - No match → list all available basenames, ask which to launch. - Multiple partial matches (X is a substring of two or more candidates) → list the matching basenames, ask which. -Do not guess. The cost of asking once is one short turn; launching the wrong project is a wrong-context Claude session that has to be killed and restarted. +Do not guess. The cost of asking once is one short turn; launching the wrong project is a wrong-context agent session that has to be killed and restarted. ## Why a separate file diff --git a/.claude/rules/working-files.md b/.claude/rules/working-files.md index 9a7270271..243226866 100644 --- a/.claude/rules/working-files.md +++ b/.claude/rules/working-files.md @@ -120,7 +120,7 @@ When the task is marked done: - *Inbox content* — `inbox/` and `daily-prep/` follow their own conventions (dated filenames, processed and moved on cadence). -## Implementation Note for Claude Sessions +## Implementation Note for Agent Sessions When the user starts a new task that's going to produce file artifacts: diff --git a/.claude/scripts/coverage-summary.el b/.claude/scripts/coverage-summary.el index eb30c6633..4b7f5c9c2 100644 --- a/.claude/scripts/coverage-summary.el +++ b/.claude/scripts/coverage-summary.el @@ -91,11 +91,15 @@ missing or malformed." (defun cj/coverage-summary--source-files (source-dir project-root) "Return *.el files directly under SOURCE-DIR, relative to PROJECT-ROOT. -Sorted; compiled files and subdirectories are out of scope." +Sorted. Compiled files and subdirectories are out of scope, as are generated +package files (`*-autoloads.el', `*-pkg.el') -- a build tool writes those, no +test covers them, and counting them as untested source skews the number." (let ((source-dir (file-name-as-directory (expand-file-name source-dir))) (project-root (file-name-as-directory (expand-file-name project-root)))) - (sort (mapcar (lambda (p) (file-relative-name p project-root)) - (directory-files source-dir t "\\.el\\'")) + (sort (seq-remove + (lambda (p) (string-match-p "\\(?:-autoloads\\|-pkg\\)\\.el\\'" p)) + (mapcar (lambda (p) (file-relative-name p project-root)) + (directory-files source-dir t "\\.el\\'"))) #'string<))) (defun cj/coverage-summary--missing (tracked source-dir project-root) |
