aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ai/specs/lint-org-skill-spec.md173
-rw-r--r--.claude/commands/lint-org.md162
-rw-r--r--todo.org21
3 files changed, 356 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.
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.
diff --git a/todo.org b/todo.org
index 46c54d7..3aa6514 100644
--- a/todo.org
+++ b/todo.org
@@ -1267,6 +1267,27 @@ See also the DoD-specific notations section under the later TODO
starting point across the DoD notation landscape (SysML, DoDAF/UAF,
IDEF1X). This entry is the execution plan for that starting point.
+** TODO [#A] Build =/lint-org= skill + wrap-up integration
+
+Spec: [[file:.ai/specs/lint-org-skill-spec.md]]
+
+A two-mode skill (=interactive=, =mechanical-only=) that runs =org-lint=,
+auto-fixes safe categories (item-number, missing-language-in-src-block,
+misplaced-planning-info, markdown-bold → single-asterisk), and walks judgment
+items (broken local-file links, invalid fuzzy links, verbatim-asterisk false
+positives, suspicious-language blocks) inline.
+
+Wrap-up integration: =wrap-it-up.org= invokes
+=/lint-org todo.org --mode=mechanical-only= after the existing
+=todo-cleanup.el --archive-done= pass. Judgment items defer to a
+carry-forward file that the next morning's daily-prep merges in, so
+wrap-up never blocks on a judgment call.
+
+Baseline that motivated this: the 2026-05-14 manual pass took =todo.org=
+from 55 → 1 lint warnings across two commits (=0d10458= signal,
+=9ad5b30= cosmetic). A nightly mechanical sweep keeps the count near
+zero forever — each day's drift is small.
+
** TODO [#A] Build =/update-skills= skill for keeping forks in sync with upstream
The rulesets repo has a growing set of forks (=arch-decide= from