aboutsummaryrefslogtreecommitdiff
path: root/claude-rules
diff options
context:
space:
mode:
Diffstat (limited to 'claude-rules')
-rw-r--r--claude-rules/commits.md446
-rw-r--r--claude-rules/cross-project.md12
-rw-r--r--claude-rules/daily-drivers.md66
-rw-r--r--claude-rules/desktop-capture.md60
-rw-r--r--claude-rules/docs-lifecycle.md75
-rw-r--r--claude-rules/emacs.md29
-rw-r--r--claude-rules/host-identity.md20
-rw-r--r--claude-rules/interaction.md56
-rw-r--r--claude-rules/knowledge-base.md29
-rw-r--r--claude-rules/locating-craig.md48
-rw-r--r--claude-rules/org-tables.md5
-rw-r--r--claude-rules/subagents.md72
-rw-r--r--claude-rules/testing.md344
-rw-r--r--claude-rules/todo-format.md315
-rw-r--r--claude-rules/triggers.md7
-rw-r--r--claude-rules/ui-prototyping.md93
-rw-r--r--claude-rules/verification.md16
-rw-r--r--claude-rules/working-files.md24
18 files changed, 972 insertions, 745 deletions
diff --git a/claude-rules/commits.md b/claude-rules/commits.md
index a3ec0f2..3283b0a 100644
--- a/claude-rules/commits.md
+++ b/claude-rules/commits.md
@@ -9,6 +9,7 @@ Claude, Claude Code, Anthropic, or any AI tool. Git uses the configured
`user.name` and `user.email` — do not modify git config to attribute
otherwise.
+
## No AI Attribution — Anywhere
Absolutely no AI/LLM/Claude/Anthropic attribution in:
@@ -19,6 +20,7 @@ Absolutely no AI/LLM/Claude/Anthropic attribution in:
- Code comments
- Commit trailers
- Release notes, changelogs, and any public-facing artifact
+- Document author metadata — an org `#+AUTHOR:` line, YAML frontmatter `author:`, a docx or PDF author property, a byline
This means:
@@ -32,133 +34,34 @@ If a tool, template, or default config inserts attribution, remove it. If
settings.json needs it, set `attribution.commit: ""` and `attribution.pr: ""`
to suppress the defaults.
-## Commit Message Format
-
-Commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/) spec.
-
-### Structure
-
- <type>[optional scope]: <description>
-
- [optional body]
-
- [optional footer(s)]
-
-### Types
-
-- `feat:` — new feature (correlates with MINOR in SemVer)
-- `fix:` — bug fix (correlates with PATCH in SemVer)
-- `refactor:` — code restructuring, no behavior change
-- `perf:` — performance improvement
-- `test:` — adding or updating tests
-- `docs:` — documentation only
-- `style:` — formatting, whitespace, missing semicolons (no code-behavior change)
-- `build:` — build system or external dependencies
-- `ci:` — CI configuration and scripts
-- `chore:` — anything else: tooling, meta, housekeeping
-
-The Conventional Commits spec doesn't mandate the type list. Add a new type only when the existing ones genuinely don't fit and the team will agree on what it means.
-
-### Scope
-
-A scope MAY follow the type, in parentheses, naming the affected area of the codebase: `feat(parser): add ability to parse arrays`. Use a single noun.
-
-### Breaking changes
-
-Either append `!` after the type or scope, or include a `BREAKING CHANGE:` footer (uppercase — required). Both at once is fine and adds detail. `!` alone is enough.
-
- feat!: drop support for Node 6
-
- BREAKING CHANGE: uses JavaScript features not available in Node 6.
-
-### Subject line
-
-Imperative mood. ≤72 characters. No trailing period. The full subject is `<type>[scope]: <description>` — the 72-char limit covers the whole thing.
-
-### Body
-
-Optional. Begins one blank line after the subject. Free-form, multiple paragraphs allowed. Don't hard-wrap body lines — write each paragraph and each bullet as a single logical line and let the renderer (GitHub, Linear, `git log`) soft-wrap. Hard wraps shrink the visible render width in web UIs and cause awkward mid-sentence breaks. The same soft-wrap rule applies to PR bodies.
-
-Skip the body when the subject line covers the change.
-
-### Footers
-
-Optional. One blank line after the body. One per line. Format: `Token: value` or `Token #value` — the git trailer convention. The token uses `-` in place of whitespace (e.g. `Reviewed-by`, `Refs`, `Acked-by`). `BREAKING CHANGE:` is the one token allowed to contain a space, and `BREAKING-CHANGE:` is treated as a synonym.
-
-### How to write the message
-
-Write commit messages as if you're explaining the change to someone debugging a failure six months from now. Focus on what changed and why, not the play-by-play of how you typed it. Short imperative summaries like "Validate input before processing" age better than diary-style notes.
-
-The body, when you need it, is where context belongs — the constraint, bug, or tradeoff that forced the change. Over time the body becomes a lightweight decision log, which is more valuable than perfectly formatted messages.
-
-Commit messages describe what changed and why, not the process that produced the change. Don't reference code review, linting, test runs, or other workflow steps in the body (e.g. "from local review," "review surfaced," "flagged by reviewer"). Reviewers and future archaeologists want the what and the why. How you got there belongs in the PR discussion, not the commit.
-
-### Examples
-
-**Subject only:**
+### Generated documents carry the human author only
- docs: correct spelling of CHANGELOG
+Every document an agent writes or updates — a daily prep, a session log, a
+spec, an explainer, a meeting summary — names the human as its sole author.
+`#+AUTHOR: Craig Jennings`, never `Craig Jennings & Claude`, and never an
+agent's name standing alone.
-**With scope:**
+The failure mode is imitation, not intent. No template stamps this line;
+agents copy the author header from whatever sits nearby — yesterday's prep,
+the workflow file they're reading, the doc next to the one they're writing.
+So a single stray `& Claude` propagates through everything generated
+afterward, and no amount of fixing individual files stops it. Fix the files
+an agent reads, and state the rule here.
- feat(lang): add Polish language
+The stakes are highest where the rule is least visible. A private notes repo
+tolerates a co-author line; many employers do not, and their policy is that
+work product carries employee names alone. An `#+AUTHOR:` line survives
+conversion into docx, a published wiki page, or a PDF handed to a customer,
+so a header written in a scratch document three months ago can surface inside
+a deliverable. Write the header correctly at creation.
-**With body and footer:**
+Two things this rule does not ask for. Don't rewrite historical records —
+archived session logs and past dated documents stay as they are, because
+they're a record of what happened rather than a live artifact. And don't
+relabel a document another agent genuinely authored: if Codex wrote it, the
+byline stays Codex. The rule removes false co-authorship, not true
+authorship.
- fix: prevent racing of requests
-
- Introduce a request id and a reference to the latest request. Dismiss incoming responses other than from the latest request.
-
- Remove timeouts which were used to mitigate the racing issue but are obsolete now.
-
- Refs: #123
-
-**Breaking change with `!`:**
-
- feat(api)!: send an email to the customer when a product is shipped
-
-**Breaking change in footer:**
-
- feat: allow provided config object to extend other configs
-
- BREAKING CHANGE: `extends` key in config file is now used for extending other config files.
-
-## Voice and Focus
-
-Applies to commit bodies, PR descriptions, and PR comments (review replies, follow-up notes, thread responses).
-
-**Write as if to a colleague.** The reader is a teammate who'll see this in `git log`, a PR feed, or a Linear thread. "I" is allowed where natural. Don't sound abstract — name the file, the function, the constraint, the symptom. Press-release voice ("This change improves...") and committee voice ("It is recommended that...") both come out. The message has to read like one engineer talking to another, not like a generated artifact.
-
-**No felt-experience narration.** Don't tell the reader how the change will feel or how often you'll use it. Phrases like "I'll feel this every time I commit", "this will be a relief", "I'm excited about" — these read as performance, not communication. State what changed and let the reader decide what to do with it.
-
-**Don't noun-ify verbs.** "The ask", "a learn", "a reveal", "the spend", "a build" — use the real noun: "the request", "the lesson", "the finding", "the budget", "the system". Verb-as-noun reads as corporate-speak and makes the sentence feel performed.
-
-**No sentence fragments in prose.** Every prose sentence needs a subject and a verb. "Two changes." or "Fix incoming." or "Body as decision log." read as bullet-list shorthand even when they're standing alone in a paragraph. Bullets and headings can be fragments — prose sentences cannot.
-
-**"I" is the author, not the user.** First person is for what *I* did or decided in this commit ("I dropped the legacy fallback because..."). It's not for describing how the software or rule behaves for whoever uses it next. "The dialog only opens if I ask" is wrong when the rule is read by someone else — that "I" becomes ambiguous. Use third-person or passive for behavior: "opens on request", "opens when asked", "opens when the user invokes it". Code and systems are the actor; "I" stays for decisions.
-
-**First person where it fits.** When the subject is you or a decision you made, use "I" ("I added X", "I kept the parameter as `Any` because..."). When the subject is a team decision or shared rationale, "we" fits. When another author's prior work is the subject, name them ("Kostya's PR #116 did X"). Third-person constructions like "This PR introduces X" or "This change restores Y" read as press-release self-narration. The commit *is* the change, so don't announce it. Code and systems can stay third-person when they're the actor ("the guard rejects...", "the serializer returns...") — first person is for describing what you did or decided, not for narrating how the code behaves.
-
-**Brief. Terse is preferred.** A one-sentence body beats a paragraph saying the same thing. If the subject line covers it, skip the body entirely. Cut every clause that restates what the diff or the PR card already shows. Length is not a proxy for care. Rhetorical padding ("worth noting", "it's important to understand") always comes out; keep what a reader will actually use.
-
-**Follow-up approvals stay terse.** A re-review that just confirms prior CHANGES_REQUESTED feedback got addressed should be `Approved.` and nothing more. The fixes are visible in the diff and in the prior review thread, so restating them adds noise. The first round of substantive review gets a real comment. Subsequent sign-offs after fixes do not. Counts as a trivial one-liner under the Step 2 exception, so the draft-file flow can be skipped.
-
-**Kind.** PR comments and review replies are directed at a specific person. Acknowledge them when it fits ("thanks for the review") without pouring it on. When you disagree or push back, frame it as your read rather than a correction ("I think...", "my read was...", "did you mean X?"). Leave room for the other person to have seen something you didn't. A polite question beats a defensive explanation. Kindness is free and makes the next review cheaper.
-
-Focus on what was wrong and what was corrected. Not the mechanics.
-Readers skimming `git log` or a PR want the before-state, the
-after-state, and the reason. They don't need a TypeScript-variance
-lesson, a compiler-inference walkthrough, or a trip through an API's
-internals. Keep the "why" to one sentence unless a subtle invariant
-genuinely needs more.
-
-Don't stack technical terms. A sentence that chains three or more type
-signatures, API names, or compiler concepts reads as a jargon wall.
-Break it into shorter sentences and translate to reader-facing
-language. "The mock returns `Promise<Mission>`, so the resolver's
-argument is `Mission`, not `unknown`" beats the full inference chain
-that produces that signature. Keep the terms a reader will grep for,
-drop the ones that name compiler internals.
## Content scope for public artifacts
@@ -185,277 +88,56 @@ Don't write "per `testing.md`, integration tests must hit a real DB" or "the rul
Edge case: when one of these files *is* the change (a commit in the rulesets repo, an edit to a project's `CLAUDE.md`), describe what changed and why without invoking the wider personal-rules layer around it. The commit can absolutely say "tighten testing rule for legacy code". It shouldn't say "per the personal-rules layer this file is loaded into…".
-Different artifact types carry different content. Don't duplicate.
-
-**PR descriptions:** four sections, in order.
-
-1. **Problem** — what's wrong, with enough detail that a teammate can
- recognize the same failure mode in their own work.
-2. **Fix** — what changed.
-3. **Why this fixes it** — causal link, one or two sentences.
-4. **How it was tested** — skip for proposals, specs, or discussions;
- required for shipped fixes.
-
-The PR is the technical artifact. It carries the detail.
-
-If the project's publishing overlay defines a ticket system, see it for
-ticket-body conventions (a ticket body is typically just the Problem and
-Fix, with the causal why and test verification left to the PR).
-
-**PR review comments** are conversational and don't follow this
-structure — they follow the Voice and Focus rules above.
-
-Verbose preambles, motivational language, and context unrelated to the
-problem belong out. Same conciseness pressure as commit-message bodies.
-
-## Review and Publish
-
-Commits and PRs are team-visible, permanent, and hard to amend once shared
-(especially after push or after a reviewer has replied). Before executing
-`git commit` or `gh pr create`, the change must pass a local code review
-*and* the message must be reviewed by the user. The flow has three steps, in
-order.
-
-### Step 0: pre-flight reconcile (mandatory)
-
-Before reviewing the diff, fetch from the remote and reconcile against the
-upstream of the current branch. Reconciliation can change the working state
-when a rebase brings in upstream commits that touch staged files, and that
-would invalidate Step 1's review. Handling drift first means the review and
-the commit message describe the post-reconcile state.
-
-1. Fetch all remotes:
-
- git fetch --all --prune
-
-2. If the current branch has no upstream (new branch, never pushed), skip
- to Step 1 — there's nothing to reconcile against, and the first push
- sets the upstream.
-
-3. Otherwise, check divergence against `@{u}`:
-
- git rev-list --left-right --count @{u}...HEAD
-
- Output is `<behind>\t<ahead>`. Decide based on the pair:
-
- - **0 behind, anything ahead** — no-op. Continue to Step 1.
- - **Behind only, clean tree** — fast-forward: `git merge --ff-only @{u}`.
- - **Behind only, dirty tree** — surface to the user. Don't auto-stash or
- auto-merge. Offer to commit or stash first, or skip the reconcile and
- proceed knowing the push may need attention later.
- - **Diverged (behind AND ahead)** — surface to the user. Ask whether to
- rebase the local commits onto upstream (default for feature branches),
- merge the upstream branch in (rare; preserves both lines), or skip and
- proceed with the divergence. Don't auto-rebase.
-
-4. **PR flow only.** Also fetch the base branch (usually `main`) and check
- whether the feature branch's base is behind. Surface this informationally;
- don't auto-rebase the feature branch without asking. The "X commits
- behind base" badge on the PR is a follow-up decision, not a reason to
- block publish.
-
-The startup workflow's `git fetch --all --prune` doesn't substitute for
-Step 0. Upstream can advance during a long session, especially across
-machines or with teammates pushing in parallel. Run Step 0 every time the
-publish flow starts.
-
-### Step 1: local code review (mandatory)
-
-Run the `review-code` skill against the change:
-
-- Before a commit: `/review-code --staged`
-- Before a PR: `/review-code` (branch diff against `main` merge-base)
-- Before commenting on someone else's PR: `/review-code <PR#>`
-
-Surface **all** findings to the user: Critical, Important, and Minor.
-
-**Default block:** any Critical or Important finding stops the flow. Fix the
-issues and re-run `/review-code` until the diff is clean. Minor findings are
-shown but do not block.
-
-**Override:** the user can bypass the block with an explicit "proceed anyway"
-(or equivalent wording). Without the explicit override, do not proceed to
-Step 2.
-
-The `review-code` skill already has a Phase 0 eligibility gate that handles
-trivial and ineligible diffs (whitespace-only, revert with obvious
-justification, already-reviewed SHA). Trust that gate; there is no "trivial
-enough to skip review" exemption on top of it.
-
-### Step 2: draft, review, publish
-
-**Voice patterns and the approval gate are two independent decisions.** Don't bundle them.
-
-*Voice patterns are always personal for publish artifacts.* Commit messages, PR titles + bodies, and PR review comments all go out under the user's name, so they always run through `/voice personal` (the full pattern walk — general + Craig's-voice + the artifact-mechanics patterns: first-person rewrite, public-artifact scope flag, praise/correction asymmetry, finding stems), regardless of whether `.ai/` is tracked. These three are personal-voice artifacts by definition — the skill's personal mode exists for exactly them. Pattern #39 (public-artifact scope flag) matters *most* on team-visible artifacts, so it must never be skipped on a PR comment or PR body. There is no "general-voice mode" for publish artifacts.
-
-*The approval gate is the only thing `.ai/`-tracking decides.* Before drafting, run this command:
-
-```
-git ls-files :/.ai/ 2>/dev/null | head -1
-```
-
-The `:/` pathspec anchors the search to the repo root, so the command works from any subdirectory. Without it, running from a subdir returns no matches even when `.ai/` is tracked at the repo root, which silently misclassifies the project.
-
-- **No output** — `.ai/` is gitignored, missing, or empty (the user's personal repos). **Gate applies**: write to `/tmp`, run `/voice personal`, print inline, ask approve / request changes / open in editor, then publish only on explicit approval.
-- **Any output** — one or more files under `.ai/` are tracked (a shared / team repo). **Gate skipped for velocity**: write to `/tmp`, run `/voice personal`, print inline, publish immediately.
-
-Either way the draft runs through `/voice personal` first. The subflows below describe the full gated path. For the gate-skipped path, run the same `/voice personal` pass, then collapse the "Ask: approve, request changes, or open in editor" step — the draft prints inline and the publish step runs immediately afterward.
-
-**For commit messages:**
-
-1. Write the proposed message to `/tmp/commit-<short-slug>.md`.
-2. Run `/voice personal` on the file. Always. The skill walks its full pattern list covering signs of AI writing, universal good-writing rules (Strunk & White, Orwell, Plain English, Garner), and Craig's voice patterns (first-person rewrite, semicolons → periods/commas, contractions, sentence-split on conjunctions, felt-experience cut, sentence-fragment rewrite, terse cut for rhetorical padding, no-emphasis-formatting, public-artifact scope flag, praise/correction asymmetry, finding stems). The commit subject line stays imperative per Conventional Commits — `/voice personal` rewrites the body, not the subject. Skip the pass for purely mechanical commits (a chore version bump, a typo fix) where the subject alone carries the message.
-3. Print the final draft inline in the terminal. Every line, exactly as it'll be committed. No truncation, no summary. State that the skill ran (e.g. "/voice personal — full pattern walk"). If pattern #39 (public-artifact scope) flagged anything, surface those warnings; the user resolves them manually.
-4. Ask: approve, request changes, or open in editor. Wait for an explicit answer. Do not open the file in `emacsclient` (or any editor) by default — print first, edit only if asked.
- - **Approve** → commit with `git commit -F /tmp/commit-<short-slug>.md`.
- - **Request changes** → make them, re-run `/voice personal`, re-print inline, ask again.
- - **Open in editor** → only if the user asks. `emacsclient -n /tmp/commit-<short-slug>.md`. After the editor closes, re-read the file, re-print the contents inline, and ask again.
-
-**For PR descriptions:**
-
-1. Write the title as line 1 and the body below it to `/tmp/pr-<slug>.md`. **Title format:** the conventional-commit subject (`refactor: remove dead if-count-is-not-None check in admin`). If the project defines a publishing overlay with a ticket system, follow it for the ticket suffix in the title and the cross-link line in the body (see the overlay).
-2. Run `/voice personal` on the file. The PR title stays imperative per Conventional Commits — `/voice personal` rewrites the body, not the title.
-3. Print the final draft inline in the terminal. Title on line 1, blank line, then body — exactly as it'll be posted. State that the skill ran. Surface any pattern #39 (public-artifact scope) warnings.
-4. Ask: approve, request changes, or open in editor. Wait for an explicit answer. Do not open the file in `emacsclient` (or any editor) by default.
- - **Approve** → continue to step 5.
- - **Request changes** → make them, re-run `/voice personal`, re-print inline, ask again.
- - **Open in editor** → only if the user asks. `emacsclient -n /tmp/pr-<ticket-or-slug>.md`. After the editor closes, re-read the file, re-print inline, ask again.
-5. Split the file on the first blank line and pass the title and body to `gh pr create --title "..." --body "$(tail -n +3 <file>)"` (or a heredoc) so formatting is preserved. Add `--reviewer <user[,user...]>` in the same call when you already know who should review.
-6. Request reviewers on the new PR if you didn't pass `--reviewer` at create time. Use `gh pr edit <N> --add-reviewer <user>`. If the repo has a `CODEOWNERS` file, GitHub auto-suggests based on touched paths. Still issue the explicit request so the reviewer gets notified. Pick reviewers per the team's convention for the area touched (often documented in the per-repo `CLAUDE.md`). For follow-up PRs, consider tagging the parent PR's author if their context would help. PRs without a human reviewer request stall — "checks passed" is not a substitute for review.
-7. **Project publishing overlay (if present).** If the project defines a publishing overlay — a `publishing-<team>.md` rule loaded from its `.claude/rules/` — run its post-create steps now: ticket cross-linking, ticket-state moves, and any other tracker integration it specifies. A project with no overlay skips this; the PR is already open and reviewers are requested, which is the complete universal flow.
-
-**For PR review comments and replies (review verdicts, threaded discussion, follow-up notes on someone else's PR or your own):**
-
-Pick the shape first. Most reviews are Shape 1.
-
-- **Shape 1 — Single review** (verdict + summary body + 0+ inline pins). The default for any post that carries a verdict (`APPROVE`, `REQUEST_CHANGES`, `COMMENT`), even when the verdict has no line-specific findings. One `gh api` call posts the summary, every inline pin, and the verdict together. review notification fires once for `APPROVE` or `REQUEST_CHANGES`.
-- **Shape 2 — Issue-thread comment** (no verdict). General PR discussion, not a review. No inline pins. No review notification.
-- **Shape 3 — Reply on an existing inline thread**. Responding to a specific prior reviewer comment. Threads under that comment. No review notification.
-
-**Inline threshold for Shape 1.** Any finding that names a `path:line` belongs as an inline comment pinned to that line. Cross-cutting observations (verdict rationale, "third PR with the same pattern", overall test-coverage gaps that don't pin to one place) stay in the summary body. There's no "fold one inline into the summary" exception — a single line-specific finding still goes inline.
-
-**Shape 1: Single review (bundled summary + inline)**
-
-1. Identify findings, split into **inline-eligible** (each names a specific `path:line`) and **summary-only** (cross-cutting). Decide the verdict.
-
-2. Write one concatenated draft to `/tmp/pr-<N>-review.md` with explicit separators:
-
- ```
- === SUMMARY ===
- <verdict summary body>
-
- === INLINE path=frontend/src/foo.tsx line=440 ===
- <inline body 1>
-
- === INLINE path=frontend/src/bar.tsx line=137 ===
- <inline body 2>
- ```
-
- The separator format is exactly `=== SUMMARY ===` and `=== INLINE path=<path> line=<n> ===`. The summary block is mandatory even for verdict-only reviews. Inline blocks are zero-or-more.
-
-3. Run `/voice personal` on the file once. The skill walks its full pattern list across every block at the same time. The separators stay intact because they aren't prose.
-
-4. Print the final draft inline in the terminal. Every block, exactly as it'll be posted, with its separator header. State that the skill ran (e.g. "/voice personal — full pattern walk across summary + 3 inline"). Surface any pattern #39 warnings.
-
-5. Ask: approve, request changes, or open in editor. Wait for an explicit answer. Do not open the file in `emacsclient` (or any editor) by default.
- - **Approve** → continue to step 6.
- - **Request changes** → make them, re-run `/voice personal` on the whole file, re-print inline, ask again.
- - **Open in editor** → only if the user asks. `emacsclient -n /tmp/pr-<N>-review.md`. After the editor closes, re-read, re-print inline, ask again.
-
-6. Split the file on the separator lines and post in **a single** `gh api` call:
-
- ```
- gh api repos/<owner>/<repo>/pulls/<N>/reviews \
- --hostname <ghe-host-or-omit> \
- -F event=REQUEST_CHANGES \
- -F body="<summary block>" \
- -F "comments[][path]=<path1>" \
- -F "comments[][line]=<line1>" \
- -F "comments[][body]=<inline 1>" \
- -F "comments[][path]=<path2>" \
- -F "comments[][line]=<line2>" \
- -F "comments[][body]=<inline 2>"
- ```
-
- `event` is one of `APPROVE`, `REQUEST_CHANGES`, `COMMENT`. The `comments[]` array can be empty for verdicts with zero line-specific findings — the call still uses the same endpoint. Pass `--hostname` for non-`github.com` hosts (a project's publishing overlay names its host when it's a GitHub Enterprise instance).
-
-7. Verify the review landed. `gh api repos/<owner>/<repo>/pulls/<N>/reviews --hostname ...` returns the latest review with bundled inlines. Confirm `state` matches the verdict and the inline count matches what was posted.
-
-8. **Project review-notification overlay (if present).** If the project defines a publishing overlay with a review-notification step (e.g. a Slack ping to the PR author), run it now — but only for `APPROVE` and `REQUEST_CHANGES` verdicts. The overlay owns the channel, the message format, the author-mention lookup, and the threading. A project with no overlay skips notification entirely. `COMMENT` verdicts and Shapes 2-3 below never notify, overlay or not.
-
-**Shape 2: Issue-thread comment (no verdict)**
-
-Use when the post is informal discussion that shouldn't appear as a review verdict (e.g. "I'd like to discuss the X approach before you continue").
-
-1. Write the proposed comment to `/tmp/pr-<N>-comment.md`.
-2. Run `/voice personal`.
-3. Print inline, ask approve/changes/edit, gate as in Shape 1 step 5.
-4. Post: `gh pr comment <N> --body-file /tmp/pr-<N>-comment.md`.
-5. Verify: `gh api repos/<owner>/<repo>/issues/<N>/comments`.
-6. No review notification.
+**Tooling-path enumeration is the same leak.** Citing a rule as authority isn't the only way the tooling layer leaks into history. A commit whose *content* must name these paths — a `.gitignore` adding `.claude/`, `CLAUDE.md`, `.ai/` — has unavoidable, correct file content, but its *message prose* must not enumerate them ("chore: ignore .claude tooling, CLAUDE.md, and session files"). On a public or shared-remote repo that enumeration exposes the tooling layer's structure in the log just as a citation would. Name the category instead: "chore: extend gitignore for local tooling and build artifacts". The same holds for any incidental mention, not only `.gitignore` commits. Two exemptions: a commit whose change *is* one of these files (the edge case above), and private single-user repos with no shared remote, where the history is the project and there's no third party to leak to.
-**Shape 3: Reply on an existing inline thread**
-Use when responding to a specific prior reviewer comment.
+## Write in the first person, as Craig
-1. Find the parent comment ID: `gh api repos/<owner>/<repo>/pulls/<N>/comments`.
-2. Write the reply to `/tmp/pr-<N>-reply-<comment-id>.md`.
-3. Run `/voice personal`.
-4. Print inline, ask approve/changes/edit, gate as in Shape 1 step 5.
-5. Post: `gh api repos/<owner>/<repo>/pulls/<N>/comments -F in_reply_to=<comment-id> -F body="$(cat /tmp/pr-<N>-reply-<comment-id>.md)"`.
-6. Verify in the same `comments` list.
-7. No review notification.
+Everything authored in or about this repo is first person: code comments, commit
+messages, PR descriptions, PR review comments, and any note that lands in the
+repo or its history. State a choice as a choice — "I swept the copies rather
+than repairing them, because a drifted copy outranked the global rule" beats
+"the copies are swept rather than repaired."
-**Approve does not authorize a merge.** Reviewing a PR never authorizes merging it. Anything in `## Merge Strategy` below applies only to merges *you* are about to perform on your own branches — and even then, the merge needs its own explicit user confirmation per the rules there. A project's publishing overlay may add a team merge practice (e.g. approve-then-author-merges, where the review notification hands the merge decision to the PR author); that's an overlay concern, not a global one.
+**The "I" is Craig.** He is the author of record on every commit, comment, and
+review in his repos, and these artifacts go out under his name. So the voice is
+his, writing about his own work — not an agent narrating what it did on his
+behalf. Never write the agent into the prose as a separate party: no "Craig
+asked me to", no "I filed this for Craig", no "needs Craig's decision". Where a
+decision is still open, it is *his* open decision, written as "I haven't decided
+whether…" or "this needs a call I haven't made yet."
-**Exception:** trivial one-liners the user dictated verbatim in the
-conversation (e.g. "commit this as `chore: bump version`", "reply just
-'thanks for the review'") can skip the draft-file step in Step 2.
-`/review-code` in Step 1 still runs when it applies; Phase 0 of that skill
-handles trivial diffs, and acknowledgment-only replies don't need it at all.
+The same holds for anyone else's work. Name them ("Kostya's PR #116 did X"),
+because they are a third party. Craig is not.
-**Single-skill gate.** Each of the three subflows above runs `/voice personal` before printing the draft — the full pattern walk covering AI-writing signs, universal good-writing rules, Craig's voice patterns, and the artifact-mechanics patterns (first-person rewrite, public-artifact scope flag, praise/correction asymmetry, finding stems). Publish artifacts (commits, PR titles + bodies, PR review comments) always use personal mode; the `.ai/`-tracking check at the top of Step 2 decides only whether the approval gate fires, not which patterns run. Running the skill is mandatory; the printed draft must have been through it. When the user asks mid-flow for "the voice pass" on an in-progress draft, that means re-run the full pattern walk — not a subset. Always state that the skill ran when announcing the printed draft (e.g. "/voice personal — full pattern walk"). Skipping the pass without flagging it is a defect. The terse/omit-needless-words cut (pattern #38) is the *last* thing the skill does before the draft is printed: read each sentence and cut it in half, keeping only what changes meaning. The draft the user first sees must already be terse — if they have to ask for an Orwell pass after seeing it, the pass was skipped.
+Third-person constructions like "This change introduces X" or "This PR restores
+Y" read as press-release self-narration. The commit is the change, so it does
+not need announcing.
-**If `/voice` is unavailable.** The skill should be installed (it ships with rulesets), but a fresh or partial environment may not have it. Don't let that block the publish, and don't skip the discipline silently. Walk the same patterns inline — they're documented in the skill, and the publish flow already names which ones matter (first-person rewrite, semicolons → periods/commas, contractions, sentence-split, felt-experience cut, fragment rewrite, terse cut, the pattern #39 public-artifact scope flag, plus the AI-writing and good-writing passes). Then state that the skill was unavailable and the pass was applied by hand (e.g. "/voice unavailable — patterns walked inline"). The gate is the pattern walk, not the tooling; the skill is the convenient way to run it, not the only way. Flag the missing skill so it gets installed.
+**The one carve-out: code is the actor when describing behavior.** A comment
+saying *what the code does* stays third person, because the subject genuinely
+is the code and not me — "the sweep only fires when the global rule exists",
+"the guard rejects a malformed payload". First person is for the decision
+behind it, third person for the behavior itself. Both often belong in the same
+comment: what it does, then why I chose it.
-### Hook-level authorization
+This is the rule the publish flow already applied to commit bodies. It lives
+here because code comments get written constantly and the publish skill is not
+loaded then.
-The Step 1 code review plus the Step 2 user approval together constitute the
-authorization gate for the publish action. No separate hook-level approval
-prompt is needed on `git commit`, `gh pr create`, `git push`, or their
-variants once Step 2 has been approved. If a hook is configured, rely on the
-flow above to be the source of truth; do not treat the hook as a second
-independent gate.
+## The publish flow lives in the `publish` skill
-## Merge Strategy
+Everything about *how* a commit, PR, or review comment gets written, reviewed,
+approved, and published — the pre-flight reconcile, the code-review gate, the
+draft/voice/approval gate, conventional-commit format, Voice and Focus, PR
+description structure, the three review shapes, merge strategy, and the
+pre-commit checklist — is in the `publish` skill. Load it before drafting a
+message, not after: the flow gates what gets written.
-- *Squash-merge is the default* for feature branches. It avoids carrying
- WIP and fix-up commits into the target branch history and produces one
- logical change per merge.
-- State the planned merge approach (squash, rebase, or merge commit) and
- the target branch *before* pushing or merging. Wait for explicit user
- confirmation before `git push`, `gh pr merge`, or any equivalent. The
- Review and Publish flow above approves the *content*; merge strategy is
- a separate decision that needs its own confirmation.
-- *Pre-push reconcile.* Right before `git push`, do one more
- `git fetch <remote> <branch>` and verify the local branch is still
- ahead-only against its upstream. If something landed between Step 0 and
- push (review and draft together can take several minutes, and another
- machine or teammate may push in that window), surface and resolve before
- the push command runs. Catching drift here is cheaper than recovering
- from a failed non-fast-forward push under publish-step pressure.
-- Override the squash default only when there's a concrete reason: a
- clean per-commit review history the user has explicitly asked for, a
- multi-commit semantic narrative the team values, etc. Squash is the
- safe default; document why when deviating.
-
-## Before Committing
-
-1. Check author identity: `git log -1 --format='%an <%ae>'` — should be the user.
-2. Scan the message for AI-attribution language (including emojis and footers).
-3. Review the diff — only intended changes staged; no unrelated files.
-4. Confirm staged files belong in the repo: nothing that the project's policy keeps untracked (the personal-tooling set in gitignore-mode projects), and in repos with a canonical/mirror split, the edit is on the canonical side — a mirror-only edit gets reverted by the next sync.
-5. Run tests and linters (see `verification.md`).
+What stays here is what must hold whether or not anything is being published,
+and where a violation is permanent and reaches other people. If the skill fails
+to load you will have to be told the flow, which is recoverable. The rules
+below are not.
## If You Catch Yourself
diff --git a/claude-rules/cross-project.md b/claude-rules/cross-project.md
index ed4a19c..c5de962 100644
--- a/claude-rules/cross-project.md
+++ b/claude-rules/cross-project.md
@@ -35,7 +35,9 @@ 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.
+
+ The wrap-up cross-project router rides this same sanctioned path: at wrap time, `wrap-it-up.org`'s router step delivers `:ROUTE_CANDIDATE:`-tagged keeper tasks to their home projects' inboxes via `route-batch` → `inbox-send` (never a direct foreign `todo.org` write), and the destination's own inbox processing files each task per its conventions.
+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.
@@ -48,6 +50,14 @@ whose canonical home is `~/code/rulesets/`. When work in a downstream project
needs one of these files to change, a local edit alone is a stopgap that the
next sync reverts. The durable change happens only in the rulesets canonical.
+Installed global paths such as `~/.claude/rules/`, `~/.claude/hooks/`,
+`~/.claude/skills/`, and rulesets-owned entries in `~/.local/bin/` are
+symlinks into the canonical repository, not downstream copies. Never edit
+through those paths from another project's session: resolving the symlink
+would dirty rulesets outside its own logging and wrap discipline. The
+`rulesets-write-boundary.py` hook mechanically denies Edit/Write targets whose
+real path lands there and directs the proposal through `inbox-send rulesets`.
+
The process, every time:
1. **Make the change locally** in the downstream project so it's usable
diff --git a/claude-rules/daily-drivers.md b/claude-rules/daily-drivers.md
new file mode 100644
index 0000000..7ee5f06
--- /dev/null
+++ b/claude-rules/daily-drivers.md
@@ -0,0 +1,66 @@
+# 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.
+
+Both machines are on the same tailnet, so the agent can usually reach the other
+one directly over tailscale ssh — it can sync, verify, or repair the other daily
+driver, not just flag the drift. Reach for that when a change needs to land on
+both boxes now. (This session repaired ratio's dotfiles and verified the fix
+over tailscale; the .emacs.d side has driven ratio the same way — `git fetch` +
+`reset --hard` and an `scp` across.)
+
+When tailscale is down or the other machine is offline, fall back to the
+original discipline: this is a prompt to think, and 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.
+
+## Reaching the other machine over tailscale
+
+`tailscale status` lists every node with its tailscale IP and online state.
+Connect by tailscale IP (e.g. `100.71.182.1`) or MagicDNS name (e.g.
+`ratio.tailf3bb8c.ts.net`) — both always resolve and connect. A bare hostname
+(`ssh ratio`) works only when MagicDNS is configured on the local machine;
+without it the bare name can fail to resolve, which makes the box look
+unreachable when it isn't. Prefer the IP or the full MagicDNS name when in
+doubt. The first connection from a new address fails host-key verification under
+`BatchMode`; add `-o StrictHostKeyChecking=accept-new` to clear it.
+
+## 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 — and after an ssh hop, to confirm which machine you landed
+on.
+
+## Current open instance
+
+None. (The org-roam knowledge-base clone + `roam-sync` timer is confirmed on
+both daily drivers, velox and ratio, as of 2026-06-30.)
diff --git a/claude-rules/desktop-capture.md b/claude-rules/desktop-capture.md
new file mode 100644
index 0000000..c4a67f9
--- /dev/null
+++ b/claude-rules/desktop-capture.md
@@ -0,0 +1,60 @@
+# Off-Workspace Windows and Captures
+
+Applies to: `**/*` (any task that opens a window or takes a screenshot on the user's live desktop)
+
+Never open a window or take a screenshot on the user's active workspace. When
+visual verification needs a real window on the user's live desktop, keep it off
+the workspace they're working in. An agent doing its own visual verification
+shouldn't hijack the desktop the user is actively using.
+
+The principle is environment-general: don't commandeer the user's active
+workspace for agent-side visual work. The recipe below is the Hyprland/Wayland
+implementation; other environments implement the same principle with their own
+off-screen mechanism.
+
+## Captures for your own verification
+
+Render and grab the window off the user's physical screen, then tear it down. On
+Hyprland this is a virtual headless output, verified non-disruptive on ratio
+2026-07-06 — the physical monitor stayed on its workspace, focused, throughout:
+
+```sh
+hyprctl output create headless # virtual output on its own workspace
+setsid <app> >/tmp/x.log 2>&1 </dev/null &
+addr=$(hyprctl -j clients | python3 -c 'import json,sys; print(next((c["address"] for c in json.load(sys.stdin) if c.get("class")=="<CLASS>"), ""))')
+hyprctl dispatch movetoworkspacesilent "<ws-on-headless>,address:$addr" # silent = keeps the user's focus
+grim -o HEADLESS-<n> /tmp/shot.png # capture the virtual output only
+pkill -f '<app>$'; hyprctl output remove HEADLESS-<n> # tear down, restore the display
+```
+
+Key constraint: `grim` captures a *visible output*, so a window merely parked on
+another Hyprland workspace can't be screenshotted — it must render on the
+headless (or another real) output. That's why a headless output, not just
+"another workspace," is the tool for self-captures. A nested compositor
+(weston/cage/sway) is the alternative on non-Hyprland Wayland or when a headless
+output isn't available; it needs the compositor installed.
+
+## Showing the user something
+
+Open it on a *separate* real workspace and tell them which one, so it never
+grabs their active workspace. They switch when ready. Craig's viewer preference
+is `imv`; launch it with `gui-open --image <file>` (dotfiles-shipped) rather
+than a bare `&` job or `hyprctl dispatch exec`: it detaches through `systemd-run
+--user` so the agent shell can't reap it, resolves the current Hyprland instance
+after a compositor restart, and confirms the viewer is mapped and visible before
+returning. An HTML render uses `gui-open <file>` (or `--browser`) the same way.
+If `gui-open` isn't on PATH, the machine needs a dotfiles pull.
+
+## Always clean up
+
+Close the window and remove any headless output afterward. Verify the user's
+display is restored: physical monitor back to its workspace, no orphan
+processes.
+
+## Related
+
+- `verification.md` — this is part of how visual verification is done without
+ disrupting the user.
+- `interaction.md` — the broader "don't disrupt the user's active work" concern.
+- `emacs.md` — the screenshot note for Emacs changes uses the same off-screen
+ capture approach.
diff --git a/claude-rules/docs-lifecycle.md b/claude-rules/docs-lifecycle.md
new file mode 100644
index 0000000..3906d86
--- /dev/null
+++ b/claude-rules/docs-lifecycle.md
@@ -0,0 +1,75 @@
+# Docs Lifecycle
+
+Applies to: `**/*` (any project carrying a `docs/` tree)
+
+How formal documents are separated from working notes, and how a document's
+lifecycle state stays visible without opening the file. Specs are the first
+instance of the shape; the pattern is reusable for any growing collection of
+processed artifacts. Full design: the docs-lifecycle spec in rulesets
+`docs/specs/`.
+
+## The shape (reusable)
+
+1. **Separate formal artifacts from working notes by location.** A formal
+ artifact proposes a buildable change and carries the full spine; everything
+ else is a note.
+2. **Lifecycle state lives in the artifact**, on a scannable, greppable
+ carrier — an org TODO keyword on a top-level status heading — with a dated
+ history of every transition.
+3. **Links use rename-safe identifiers** so a move or rename never orphans
+ inbound references.
+4. A collection **earns this treatment when "which of these are live?" starts
+ requiring a file-by-file read.**
+
+## The spec instance
+
+- `docs/specs/` holds formal specs only — a doc with both a `Decisions`
+ section and an `Implementation phases` section (the spec-create spine).
+ `docs/design/` holds everything else: brainstorms, proposals, inventories,
+ research notes, frozen source material. Spec filenames end `-spec.org`
+ (spec-review's precondition keys on it); no status suffixes ever.
+- Every spec opens with a top-level status heading directly after the file
+ header, carrying the lifecycle keyword, an `:ID:` UUID, and dated history
+ lines (newest first). The keyword header is two sequences, and both lines
+ are required:
+
+ #+TODO: TODO | DONE
+ #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+ The first drives `Decisions` / `Review findings` tasks and their `[/]`
+ cookies; the second is the lifecycle. They share no keyword — never merge
+ them into one line, and never drop the first (that silently breaks the
+ cookies that gate readiness).
+- **The heading keyword is authoritative.** The Metadata table's `Status`
+ field mirrors it in lowercase; on disagreement the heading wins. A
+ transition is three lines in one file — keyword, history line, mirror — and
+ never a rename or a link edit.
+- **Every flip has a named owner:** spec-create stamps `DRAFT`; spec-review
+ flips `DRAFT` → `READY` on a passing gate; spec-response flips `READY` →
+ `DOING` when it decomposes phases into build tasks (stamping the spec's
+ UUID as a `:SPEC_ID:` property on the build parent, and always emitting a
+ final "flip the spec to IMPLEMENTED" task); task-audit flags any `DOING`
+ spec whose `:SPEC_ID:`-bound parent is closed, archived, or missing.
+ Terminal states (`IMPLEMENTED` / `SUPERSEDED` / `CANCELLED`) always carry a
+ stated reason in the history line.
+- **The status board is one grep:**
+
+ rg -H '^\* (DRAFT|READY|DOING|IMPLEMENTED|SUPERSEDED|CANCELLED) ' docs/specs/
+
+- **Legacy compatibility:** projects that haven't run the one-time `spec-sort`
+ retrofit (no `:LAST_SPEC_SORT:` marker in `.ai/notes.org` Workflow State)
+ keep their legacy spec locations reviewable; the `docs/specs/` requirement
+ hardens only after the sort runs.
+- **Cross-doc links to specs are `file:` links for now.** Specs carry `:ID:`
+ UUIDs, but conversion to `[[id:...]]` is a gated follow-up (the Emacs id
+ index has to know about project docs first) — don't convert links ad hoc.
+
+## Watch for
+
+- Editing the `#+TODO:` header down to one sequence — the `[/]` cookies stop
+ computing and readiness gates go vacuous.
+- Bare `[N/N]` tokens in prose or list items — org's cookie updater rewrites
+ them; spell counts out in words outside real cookie positions.
+- A "done" spec whose keyword still says `DOING` — that's the failure this
+ convention exists to prevent; flip it with a history line rather than
+ leaving it for the audit to catch.
diff --git a/claude-rules/emacs.md b/claude-rules/emacs.md
index 702b40e..846888d 100644
--- a/claude-rules/emacs.md
+++ b/claude-rules/emacs.md
@@ -1,3 +1,8 @@
+---
+paths:
+ - "**/*.el"
+---
+
# Working With Craig's Running Emacs
Applies to: `**/*.el` (and any task that edits Craig's Emacs configuration)
@@ -27,3 +32,27 @@ 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.
+
+## SVG Rendering for Emacs App UIs
+
+Consider SVG rendering (`svg.el`) as the default for any Emacs app UI with real visual structure — boards, meters, gauges, status panels (Craig's directive, 2026-07-11). Verified on Emacs 30.2 pgtk via jotto's librsvg spike: `svg.el` cleanly renders rounded rects, linear gradients, fill-opacity, bold/italic/letter-spaced text, stroked shapes, and theme-derived colors.
+
+The payoff is the prototyping pipeline: design the UI as an HTML prototype first (per `ui-prototyping.md` — browser iteration), then port near-1:1 to `svg.el`, since browser SVG and librsvg share primitives.
+
+Constraints to design around:
+
+- **GUI-only.** No tty rendering — ship a second text renderer or accept GUI-only.
+- **The SVG region is an image, not text.** Input stays minibuffer/keymaps; no point-and-click into the drawing.
+- **Theme colors are pulled by hand.** Read them from faces at render time (`face-attribute`); the image inherits nothing.
+- **Whole-image regeneration per state change.** Design the render as a pure state → SVG function so regeneration is cheap to reason about.
+- **Stick to a librsvg-safe subset.** No SMIL, no JS-in-SVG, conservative CSS and filters. `feDropShadow` renders without error but is visually unverified — don't lean on it.
+
+Recommended shape: hybrid layouts — SVG for boards, meters, and visual state; text buffers for prose and transcripts.
+
+Prior art: archsetup's instrument-console widget gallery (`docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html` in archsetup); jotto's spec records the full pipeline and constraint sheet.
diff --git a/claude-rules/host-identity.md b/claude-rules/host-identity.md
new file mode 100644
index 0000000..9f58392
--- /dev/null
+++ b/claude-rules/host-identity.md
@@ -0,0 +1,20 @@
+# Host-Identity Guard
+
+Applies to: `**/*` (any tracked or synced project file)
+
+Never assert mutable environment identity as a fixed fact in a file that git tracks or the template sync distributes. A `CLAUDE.md` or notes file claiming "This machine is ratio", a current OS version, an IP, or "the laptop" lands identical on every machine, so the claim is false everywhere but its origin — and an agent that trusts it reasons backwards the whole session.
+
+## The Rule
+
+- **Don't write fixed identity claims** — "this machine is X", "the current host is X", "we're on the laptop" — in `CLAUDE.md`, `notes.org`, rules files, or any other tracked/synced doc.
+- **Derive identity at runtime and name the command.** The correct phrasing in a doc is an instruction, not a fact: "run `uname -n` to find the hostname." (`uname -n` is the source of truth — the `hostname` binary is often absent, and `uname -r` is the kernel release, not the host.)
+- **Describing the fleet is fine; claiming the current member is not.** "The fleet is ratio (workstation) and velox (laptop)" is a durable fact and belongs in a doc (see `daily-drivers.md`). "This machine is ratio" is a snapshot that rots the moment the file syncs.
+- The same applies to any mutable environment fact: current OS release, current IP, current display topology. State how to derive it, not what it was when the file was written.
+
+## Worked failure
+
+archsetup, 2026-06-21: its `CLAUDE.md` asserted "This machine is **ratio**" as a fixed fact. A session running on velox reasoned from that line all session — skipping velox-only reminders as "not applicable, we're on ratio" — exactly backwards. The fix replaced the claim with the `uname -n` instruction.
+
+## Enforcement
+
+The startup workflow runs a read-only probe that greps `CLAUDE.md` and `.ai/notes.org` for fixed-identity phrasing and surfaces any hit as a startup finding. The probe flags for human judgment; it never blocks. When it fires, replace the claim with the runtime derivation, not a fresher snapshot.
diff --git a/claude-rules/interaction.md b/claude-rules/interaction.md
index 1fd0334..b5798bd 100644
--- a/claude-rules/interaction.md
+++ b/claude-rules/interaction.md
@@ -2,11 +2,50 @@
Applies to: `**/*`
-How Claude communicates with the user during a session — choice prompts, status updates, decision points.
+How the agent reasons with the user and communicates during a session — how interpretations are formed, then how choices, status, and decision points are presented.
+
+## Collaborative Peer Reasoning
+
+Treat the conversation as joint reasoning between peers. The user's words are evidence of intent, not merely a string to execute literally. Use the request, prior context, current state, and likely downstream consequences to form a working interpretation.
+
+### Infer first; clarify at material forks
+
+Infer the intended outcome and proceed when reasonable interpretations lead to the same action. When two plausible interpretations would produce materially different outcomes, strategies, or external effects, state the working interpretation and ask one focused question before crossing that fork. Do not interrupt for reversible implementation details that can be resolved with ordinary judgment.
+
+### Test conclusions before committing to them
+
+Do not promote the first plausible explanation or plan into a conclusion. Check the strongest reasonable alternative, test the assumptions that distinguish it, and weight the tradeoffs that decide between them. Separate verified facts, inferences, and recommendations when the distinction matters. Calibrate confidence instead of projecting certainty.
+
+For a consequential judgment, a useful response shape is: working interpretation, evidence, strongest alternative, recommendation, confidence, and the one clarification that would change the action. Do not force this scaffold onto simple tasks.
+
+### Corrections update the model
+
+Treat a user correction as new evidence that changes the working model. Reconcile its downstream implications immediately: assumptions, source selection, plans, scheduled actions, task state, and conclusions already reached. Do not reduce a substantive correction to a wording change or preserve stale premises silently.
+
+A correction is not automatically true merely because the user made it. If it conflicts with verified evidence, explain the conflict directly and identify what would resolve it. If the correction is supported, change course cleanly without defending the earlier answer.
+
+### Neither sycophantic nor adversarial
+
+Agreement follows evidence and reasoning, not deference. Disagreement serves the shared outcome, not the defense of a prior position. State a contrary view when it changes the decision, give the evidence behind it, and leave room for missing context. Once new evidence resolves the issue, stop arguing the old case.
+
+### Process serves the outcome
+
+Rules and workflows are constraints on the work, not substitutes for judgment. Apply them in service of the user's intended outcome. If a literal workflow interpretation produces a disproportionate, surprising, or strategically different result, surface that implication before proceeding.
+
+Failure signs:
+
+- Executing the narrow literal request while ignoring an evident intended outcome.
+- Asking about a reversible detail while failing to clarify a consequential fork.
+- Presenting one plausible account as settled without testing its strongest alternative.
+- Treating a correction as local wording while leaving downstream assumptions unchanged.
+- Agreeing to preserve rapport or resisting to preserve authority.
+- Letting procedural completeness overwhelm the value of the task.
+
+This is a working collaboration contract, not immutable wording. Refine the section as real sessions expose better distinctions or failure modes; route the feedback to the canonical file rather than accumulating project-local exceptions.
## 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.
@@ -54,6 +93,17 @@ In conversational output to the user, do not use Markdown bold (`**...**`) or in
- Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s.
- Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine.
-- Fenced code blocks (triple backtick) are acceptable when the user explicitly wants a block to copy — they don't invert the way inline spans do. Default to plain text otherwise.
+- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path.
This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation.
+
+## Showing Craig Visuals
+
+Craig runs Claude Code inside Emacs EAT (through tmux). EAT renders SendUserFile and inline terminal images as an `[image] path.png` text line — the visual itself never appears. In one session ~20 renders went out that way and Craig approved UI he had never seen (takuzu, 2026-07-11). Never rely on SendUserFile or inline image display to show a visual. SendUserFile stays fine for *delivering* a file; it just doesn't display one.
+
+Two display lanes, by what the visual is for:
+
+- **Durable or interactive visuals** — HTML prototypes, full renders, anything Craig will study or click: open with `gui-open <abs-path>` (dotfiles-shipped; detaches through `systemd-run --user` so the agent shell can't reap it, resolves the Hyprland instance after a compositor restart, and confirms the window is actually visible before returning). It auto-detects image vs HTML; `--image` / `--browser` force. Place it off Craig's active workspace and name it, per `desktop-capture.md`.
+- **Quick inline glances** — a chart, a did-it-render check: sixel in the terminal. Encode with ImageMagick (`magick <img> sixel:<out>`; img2sixel silently emits zero bytes on some builds) and display in a separate tmux window (`tmux new-window -d -n <name>`, then `tmux send-keys -t <name> "clear; cat <out>" Enter`, tell Craig the window name) rather than the Claude Code pane, whose TUI repaints over anything drawn into it. With native tmux sixel active, the image lives in tmux's grid and survives window switches, scrolling, and resizing.
+
+**Capability gate.** Native sixel needs two config pieces: EAT answering the XTWINOPS cell-size query (patched eat.el, owned by .emacs.d) and `terminal-features 'xterm*:sixel'` in tmux.conf (owned by dotfiles). Check before relying on it: `tmux display -p '#{client_cell_width}'` — nonzero means go; 0 means the chain is missing a piece, so fall back to the browser lane. (Diagnosed 2026-07-13: tmux won't transmit sixel until it knows the client's cell pixel size, and stock EAT 0.9.4 silently ignores the CSI 14 t query tmux uses to ask.)
diff --git a/claude-rules/knowledge-base.md b/claude-rules/knowledge-base.md
index 5658498..146a5e4 100644
--- a/claude-rules/knowledge-base.md
+++ b/claude-rules/knowledge-base.md
@@ -2,7 +2,7 @@
Applies to: `**/*`
-Craig's org-roam knowledge base is the shared, cross-project store for durable agent knowledge. It lives at `~/org/roam/` — a git repo (origin `git@cjennings.net:roam.git`), auto-synced on Craig's machines by the `roam-sync` systemd timer. Per-project harness memory stays the fast capture layer; durable facts get promoted here.
+Craig's org-roam knowledge base is the shared, cross-project store for durable agent knowledge. It lives at `~/org/roam/` — a git repo (origin `cjennings@cjennings.net:git/roam.git`), auto-synced on Craig's machines by the `roam-sync` systemd timer. Per-project harness memory stays the fast capture layer; durable facts get promoted here.
## Reading (any project)
@@ -22,13 +22,15 @@ Pull before querying (`git -C ~/org/roam pull --ff-only`); skip silently if offl
Classify the project before any write. The source of truth is the work-root denylist below — never inference from remotes, names, or task content:
- **Work** — project root is, or sits under, a denylisted root. No KB write, ever. Record durable facts per that project's own conventions.
-- **Personal** — project root sits under `~/code/`, `~/projects/`, or `~/.emacs.d` and is not denylisted. KB writes allowed.
+- **Personal** — project root sits under `~/code/`, `~/projects/`, `~/.emacs.d`, or `~/.dotfiles` and is not denylisted. KB writes allowed.
- **Unknown** — anything else. No KB write.
Work-root denylist (confirmed by Craig, 2026-06-10): `~/projects/work`
**Refusal contract** (work and unknown alike): state the classification, name the durable fact in a one-line redacted summary, and say where it was or wasn't written — so Craig can re-route it deliberately instead of losing it silently.
+**Scope of the denylist — durable KB-node writes only.** The work-denylist governs one thing: promoting a durable fact into a new `agents/` node. It is a confidentiality guard so work-confidential material doesn't land in the personal cross-machine store. It is *not* a general "don't touch roam from a work project" boundary. Roam is a *shared resource*, not another project's product scope. Reading it (any project) and *tidying the shared roam inbox* — processing, routing, and filing the capture items in `~/org/roam/inbox.org`, e.g. via inbox-zero — are allowed from any project session, work included; that is housekeeping on a shared resource, not a durable-fact write. Only the durable-node promotion stays work-denylisted. Do not park roam-inbox tidying as a cross-project boundary crossing (a sentry inbox-zero pass did exactly that on 2026-07-19 — the error this note closes).
+
A write is one node per fact, under `agents/`, roam-valid so Craig's org-roam indexes it:
```
@@ -43,7 +45,26 @@ A write is one node per fact, under `agents/`, roam-valid so Craig's org-roam in
<the fact, with [[id:...]] links to related nodes>
```
-Pull before writing, commit and push after (`git -C ~/org/roam add -A && git commit && git push`) — same session discipline as any repo. Never edit Craig's hand-authored nodes; link to them. This write autonomy is scoped to the KB alone — it is not permission to send email, comment on tickets, or post to any public or external channel.
+Pull before writing (`git -C ~/org/roam pull --ff-only`, read-only). Then acquire the roam-write lock, write the node, and trigger roam-sync to commit and push — roam-sync stays the roam repo's only committer (the 2026-06-24 one-git-owner rule). The tree is chronically dirty from live captures, so an agent's own `git add -A && commit` could sweep an in-flight capture into a stray commit; edit-plus-trigger avoids that. Never edit Craig's hand-authored nodes; link to them. This write autonomy is scoped to the KB alone — it is not permission to send email, comment on tickets, or post to any public or external channel.
+
+The write block, with the lock and the trigger:
+
+```sh
+# Acquire the roam-write lock so a concurrent sentry pass or inbox writer can't
+# race this write. Callers pass a name; agent-lock owns the path (tmpfs).
+if [ -x .ai/scripts/agent-lock ]; then
+ if ! .ai/scripts/agent-lock acquire roam-write --wait; then
+ # Busy after the bounded wait — surface and stop, don't write unlocked.
+ echo "roam-write lock held by another writer; try again shortly" >&2
+ exit 1
+ fi
+fi
+# ... write ~/org/roam/agents/<ts>-<slug>.org ...
+systemctl --user start roam-sync.service # roam-sync commits + pushes
+[ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock release roam-write
+```
+
+Degrade gracefully when `agent-lock` isn't installed (an older checkout mid-sync): the guard above is skipped and the write proceeds unlocked — today's behavior. Only a *present* helper reporting the lock busy after its bounded wait stops the write; an *absent* helper never blocks it.
## What goes in, what stays out
@@ -53,7 +74,7 @@ Pull before writing, commit and push after (`git -C ~/org/roam add -A && git com
## Capture, then promote
-Harness memory (`~/.claude/projects/<enc>/memory/`) remains the per-project capture layer: fast, automatic, allowed to be at-risk. At wrap-up (or a task audit, or an explicit prompt), promote facts that meet the inclusion bar into the KB as nodes. The wrap-up workflow asks; answer it honestly — promotion discipline is what keeps the capture layer from silting up.
+Harness memory (`~/.claude/projects/<enc>/memory/`) remains the per-project capture layer: fast, automatic, allowed to be at-risk. An agent running without harness memory (a non-Claude runtime) captures into its session log instead and promotes from there — the promotion discipline is the same. At wrap-up (or a task audit, or an explicit prompt), promote facts that meet the inclusion bar into the KB as nodes. The wrap-up workflow asks; answer it honestly — promotion discipline is what keeps the capture layer from silting up.
## Inventory
diff --git a/claude-rules/locating-craig.md b/claude-rules/locating-craig.md
new file mode 100644
index 0000000..c327e3f
--- /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/org-tables.md b/claude-rules/org-tables.md
index 1b70085..bc9d27d 100644
--- a/claude-rules/org-tables.md
+++ b/claude-rules/org-tables.md
@@ -1,3 +1,8 @@
+---
+paths:
+ - "**/*.org"
+---
+
# Org Table Standard
Applies to: `**/*.org`
diff --git a/claude-rules/subagents.md b/claude-rules/subagents.md
index 8578dea..e52d906 100644
--- a/claude-rules/subagents.md
+++ b/claude-rules/subagents.md
@@ -34,6 +34,66 @@ This is the same boundary the "Don't Subagent At All" section and the
"Subagenting trivial work" anti-pattern draw; treat it as an explicit gate
at dispatch time.
+Every size-based rule in this file — the cost gate here, "Don't Subagent At
+All", the trivial-work anti-pattern — is subject to the isolation override
+below.
+
+## Isolation Override — When Size Doesn't Gate
+
+Every size heuristic in this file rests on one assumption: that the main
+thread could do the task itself just as well, so the only question is
+whether delegating is worth the overhead. When that assumption fails, the
+heuristics don't apply, and a five-line task can require a subagent that a
+five-hundred-line one wouldn't.
+
+The assumption fails whenever **the main thread is structurally disqualified
+from the task** — not slower at it, disqualified. The test: would the main
+thread's own context make its answer *less* trustworthy? If holding the
+context is what corrupts the judgment, then doing it inline doesn't save the
+overhead, it destroys the result. The isolation *is* the deliverable, and
+"it's only a small diff" is not an argument against it.
+
+**The standing instance is the pre-commit code review** (`publish` skill,
+Step 1). The author cannot review their own change, because a self-review
+checks the diff against the author's own model of it and cannot check the
+model. Errors that survive a self-review are the ones that were never in the
+diff — an inherited scope, an estimated blast radius, a fix correct for the
+case in mind and wrong for the one never considered. So that review is
+dispatched on *every* commit including a one-line one, and the ~10-tool-call
+floor, the single-function rule, and the trivial-work anti-pattern are all
+overridden there by design.
+
+Other cases with the same shape: verifying a claim the main thread already
+committed to in conversation, and any second opinion where the first opinion
+is already in context. If you find yourself reasoning "I already know the
+answer, so a subagent is wasteful," check whether already knowing it is the
+problem.
+
+This override widens *what* gets dispatched. Scope, constraints, and output
+format are still required, and arguably matter more here, since an isolated
+agent can't fall back on shared context to fill a gap.
+
+**Field 2 of the Prompt Contract inverts under this override, and the
+inversion is the whole point.** Normally field 2 says to paste the relevant
+output verbatim and include what you learned in earlier turns. Do that for an
+isolation dispatch and you hand over the very model you spawned the agent to
+escape — a reviewer given your findings reviews your findings. So for an
+isolation dispatch, field 2 is *the artifact under test and the independent
+record of what was asked, and nothing else*: the diff, a one-line claim of
+what it does, and the ticket or plan where one exists. The conversation, the
+rationale, and the dead ends are withheld on purpose.
+
+Keep the requirement source in. A ticket is not your model of the change; it
+was written before the work, usually by someone else, and it is the only
+thing that can contradict your claim about your own diff.
+
+**The output is a judgment, so the review gate resolves differently.** The
+Review-Gate Cadence below says subagent output is a claim to be verified
+before moving on, which is right when the deliverable is *work*. When the
+deliverable is *a judgment about your work*, verifying it against your own
+reading reinstates exactly the bias the dispatch removed. Disagreement goes
+to the user to adjudicate, not back to the author's own judgment.
+
## When to Spawn a Subagent
### Parallel-safe (spawn multiple in parallel)
@@ -63,11 +123,15 @@ at dispatch time.
### Don't Subagent At All
+Unless the Isolation Override applies — these are efficiency rules, and they
+lapse when the main thread's own context is what makes its answer untrustworthy.
+
- **The target is already known** and the work fits in under ~10 tool calls.
- **Single-function logic** — one Read + one Edit is faster than briefing
an agent.
- **You can see the answer from context** — don't spawn a researcher for
- something already on screen.
+ something already on screen. (The inverse of this one is the override's
+ clearest case: when *having* seen it is the disqualification, dispatch.)
## Prompt Contract
@@ -138,7 +202,11 @@ fix), then dispatch the fix with a specific contract.
- **Retrying a failed subagent task in the orchestrator** — pollutes
context. Dispatch a fix agent instead.
- **Subagenting trivial work** — one Read + one Edit doesn't need an
- agent; spawn overhead exceeds benefit.
+ agent; spawn overhead exceeds benefit. Except under the Isolation
+ Override, where a one-line diff still gets its own reviewer.
+- **Reviewing your own change inline** — the mirror-image failure, and the
+ more expensive one. Skipping a dispatch to save overhead on a small diff
+ costs a review that could only have come from outside your context.
- **Skipping review between tasks** — compounding bugs are much harder to
unwind than any single bug.
- **Letting the agent decide scope** — "figure out what needs changing"
diff --git a/claude-rules/testing.md b/claude-rules/testing.md
index b3fa5bf..dd15282 100644
--- a/claude-rules/testing.md
+++ b/claude-rules/testing.md
@@ -16,340 +16,30 @@ TDD is the default workflow for all code, including demos and prototypes. **Writ
Do not skip TDD for demo code. Demos build muscle memory — the habit carries into production.
-### Understand Before You Test
-Before writing tests, invest time in understanding the code:
+## Test Categories — required for all code
-1. **Explore the codebase** — Read the module under test, its callers, and its dependencies. Understand the data flow end to end.
-2. **Identify the root cause** — If fixing a bug, trace the problem to its origin. Don't test (or fix) surface symptoms when the real issue is deeper in the call chain.
-3. **Reason through edge cases** — Consider boundary conditions, error states, concurrent access, and interactions with adjacent modules. Your tests should cover what could actually go wrong, not just the obvious happy path.
+Every unit under test needs all three, not just the happy path:
-### Adding Tests to Existing Untested Code
+1. **Normal** — standard inputs, common workflows, typical volumes.
+2. **Boundary** — zero, one, max, empty vs null, single-element collections, unicode, very long input, timezone and date edges.
+3. **Error** — invalid input, type mismatches, network failure, missing parameters, permission denied, resource exhaustion, malformed data.
-When working in a codebase without tests:
+The negative and boundary cases are the ones that find bugs. A unit with only
+Normal coverage is not tested, it is demonstrated.
-1. Write a **characterization test** that captures current behavior before making changes
-2. Use the characterization test as a safety net while refactoring
-3. Then follow normal TDD for the new change
+## The rest of the standard lives in the `testing-standards` skill
-## Test Categories (Required for All Code)
+Characterization tests for untested code, the per-category detail, combinatorial
+and property-based and mutation testing, organization and the pyramid,
+integration-test rules, naming, the test-quality rules (independence,
+determinism, mocking boundaries, signs of overmocking), the
+refactor-when-tests-are-hard principle, coverage targets, the spike exception,
+and the anti-pattern list are all in the `testing-standards` skill. Load it when
+writing tests.
-Every unit under test requires coverage across three categories:
-
-### 1. Normal Cases (Happy Path)
-- Standard inputs and expected use cases
-- Common workflows and default configurations
-- Typical data volumes
-
-### 2. Boundary Cases
-- Minimum/maximum values (0, 1, -1, MAX_INT)
-- Empty vs null vs undefined (language-appropriate)
-- Single-element collections
-- Unicode and internationalization (emoji, RTL text, combining characters)
-- Very long strings, deeply nested structures
-- Timezone boundaries (midnight, DST transitions)
-- Date edge cases (leap years, month boundaries)
-
-### 3. Error Cases
-- Invalid inputs and type mismatches
-- Network failures and timeouts
-- Missing required parameters
-- Permission denied scenarios
-- Resource exhaustion
-- Malformed data
-
-## Combinatorial Coverage
-
-For functions with 3+ parameters that each take multiple values (feature-flag
-combinations, config matrices, permission/role interactions, multi-field
-form validation, API parameter spaces), the exhaustive test count explodes
-(M^N) while 3-5 ad-hoc cases miss pair interactions. Use **pairwise /
-combinatorial testing** — generate a minimal matrix that hits every 2-way
-combination of parameter values. Empirically catches 60-90% of combinatorial
-bugs with 80-99% fewer tests.
-
-Invoke `/pairwise-tests` on the offending function; continue using `/add-tests`
-and the Normal/Boundary/Error discipline for the rest. The two approaches
-complement: pairwise covers parameter *interactions*; category discipline
-covers each parameter's individual edge space.
-
-Skip pairwise when: the function has 1-2 parameters (just write the cases),
-the context requires *provably* exhaustive coverage (regulated systems — document
-in an ADR), or the testing target is non-parametric (single happy path,
-performance regression, a specific error).
-
-## Escalation Beyond Category and Pairwise
-
-The Normal/Boundary/Error categories and the pairwise matrix are the default
-discipline. Two further techniques escalate beyond them — reach for them when
-the default leaves a gap, not on every unit.
-
-### Property-Based Testing
-
-When an invariant holds across a broad input domain — round-trips
-(`decode(encode(x)) == x`), idempotence (`f(f(x)) == f(x)`), ordering
-invariants (output is always sorted), or any "output always satisfies X" —
-generate inputs and assert the property instead of enumerating cases. The
-generator explores corners you wouldn't think to write by hand, and a
-failing case shrinks to a minimal reproducer. Use the standard tool for the
-language (Hypothesis for Python, fast-check for JS, proptest for Rust).
-State the property as the test name and let the framework supply the inputs.
-
-Reach for this when the behavior is a law over a domain rather than a fixed
-set of examples. Keep category-discipline cases for the specific edges that
-must always hold; the property test covers the space between them.
-
-### Mutation Testing
-
-When line coverage is high but you suspect the assertions are thin — tests
-that execute the code without checking its output, or that pass with a
-function body replaced by a stub — use mutation testing to measure whether
-the suite actually kills injected faults. The tool flips conditionals, swaps
-operators, and deletes statements, then reruns the suite; a surviving mutant
-is a fault the tests didn't catch. Use mutmut or cosmic-ray for Python,
-Stryker for JS. High line coverage with a low mutation score means weak
-assertions, not a tested codebase.
-
-Reach for this on critical logic where coverage looks reassuring but you
-want evidence the tests would fail on a regression. It's a diagnostic, not a
-gate on every change — mutation runs are slow.
-
-## Test Organization
-
-Typical layout:
-
-```
-tests/
- unit/ # One test file per source file
- integration/ # Multi-component workflows
- e2e/ # Full system tests
-```
-
-Per-language files may adjust this (e.g. Elisp collates ERT tests into
-`tests/test-<module>*.el` without subdirectories).
-
-### Testing Pyramid
-
-Rough proportions for most projects:
-- Unit tests: 70-80% (fast, isolated, granular)
-- Integration tests: 15-25% (component interactions, real dependencies)
-- E2E tests: 5-10% (full system, slowest)
-
-Don't duplicate coverage: if unit tests fully exercise a function's logic,
-integration tests should focus on *how* components interact — not repeat the
-function's case coverage.
-
-## Integration Tests
-
-Integration tests exercise multiple components together. Two rules:
-
-**The docstring names every component integrated** and marks which are real vs
-mocked. Integration failures are harder to pinpoint than unit failures;
-enumerating the participants up front tells you where to start looking.
-
-Example:
-
-```
-def test_integration_refund_during_sync_updates_ledger_atomically():
- """Refund processed mid-sync updates order and ledger in one transaction.
-
- Components integrated:
- - OrderService.refund (entry point)
- - PaymentGateway.reverse (MOCKED — returns success)
- - Ledger.credit (real)
- - db.transaction (real)
-
- Validates:
- - Refund rolls back if ledger write fails
- - Both tables updated or neither
- """
-```
-
-**Write an integration test when** multiple components must work together,
-state crosses function boundaries, or edge cases combine. **Don't** when
-single-function behavior suffices, or when mocking would erase the interaction
-you meant to test.
-
-## Naming Convention
-
-- Unit: `test_<module>_<function>_<scenario>_<expected>`
-- Integration: `test_integration_<workflow>_<scenario>_<outcome>`
-
-Examples:
-- `test_cart_apply_discount_expired_coupon_raises_error`
-- `test_integration_order_sync_network_timeout_retries_three_times`
-
-Languages that prefer camelCase, kebab-case, or other conventions keep the
-structure but use their idiom. Consistency within a project matters more than
-the specific case choice.
-
-## Test Quality
-
-### Independence
-- No shared mutable state between tests
-- Each test runs successfully in isolation
-- Explicit setup and teardown
-
-### Determinism
-- Never hardcode dates or times — generate them relative to `now()`
-- No reliance on test execution order
-- No flaky network calls in unit tests
-- Time/clock-mocking helpers must avoid two recurring failure modes:
- - *Infinite recursion.* The helper must not call the primitive it's
- replacing. If the mock for `now()` calls `now()`, the test stack
- overflows. Compute the mock value from a fixed source (a captured
- instant, an injected fake clock).
- - *Scope-shadowing without reach.* A mock that only exists inside
- the test function won't affect production code that reads the
- symbol through its canonical path. Replace the symbol at its
- definition site (monkey-patch the module attribute in Python,
- redefine the global in Lisp, swap the package-level binding in
- Go, replace the named export in JavaScript) — or inject a fake
- via dependency-inversion. Don't lean on scope-shadowing
- primitives (Lisp `let`, Python local rebind, JS shadowed `let`)
- that fence the mock to the test's lexical scope; production code
- won't see them and the test passes against the real clock.
-
-### Performance
-- Unit tests: <100ms each
-- Integration tests: <1s each
-- E2E tests: <10s each
-- Mark slow tests with appropriate decorators/tags
-
-### Mocking Boundaries
-Mock external dependencies at the system boundary:
-- Network calls (HTTP, gRPC, WebSocket)
-- File I/O and cloud storage
-- Time and dates
-- Third-party service clients
-
-Never mock:
-- The code under test
-- Internal domain logic
-- Framework behavior (ORM queries, middleware, hooks, buffer primitives)
-
-### Signs of Overmocking
-
-Ask yourself:
-
-- Would this test still pass if I replaced the function body with `raise NotImplementedError` (or equivalent)? If yes, the mocks are doing the work — you're testing mocks, not code.
-- Is the mock more complex than the function being tested? Smell.
-- Am I mocking internal string / parsing / decoding helpers? Those aren't boundaries — they're the work.
-- Does the test break when I refactor without changing behavior? Good tests survive refactors; overmocked ones couple to implementation.
-
-When tests demand heavy internal mocking, the fix isn't better mocks — it's
-restructuring the code (see *If Tests Are Hard to Write* below).
-
-### Testing Code That Uses Frameworks
-
-When a function mostly delegates to framework or library code, test *your*
-integration logic:
-- ✓ "I call the library with the right arguments in the right context"
-- ✓ "I handle its return value correctly"
-- ✗ "The library works in 50 scenarios" — trust it; it has its own tests
-
-For polyglot behavior (e.g., comment handling across C/Java/Go/JS), test 2-3
-representative modes thoroughly plus a minimal smoke test in the others.
-Exhaustive permutations are diminishing returns.
-
-### Test Real Code, Not Copies
-
-Never inline or copy production code into test files. Always `require`/`import`
-the module under test. Copied code passes even when production breaks — the
-bug hides behind the duplicate.
-
-Mock dependencies at their boundary; exercise the real function body.
-
-### Error Behavior, Not Error Text
-
-Test that errors occur with the right type; don't assert exact wording:
-- ✓ Right exception type (`pytest.raises(ValueError)`, `(should-error ... :type 'user-error)`)
-- ✓ Regex on values the message *must* contain (e.g., the offending filename)
-- ✗ `assert str(e) == "File 'foo' not found"` — breaks when prose changes even though behavior is unchanged
-
-Production code should emit clear, contextual errors. Tests verify the
-behavior (raised, caught, returned nil) and values that must appear — not the
-prose.
-
-## If Tests Are Hard to Write, Refactor the Code
-
-If a test needs extensive mocking of internal helpers, elaborate fixture
-scaffolding, or mocks that recreate the function's own logic, the production
-code needs restructuring — not the test.
-
-Signals:
-- Deep nesting (callbacks inside callbacks)
-- Long functions doing multiple things ("fetch AND parse AND decode AND save")
-- Tests that mock internal string / parsing / I/O helpers
-- Tests that break on refactors with no behavior change
-
-Fix: extract focused helpers (one responsibility each), test each in isolation
-with real inputs, compose them in a thin outer function. Several small unit
-tests plus one composition test beats one monster test behind a wall of mocks.
-
-## Coverage Targets
-
-- Business logic and domain services: **90%+**
-- API endpoints and views: **80%+**
-- UI components: **70%+**
-- Utilities and helpers: **90%+**
-- Overall project minimum: **80%+**
-
-New code must not decrease coverage. PRs that lower coverage require justification.
-
-## TDD Discipline
-
-TDD is non-negotiable. These are the rationalizations agents use to skip it — don't fall for them:
-
-| Excuse | Why It's Wrong |
-|--------|----------------|
-| "This is too simple to need a test" | Simple code breaks too. The test takes 30 seconds. Write it. |
-| "I'll add tests after the implementation" | You won't, and even if you do, they'll test what you wrote rather than what was needed. Test-after validates implementation, not behavior. |
-| "Let me just get it working first" | That's not TDD. If you can't write a failing test, you don't understand the requirement yet. |
-| "This is just a refactor" | Refactors without tests are guesses. Write a characterization test first, then refactor while it stays green. |
-| "I'm only changing one line" | One-line changes cause production outages. Write a test that covers the line you're changing. |
-| "The existing code has no tests" | Start with a characterization test. Don't make the problem worse. |
-| "This is demo/prototype code" | Demos build habits. Untested demo code becomes untested production code. |
-| "I need to spike first" | Spikes are fine — under the protocol below. Throw the spike away, then write the first failing test before productionizing. |
-
-If you catch yourself thinking any of these, stop and write the test.
-
-### The Spike Exception (Disciplined)
-
-TDD stays the default. The one sanctioned way to write code before a test is
-a spike — exploratory code that answers "is this approach even viable?" when
-you can't yet write a meaningful failing test because the shape of the
-solution is unknown. A spike is disciplined only when all three hold:
-
-1. **Timebox it.** Set a limit before starting (an hour, an afternoon) and
- stop when it's up. An open-ended spike is just untested implementation
- wearing a different name.
-2. **Do not commit spike code.** The spike is a learning artifact, not a
- deliverable. It never enters the branch history. Keep it in a scratch
- file or a throwaway worktree.
-3. **Throw the spike away, then start with a failing test.** Once the spike
- has answered the viability question, delete it. Write the first failing
- test against the now-understood behavior, then productionize under normal
- Red/Green/Refactor. The production code is written test-first even though
- the exploration wasn't — you don't promote the spike into production by
- bolting tests on after.
-
-The spike buys understanding, not code. If you find yourself keeping the
-spike because rewriting it feels wasteful, the timebox was too long or the
-problem was tractable enough to TDD from the start.
-
-## Anti-Patterns (Do Not Do)
-
-- Hardcoded dates or timestamps (they rot)
-- Testing implementation details instead of behavior
-- Mocking the thing you're testing
-- Mocking internal helpers (string ops, parsing, decoding) — those are the work
-- Inlining production code into test files — always `require` / `import` the real module
-- Asserting exact error-message text instead of type + key values
-- Shared mutable state between tests
-- Non-deterministic tests (random without seed, network in unit tests)
-- Testing framework behavior instead of your code
-- Ignoring or skipping failing tests without a tracking issue
+What stays here is what has to be true before any code is written, which is when
+no skill has been summoned yet: test first, and cover all three categories.
## Content scope
diff --git a/claude-rules/todo-format.md b/claude-rules/todo-format.md
index b1fb57b..8038b98 100644
--- a/claude-rules/todo-format.md
+++ b/claude-rules/todo-format.md
@@ -1,3 +1,8 @@
+---
+paths:
+ - "**/*.org"
+---
+
# Todo Entry Format
Applies to: `**/*.org` (org-mode todo and inbox files)
@@ -5,6 +10,56 @@ Applies to: `**/*.org` (org-mode todo and inbox files)
How task entries are structured in org-mode todo files (`todo.org`,
`inbox.org`, any GTD-style org file). Same shape across every project.
+## Stamp `:LAST_REVIEWED:` when you create the task, not a cycle later
+
+Every task filed at `**` with a priority cookie carries a `:LAST_REVIEWED:`
+property from the moment it's written:
+
+```
+** TODO [#B] Terse topic phrase :tag:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+Body.
+```
+
+Use today's date, from `date +%F`. The org-native `[YYYY-MM-DD Day]` form is
+equally valid; both parse.
+
+**Why.** Writing a task *is* reviewing it. Whoever files it has just written
+the body, chosen the wording, and graded the priority against the scheme — that
+is the same judgment `task-review` applies, made with better context, because
+the reason for the task is still in the room. Leaving the stamp off asserts the
+opposite: `task-review-staleness.sh` sorts a missing property first, as
+never-reviewed, so a task filed today arrives at the top of tomorrow's review
+batch and gets "reviewed" by someone re-deriving what its author knew a day
+earlier. That is ceremony, and ceremony teaches people to click through the
+real thing.
+
+The concrete case: the 2026-07-23 sweep filed eight tasks in one night. Every
+one landed unstamped, and the staleness count went 13 → 22 while the list got
+*more* accurate, not less. The number stopped measuring drift and started
+measuring recent activity.
+
+**This applies to every path that files a task**, not just the inbox: inbox
+filing, triage intake, spec decomposition, task audit, a bug found mid-session,
+a task you write by hand. If you wrote a task body today, stamp it today.
+
+**What it does not do.** The stamp never means "correct forever" — it means
+"a person judged this on that date." A task filed today still enters the
+review rotation on the normal cycle; it just enters it on the *next* cycle
+rather than immediately. And it's a claim about a real event, so don't stamp
+a task you didn't actually consider: a bulk import of someone else's list
+is genuinely unreviewed, and stamping it would convert "nobody has read
+these" into a false "reviewed today."
+
+**Enforcement.** `lint-org.el`'s `task-missing-last-reviewed` checker flags any
+open `**` task with a priority cookie and no stamp, scoped to exactly the
+headings `task-review-staleness.sh` selects, so the checker and the staleness
+count never disagree. It's judgment-only and never auto-fixes: nothing can know
+when an unstamped task was actually last considered, and writing today's date
+onto an old one would destroy the very signal the property carries.
+
## Priority and Tag Scheme Header
Every project's `todo.org` opens with a top-level section named
@@ -24,8 +79,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 +88,152 @@ 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.
+### Hard definitions: `:solo:` and `:quick:` (fixed across projects)
+
+A project's scheme may add or rename its other tags, but these two carry
+fixed definitions everywhere, because autonomous execution
+(work-the-backlog / the no-approvals speedrun) reads `:solo:` as its
+eligibility gate and trusts the author's tag rather than re-deriving
+autonomy at run time.
+
+“Speedrunnable” is shorthand for `:solo:`. The `:quick:` tag plays no part
+in that definition.
+
+- **`:solo:` — autonomy.** The task can be completed *and verified* without
+ Craig's involvement beyond at most one or two quick decisions that can be
+ stated and answered before work starts. No open design question, no
+ "weigh these approaches," no waiting on Craig mid-task. Three gates, all
+ must hold: *buildable* (the agent has the capability and access),
+ *verifiable by the agent* (an objective or local check it can run itself —
+ handing off a residual human-in-the-loop confirmation as a structured
+ manual-testing reminder does not disqualify), and *no deliberation* (a
+ quick, upfront-answerable factual question is allowed — it gets batched
+ into the speedrun's pre-flight Q&A; a genuine design or preference call
+ is not). A wrong `:solo:` is worse than none: it tells Craig he can hand
+ the task off and walk away when he can't.
+- **`:quick:` — effort hint only.** Likely 30 minutes or less from start
+ through verification. Informational, for batching and estimating a run's
+ duration; never an eligibility gate. `:quick:` and `:solo:` are
+ orthogonal — a bounded refactor can be `:solo:` but slow; a five-minute
+ change hinging on a preference call is `:quick:` but not `:solo:`.
+
+Both tags are applied at task creation and **re-checked as a mandatory
+step** in the task-review and task-audit workflows, so the run-time gate
+can trust the tag. A review or audit that skips the `:solo:`/`:quick:`
+assessment is incomplete.
+
+### Making an open-ended task measurable (so it can be `:solo:`)
+
+A task phrased as the *absence* of something — "find bugs until none are
+visible," "refactor until no worthwhile opportunities remain," "clean this
+up until it's good" — cannot be `:solo:`, because it fails the
+*verifiable-by-the-agent* gate. Absence isn't falsifiable: an agent can
+always look once more, so "done" is a judgment call, which is exactly what
+`:solo:` forbids. The fix is not to drop the task but to give it an
+objective completion criterion. Four moves convert a fuzzy goal into a
+measurable one:
+
+1. **Bound the surface.** Enumerate the concrete units the task covers (the
+ N functions, the M files, the named code paths). The done-set is that
+ list, not the platonic set of all possible defects. Every claim is made
+ against the list, so "covered" is checkable where "found everything"
+ isn't.
+2. **Net the behavior.** Bring the surface under characterization tests
+ (Normal/Boundary/Error per unit — see `testing.md`, and the
+ `testing-standards` skill for the characterization recipe) before changing
+ anything. This is the objective floor: writing a characterization test is
+ mechanical (record what the code does, not what it should), so it scales
+ across the surface, and it doubles as the safety net that makes any
+ later refactor falsifiable.
+3. **Disposition every finding.** Run the relevant audits (a fixed
+ footgun/OWASP checklist, `/refactor`, `/review-code`) and give **every**
+ finding a verdict: fixed, filed as its own task, or declined with a
+ one-line reason. "Looked and it's fine" is not a disposition. The
+ measurement is zero undispositioned findings, not zero findings.
+4. **Gate on an objective floor.** Static analysis clean (linter,
+ type-checker, `shellcheck`), the test suite green before and after, and
+ coverage of the enumerated surface (a per-unit test checklist, or a real
+ coverage number where the tooling exists).
+
+The **qualifying answer** is then a dispositioned report — surface split
+covered/uncovered, tests before → after, static-analysis result, the audit
+matrix fully dispositioned, all green — not a claim of perfection. The
+honest limit stays honest ("no visible bugs" means "every enumerated path
+passes its characterization set and clears the audit," never "zero bugs
+exist"), but the criterion is now falsifiable, which is what lets the task
+carry `:solo:`. Write these criteria into the task body at creation or
+review time; a task that can't be given them stays non-`:solo:` until it
+can.
+
+### 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.
+
+**Don't double-count rarity.** Grade severity by the rate of harm once the
+failure state is entered, not by how rare it is to enter. Frequency already
+carries the rarity; letting it discount severity too grades the same fact twice,
+and that buries exactly the bugs that compound — the ones where a rare trigger
+produces unbounded harm. A leak that repeats every timeout period until the
+process restarts is Major even when reaching that state is a rare edge case
+("accumulates slowly" describes a bounded trickle, not a fixed-rate leak with no
+workaround). Grade the *being-in-it*, and let the frequency row carry the
+*getting-into-it*.
+
+**Record the grading in the task body.** State the severity band, the frequency
+row, and the arithmetic (e.g. "Major severity × rare edge case = P3 = [#C]"). A
+bare priority cookie can't be argued with; a stated read can be re-checked
+against the source and corrected. This is what lets a misgrade move — a chime
+watchdog bug went [#D] → [#C] an hour after grading precisely because the read
+was written down and re-checked.
+
+**Disagreeing with a grade means fixing an input.** If a letter looks wrong,
+don't override the letter — re-read the severity band and the frequency row
+against the source and correct whichever input is wrong. Overriding the cookie
+directly turns the matrix into a formality and puts you back to grading by
+instinct, which is the thing it exists to replace.
+
## The Rule
A todo entry has two parts:
@@ -117,6 +318,7 @@ A completed sub-task disappears as a task and becomes an in-place event-log entr
2. Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`.
3. Reword the original imperative title into the past-tense action that landed. Trim or restate if the original wording doesn't fit the action.
4. Drop the `TODO`/`DOING` keyword, the priority cookie, and the tags. The body stays as the record of what was done (if useful).
+5. Remove any `SCHEDULED:`/`DEADLINE:` planning line. The completion time lives in the heading now, so `CLOSED:` is redundant and an active planning date on a historical log entry is always wrong. Org renders any headline carrying an active `<...>` `SCHEDULED`/`DEADLINE` on the agenda, keyword or not, so a stale one pins the finished entry there as weeks-overdue forever. An interactive close (`org-log-done`) stamps `CLOSED:` but never strips a pre-existing planning line, which is exactly how the stale dates survive.
**Example:**
@@ -126,11 +328,13 @@ becomes
*** 2026-05-15 Fri @ 12:58:08 -0500 Wired yasnippet for universal availability
+**Enforcement.** This is applied at close time by whoever closes the task, but an interactive org close (`org-log-done` flips the keyword to `DONE` and stamps `CLOSED:`) never applies the dated rewrite, so level-3+ closes accumulate as `DONE` keywords. `todo-cleanup.el --convert-subtasks` (run in the `clean-todo` and wrap-up cleanup passes) normalizes them mechanically: it rewrites any level-3+ `DONE`/`CANCELLED`/`FAILED` heading into the dated form above, pulling the timestamp from the `CLOSED` cookie, dropping the whole planning line (`CLOSED`, `SCHEDULED`, and `DEADLINE` together — step 5), and keeping the heading text verbatim (a batch tool can't reliably past-tense a title — polish wording by hand where it matters). `lint-org.el` flags any that slip through: checker `subtask-done-not-dated` for a still-keyworded sub-task, and `dated-log-heading-active-timestamp` for a dated entry that kept an active `SCHEDULED`/`DEADLINE`. So the depth rule holds even when tasks are closed interactively rather than by an agent applying this section.
+
### Why depth-based
The agenda view (`org-agenda`) shows entries at the section + top-task level. Letting `**` tasks stay task-shaped preserves their visibility as "things that recently shipped." Letting `***+` sub-tasks flip to dated entries keeps the agenda from being clogged with a long list of completed sub-tasks at every depth — those become history within their parent instead.
-`VERIFY` is the documented exception: it follows the dated-rewrite rule at **all** depths (including `**`), because a resolved VERIFY is an answered question rather than a finished task. See the VERIFY section below.
+`VERIFY` follows the dated-rewrite rule at `***` and deeper, the same as any sub-task. At `**` it does *not*: a top-level VERIFY completes task-shaped — a `DONE`/`CANCELLED` keyword plus a `CLOSED:` line, exactly like a top-level `TODO`. Dated headers never appear at `**`. Level 2 always carries a terminal keyword; dated headers are a `***`-and-deeper shape only. See the VERIFY section below.
## VERIFY tasks
@@ -191,19 +395,35 @@ The sibling rule is the active force that keeps `todo.org` flat. Without
it, VERIFYs accumulate one level deeper than their trigger every time —
turning a clean parent tree into a long pole of nested sub-headings.
-### Completion — dated rewrite + content replacement
+### Completion — depth decides the heading shape
+
+When a VERIFY resolves, **rewrite the heading and body together**. The body
+replacement is the same at every depth (step 2 below); the heading shape
+depends on the VERIFY's level, mirroring the depth-based rule for ordinary
+tasks — dated entries at `***` and deeper, terminal keyword at `**`.
+
+1. **Replace the heading — by depth.**
+
+ - **At `***` and deeper — dated event-log entry.** Drop the `VERIFY`
+ keyword (and any priority cookie / tags) and replace with a timestamp +
+ short description:
-When a VERIFY resolves, **rewrite the heading and body together** at the
-same depth — regardless of whether the VERIFY is at `**` or `***`:
+ *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done>
-1. **Replace the heading.** Drop the `VERIFY` keyword (and any priority
- cookie / tags) and replace with a timestamp + short description:
+ Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`.
+ Remove any `SCHEDULED:`/`DEADLINE:` planning line too, same as the
+ sub-task rule above — a dated event-log entry carries its date in the
+ heading, and an active planning date left on it pins the finished entry
+ to the agenda forever.
- *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done>
+ - **At `**` — terminal keyword, like any top-level task.** Change
+ `VERIFY` to `DONE` (answered / check passed) or `CANCELLED` (abandoned),
+ keep the heading text, priority cookie, and tags, and add a
+ `CLOSED: [YYYY-MM-DD Day]` line. Never a dated heading — a `**` dated
+ header is a defect; repair it to `DONE`/`CANCELLED` + `CLOSED:`.
- Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`.
- Match the original depth (a `**` VERIFY becomes `** YYYY-MM-DD ...`;
- a `***` VERIFY becomes `*** YYYY-MM-DD ...`).
+ ** DONE [#B] <original VERIFY topic> :tags:
+ CLOSED: [2026-05-15 Fri]
2. **Replace the body.** Drop the original question/instruction prose and
replace with either:
@@ -213,16 +433,18 @@ same depth — regardless of whether the VERIFY is at `**` or `***`:
instruction or pending-decision marker — what was done, when, where
the artifact lives).
-The completed VERIFY becomes an in-place event log entry. The original
-question is preserved by the dated heading + body shape; anyone scanning
-the agenda or `git log` can see what was asked and what landed.
+Either way the completed VERIFY records what was asked and what landed: at
+`***` and deeper as a dated event-log entry, at `**` as a `DONE`/`CANCELLED`
+task whose body holds the answer. Anyone scanning the agenda or `git log`
+can see both.
-**Note on the top-level case.** Regular `**` DONE tasks stay task-shaped
-with a `DONE` keyword + `CLOSED:` line per *Completion — depth-based*
-above. VERIFYs at `**` are the exception — they convert to dated log
-entries on completion because a resolved VERIFY isn't a "done task," it's
-an answered question. The dated-rewrite rule wins for VERIFYs at all
-depths.
+**Note on the top-level case.** A `**` VERIFY completes exactly like a `**`
+`TODO`: a `DONE`/`CANCELLED` keyword + `CLOSED:` line, with the answer or
+action in the body. The earlier habit of dating a resolved top-level VERIFY
+— treating "answered question, not a finished task" as license for a `**`
+dated header — is retired. It put dated headers at level 2, where the agenda
+truncates them out of a clean keyword scan. Dated rewrite is for `***` and
+deeper only; `**` always carries a terminal keyword.
### Don't leave stale placeholders
@@ -249,3 +471,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 e45e660..3c4ea6d 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/ui-prototyping.md b/claude-rules/ui-prototyping.md
new file mode 100644
index 0000000..c453258
--- /dev/null
+++ b/claude-rules/ui-prototyping.md
@@ -0,0 +1,93 @@
+# UI Prototyping for Specs
+
+Applies to: `**/*` (any spec whose deliverable has a non-trivial UI)
+
+How to settle a UI design before committing implementation code: research the
+category, brainstorm the UX in the spec, build a handful of full working
+prototypes, then iterate one to a final. The prototype is the evidence a design
+decision is recorded against. Discovered building archsetup's timer-panel spec,
+promoted here so any UI-bearing spec follows the same shape.
+
+## When this applies — non-trivial UI only
+
+The process fires when a spec's deliverable is a real UI: a panel, a
+multi-control surface, a visual layout with interacting parts. Not a single
+dialog, a CLI flag, or a one-off prompt. The test: if "which of these layouts
+is right?" can't be answered from a sentence, it qualifies.
+
+A spec with no UI, or a trivial one, skips this rule entirely.
+
+## The process
+
+### 1. Research first — during brainstorming, before any prototype
+
+Before building anything, survey how existing and best-in-class tools solve the
+same UX: the category's well-regarded apps, prior art, the conventions users
+already expect. Feed the findings into the spec's Goals and Design so the UX is
+understood before a single prototype exists. Prototyping blind wastes iterations
+re-deriving what a 20-minute survey would have told you. Cite the sources in the
+spec.
+
+### 2. Brainstorm the UX in the spec
+
+Informed by the research, write the goals, the interactions, and the functional
+surface into the spec. This is the "what and why" the prototypes make real.
+
+### 3. Prototype — ~5 distinct directions, then iterate one to final
+
+Build about five genuinely different directions — distinct layouts and
+interaction models, not variations of one — as full working prototypes over one
+shared engine, in the project's design language. Pick a direction, then iterate
+that one across numbered passes to the final. Save each meaningful pass as its
+own numbered prototype so the design history is walkable.
+
+For an Emacs app UI, the production port target is `svg.el` — browser SVG and
+librsvg share primitives, so the winning prototype ports near-1:1. Keep every
+prototype within the librsvg-safe constraint sheet in `emacs.md` (SVG Rendering
+section) so the port stays mechanical.
+
+### 4. Full working prototypes, not mockups
+
+The prototypes must be functional: real state, real controls, real behavior, so
+decisions are made against how it feels to use rather than against a picture. A
+static mockup hides the interaction problems that only surface when you drive it.
+
+### 5. Naming and location
+
+`docs/prototypes/<spec-name>-prototype-<N>.html`, where `<spec-name>` is the
+spec's dated slug with the `-spec` suffix dropped, and `N` is the iteration
+number. For `docs/specs/2026-07-02-timer-panel-spec.org`, the prototypes are
+`docs/prototypes/2026-07-02-timer-panel-prototype-1.html`, `-2.html`, `-3.html`.
+
+### 6. Link from the spec; keep every iteration in history
+
+The spec links the final prototype in its design section, and keeps links to
+every prior iteration in a "Prototype iterations" subsection under the status
+heading — newest last — so the design's evolution is walkable from the spec.
+
+### 7. Decisions get recorded once seen working
+
+A design decision moves into the spec's Decisions only after it has been seen
+working in a prototype. "Resolved live through the prototype iteration" — the
+prototype is the evidence, not an argument on the page.
+
+## How the spec workflows hook in
+
+- **spec-create**: for a non-trivial-UI spec, add the "research → brainstorm →
+ prototype (5 directions → iterate)" step before the design is treated as
+ settled, and require the "Prototype iterations" subsection under the status
+ heading.
+- **spec-review**: for a non-trivial-UI spec, verify the process ran — research
+ cited, final prototype linked, iterations present in history, and each UI
+ design decision backed by a prototype rather than asserted.
+
+Both workflows point here rather than restating the process, so the rule stays
+the single source of truth.
+
+## Why
+
+A UI design argued on paper is a guess. Five working directions surface the
+interaction problems a mockup hides, and iterating one of them to a final makes
+the tradeoffs concrete before any production code is written. Recording each
+decision only once it's been seen working keeps the spec honest: the Decisions
+section documents what the prototype proved, not what the author hoped.
diff --git a/claude-rules/verification.md b/claude-rules/verification.md
index 1bbd8dd..2c2088e 100644
--- a/claude-rules/verification.md
+++ b/claude-rules/verification.md
@@ -13,6 +13,18 @@ This applies to every completion claim:
- "Bug is fixed" → Run the reproduction steps. Confirm the bug is gone.
- "No regressions" → Run the full test suite, not just the tests you added.
+## Green Baseline Before Starting Work
+
+Run the test suite before you start work, not only before you finish. A clean run at the start confirms the tree is in the known-good state you assume it is, so the baseline you build on and measure your changes against is actually green.
+
+If the suite is red before you touch anything, fix or explicitly triage the failure first. A pre-existing failure left in place poisons every later "did I break this?" check: you can't separate your own regressions from the noise, and the end-of-work run stops being readable as pass/fail at a glance. Work that assumes a known-good base may also be built on a broken assumption you never saw.
+
+When a pre-existing failure genuinely can't be fixed before the work begins (out of scope, or it needs a decision), record it as a tracked task with the diagnosis and carry its name forward. The green bar for the rest of the work is then explicitly "only this known failure remains," not a silent tolerance for red.
+
+Two cases are not failures of this rule. A project with no suite has nothing to baseline — note that and proceed. A suite that can't run (no network, a missing dependency, a sandbox limit) is the "When You Cannot Verify" case below, not a work-blocker — record what you couldn't run and proceed with the risk named.
+
+This is the start-of-work counterpart to the Before Committing gate below: one confirms the ground is solid before you build, the other confirms you didn't crack it.
+
## What Fresh Means
- Run the verification command **now**, in the current session
@@ -56,6 +68,8 @@ Do not let an unverifiable check vanish into a confident summary. State it plain
Some checks can only be run by the user: interactive UI a script can't drive, a live external service, visual rendering (colors, layout, faces), a real device, or anything where the verification *is* a human looking at the result. When the gap needs the user's hands or eyes — not just a command they could paste — don't bury the steps in prose. Write them as a structured, runnable checklist in the project's task file.
+When *you* need a window on the user's live desktop to verify something visually — render a panel, grab a screenshot — keep it off the workspace they're actively using. Capture on an off-screen output and tear it down; when showing the user something, open it on a separate workspace and name it. See `desktop-capture.md` for the rule and the Hyprland recipe.
+
Create (or append to) a single parent task named **"Manual testing and validation"** in the project's todo file (`todo.org`, or the project's equivalent). Under it, write **one org sub-header per test**:
- **Title** — descriptive, naming the behavior under test (not "test 1").
@@ -92,7 +106,7 @@ Use this whenever the verification gap from "When You Cannot Verify" above is a
## Before Committing
Before any commit:
-1. Run the test suite — confirm all tests pass
+1. Run the full test suite as its own command, read the result, and commit only when failures are zero — never bundle the run with the commit (e.g. `make test; git commit`), where a red suite can't stop the commit. Run the whole suite, not just the touched file: a change can break a test elsewhere. If the suite can't run, that's "unable to verify" (see When You Cannot Verify above) — surface it, don't commit silently.
2. Run the linter — confirm no new warnings
3. Run the type checker — confirm no new errors
4. Review the diff — confirm only intended changes are staged
diff --git a/claude-rules/working-files.md b/claude-rules/working-files.md
index 9a72702..b915579 100644
--- a/claude-rules/working-files.md
+++ b/claude-rules/working-files.md
@@ -26,6 +26,28 @@ hits a nested path instead of a single canonical name. Always rename the
files individually with a shared prefix so they sort together but live as
flat siblings in `assets/`.
+## `working/` Is Version-Controlled From Creation
+
+`working/` holds the project work currently being developed, so it is
+tracked in git the moment a task subdirectory and its artifacts are created —
+not staged locally and excluded until it graduates. `working/` is the tracked
+home of in-progress work, and it is never added to `.gitignore` (the
+install/sweep tooling deliberately leaves it out).
+
+Filing on completion **reorganizes** durable artifacts into their permanent
+homes; it does not mark the point at which they first become durable. The
+artifacts were durable — and tracked — from creation. Graduation is a move,
+not a promotion from throwaway to keep.
+
+The corollary: genuinely disposable work does not belong in `working/`.
+Ephemeral, single-use, or regenerable artifacts — scratch output, a
+throwaway conversion, intermediate data you will delete — go in a project-root
+`temp/` directory (gitignored) or system `/tmp`, never in `working/`.
+`working/` is for work that will graduate; `temp/` is for work that will be
+thrown away. The install tooling ignores `temp/` in both track and
+gitignore-mode projects, since ephemerality is independent of whether a
+project tracks its `.ai/` tooling.
+
## Directory Layout
<project-root>/
@@ -120,7 +142,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: