<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/claude-templates/.ai, branch main</title>
<subtitle>Claude Code skills, rules, and language bundles
</subtitle>
<id>https://git.cjennings.net/rulesets/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/rulesets/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/'/>
<updated>2026-07-27T19:13:32+00:00</updated>
<entry>
<title>refactor(rules): split testing.md, fix the approval gate, require first person</title>
<updated>2026-07-27T19:13:32+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-27T19:13:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=2f45b6e0010ce375e2c52c91a6537c6d6e8bc0a5'/>
<id>urn:sha1:2f45b6e0010ce375e2c52c91a6537c6d6e8bc0a5</id>
<content type='text'>
Three changes to the same layer.

I split testing.md the way I split commits.md, by what has to be resident rather than by size. What stays is the standing directive: TDD is the default, write the failing test first, and every unit needs Normal, Boundary, and Error cases. That has to fire before any code is written, which is exactly when no skill has been summoned, so it can't ride a trigger. Everything else moved to the testing-standards skill: characterization recipes, the per-category detail, property-based and mutation testing, the pyramid, integration rules, naming, the test-quality and mocking rules, coverage targets, the spike exception, and the anti-patterns. 2,824 words down to 347.

I fixed the approval gate in the publish flow. It decided whether to ask for approval by checking whether .ai/ is tracked, using that as a proxy for "team repo." The proxy was wrong in the direction that matters: rulesets, home, and work all track .ai/ while all three are private single-user repos, so the rule skipped the gate on the three projects I use most. It now checks whether any remote is on a host other than cjennings.net, which is the thing that actually decides whether someone else reads the log. Every current project resolves to gate-applies, which matches how the flow has actually been run.

I also added a first-person directive to the always-loaded core. One already existed for commit bodies and PR prose, but it moved into the publish skill with everything else, and it never covered code comments at all. Now everything I author in or about the repo is first person, with one carve-out: a comment describing what the code does stays third person, because there the code is the actor and not me.

Separately I split the publish skill internally. PR descriptions and the three review shapes moved to references/pull-requests.md, since a plain commit never needs them.

Always-loaded rules are now about 28,900 tokens, down from 57,800 this morning.

One risk on the record. testing.md's margin is thinner than commits.md's was. If testing-standards fails to trigger while I'm writing tests I lose the mocking-boundary rules, which is a quality regression rather than a permanent one, but it is a real bet where commits.md's was not. I also moved the TDD rationalization table rather than cutting it. The posts argue that kind of over-argument is counterproductive now, but removing your defense against me skipping TDD is your call, not mine.
</content>
</entry>
<entry>
<title>refactor(rules): split commits.md into invariants plus a publish skill</title>
<updated>2026-07-27T18:58:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-27T18:58:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=931f364864441676eb250cecd074c4876012a1cd'/>
<id>urn:sha1:931f364864441676eb250cecd074c4876012a1cd</id>
<content type='text'>
commits.md was 12,800 tokens, larger than the next four rules combined, and it loaded in every session in every project whether or not anything was being published. Most of it is procedure that only applies when a commit or PR is in play.

I split it by blast radius rather than by size. What stays always-loaded is what must hold whether or not I'm publishing, and where a violation is permanent and reaches other people: author identity, the no-AI-attribution ban, the generated-document byline rule, and the public-artifact content-scope rules. That core is 1,027 words, about 2,342 tokens.

What moved into the publish skill is everything about how a message gets written, reviewed, approved, and published. The pre-flight reconcile, the code-review gate, the draft and voice and approval gate, conventional-commit format, Voice and Focus, PR description structure, the three review shapes, merge strategy, and the pre-commit checklist.

The failure modes are why the line falls there. If the skill doesn't load I don't know the publish flow and have to be told, which is visible and recoverable. I don't silently commit with AI attribution, because that guard never moved. Only the recoverable half is on the skill-triggering bet.

I verified the split by using it. The skill registered mid-session and I invoked it to publish this commit, and it loaded with the flow intact.

Content is conserved: 5,561 words in, 5,898 across both files, the difference being the frontmatter and the pointer I added to the core. I repointed five cross-references in voice, review-code, inbox.org, and no-approvals.org that named sections which moved.

One thing I left alone. The skill is a single 4,871-word file, and both posts argue a long skill should split internally rather than sit as one blob. It loads on demand now, which is the win worth taking today. Splitting it further is its own change.
</content>
</entry>
<entry>
<title>fix(hooks): don't let a wrap sentinel outlive its session</title>
<updated>2026-07-27T18:32:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-27T18:32:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=2c664cb5651fbf03bc52d7848add0c571708adcd'/>
<id>urn:sha1:2c664cb5651fbf03bc52d7848add0c571708adcd</id>
<content type='text'>
wrap-it-up drops /tmp/ai-wrap-teardown-&lt;project&gt; to ask the Stop hook to kill the tmux session once the wrap certifies clean. When certification fails the Stop hook blocks and leaves the sentinel armed, which I did on purpose so a wrap blocked by a dirty tree can retry on a later stop without re-running the workflow.

