aboutsummaryrefslogtreecommitdiff
path: root/.claude
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-14 18:46:32 -0500
committerCraig Jennings <c@cjennings.net>2026-05-14 18:46:32 -0500
commit9f62a7cadf37e3f453efbb0cdf253bcafb1b6393 (patch)
tree2d1928d0e8041ca0663a132385602d1bf1e17c27 /.claude
parentf5b8688aed8ec698220a67c2dbfbcae22e7575f4 (diff)
downloadrulesets-9f62a7cadf37e3f453efbb0cdf253bcafb1b6393.tar.gz
rulesets-9f62a7cadf37e3f453efbb0cdf253bcafb1b6393.zip
feat(lint-org): add /lint-org command + file design spec
A new /lint-org command at .claude/commands/lint-org.md orchestrates the elisp script: invokes it, parses the stdout plist stream, walks each judgment item with the user via inline numbered options (per interaction.md, no popup), and reports pre/post-pass deltas. Two modes: interactive (default, walks judgments now) and mechanical-only (defers them to a follow-ups file via --followups-file). The spec at .ai/specs/lint-org-skill-spec.md is the design doc that motivated this work, captured from yesterday's manual 55→1 lint pass on todo.org. todo.org gains a [#A] entry pointing at the spec.
Diffstat (limited to '.claude')
-rw-r--r--.claude/commands/lint-org.md162
1 files changed, 162 insertions, 0 deletions
diff --git a/.claude/commands/lint-org.md b/.claude/commands/lint-org.md
new file mode 100644
index 0000000..953629c
--- /dev/null
+++ b/.claude/commands/lint-org.md
@@ -0,0 +1,162 @@
+---
+description: Run org-lint over a tracked .org file, apply mechanical auto-fixes silently, then walk judgment items with the user. Mechanical categories — item-number (add [@N] counters to drifted bullets), missing-language-in-src-block (convert bare #+begin_src to #+begin_example), misplaced-planning-info (merge multi-line CLOSED:/DEADLINE:/SCHEDULED: onto one line), misplaced-heading markdown-bold case (**X.** → *X.*). Judgment categories — broken file: links, invalid fuzzy links, verbatim-asterisk inside body prose (=*** Foo=), unknown source-block languages. Modes — `interactive` (default) walks each judgment with inline numbered options and applies user-chosen resolutions; `mechanical-only` applies mechanical, appends remaining judgment items to a carry-forward file for next-morning review. Defaults FILE to `todo.org` in cwd. Backs up the file to /tmp before any modification. Use to clean up accumulated org-lint warnings on a tracked file. Do NOT use for markdown files (wrong tool), for org files outside the project (cross-project boundary), or to enforce style rules beyond what org-lint flags.
+disable-model-invocation: true
+---
+
+# /lint-org — Sweep an org file's lint warnings
+
+Apply mechanical fixes silently, walk judgment items with the user.
+
+## Usage
+
+```
+/lint-org [FILE] [--mode=interactive|mechanical-only]
+```
+
+- `FILE` — path to the .org file. Defaults to `todo.org` in the current working directory.
+- `--mode` — `interactive` (default) or `mechanical-only`.
+- Backs up the file to `/tmp/<basename>.before-lint-pass.<timestamp>` before any modification.
+
+## Scope
+
+In scope:
+
+- `todo.org` and other tracked org files at the project root or under `.ai/`.
+- Mechanical fixers listed in **Categorization** below.
+- Judgment items in the four documented categories, plus a generic "unhandled" fallback.
+
+Out of scope (refuse, don't try to lint):
+
+- Markdown files — use a markdownlint tool.
+- Org files outside the current project — flag a cross-project boundary per `protocols.org`.
+- Style rules beyond what `org-lint` flags (line length, sentence reflow, etc.).
+
+## Categorization
+
+**Mechanical (auto-fixed in non-`--check` modes):**
+
+| Checker | Fix |
+|---------|-----|
+| `item-number` | Add `[@N]` directive: `4. content` → `4. [@4] content`. |
+| `missing-language-in-src-block` | Convert bare `#+begin_src` ... `#+end_src` to `#+begin_example` ... `#+end_example`. |
+| `misplaced-planning-info` | Merge multi-line `CLOSED:`/`DEADLINE:`/`SCHEDULED:` onto a single canonical line (CLOSED → DEADLINE → SCHEDULED order). |
+| `misplaced-heading` *(markdown-bold case)* | `**X.**` at start of line → `*X.*`. The verbatim-asterisk case (`=*** Foo=` in body prose) stays judgment. |
+
+**Judgment (walked inline in `interactive` mode, deferred in `mechanical-only`):**
+
+| Checker | Resolutions to offer |
+|---------|----------------------|
+| `link-to-local-file` | (1) Repair to a renamed/moved file by heuristic search. (2) Drop to `=verbatim=` text with a "(file never landed)" annotation. (3) Leave as-is and file a TODO entry. (4) Skip. |
+| `invalid-fuzzy-link` | (1) Repair to a `[[*Heading]]` ref if a similar heading exists. (2) Drop to `=verbatim label=` text. (3) Skip. |
+| `misplaced-heading` *(verbatim-asterisk case)* | (1) Strip asterisks and rephrase to preserve semantics. (2) Convert surrounding markup to `~code~` style. (3) Skip. |
+| `suspicious-language-in-src-block` | (1) Emit an Emacs init one-liner that registers the language. (2) Change the block label to `text` or `example`. (3) Skip. |
+| anything else | Surface the raw `org-lint` message and ask the user how to proceed. |
+
+## Phase A — Run the script
+
+1. Resolve `FILE` — argument if given, else `todo.org` in cwd. If the file doesn't exist, abort with a clear error.
+2. Confirm `FILE` is inside the current project (no cross-project boundary). If not, follow `protocols.org` — stop and ask.
+3. Invoke the script:
+
+ ```bash
+ emacs --batch -q -l .ai/scripts/lint-org.el FILE
+ ```
+
+ The script applies every mechanical fix, then emits structured stdout. First line is a summary; each subsequent line is a plist describing one issue:
+
+ ```
+ ;; lint-org: file=todo.org mechanical=4 judgment=11
+ (:kind mechanical-fixed :line 23 :checker item-number :msg "...")
+ (:kind judgment :line 41 :checker link-to-local-file :msg "...")
+ ...
+ ```
+
+4. Parse the output into two lists: `mechanical-fixed` and `judgment`. Save the counts.
+
+## Phase B — Handle judgments
+
+### `interactive` mode (default)
+
+Walk each judgment item one at a time. For each:
+
+1. Read the file at the reported line plus a few lines of surrounding context (use the Read tool with `offset` and `limit`).
+2. Present the item with inline numbered resolutions per `interaction.md` (no popup menu). Shape:
+
+ ```
+ ### Judgment 3/11 — line 87, link-to-local-file
+
+ Context (lines 85-89):
+ ...
+ See [[file:notes/2025-old-plan.org][the original plan]].
+ ...
+
+ Resolutions:
+ 1. Repair — closest match in the repo: `notes/2026-old-plan.org`. Update the link.
+ 2. Drop to verbatim — `=2025-old-plan.org=` with a "(file never landed)" annotation.
+ 3. Leave as-is and add a TODO to create `notes/2025-old-plan.org`.
+ 4. Skip — leave the warning in place.
+
+ Pick a number.
+ ```
+
+3. Apply the chosen resolution with `Edit`. If the user picks "Skip," do nothing.
+4. Move to the next judgment.
+
+After the walk:
+
+5. Re-run `emacs --batch -q -l .ai/scripts/lint-org.el --check FILE` to get post-pass counts.
+6. Report: pre-pass total, mechanical applied, judgments resolved (per resolution), judgments skipped, post-pass total.
+
+### `mechanical-only` mode
+
+No interactive walk. Append remaining judgment items to a carry-forward file so the next morning's review picks them up.
+
+1. Determine the carry-forward path:
+ - `$LINT_ORG_FOLLOWUPS` if set in the environment.
+ - Else `~/projects/work/inbox/lint-followups.org` if `~/projects/work/inbox/` exists.
+ - Else `<project-root>/.ai/lint-followups.org`.
+2. Append a dated heading and one entry per judgment:
+
+ ```org
+ * 2026-05-14 lint-org follow-ups — todo.org
+ ** TODO line 87 — link-to-local-file — Link to non-existent local file "notes/2025-old-plan.org"
+ ** TODO line 132 — invalid-fuzzy-link — Unknown fuzzy location "Architecture Spike"
+ ...
+ ```
+
+ Use absolute today's date (run `date "+%Y-%m-%d"`) — no relative dates.
+3. Re-run lint in `--check` mode for post-pass counts.
+4. Report: pre-pass total, mechanical applied, judgments deferred to `<carry-forward path>`, post-pass total. Don't block.
+
+## Phase C — Final report
+
+Print a concise summary in either mode:
+
+```
+## /lint-org — todo.org
+
+Pre-pass: 15 issues across 5 categories.
+Mechanical applied: 4 (item-number ×2, missing-language ×1, misplaced-planning ×1).
+Judgments resolved: 8 (repaired ×3, dropped to verbatim ×4, todo-filed ×1).
+Judgments skipped: 3.
+Post-pass: 3 issues remain.
+```
+
+The file is left uncommitted — the user reviews the diff and commits if it looks right. Never auto-commit.
+
+## Edge cases
+
+- **File doesn't exist** — surface the error from the script; don't continue.
+- **Zero lint issues** — report `Pre-pass: 0 issues. Nothing to do.` and exit. `mechanical-only` mode must not write an empty section to the carry-forward file in this case.
+- **Script fails** — `org-lint` errors, malformed file, emacs missing. Surface the raw stderr, don't claim success.
+- **Mechanical fixer declines** — the script emits the item as `judgment` if a fixer's preconditions don't hold (already fixed, unexpected shape). Treat as a normal judgment.
+- **User cancels mid-walk** — leave already-applied resolutions in place. The file is uncommitted, so a `git checkout -- FILE` reverts cleanly. Surface that option if asked.
+- **Judgment item depends on context the model can't access** — always offer "Skip" so the user can defer.
+
+## Anti-patterns
+
+- Auto-applying judgment resolutions without user input. Every judgment requires explicit selection.
+- Modifying lines outside the flagged issue. Each fix touches one site.
+- Committing the changes on the user's behalf. The file is left uncommitted by design.
+- Editing the carry-forward file's prior entries. Append a new dated section per run; old sections stay.
+- Adding new categories silently. New mechanical categories need a test in `tests/test-lint-org.el` and a docs update here.