<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/scripts, 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-06-13T18:49:21+00:00</updated>
<entry>
<title>feat(hooks): title sessions host-project with a hyphen, no space</title>
<updated>2026-06-13T18:49:21+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-13T18:49:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=f537150ff3f67899d27a7f121bc302f61a307c1c'/>
<id>urn:sha1:f537150ff3f67899d27a7f121bc302f61a307c1c</id>
<content type='text'>
The SessionStart hook joined host and project with a space ("ratio rulesets"), which reads as two words in the claude.ai/code and mobile session lists. I changed the join to "$host-$project" ("ratio-rulesets") so the title is one token, and updated the three session-title-hook.bats expectations test-first.
</content>
</entry>
<entry>
<title>feat(commands): /update-skills syncs forks with upstream via 3-way merge</title>
<updated>2026-06-11T22:05:03+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-11T22:05:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=da93ffd91dea133963ffceaff24d41bc76b8ff93'/>
<id>urn:sha1:da93ffd91dea133963ffceaff24d41bc76b8ff93</id>
<content type='text'>
Upstream releases fixes worth pulling into the forks (arch-decide, playwright-js, playwright-py) without losing our local modifications. Each fork now has a manifest at upstreams/&lt;name&gt;/ plus a committed baseline snapshot that is the 3-way merge base. scripts/update-skills.py classifies each file's drift and merges to stdout. The command owns per-file confirmation, per-hunk conflict prompts, and every target write.

I centralized manifests under upstreams/ instead of per-skill dotfile dirs because arch-decide is now two flat files in commands/ and can't carry one. A "files" map in its manifest handles the upstream rename of SKILL.md to arch-decide.md.

I seeded baselines from today's upstream HEADs, so pre-existing local modifications classify as local-only from here on. git merge-file signals hard errors as exit 255, which subprocess reports as positive. The guard treats anything 128 and up as an error so a binary-file failure isn't misread as a conflict.
</content>
</entry>
<entry>
<title>feat(hooks): title sessions "host project" for the remote session list</title>
<updated>2026-06-11T18:23:13+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-11T18:23:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=bdc9a5d6e1320032770f54c747c210e4f465c399'/>
<id>urn:sha1:bdc9a5d6e1320032770f54c747c210e4f465c399</id>
<content type='text'>
Remote sessions showed up on claude.ai/code and mobile under auto-generated names, so picking the right one meant guessing. Claude Code 2.1.152+ lets a SessionStart hook set the title via hookSpecificOutput.sessionTitle.

hooks/session-title.sh emits "&lt;uname -n&gt; &lt;project&gt;" (ratio rulesets, velox work) on startup and resume. Project is the git-toplevel basename so a session started in a subdirectory still names the project, with the cwd basename as fallback. The hook stays silent when a title already exists, so a /rename or an earlier run isn't clobbered on resume. The harness ignores titles on clear and compact, so the settings matcher restricts to startup|resume.