I never bounded that retry to the session. An uncertified sentinel sat armed indefinitely and fired in whatever session next reached a clean tree. work's 11:37 wrap today left one armed. The 13:20 session ran startup, committed the task filing and the template sync, went clean, and the next stop consumed the two-hour-old sentinel and killed the terminal mid-work. Every stop before those commits had been blocked by the same sentinel failing certification, so the session spent its whole life either blocked or dead. archsetup's had been armed since Saturday on a live attached terminal, and home's was armed and waiting.

session-start-disarm.sh clears the project's sentinels at SessionStart. A new session means the wrap that armed one is gone, so its pending teardown is meaningless. Within-session retry is untouched, since the hook only runs at session start, and a test pins that. If teardown is still wanted, wrap-it-up re-arms it.

I disarmed the three live ones by hand before writing this, backed up under /tmp/disarmed-sentinels.

Four tests cover the disarm, one pins the retry behavior I did not want to lose. The scoping test matters most: a concurrent session in another project keeps its own sentinel.
</content>
</entry>
<entry>
<title>feat: add peer-reasoning contract and fix two silent probe defects</title>
<updated>2026-07-27T15:48:12+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-27T15:48:12+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=6c1ea8bbc3775fe7c481f41b4f30e0c0110a9339'/>
<id>urn:sha1:6c1ea8bbc3775fe7c481f41b4f30e0c0110a9339</id>
<content type='text'>
I added a Collaborative Peer Reasoning section to the interaction rules. It governs how an interpretation forms before any rule about presenting choices: infer first and clarify only at material forks, test a conclusion against its strongest alternative, let a correction update the downstream model instead of just the wording. The file's framing line widened to match.

Two probes were failing silently. The startup KB nudge looked up the best-practices node by grepping file content for its slug. A roam node's slug lives in its filename, so the lookup always returned empty. The nudge pointed at nothing in every project and every session, for as long as it shipped. It matches the filename now, through find rather than a glob so zsh doesn't abort on no match.

The browser rule told agents to open URLs with a form ending in &amp;&gt;/dev/null &amp;. That discards the "Opening in existing browser session." line confirming the tab opened. Warm and cold start now split: foreground and read the confirmation when Chrome is already up, detach only when it isn't. The confirmation is on stdout, verified rather than assumed.

I filed two tasks from handoffs. The sentry triage split needs a work-vs-personal classification mechanism before its wording can move, because the current rule excludes by category and category can't express that split. The publish-lock design is approved but carries three open gaps. The load-bearing one is a lock held across an unbounded human approval pause.

I also swept the old processed handoffs out of inbox/. History keeps them.
</content>
</entry>
<entry>
<title>feat: enforce clean wraps and inbox-safe sync</title>
<updated>2026-07-25T20:35:14+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-25T20:35:14+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=f2609d9f9ad33486bef43211d753ba53e1e24181'/>
<id>urn:sha1:f2609d9f9ad33486bef43211d753ba53e1e24181</id>
<content type='text'>
Centralize repository-state checks, bind teardown to a certified clean HEAD, and allow inbox-only refreshes. Guard installed symlinks from cross-project writes and add regression coverage.
</content>
</entry>
<entry>
<title>docs: define speedrunnable backlog tasks</title>
<updated>2026-07-25T20:34:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-25T20:34:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=3203bd803b6a05d10781634e7e18742879c1046a'/>
<id>urn:sha1:3203bd803b6a05d10781634e7e18742879c1046a</id>
<content type='text'>
Clarify that speedrunnable means the solo autonomy tag; quick remains only an effort hint.
</content>
</entry>
<entry>
<title>fix(lint-org): stop flagging correctly paired verbatim blocks</title>
<updated>2026-07-25T06:50:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-25T06:50:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=8822b0dab56ca50269771b4c9d06b78657c2b8b4'/>
<id>urn:sha1:8822b0dab56ca50269771b4c9d06b78657c2b8b4</id>
<content type='text'>
org-lint's invalid-block checker flags both delimiters of a paired example or src block whose body holds a heading-shaped line. Its parser reads the `** ` as a structural break and loses the open block. Every org file that documents org syntax inside a block collects two false findings.

