<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/Makefile, 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-11T16:35:45+00:00</updated>
<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(typescript): add coverage-summary to the TypeScript bundle</title>
<updated>2026-05-31T18:57:40+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T18:57:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=ee903e4b63257573773e93d10612250e3634cae9'/>
<id>urn:sha1:ee903e4b63257573773e93d10612250e3634cae9</id>
<content type='text'>
Last language in the coverage-summary fan-out, after Elisp, Python, and Go. Same kernel: count every source file on disk that's absent from the coverage report as 0% and weight the project number by file, so an untested file stays visible instead of being averaged away.

The script at languages/typescript/claude/scripts/coverage-summary.js parses an Istanbul json-summary report (the coverage-summary.json that c8, Vitest, and Jest all emit), takes per-file statements covered over total, and reports a file-weighted number plus the missing files. It walks the source dir for .ts/.js, skipping test files, declarations, and node_modules. Node built-ins only, so it runs via node with no install, and it doesn't reimplement the per-file table nyc already prints.

Tests are black-box, run with node's own test runner: a temp tree plus a json-summary report, the script invoked via node, output asserted. They cover missing-file detection, all-tracked, test-file and node_modules exclusion, and the missing-report error. make test gained a node --test discovery path for languages/*/tests, guarded so environments without Node skip it cleanly. As with Python, the TypeScript bundle had no gitignore-add.txt, which would have left the script un-gitignored on install, so I added one.

This finishes the fan-out: coverage-summary now ships in all four bundles, each parsing its own tool's report behind the same file-weighted, missing-as-0% kernel. I proved the Go and TypeScript scripts by running them (Go against a live profile, TS against a synthetic report and the CLI). Python and TypeScript weren't run against a live coverage tool, since neither coverage.py nor nyc is installed here, so the first adopter of each should check against a real report.
</content>
</entry>
<entry>
<title>feat(go): add coverage-summary as a Go bundle coverage slice</title>
<updated>2026-05-31T18:07:40+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T18:07:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=47ca509e69b6a1472a735a4b9521a952e7434491'/>
<id>urn:sha1:47ca509e69b6a1472a735a4b9521a952e7434491</id>
<content type='text'>
Third language in the coverage-summary fan-out, after Elisp and Python. Same kernel: count every source file on disk that's absent from the coverage profile as 0% and weight the project number by file, so an untested file stays visible instead of being averaged away.

The script at languages/go/claude/scripts/coverage-summary.go parses a cover.out profile, maps each import-path-qualified entry back to an on-disk relative path using the module path from go.mod, and reports a file-weighted number plus the missing files. It's standard library only, so it runs anywhere via go run, and it doesn't reimplement the per-function table that go tool cover -func already prints. I proved it against a real go test -coverprofile run, not just a synthetic fixture, since the Go toolchain is installed here.

Two findings to flag. Modern go test ./... already lists every module package in the profile at 0% even when untested, so for in-module code the missing-file list is usually empty. The detection earns its keep on build-tagged files and dirs outside ./.... And this is a coverage-only slice of a Go bundle that doesn't otherwise exist yet: there's no go.md rule file, so sync-language-bundle.sh can't fingerprint it (detection keys on a bundle's own .claude/rules). The script installs via make install-lang LANG=go but won't be sync-maintained until the Go bundle gets real rules and a CLAUDE.md. Building that out is the natural companion task.

Tests are black-box: a Go test in its own throwaway module runs the script via go run against temp fixtures and checks output, so the shipped script dir stays test-free. They cover missing-file detection, all-tracked, _test.go exclusion, and the missing-report error. make test gained a go test discovery path for languages/*/tests, guarded so environments without Go skip it cleanly.
</content>
</entry>
<entry>
<title>feat(python): add coverage-summary to the Python bundle</title>
<updated>2026-05-31T17:31:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T17:31:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=af478a42b18c4d5e0712c4cb43036126d36c56b5'/>
<id>urn:sha1:af478a42b18c4d5e0712c4cb43036126d36c56b5</id>
<content type='text'>
Second language in the coverage-summary fan-out, after the Elisp pilot. Same kernel: a module no test imports never appears in coverage.py's report, so a line-weighted total skips it silently and the suite looks healthier than it is. This counts every source file on disk that's absent from the report as 0% and weights the project number by file, so untested modules stay visible.

The script at languages/python/claude/scripts/coverage-summary.py parses coverage.py's JSON (files[path].summary.covered_lines / num_statements), resolves report paths against the report's directory since coverage records them relative to where it ran, and recurses the source dir for *.py. Unlike the Elisp version it doesn't print a per-file table, because coverage.py's own coverage report already does. The script adds the missing-file accounting that report lacks. It uses only the standard library, parsing the report rather than importing coverage.

The Python run confirmed the plumbing from the pilot is genuinely generic. install-lang and sync deliver the script and the project-owned coverage-makefile.txt with no Python-specific code. The one gap I had to close: the Python bundle shipped without a gitignore-add.txt, so the .claude/ footprint wasn't ignored and the script would have been committable. Added one mirroring the Elisp footprint plus Python artifacts (__pycache__, .coverage, coverage.json). make test gained a languages/*/tests/test_*.py discovery path alongside the existing Elisp ERT one.