Wired in settings.json and the install-hooks snippet. As a default hook it reaches every machine through make install on the next session start.
</content>
</entry>
<entry>
<title>feat(install): adopt the statusline script into the managed set</title>
<updated>2026-06-11T16:35:45+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-11T16:35:45+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=3df14fc985ddad041c290c732b5b5b8eae41f68e'/>
<id>urn:sha1:3df14fc985ddad041c290c732b5b5b8eae41f68e</id>
<content type='text'>
An archsetup session added a statusLine entry to the tracked settings.json on 2026-06-11 (Craig's request), pointing at ~/.claude/statusline-command.sh, but the script itself lived outside the repo on one machine. This commits the settings entry and brings the script into .claude/, linked by make install like the rest of the config, so it reaches every machine on the next session.

Two fixes over the original: uname -n instead of hostname (Arch doesn't ship hostname by default, so the host rendered empty with stderr noise), and the tilde replacement is escaped (unquoted, bash expands the replacement ~ straight back to $HOME, which defeated the abbreviation). scripts/tests/statusline-command.bats covers the format, branch handling, and the no-stderr contract.
</content>
</entry>
<entry>
<title>fix(install): link default hooks in make install</title>
<updated>2026-06-11T16:32:40+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-11T16:32:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=d576fc217ba304b48dfb1c54b92bc1849397fd9b'/>
<id>urn:sha1:d576fc217ba304b48dfb1c54b92bc1849397fd9b</id>
<content type='text'>
session-clear-resume.sh shipped 2026-06-02 with its settings.json entry, but make install didn't cover hooks and nothing re-ran install-hooks, so the symlink only existed on machines that had linked it by hand. Everywhere else the hook errored silently on every /clear.

make install now links DEFAULT_HOOKS alongside skills, rules, config, and bin scripts, so the startup workflow's install step propagates new hooks machine-wide. Opt-in hooks stay manual. scripts/tests/install-hooks-link.bats covers the new section. The SessionStart-on-clear todo task closes with this: the hook feature already existed, and the gap was distribution.
</content>
</entry>
<entry>
<title>feat(kb): monthly hygiene report for agent KB nodes</title>
<updated>2026-06-10T23:21:15+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-10T23:21:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=b0140951ebe0f0c2d33a868a2d1cda2eafd29044'/>
<id>urn:sha1:b0140951ebe0f0c2d33a868a2d1cda2eafd29044</id>
<content type='text'>
Phase 4 of the agent KB spec. kb-hygiene.sh inventories :agent: nodes, flags orphans (no id: link anywhere in the KB points at them), duplicate titles, and stray conflict files, then writes an org report into the rulesets inbox for the normal inbox flow to propose dispositions. Read-only by design — it never deletes. A monthly systemd user timer (Persistent=true) runs it; bats covers the counts, orphan detection, duplicates, conflict tally, and the missing-KB error path.
</content>
</entry>
<entry>
<title>feat(kb): roam-sync script + timer units, old roam path repointed</title>
<updated>2026-06-10T23:13:03+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-10T23:13:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=fcf554a6be8b02aeb9c521ea5d7b7d86465aea0f'/>
<id>urn:sha1:fcf554a6be8b02aeb9c521ea5d7b7d86465aea0f</id>
<content type='text'>
Phase 0 of the agent KB spec: the org-roam KB now lives at ~/org/roam as a git repo on cjennings.net. roam-sync.sh (bats-tested: commit, rebase, push, conflict-abort) runs from a 15-minute systemd user timer; canonical unit files live in scripts/systemd/. Live references to the old ~/sync/org/roam path (the task-list pointer, the journal workflow, the notes template) repoint to ~/org/roam, and a transition symlink at the old location covers stragglers.
</content>
</entry>
<entry>
<title>feat(install-ai): gitignore the full personal-tooling set, add backfill sweep</title>
<updated>2026-06-10T06:14:46+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-10T06:14:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=cc72aa635f733da36010567c8718b1ede7622c52'/>
<id>urn:sha1:cc72aa635f733da36010567c8718b1ede7622c52</id>
<content type='text'>
A gitignore-mode project only ignored .ai/. CLAUDE.md was left untracked but not ignored, so an accidental git add or a codify run could still commit a personal CLAUDE.md, the private rule copies under .claude/, or an AGENTS.md. install-ai now ignores the whole set (.ai/, .claude/, CLAUDE.md, AGENTS.md) at bootstrap, line-idempotent so an existing .gitignore isn't duplicated.

.claude/ goes in the set because it's rulesets-owned (copies of claude-rules/*.md plus the language bundle's rules, hooks, and settings), re-synced from rulesets every startup, so git isn't how it travels. Ignoring it also keeps those private rule copies out of the repo, which ignoring CLAUDE.md alone would miss. The gate is unchanged: track-mode projects (personal/doc repos, team repos sharing config) keep tracking the set.

sweep-gitignore-tooling.sh backfills the set across existing gitignore-mode projects, idempotent and skipping track-mode by design. It warns when a now-ignored path is already tracked, since the ignore won't untrack it. protocols.org states the policy once.
</content>
</entry>
<entry>
<title>fix(language-bundle): don't re-drop the coverage fragment once adopted</title>
<updated>2026-06-03T18:30:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-03T18:30:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=36e57f15e2cc191172016c50d45b5bf5d71933e5'/>
<id>urn:sha1:36e57f15e2cc191172016c50d45b5bf5d71933e5</id>
<content type='text'>
The startup bundle sync re-dropped from-rulesets-coverage-makefile.txt into a project's inbox on every run, even after the project had adopted the targets. inbox_drop only treated the fragment as adopted if coverage-makefile.txt still sat at the project root or waited in the inbox. But install-lang tells users the opposite: copy the targets into your Makefile, then delete the fragment. So a project that followed the documented path got the drop re-suggested forever (deleted three sessions running in one case).

I guarded the drop so a project Makefile that already defines the distinctive coverage-summary target counts as adopted. The check lives at the call site, keeping inbox_drop generic. Added two bats cases: targets-in-Makefile suppresses the drop, an unrelated Makefile still gets it.
</content>
</entry>
<entry>
<title>feat(go): build out the full Go language bundle</title>
<updated>2026-06-02T23:22:11+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-02T23:22:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=3a06aff7eec20814f6b51b72691f4140668189c2'/>
<id>urn:sha1:3a06aff7eec20814f6b51b72691f4140668189c2</id>
<content type='text'>
The Go bundle was coverage-slice-only. Because it shipped no rule files, sync-language-bundle.sh (which fingerprints a project's bundle by spotting one of its rule files in .claude/rules/) couldn't detect it, so the coverage slice it did ship never stayed in sync. Adding the rules is what makes the bundle sync-maintainable, which was the point.

Brought Go to the full tier, matching elisp:
- claude/rules/go.md and go-testing.md, the style and testing rules (table-driven tests, go test -race, errors.Is over message matching, how the coverage slice fits). These two are also the sync fingerprint.
- claude/hooks/validate-go.sh, a PostToolUse hook that runs gofmt and go vet on each edited .go file. go vet type-checks, so compile and syntax errors surface at edit time. It deliberately doesn't auto-run tests, since a package's tests can be slow or integration-tagged and shouldn't fire on every keystroke.
- claude/settings.json, Go permissions plus the hook wiring.
- githooks/pre-commit, a secret scan and a gofmt check on staged .go.
- CLAUDE.md, the seed.

validate-go.sh is TDD'd by scripts/tests/validate-go.bats: a clean file passes, gofmt and vet failures both block with the JSON payload, and non-go, missing, or empty paths are ignored. I updated install-lang.bats test 7, which asserted Go installs no CLAUDE.md, to check the full bundle instead. Verified with a real install into a throwaway project and a green make test.
</content>
</entry>
</feed>
