diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-14 18:46:32 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-14 18:46:32 -0500 |
| commit | 9f62a7cadf37e3f453efbb0cdf253bcafb1b6393 (patch) | |
| tree | 2d1928d0e8041ca0663a132385602d1bf1e17c27 /.ai | |
| parent | f5b8688aed8ec698220a67c2dbfbcae22e7575f4 (diff) | |
| download | rulesets-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 '.ai')
| -rw-r--r-- | .ai/specs/lint-org-skill-spec.md | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/.ai/specs/lint-org-skill-spec.md b/.ai/specs/lint-org-skill-spec.md new file mode 100644 index 0000000..46660dc --- /dev/null +++ b/.ai/specs/lint-org-skill-spec.md @@ -0,0 +1,173 @@ +# Spec: `/lint-org` skill + wrap-up integration + +Drafted 2026-05-14 after a one-off pass cleared todo.org from 55 → 1 org-lint warnings. The pass surfaced enough recurring patterns to justify codifying them into a skill instead of relying on ad-hoc runs. + +## Problem + +`todo.org` (and other tracked org files in `~/projects/work/`) accumulate lint warnings as the file gets edited. Categories observed in the 2026-05-14 baseline: + +- Stale `[[file:...]]` refs when target files get moved/renamed/deleted. +- Stale `[[todo.org:NNN]]` line-number fuzzy links when headings shift. +- `CLOSED:` and `DEADLINE:` / `SCHEDULED:` on separate lines instead of one. +- Numbered lists fragmented across paragraph breaks (`4. … blank … 5.` looks like a new list starting at 5). +- Bare `#+begin_src` blocks (prose drafts, no language slug). +- Markdown-style `**Bold.**` at start of paragraphs (org reads it as a possible level-2 heading). +- Heading-pattern characters (`***`, `*`) inside `=verbatim=` markup, which trips the misplaced-heading detector even though the markup is technically correct. +- Src-blocks tagged with languages org doesn't know natively (`markdown`). + +Without a regular sweep these compound. The 2026-05-14 baseline accumulated 55 issues over months of editing. A nightly sweep keeps the count near zero forever, because each day's drift is small. + +## Design + +Two pieces, depending on each other but not coupled: + +1. **`/lint-org` skill** — does the work. Invocable ad-hoc on any org file. Runs in two modes: `interactive` (default; presents judgment items as numbered options) and `mechanical-only` (auto-fixes safe categories, logs judgment items elsewhere for follow-up). +2. **Wrap-up workflow integration** — `wrap-it-up.org` calls `/lint-org todo.org --mode=mechanical-only` after the existing `todo-cleanup.el --archive-done` pass. Judgment items get logged into the next day's daily-prep so they surface in the morning. + +The skill is the implementation; the workflow is the cadence. The skill works standalone if Craig wants to run it manually mid-day or against another file (`session-context.org`, prep docs, etc.). + +## Skill: `/lint-org` + +### Usage + +``` +/lint-org [FILE_PATH] [--mode=interactive|mechanical-only] +``` + +- `FILE_PATH` defaults to the project's primary org file (heuristic: look for `todo.org` in the cwd's `.ai/` root, fall back to asking). +- `--mode` defaults to `interactive`. + +### Modes + +**`interactive`** (default — ad-hoc invocation, Craig at the keyboard): + +1. Run org-lint via `emacs --batch`. +2. Categorize issues into *mechanical* and *judgment* buckets. +3. Auto-apply the mechanical fixes silently. +4. Walk each judgment item with Craig — inline numbered options per the no-popup rule in `interaction.md`. +5. Re-run lint; report before/after totals. + +**`mechanical-only`** (called by wrap-it-up at end of day): + +1. Run org-lint. +2. Auto-apply only the mechanical fixes. +3. Append a "Lint follow-ups" section to tomorrow's daily-prep (or a known carry-forward inbox) listing each judgment item with its line number, category, and proposed resolutions. +4. Re-run lint; report before/after totals + count of items deferred to tomorrow's prep. +5. Never block the workflow on a judgment item — defer. + +### Issue categorization + +#### Mechanical (always-safe auto-fixes) + +| Category | Fix | +|----------|-----| +| `item-number` | Add `[@N]` directive: `4. content` → `4. [@4] content`. The bullet number stays, the directive tells org to treat it as item N regardless of position in the parsed list. | +| `missing-language-in-src-block` | Convert bare `#+begin_src ... #+end_src` to `#+begin_example ... #+end_example`. Caveat: only when the block contains prose; if a future case has code without a language slug, the right fix is to add the language (defer to judgment mode). | +| `misplaced-planning-info` | Merge two-line `CLOSED:` / `DEADLINE:` / `SCHEDULED:` blocks onto a single line. Use the file's existing convention (probe with grep — typically `CLOSED: [...] DEADLINE: <...>` order). If no existing convention, default to `CLOSED:` first, then `DEADLINE:`/`SCHEDULED:`. | +| `misplaced-heading` (markdown-bold case) | `**X**` at the start of a paragraph (where X is short prose followed by `.` or `,`) → `*X*` (org-mode single-asterisk bold). Distinguish from real heading-misplacement by: line is in body of a heading, the `**...**` is short (under ~50 chars), and there's no following blank line + body that would imply a real heading. | + +#### Judgment (interactive prompt or defer to prep) + +| Category | Resolutions to offer | +|----------|----------------------| +| `link-to-local-file` | 1. Repair to a renamed/moved file (heuristic search for matching basename or normalized name). 2. Drop to `=verbatim=` text with a "(file never landed)" annotation. 3. Leave as-is + file a TODO entry to create the file. 4. Skip (leave the warning). | +| `invalid-fuzzy-link` | 1. Repair to a `[[*Heading name]]` ref if a heading with similar title exists. 2. Drop to `=verbatim label=` text. 3. Skip. | +| `misplaced-heading` (verbatim-asterisk case — `=*** Foo=` inside body prose) | 1. Strip the asterisks and rephrase to preserve the semantic context (e.g. add "level-3" before `=Foo=`). 2. Convert the surrounding markup to `~Foo~` (code style) which doesn't trip the detector. 3. Skip. | +| `suspicious-language-in-src-block` | 1. Surface an Emacs-init one-liner to register the language (the right fix when the language label is accurate but unknown to org-babel — e.g. `markdown`). 2. Change the block label to `text` or `example`. 3. Skip. | + +### Implementation notes + +- **Lint runner.** Use `emacs --batch -Q --eval` to load `org-lint` and emit a structured report. The skill should NOT depend on Craig's interactive Emacs config — `-Q` ensures clean lint behavior across machines. +- **Result parsing.** `org-lint` returns a list of `(id [marker trust msg checker])`. The marker carries the line number as a string with a text property. The checker is a struct whose name field identifies the category. +- **Bulk transforms.** Mechanical fixes that span many lines (e.g. `[@N]` directives across a flagged range) are cleaner via an `awk` pipeline than 20 separate `Edit` calls. Use `awk` for: per-line bullet-number directives, bulk `#+begin_src` → `#+begin_example` conversions when ALL flagged bare blocks share the exact pattern (probe-count first). +- **Per-site transforms.** Markdown-bold conversions and verbatim-asterisk fixes need the surrounding sentence for context; use `Edit` per site rather than `awk`. +- **Backup.** Before applying any transform, copy the file to `/tmp/<basename>.before-lint-pass.<timestamp>` so an unexpected change is recoverable without git. +- **Re-run cadence.** After each fix batch, re-run lint and report deltas. If a fix raised a new warning (rare but possible — e.g. converting `*X*` could change paragraph parsing), surface it before continuing. + +### Output format + +For interactive mode: + +``` +## /lint-org — todo.org + +Pre-pass: <N> issues across <M> categories. + +### Mechanical fixes applied (auto) + +- <category>: <count> fixed at lines <lines> +- ... + +### Judgment items (resolve inline) + +1. line <L> — <category>: <one-line description> + Resolutions: + 1.1 <option a> + 1.2 <option b> + ... + Pick. + +[repeated per judgment item] + +### Final state + +Post-pass: <N> issues. <category breakdown if any>. +``` + +For mechanical-only mode: + +``` +## /lint-org --mode=mechanical-only — todo.org + +Pre-pass: <N>. Mechanical applied: <M>. Deferred to <tomorrow's daily-prep>: <K>. +Post-pass: <N - M> issues remain (logged as carry-forward). +``` + +## Wrap-up workflow integration + +Modify `~/code/rulesets/.ai/workflows/wrap-it-up.org` to add a step *after* the existing `todo-cleanup.el --archive-done` pass: + +``` +*** Lint todo.org + +Run =/lint-org todo.org --mode=mechanical-only=. Auto-fixes accumulated +day-drift (numbered-list fragmentation, planning-info splits, +markdown-bold habits, bare src-blocks). Judgment items (broken link +refs, suspicious-language blocks) get logged into tomorrow's daily-prep +under a =* Lint follow-ups= heading so they surface in the morning +without blocking the wrap-up. +``` + +Optionally extend to lint other tracked org files (`session-context.org` for the session, any prep doc in `inbox/`) if the daily-drift on those files is worth catching. Start with just todo.org. + +## Edge cases the skill must handle + +- **The file doesn't exist.** Surface to user; don't fail silently. +- **The file has zero lint issues.** Report cleanly and exit. Mechanical-only mode should not write anything to daily-prep in this case. +- **The lint runner fails.** Emacs not installed, `--batch` errors, file syntax breaks the org parser. Surface the raw error; don't claim "all clean." +- **A mechanical fix would touch a heading that's part of an active TODO/DOING/VERIFY task body.** Mechanical fixes touch *body content* only (planning lines, list bullets, src-block markers, paragraph emphasis). They never modify a heading line itself. If a heading itself trips lint (e.g. the title contains heading-pattern chars), the fix is judgment. +- **The file is being actively edited by Craig in Emacs.** Lint runs against the on-disk version. If Craig has unsaved buffer changes, the skill's transforms could conflict on next save. Probe via `emacsclient --eval` to see if a buffer is dirty and warn before proceeding. Or just always run against the on-disk version and trust git to catch collisions. +- **A judgment item depends on context the skill can't access.** E.g. a broken `[[file:...]]` ref where Craig knows the file lives in a private path the skill can't see. The "skip" option must always be available; never force a fix. + +## Out of scope + +- Linting markdown files. Different tool (markdownlint or similar). +- Linting org files inside the `~/.ai/` ruleset itself (those are personal-tooling and the lint expectations differ). +- Auto-fixing the `suspicious-language-in-src-block` category by editing the source block. The right fix is usually to register the language in Emacs init, not to change the block. Skill should offer both options. +- Reformatting / reflowing. Lint silences are the only goal; whitespace and prose style are not in scope. + +## Open questions + +1. **Should mechanical-only mode write directly to tomorrow's daily-prep, or to a known carry-forward inbox file?** Daily-prep is generated each morning, so writing into it from the previous night requires the daily-prep generator to be lint-aware. A carry-forward file (`~/projects/work/inbox/lint-followups.org`) that the morning generator merges in is more decoupled. Probably the right call. +2. **Should the skill be parameterized to lint multiple files in one invocation?** `/lint-org todo.org session-context.org` would be useful for the wrap-up to lint everything at once. Defer until the multi-file need is real. +3. **Should mechanical fixes go to a separate commit from judgment fixes?** Today's pass did one commit per session (signal vs cosmetic). For the daily wrap-up cadence, mechanical fixes are likely small enough that batching with the day's other todo.org changes is fine. Don't over-engineer. + +## Reference: today's pass + +The 2026-05-14 cleanup pass that surfaced this need: + +- Total: 55 issues at start. +- Signal categories (17): 5 misplaced-planning-info, 8 link-to-local-file, 4 invalid-fuzzy-link. Resolved in commit `0d10458`. +- Cosmetic categories (36 → 1): 20 item-number, 11 missing-language-in-src-block, 4 misplaced-heading, 1 suspicious-language-in-src-block. Resolved in commit `9ad5b30`. Remaining 1 left intentionally for an Emacs init fix. + +Skill should reproduce the same triage. The judgment categories (link repairs, fuzzy-link drops) are where Craig's input matters and where today's interactive pass spent most of the time. |