Tests: 12 pytest covering the parser, the file-weighted number, and the missing-file detection including subpackage recursion, plus an install-lang check that the script lands in the gitignored footprint. I proved it against a report matching coverage.py's documented schema and the CLI end to end, but not against a live coverage json run, because coverage.py isn't installed in this repo's env. The first project to adopt it should sanity-check against a real report.
</content>
</entry>
<entry>
<title>feat(elisp): add coverage-summary to the Elisp bundle with missing-file detection</title>
<updated>2026-05-31T16:43:03+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T16:43:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=b46619cd17ed4e36f2e59c1b600078521b2049ef'/>
<id>urn:sha1:b46619cd17ed4e36f2e59c1b600078521b2049ef</id>
<content type='text'>
A line-weighted coverage total has a blind spot: a module no test loads never shows up in the SimpleCov report, so it can't drag the number down. The suite looks healthier than it is. This adds a summary that counts every source file on disk against the report and treats an absent file as 0%, weighting the project number by file instead of by line so untested modules stay visible.

The script ships at languages/elisp/claude/scripts/coverage-summary.el, self-contained on stock Emacs (just the built-in json). It parses the undercover SimpleCov shape directly rather than depending on the editor's coverage engine, so it runs anywhere the bundle lands. I proved it against a real 103-file report: 93 tracked, 27 untested modules surfaced, project number 66.4%.

Delivery follows the bundle convention. The script lives under the gitignored .claude/ footprint and gets auto-fixed on drift by sync-language-bundle.sh, which I made generic for any claude/scripts/* rather than coverage-specific. The Makefile targets ship as a project-owned fragment (languages/elisp/coverage-makefile.txt) that install-lang.sh seeds at the project root and sync drops into .ai/inbox/ when that convention exists. The bundle never edits the project's own Makefile.

Tests: 12 ERT for the kernel (Normal/Boundary/Error per function), wired into make test via a new languages/*/tests/ discovery path, plus bats for the sync auto-fix and the inbox-drop guards.

This is the Elisp pilot. The pattern is proven, so fanning out to Python, Go, and TypeScript is now a follow-up. Each one needs only its own parser and fragment. The plumbing is already generic.
</content>
</entry>
<entry>
<title>feat(signal): page-signal CLI wrapper + workflows + cross-project broadcast helper</title>
<updated>2026-05-29T19:51:53+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-29T19:51:53+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=664bf01ceaccf730cb636463cc8587cd1d966192'/>
<id>urn:sha1:664bf01ceaccf730cb636463cc8587cd1d966192</id>
<content type='text'>
Three coupled additions ship together.

claude-templates/bin/page-signal is a bash wrapper around signal-cli
send. It defaults to --note-to-self for safety. The wrapper supports
--file for attachments, --to &lt;+number&gt; for outbound (explicit per
call, no defaults, no batch), --quiet, and --json. Exit codes: 0
sent, 1 signal-cli failure, 2 usage error, 3 signal-cli not
installed.

claude-templates/.ai/workflows/page-signal.org carries the
discrimination rules and safety rails. When desktop notify covers it,
don't reach for Signal. Long-running task completion is the canonical
case. Outbound to other contacts requires explicit Craig instruction
per send. A known-limitation note covers the current notification
gap. signal-cli registered on Craig's primary number means messages
don't fire notifications until the pending Google Voice registration
lands.

claude-templates/.ai/workflows/cross-project-broadcast.org and its
helper cross-project-broadcast.py fan out a single message file to
every AI project's inbox in one operation. Discovery is
fingerprint-based: any directory under ~/code, ~/projects, ~/.emacs.d
with both .ai/protocols.org and a top-level inbox/ is broadcastable.
Senders are auto-excluded. Verified discovery against 23
broadcastable targets.

Makefile's install target gains a general bin/ loop. The previous
version hardcoded bin/ai. The new version iterates over every
executable under claude-templates/bin/ and symlinks each into
~/.local/bin/. install-hooks (existing Claude hook installer) is
unchanged. install-githooks (sync-check pre-commit hook setup, added
earlier today) is unchanged. The bin/ loop now picks up bin/page-signal
automatically.