The checker is org-lint's own, not one lint-org implements, so I filtered its output instead of patching a local checker. `lo--matched-block-regions` pairs blocks by scanning lines under org's real rule (once open, only the block's own `#+end_TYPE` closes it). A finding inside a matched region gets dropped, delimiters included, since that's where org-lint reports. I scanned lines rather than asking org because org's parser is what's confused.

An unterminated block never lands in a matched region, so it still reports. Three tests cover the false positives: a heading in an example block, a literal `#+end_example` inside a src block, and uppercase delimiters. Two more guard the real finding, including a file with one paired and one unterminated block that proves the suppression is per-block.
</content>
</entry>
<entry>
<title>fix: harden the org-file mutators and close four verified bugs</title>
<updated>2026-07-24T15:56:50+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T15:56:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=267d1de7b8a8e7fd22b156433567c88216ec3d0f'/>
<id>urn:sha1:267d1de7b8a8e7fd22b156433567c88216ec3d0f</id>
<content type='text'>
An overnight hygiene run over this repo, reviewed adversarially afterward. Every problem below was reproduced before it was fixed, and every fix was re-checked by a skeptic who was told to refute it.

cj-remove-block could silently destroy content. Its range check validated only the first and last lines, so a span from one cj block's opener to a later block's closer passed and the removal deleted everything between — prose, headings, whole tasks, with a zero exit. That is the failure the check exists to prevent, and drift is its normal case, since the calling skill edits the file while processing. It now refuses a range holding more than one block, backs up first, and writes atomically while preserving the file's mode and following symlinks to the real target.

todo-cleanup rewrote todo.org with no backup, alone among the three tools that mutate these files. It now copies to the temp dir like lint-org does. Both backup helpers suffix rather than overwrite, because a second-resolution stamp collapsed two back-to-back invocations into one backup holding already-mutated content — and running todo-cleanup twice in a row is the shipped path.

audit.bats failed about one run in eight, in teardown rather than in its assertions, so a passing test reported as a false red. git commit spawns a detached maintenance process that is still writing packs when the commit returns, racing the cleanup. The fixture disables it.

route_recommend downgraded a correct strong match to weak when two projects shared a basename. lint.sh now sweeps the scripts that get symlinked onto PATH, which were the only shell in the repo with no gate over them.

Two test defects found in review and fixed here: a suite that deleted real backups from the shared temp dir, and one that depended on that directory's contents. Isolation now lives in an autouse fixture rather than in each test's memory.

Known and filed, not closed: the range check still cannot prove the range is the block that was scanned, so drift by exactly one whole block deletes the wrong annotation. That needs a content assertion and a CLI contract change.
</content>
</entry>
<entry>
<title>feat(sentry): add refactor finding and an opt-in solo-implementation pass</title>
<updated>2026-07-24T05:49:25+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T05:49:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=17933a6bc4f30b59dc7fd058736a8ba9b3344a6a'/>
<id>urn:sha1:17933a6bc4f30b59dc7fd058736a8ba9b3344a6a</id>
<content type='text'>
Two additions from Craig, both landing on the throwaway branch that already makes a bad night a deleted branch.

Pass 11 now finds refactoring opportunities alongside bugs, filing each as a graded task. Still find-never-fix: a fresh finding files a task and stops, because a just-found bug can be a misread (one was filed and retracted two fires apart on 2026-07-23). The task is the checkpoint.

New pass 12 implements the backlog's solo, decision-free tasks on the branch, off by default behind :SENTRY_MAY_IMPLEMENT:. It reuses work-the-backlog's unattended-loop contract: only the ready bucket runs, anything needing a decision defers to a VERIFY, and nothing pushes. The morning review is the gate. Its /review-code runs the premise check first (reproduce the bug before judging the diff), because a filed claim never gets a review, and that skeptical review is what makes fixing on a branch safe. A finding that fails its premise check isn't implemented.

The marker is separate from :COMMIT_AUTONOMY: on purpose: hygiene is a two-minute merge, implemented code is a review session, so the owner opts in per project.
</content>
</entry>
<entry>
<title>fix(lint-org): scope the todo-format checkers out of docs/specs/</title>
<updated>2026-07-24T05:02:13+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T05:02:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=c38bab9f0005a7da82487674d3d8bceca36368a9'/>
<id>urn:sha1:c38bab9f0005a7da82487674d3d8bceca36368a9</id>
<content type='text'>
The five todo-format-family checkers encode todo.org completion conventions, but they ran on every org file, so they misfired across docs/specs/. That was 100 findings over nine specs in this repo, and the same noise in every project carrying specs. A spec's Decisions section legitimately uses a level-2 DONE with no CLOSED cookie. Its review-history uses level-2 dated headings. Neither is a completion defect there.

A path guard now skips the family on any file under a docs/specs/ segment, the canonical spec home per the docs-lifecycle rule. Link, table, and structural checks still run on specs, so a broken link in one still flags. todo.org and every non-spec org file keep the full checker set.

Folds in task-missing-last-reviewed alongside the four named checkers, since it's the same family and misfires the same way on a spec's phase tasks. Correcting an earlier note in the task: these are lint-org.el's own checkers, not org-lint's, so the fix is a local guard rather than filtering upstream output.
</content>
</entry>
</feed>
