1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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.
|