INDEX entries for both new workflows landed under Tools and meta.

No bats tests on the new scripts. page-signal was smoke-tested with a
live send. The send succeeded. The notification gap is covered by the
workflow's known-limitation note. cross-project-broadcast.py was
smoke-tested via --list against the live project set. Tests can be
added when the broadcast pattern proves out across multiple use cases.
</content>
</entry>
<entry>
<title>feat(mcp): add uninstall + --check + README section for MCP pipeline</title>
<updated>2026-05-28T14:20:08+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T14:20:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=814695eae81dd1c63d75cae87375e703bb388243'/>
<id>urn:sha1:814695eae81dd1c63d75cae87375e703bb388243</id>
<content type='text'>
Three coupled additions close the MCP pipeline thread.

mcp/install.py grew --uninstall and --check modes via argparse. The
default install behavior is unchanged.

--uninstall iterates over servers.json and runs `claude mcp remove
&lt;name&gt; -s user` for each, skipping anything not registered. Idempotent.

--check is the dry-run drift report. For each server, classify as ok
(in both servers.json and `claude mcp list`), MISSING (configured but
not registered), or EXTRA (registered but not in servers.json). Exit
non-zero only on MISSING since EXTRA entries are often deliberate (the
claude.ai web servers register out-of-band). Smoke test against the
live config: 9 ok, 0 missing, 3 EXTRA, exit 0.

Two new Makefile targets:
- make uninstall-mcp invokes the --uninstall mode.
- make check-mcp invokes the --check mode.

README.org gained an MCP section under Two install modes covering all
three targets, the OAuth-token-on-disk story, and a pointer to
mcp/README.org for the full pipeline.

Closes TODO #7 (uninstall + --check) and TODO #8 (README MCP section).
</content>
</entry>
<entry>
<title>feat(status): add `make status` for compact health summary</title>
<updated>2026-05-28T14:13:51+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T14:13:51+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=bdf755d33aa6a207a538c85f18e38cc03f14e529'/>
<id>urn:sha1:bdf755d33aa6a207a538c85f18e38cc03f14e529</id>
<content type='text'>
scripts/status.sh prints a six-line summary composing existing checks:
- audit + doctor (one call, since audit.sh runs doctor internally)
- canonical/mirror sync state via sync-check.sh
- open todo count under * &lt;Project&gt; Open Work
- inbox count (excluding .gitkeep and PROCESSED- prefixes)
- git working-tree state with ahead/behind upstream

Sample output:

  rulesets status — 2026-05-28 09:13 CDT
    audit  Summary: 41 ok, 0 warnings, 2 failures
    sync   canonical = mirror
    todo   22 open
    inbox  1 unprocessed
    git    main dirty — in sync with origin/main

The script adds no new logic beyond formatting. `make status` is the
entry point.

The scope here is limited per the triage disposition for codex item
#12. The rest of #12 was rejected. `make sync` duplicates the existing
sync flow, `make health` wraps existing checks without adding signal,
`make bootstrap-project` duplicates `install-ai` + `install-lang`.
</content>
</entry>
<entry>
<title>feat(sync-check): canonical/mirror drift detection + pre-commit hook</title>
<updated>2026-05-28T14:11:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T14:11:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=9f84ea2c7854e35ae30c0fb5fbd63f7b7115fb41'/>
<id>urn:sha1:9f84ea2c7854e35ae30c0fb5fbd63f7b7115fb41</id>
<content type='text'>
scripts/sync-check.sh diffs claude-templates/.ai/{protocols.org,
workflows,scripts} against the .ai/ mirror. Exits 0 when clean, 1 with
a diff report on drift, 2 outside a rulesets-shaped repo or git
checkout. --fix mode rsyncs canonical -&gt; mirror and re-checks, then
prompts to re-stage.

githooks/pre-commit wraps the script. Commits abort on drift so the
issue surfaces at publish time, not at the next session's startup
rsync.

Two new Makefile targets:
- make sync-check [FIX=1] runs the script (FIX=1 passes --fix
  through).
- make install-githooks sets core.hooksPath=githooks (idempotent).

scripts/tests/sync-check.bats holds 8 tests covering clean,
drift-per-path, --fix, extra-file removal, missing canonical, and
outside-git. All eight pass.

This catches the exact drift I had to fix manually during this
morning's audit pass. The mirror's open-tasks.org PROPERTIES drawer
sat below a sub-heading because the mirror commit was older than
canonical.
</content>
</entry>
</feed>
