diff options
Diffstat (limited to 'claude-templates')
| -rw-r--r-- | claude-templates/.ai/protocols.org | 4 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/flashcard-to-anki.py | 26 | ||||
| -rwxr-xr-x | claude-templates/.ai/scripts/inbox-send.py | 23 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/route_recommend.py | 136 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py | 31 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_inbox_send.py | 46 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_route_recommend.py | 124 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/INDEX.org | 9 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/code-quality.org | 83 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/inbox.org | 2 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/open-tasks.org | 25 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/readability-audit.org | 242 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/suspend.org | 112 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/task-audit.org | 15 |
14 files changed, 862 insertions, 16 deletions
diff --git a/claude-templates/.ai/protocols.org b/claude-templates/.ai/protocols.org index 3048df2..46bea50 100644 --- a/claude-templates/.ai/protocols.org +++ b/claude-templates/.ai/protocols.org @@ -242,6 +242,10 @@ Execute the wrap-up workflow (details in Session Protocols section below): 2. Git commit and push all changes 3. Valediction summary +** "Suspend the session" / "Suspend" / "I need to go" / "Stick a pin in everything" + +Execute the suspend workflow ([[file:workflows/suspend.org][suspend.org]]): a capture-only mid-session pause for an abrupt departure. It appends a resume-weighted =SUSPENDED= entry to the Session Log, notes uncommitted work, and LEAVES =.ai/session-context.org= in place so the next startup resumes from it — no archive, no teardown, no valediction. The capture-only counterpart to "wrap it up" (which ends + archives + tears down) and to =/flush= (which prompts =/clear= and resumes the same session). "I need to go" is broad — if it reads as a conversational aside, confirm before suspending. + * User Information ** Calendar Management diff --git a/claude-templates/.ai/scripts/flashcard-to-anki.py b/claude-templates/.ai/scripts/flashcard-to-anki.py index 7227683..ca4c70b 100755 --- a/claude-templates/.ai/scripts/flashcard-to-anki.py +++ b/claude-templates/.ai/scripts/flashcard-to-anki.py @@ -13,9 +13,11 @@ Parses org-drill structure: text (sans :drill: tag). Back = entry body with newlines converted to <br>. -Deck name defaults to the input basename, case preserved. Deck and model -IDs are derived from the deck name via stable hash so re-importing the -same deck updates existing cards instead of duplicating them. +Deck name defaults to the org #+TITLE: (so the phone deck reads as the +curated title), falling back to the input basename when the source has +no #+TITLE. Deck and model IDs are derived from the deck name via stable +hash so re-importing the same deck updates existing cards instead of +duplicating them. Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is a mobile-Anki artifact the phone picks up from its sync dir, so it lands @@ -177,7 +179,19 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck: return deck -def default_deck_name(input_path: Path) -> str: +def default_deck_name(input_path: Path, org_text: str) -> str: + """Deck name defaults to the org #+TITLE:, falling back to the basename. + + The #+TITLE drives both the org-drill display in Emacs and the Anki + deck name on the phone, so the consumed deck reads as the curated + title ("Refutations") rather than the filename slug + ("refutation-drill"). Falls back to the input basename (case + preserved) when the source has no non-empty #+TITLE line. + """ + for line in org_text.splitlines(): + m = re.match(r"^#\+TITLE:\s*(.*\S)\s*$", line, re.IGNORECASE) + if m: + return m.group(1).strip() return input_path.stem @@ -197,7 +211,7 @@ def main() -> int: ) parser.add_argument( "--deck", - help="Deck name. Defaults to the input basename.", + help="Deck name. Defaults to the org #+TITLE, or the input basename.", ) parser.add_argument( "--output", @@ -213,7 +227,7 @@ def main() -> int: return 1 org_text = input_path.read_text(encoding="utf-8") - deck_name = args.deck or default_deck_name(input_path) + deck_name = args.deck or default_deck_name(input_path, org_text) output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve() output_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/claude-templates/.ai/scripts/inbox-send.py b/claude-templates/.ai/scripts/inbox-send.py index 5373bd4..1362a1f 100755 --- a/claude-templates/.ai/scripts/inbox-send.py +++ b/claude-templates/.ai/scripts/inbox-send.py @@ -136,8 +136,21 @@ def slugify_filename(stem: str, max_length: int = MAX_SLUG_LENGTH) -> str: return truncated.strip("-._") +def display_name(path: Path) -> str: + """The name a project is referred to by — its basename with dots stripped. + + Dotted directories (`.emacs.d`, `.dotfiles`) are awkward to name in + conversation, so they're addressed dot-stripped: `emacsd`, `dotfiles`. + """ + return path.name.replace(".", "") + + def find_target(target_name: str, projects: list[Path]) -> Path | None: - """Resolve `target_name` against the project list (basename or numeric index).""" + """Resolve `target_name` against the project list (basename or numeric index). + + An exact basename match wins. Failing that, a dot-stripped alias matches — + so `emacsd` resolves `.emacs.d` and `dotfiles` resolves `.dotfiles`. + """ if target_name.isdigit(): idx = int(target_name) - 1 if 0 <= idx < len(projects): @@ -146,6 +159,10 @@ def find_target(target_name: str, projects: list[Path]) -> Path | None: for p in projects: if p.name == target_name: return p + norm = target_name.replace(".", "") + for p in projects: + if display_name(p) == norm: + return p return None @@ -206,9 +223,9 @@ def print_project_list(projects: list[Path], current: Path | None) -> None: print("No projects (.ai/ + inbox/) found under the configured roots.") return print(f"Available .ai projects ({len(others)}):") - width = max(len(p.name) for p in others) + width = max(len(display_name(p)) for p in others) for i, p in enumerate(others, 1): - print(f" {i}. {p.name:<{width}} {p}") + print(f" {i}. {display_name(p):<{width}} {p}") def main() -> int: diff --git a/claude-templates/.ai/scripts/route_recommend.py b/claude-templates/.ai/scripts/route_recommend.py new file mode 100644 index 0000000..7b36405 --- /dev/null +++ b/claude-templates/.ai/scripts/route_recommend.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Wrap-up routing recommendation engine. + +Given an inbox keeper's text and a list of candidate project names, infer which +project the item belongs to, with a confidence tier: + + strong a project's name (or its dot-stripped form, or a path containing it) + appears literally in the item + weak a distinctive name token overlaps, but the full name doesn't + none no overlap; the item stays put + +A multi-way tie at the top tier is ambiguous, so it downgrades to weak with a +deterministic pick (most token overlap, then alphabetical). An empty candidate +list yields none. + +The pure core is `recommend(item, projects) -> (destination, confidence)` — the +shape the wrap-up router (Phase 4) and the process-inbox marker (Phase 2) both +call. The CLI wires it to inbox-send.py's `discover_projects` so the candidate +set is the same project universe inbox-send already knows. + +CLI: + route_recommend.py --item "<text>" [--exclude <current-project>] +prints "<destination>\\t<confidence>" on a match, or "none". +""" + +import argparse +import importlib.util +import re +import sys +from pathlib import Path + +# A distinctive-enough token for weak matching; shorter tokens (of, to, id) are +# too noisy to route on. +MIN_WEAK_TOKEN = 4 + +_TOKEN_RE = re.compile(r"[a-z0-9]+") + + +def _tokens(text: str) -> set[str]: + return set(_TOKEN_RE.findall(text.lower())) + + +def _name_variants(name: str) -> set[str]: + """A project name and its dot-stripped alias (.emacs.d -> emacsd).""" + return {v for v in (name.lower(), name.replace(".", "").lower()) if v} + + +def _literal_present(name: str, item_lower: str) -> bool: + """True if a name variant appears in the item on word-ish boundaries. + + Boundaries keep 'home' from matching inside 'homeowner' while still + matching it inside a path ('~/code/home/...') or a hyphenated name. + """ + for variant in _name_variants(name): + if re.search(r"(?<![a-z0-9])" + re.escape(variant) + r"(?![a-z0-9])", item_lower): + return True + return False + + +def _tiebreak(candidates: list[str], item_tokens: set[str]) -> str: + """Most token overlap first, then alphabetical — deterministic.""" + return sorted(candidates, key=lambda p: (-len(_tokens(p) & item_tokens), p))[0] + + +def recommend(item: str, projects: list[str]) -> tuple[str | None, str]: + """Infer the destination project for `item` from `projects`. + + Returns (destination, confidence). confidence is "strong" / "weak" / "none"; + destination is None exactly when confidence is "none". + """ + if not projects: + return (None, "none") + + item_lower = item.lower() + item_tokens = _tokens(item) + + strong: list[str] = [] + weak: list[str] = [] + for project in projects: + if _literal_present(project, item_lower): + strong.append(project) + continue + name_tokens = {t for t in _tokens(project) if len(t) >= MIN_WEAK_TOKEN} + if name_tokens & item_tokens: + weak.append(project) + + if len(strong) == 1: + return (strong[0], "strong") + if len(strong) > 1: + return (_tiebreak(strong, item_tokens), "weak") + if len(weak) == 1: + return (weak[0], "weak") + if len(weak) > 1: + return (_tiebreak(weak, item_tokens), "weak") + return (None, "none") + + +def _load_inbox_send(): + """Load the sibling kebab-named inbox-send.py as a module for its discovery.""" + path = Path(__file__).with_name("inbox-send.py") + spec = importlib.util.spec_from_file_location("inbox_send", path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def discover_destination_names(exclude: str | None = None) -> list[str]: + """The candidate project names, reusing inbox-send's discovery. + + `exclude` drops the current project (matched by exact name or dot-stripped + alias) so the engine never recommends routing an item to where it already is. + """ + mod = _load_inbox_send() + names = [p.name for p in mod.discover_projects(mod.resolve_roots())] + if exclude: + drop = _name_variants(exclude) + names = [n for n in names if not (_name_variants(n) & drop)] + return names + + +def main() -> int: + parser = argparse.ArgumentParser(description="Recommend a routing destination for an inbox keeper.") + parser.add_argument("--item", required=True, help="the keeper's text") + parser.add_argument("--exclude", help="current project to exclude from candidates") + args = parser.parse_args() + + projects = discover_destination_names(exclude=args.exclude) + destination, confidence = recommend(args.item, projects) + print("none" if destination is None else f"{destination}\t{confidence}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py b/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py index 058b0cd..87008a8 100644 --- a/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py +++ b/claude-templates/.ai/scripts/tests/test_flashcard_to_anki.py @@ -34,14 +34,33 @@ def test_default_output_path_targets_phone_anki_dir(drill): assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg" -def test_default_deck_name_is_raw_basename(drill): - """Deck name is the input basename with case preserved; #+TITLE is ignored.""" - assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat" +def test_default_deck_name_uses_org_title(drill): + """The #+TITLE drives the Anki deck name, not the filename slug.""" + org = "#+TITLE: Refutations\n* Section\n** Q? :drill:\na\n" + assert drill.default_deck_name(Path("/x/refutation-drill.org"), org) == "Refutations" -def test_default_deck_name_keeps_hyphens(drill): - """A hyphenated basename is kept verbatim rather than title-cased.""" - assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill" +def test_default_deck_name_title_is_trimmed(drill): + """Surrounding whitespace on the #+TITLE value is stripped.""" + org = "#+TITLE: DeepSat Flashcards \n" + assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "DeepSat Flashcards" + + +def test_default_deck_name_title_match_is_case_insensitive(drill): + """A lowercase #+title: keyword is still recognized.""" + org = "#+title: Health Flashcards\n" + assert drill.default_deck_name(Path("/x/health-drill.org"), org) == "Health Flashcards" + + +def test_default_deck_name_falls_back_to_basename_without_title(drill): + """No #+TITLE line falls back to the input basename, case preserved.""" + org = "* Section\n** Q? :drill:\na\n" + assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "deepsat" + + +def test_default_deck_name_blank_title_falls_back_to_basename(drill): + """An empty #+TITLE value is ignored in favour of the basename.""" + assert drill.default_deck_name(Path("/x/health-drill.org"), "#+TITLE: \n") == "health-drill" # --- section_to_tag (pure) --- diff --git a/claude-templates/.ai/scripts/tests/test_inbox_send.py b/claude-templates/.ai/scripts/tests/test_inbox_send.py index a0094dc..cb60e63 100644 --- a/claude-templates/.ai/scripts/tests/test_inbox_send.py +++ b/claude-templates/.ai/scripts/tests/test_inbox_send.py @@ -97,6 +97,52 @@ class TestInboxSendDiscovery: result = run_script(["--list"], roots=[tmp_path / "does-not-exist"]) assert result.returncode == 0 + def test_inbox_send_list_displays_dot_stripped_name(self, project_root, run_script, tmp_path): + """Dotted project basenames display dot-stripped (.emacs.d → emacsd).""" + project_root(".emacs.d") + result = run_script(["--list"], roots=[tmp_path / "projects"]) + assert "emacsd" in result.stdout + + +class TestInboxSendDotAlias: + """A dotted project basename resolves both verbatim and dot-stripped.""" + + def test_resolves_by_dot_stripped_alias(self, project_root, run_script, tmp_path): + """'emacsd' delivers to the .emacs.d project.""" + project_root(".emacs.d") + cwd = project_root("source") + run_script( + ["emacsd", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(files) == 1 + + def test_resolves_by_exact_dotted_name_still(self, project_root, run_script, tmp_path): + """Backward-compat: the verbatim '.emacs.d' target still resolves.""" + project_root(".emacs.d") + cwd = project_root("source") + run_script( + [".emacs.d", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + files = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(files) == 1 + + def test_exact_match_wins_over_alias(self, project_root, run_script, tmp_path): + """An exact basename match is preferred over a dot-stripped collision.""" + project_root("emacsd") # exact + project_root(".emacs.d") # would also normalize to 'emacsd' + cwd = project_root("source") + run_script( + ["emacsd", "--text", "hi"], + cwd=cwd, roots=[tmp_path / "projects"], + ) + exact = list((tmp_path / "projects" / "emacsd" / "inbox").iterdir()) + dotted = list((tmp_path / "projects" / ".emacs.d" / "inbox").iterdir()) + assert len(exact) == 1 + assert dotted == [] + # ---------------------------------------------------------------------- # Slug derivation from text and from filenames diff --git a/claude-templates/.ai/scripts/tests/test_route_recommend.py b/claude-templates/.ai/scripts/tests/test_route_recommend.py new file mode 100644 index 0000000..acc4755 --- /dev/null +++ b/claude-templates/.ai/scripts/tests/test_route_recommend.py @@ -0,0 +1,124 @@ +"""Tests for route_recommend.py — the wrap-up routing recommendation engine. + +The core is a pure function recommend(item, projects) -> (destination, confidence): +- strong: a project's name (or its dot-stripped form) appears literally in the item +- weak: a distinctive name token overlaps, but the full name doesn't +- none: no overlap; the item stays put (destination is None) + +A multi-way tie at the top tier downgrades to weak with a deterministic pick. +An empty project list yields none. + +The CLI wires this to inbox-send.py's discover_projects (sandboxed here via the +INBOX_SEND_ROOTS env var, the same hook inbox-send's own tests use). +""" + +import subprocess +import sys +from pathlib import Path + +SCRIPTS = Path(__file__).parent.parent +SCRIPT = SCRIPTS / "route_recommend.py" +sys.path.insert(0, str(SCRIPTS)) + +import route_recommend as rr # noqa: E402 + + +# --- pure function: the five spec'd cases ----------------------------------- + +def test_strong_match_named_literally(): + dest, conf = rr.recommend("fix the rulesets refactor command", ["rulesets", "home", "work"]) + assert (dest, conf) == ("rulesets", "strong") + + +def test_strong_match_via_dot_stripped_name(): + # ".emacs.d" addressed as "emacsd" in the item is still a literal hit. + dest, conf = rr.recommend("update the emacsd ai-term module", [".emacs.d", "rulesets"]) + assert (dest, conf) == (".emacs.d", "strong") + + +def test_strong_match_dotted_name_verbatim(): + dest, conf = rr.recommend("patch .emacs.d startup", [".emacs.d", "rulesets"]) + assert (dest, conf) == (".emacs.d", "strong") + + +def test_weak_match_topic_token_only(): + # "wttrin" is a token of "emacs-wttrin" but the full name isn't present. + dest, conf = rr.recommend("the wttrin weather bug", ["emacs-wttrin", "rulesets"]) + assert (dest, conf) == ("emacs-wttrin", "weak") + + +def test_no_match_stays_put(): + dest, conf = rr.recommend("calibrate the telescope mount", ["rulesets", "deepsat"]) + assert dest is None + assert conf == "none" + + +def test_two_project_strong_tie_downgrades_to_weak(): + # Both named literally → ambiguous → weak, deterministic tie-break (alphabetical). + dest, conf = rr.recommend("sync rulesets and home configs", ["rulesets", "home", "work"]) + assert conf == "weak" + assert dest == "home" # tie-break: most-overlap then alphabetical + + +def test_empty_project_list_is_none(): + assert rr.recommend("anything at all", []) == (None, "none") + + +# --- boundary / robustness -------------------------------------------------- + +def test_literal_name_requires_word_boundary(): + # "home" must not match inside "homeowner". + dest, conf = rr.recommend("the homeowner association meeting", ["home", "rulesets"]) + assert dest is None and conf == "none" + + +def test_path_mention_counts_as_literal(): + dest, conf = rr.recommend("edit ~/code/rulesets/Makefile", ["rulesets", "home"]) + assert (dest, conf) == ("rulesets", "strong") + + +def test_strong_beats_weak_when_both_present(): + # "rulesets" named literally (strong) outranks an emacs-wttrin token hit (weak). + dest, conf = rr.recommend("the wttrin fix belongs in rulesets", ["rulesets", "emacs-wttrin"]) + assert (dest, conf) == ("rulesets", "strong") + + +# --- CLI + discovery reuse (sandboxed roots) -------------------------------- + +def _run(args, roots, item): + import os + env = {"PATH": os.environ.get("PATH", ""), "HOME": os.environ.get("HOME", "/tmp"), + "INBOX_SEND_ROOTS": ":".join(str(r) for r in roots)} + return subprocess.run([sys.executable, str(SCRIPT), "--item", item, *args], + capture_output=True, text=True, env=env) + + +def _mk_project(tmp_path, name): + proj = tmp_path / "projects" / name + (proj / ".ai").mkdir(parents=True, exist_ok=True) + (proj / "inbox").mkdir(exist_ok=True) + return proj + + +def test_cli_discovers_and_recommends(tmp_path): + _mk_project(tmp_path, "foo") + _mk_project(tmp_path, "bar") + r = _run([], roots=[tmp_path / "projects"], item="fix the foo widget") + assert r.returncode == 0 + assert r.stdout.strip() == "foo\tstrong" + + +def test_cli_no_match_prints_none(tmp_path): + _mk_project(tmp_path, "foo") + r = _run([], roots=[tmp_path / "projects"], item="unrelated grocery list") + assert r.returncode == 0 + assert r.stdout.strip() == "none" + + +def test_cli_exclude_drops_current_project(tmp_path): + _mk_project(tmp_path, "foo") + _mk_project(tmp_path, "bar") + # Item names foo, but foo is excluded as the current project → no other match. + r = _run(["--exclude", "foo"], roots=[tmp_path / "projects"], item="fix the foo widget") + assert r.returncode == 0 + assert r.stdout.strip() == "none" diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org index eef81df..a474b29 100644 --- a/claude-templates/.ai/workflows/INDEX.org +++ b/claude-templates/.ai/workflows/INDEX.org @@ -22,6 +22,8 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Triggers: "wrap it up", "that's a wrap", "let's call it a wrap" - No-teardown triggers: "wrap it up with summary", "wrap it up and summarize" - Shutdown trigger: "wrap it up and shutdown" +- =suspend.org= — capture-only mid-session pause for an abrupt departure: append a resume-weighted =SUSPENDED= entry to the Session Log, note uncommitted work, and LEAVE =.ai/session-context.org= in place so the next startup resumes from it. The capture-only counterpart to =wrap-it-up= (which archives + tears down) and to =flush= (=/flush=, which prompts =/clear= and resumes the same session). Provides only the capture half; startup's interrupted-session path is the resume half. + - Triggers: "suspend the session", "suspend", "I need to go", "stick a pin in everything" - =retrospective.org= — post-mortem after a tough session. - Triggers: "let's do a retrospective", "retrospective time" @@ -87,6 +89,13 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - =spec-response.org= — fold a spec review back in: decide accept / modify / reject for every finding, weave accepts into the spec body, complete each finding task in place (the reason recorded on modifies and rejects), reconcile cross-spec tensions, iterate to implementation-ready. The *author* side; consumes the =* Review findings= =spec-review.org= produces. - Triggers: "respond to the review", "process the spec reviews", "spec-response workflow", "fold in the review" +** Code quality + +- =code-quality.org= — one trigger that sequences every behavior-preserving quality pass over a scope of existing code: =/refactor= (complexity, duplication, dead-code, simplification) then =readability-audit= (comments, headers, names, organization), then surfaces the =:refactor:= tasks readability filed and any deferred =/refactor= findings. A thin orchestrator — each pass keeps its own gate. Excludes =/simplify= (that's for the current diff, not existing code). + - Triggers: "code quality sweep", "quality sweep", "run every quality pass on <scope>", "give me every pass on <scope>" +- =readability-audit.org= — make code readable to a future maintainer: audit file-top commentary, inline comments (why-not-what), names (intention-revealing), and organization (co-location / stepdown / cohesion). The cheap comment- and name-only fixes (dimensions A/B/C) land inline, verified by a green suite; the structural findings (dimension D — split a module, rename a public symbol) are *filed* as =:refactor:= tasks, not done here. Language-agnostic. Feeds =/refactor= (which executes the filed structural work); distinct from =/refactor='s metric scans and =/simplify='s diff cleanup. + - Triggers: "let's run the readability-audit workflow", "audit the comments and commentary in <area>", "clean up the structure/organization of <module>", "readability audit" + ** Tools and meta - =process-meeting-transcript.org= — record → transcript → labeled archive. diff --git a/claude-templates/.ai/workflows/code-quality.org b/claude-templates/.ai/workflows/code-quality.org new file mode 100644 index 0000000..2406f4c --- /dev/null +++ b/claude-templates/.ai/workflows/code-quality.org @@ -0,0 +1,83 @@ +#+TITLE: Code-Quality Sweep Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-06-28 + +* Overview + +One trigger that runs every behavior-preserving quality pass over a scope of +*existing* code, in order, then surfaces what got filed for later. It's a thin +orchestrator — each pass keeps its own discipline and its own confirm gate; this +workflow only sequences them and collects the residue. + +The passes it chains: + +1. =/refactor= — structural and logic cleanup on measurable metrics (complexity, + duplication, dead-code) plus the simplification lens. +2. =readability-audit= ([[file:readability-audit.org][readability-audit.org]]) — prose and human-reader clarity + (comments, file headers, names, organization). + +It deliberately does *not* run =/simplify=: that works the current uncommitted +diff, not existing committed code, so it belongs to the moment you've just made a +change, not to a sweep of code already in the tree (see "The /simplify boundary" +below). + +* When to Use This Workflow + +- "code quality sweep" / "quality sweep" +- "run every quality pass on <scope>" / "full quality pass on <scope>" +- "give me every pass on <file/module/tree>" + +Do NOT use it for: +- *In-flight diff cleanup* — that's =/simplify= on the change you just made. +- *Bug hunting* — these passes are behavior-preserving; for defects use =debug= + or =/review-code=. +- *Performing the structural refactors it files* — those become =:refactor:= + tasks; work them later via =/refactor rename= / =/refactor simplification= or + =/start-work=. + +* Steps + +** 1. Scope + +Pick the target: one file, a named module set, or the whole tree (honor +=.aiignore=). The same scope is passed to both passes so they cover the same +code. + +** 2. /refactor <scope> + +Run =/refactor= on the scope. Its default full scan covers complexity, +duplication, dead-code, and simplification. It presents findings and applies +only what's approved (its own gate) — structure and logic first, so the +readability pass audits the cleaned-up code. + +** 3. readability-audit on <scope> + +Run the readability-audit workflow on the same scope. Its cheap comment- and +name-only fixes (dimensions A/B/C) land inline and are verified by a green +suite; its structural findings (dimension D — split a module, rename a public +symbol) are *filed* as =:refactor:= tasks rather than done here. + +** 4. Surface the residue + +Collect and report what the sweep left behind for later work: + +- The =:refactor:= tasks readability-audit filed (the structural backlog). +- Any =/refactor= findings deferred rather than applied in step 2. + +That residue is the "do this next" list the sweep produces; it's not a failure +to finish, it's the structural work that needs its own design and test pass. + +* The /simplify boundary + +=/simplify= and this sweep don't overlap: =/simplify= cleans the *current diff* +and applies its fixes directly, so reach for it right after making a change, +before committing. This sweep works *existing committed code* and runs the +scan-and-present passes. One trigger can't sensibly do both — a diff you're +holding and a tree you're auditing are different inputs. + +* Verification + +Each pass owns its verification (=/refactor= runs the suite after applying; +readability-audit verifies inline fixes against a green suite). The umbrella +adds nothing beyond sequencing, so when both passes report green, the sweep is +clean — confirm that before reporting done rather than assuming it. diff --git a/claude-templates/.ai/workflows/inbox.org b/claude-templates/.ai/workflows/inbox.org index c442d17..5fc855f 100644 --- a/claude-templates/.ai/workflows/inbox.org +++ b/claude-templates/.ai/workflows/inbox.org @@ -114,6 +114,8 @@ The item extends a task already filed. Update the parent TODO's body with a date ** File as TODO Substantive but waits, or needs design/triage before implementation. Add the TODO under =* <Project> Open Work= with priority + tags per the priority-scheme check (core §6). Body summarizes the proposal and links the inbox content if it's been moved to =docs/design/=. Delete the inbox file (or move it to =docs/design/= first if the content survives). +*Blocking-dependency handoff.* A special shape: another project sends a note that *this* project's work is blocking one of theirs ("your task X is blocked on us — we need Y"). File or link the owning task, tag it =:blocker:=, and name the requesting project in the body (see the cross-project dependency convention in =todo-format.md=). The =:blocker:= tag makes =open-tasks.org= surface that task *first*, since clearing it unblocks the other project. Dedup against an existing task rather than filing a duplicate. When the work later lands, drop =:blocker:= and notify the waiting project (=inbox-send <their-project> --text "Delivered: <what> — you're unblocked."=) so it can lift its own =:blocked:=. + ** Defer Rename in place to =inbox/PROCESSED-<original-filename>= and add a brief comment line at the top: =# Deferred YYYY-MM-DD: <condition>=. Don't accumulate deferred items indefinitely — sweep them on a future process pass when the condition is met or the deferral has aged out. diff --git a/claude-templates/.ai/workflows/open-tasks.org b/claude-templates/.ai/workflows/open-tasks.org index fe782d6..4ba29dd 100644 --- a/claude-templates/.ai/workflows/open-tasks.org +++ b/claude-templates/.ai/workflows/open-tasks.org @@ -176,6 +176,10 @@ Next Mode answers two questions in one output: "what matters most right now?" (t Apply the prioritization cascade in order. Stop at the first matching step. This is the importance/urgency answer. +*Exclude blocked tasks.* A task tagged =:blocked:= has an unmet cross-project dependency (its body names the project and the work owed, per =todo-format.md=). It can't be worked until that other project delivers, so it is *never* the cascade recommendation — skip it at every cascade step below. Blocked tasks are surfaced on their own in Step 3 so the stalled dependency stays visible instead of silently dropping out of view. + +*Surface blocking tasks first.* The mirror of the above: a task tagged =:blocker:= is holding up work in *another* project (its body names which project and what's owed, per =todo-format.md=). Clearing it unblocks that project, so it carries borrowed urgency — surface it at the *top* of the cascade recommendation regardless of its own priority cookie, ahead of the normal In-Progress / deadline / priority order. When several =:blocker:= tasks exist, lead with the one blocking the most, or the longest. This is the "do the thing that unblocks someone else first" rule; a =:blocker:= task left at its own low priority is exactly how a cross-project dependency stalls. + **** 1. In-Progress Tasks - Look for tasks marked =DOING= or partially complete. - *If found:* Recommend that task (always finish what's started). @@ -228,11 +232,22 @@ Within each row, pick a single task per the same-level tie-breakers above (block The friction filter is the override path. When the cascade winner is partially blocked, hardware-dependent, or simply too large for the user's current state, one of the friction rows is what they pick instead. +*** Step 3 — Blocked-on-other-projects surface + +Independently of the cascade and the friction filter, collect every open task tagged =:blocked:=. These are tasks this project can't advance until another project delivers; surfacing them keeps a cross-project dependency from rotting at low priority on the other side — the exact failure the tag exists to prevent (a blocked task whose blocker is a =[#D]= in another project sits forever otherwise). + +For each blocked task, read its body for the blocking project and what's owed, and present one line: the task, the blocking project, and what that project owes. Then offer — per blocked task — to nudge the blocker: an =inbox-send <project> --text= note naming what's needed and why it's blocking, so the dependency gets attention in the project that owns it. Don't send without the user's go. + +If no =:blocked:= tasks exist, omit this surface entirely (the common case). + *** Output Format -Pair the cascade recommendation with the friction block beneath it. Recommendation-at-item-1 convention applies to the friction rows — quick+solo first, since it's the strongest low-friction pick. +Pair the cascade recommendation with the friction block beneath it, and the blocked-on-other-projects surface (Step 3) beneath that when any blocked task exists. Recommendation-at-item-1 convention applies to the friction rows — quick+solo first, since it's the strongest low-friction pick. #+begin_example +Unblocks other projects (do these first): +- ai-term wrap-teardown companion — :blocker:, unblocks rulesets (the three ai-term functions) + Cascade recommendation (importance/urgency): - Fix org-noter reliability — [#A], Method 1, 8/18 complete, blocks daily reading/annotation @@ -240,17 +255,25 @@ If you want lower friction instead: 1. Quick + solo: Bump linter config — [#C] :quick:solo:, ~15 min 2. Quick: Confirm new dirvish setup — [#B] :quick:, needs your eye 3. Solo: Refactor config-utilities — [#B] :solo:, bounded but multi-hour + +Blocked on other projects (can't advance until the blocker delivers): +- Wrap-teardown feature — blocked by emacsd: ai-term companion functions — nudge? #+end_example +The =:blocker:= surface sits at the very top — clearing one of those is the highest-leverage thing on the list, since it frees work in another project. Omit it when no =:blocker:= task exists (the common case). + Include for each row: - Task name / description. - Priority + tag cluster. - One-line reasoning. For the cascade row, name which cascade step matched. For friction rows, an effort hint when one is obvious. - Progress indicator (for V2MOM-structured todos) on the cascade row only. +- For a =:blocker:= row: the project it unblocks and what's owed (from the task body). +- For a blocked row: the blocking project and what it owes (from the task body), plus the nudge offer. **** Edge cases - *Empty friction block.* If no =:quick:= or =:solo:= tagged tasks exist in the open set, omit the friction block entirely. Present only the cascade recommendation. +- *No =:blocker:= tasks.* Omit the "Unblocks other projects" surface entirely (the common case) — show it only when a task carries the =:blocker:= tag. - *Dedupe.* If the cascade recommendation IS the same task as one of the friction rows (e.g. it's =:quick:solo:= and also won the cascade), show it once at the top with both labels. Don't list it twice. - *Decline behavior.* If the user declines the cascade recommendation, drop straight to the friction block as the natural next prompt. Do not fall through to lower-cascade-tier tasks; the friction filter IS the override. diff --git a/claude-templates/.ai/workflows/readability-audit.org b/claude-templates/.ai/workflows/readability-audit.org new file mode 100644 index 0000000..8223a03 --- /dev/null +++ b/claude-templates/.ai/workflows/readability-audit.org @@ -0,0 +1,242 @@ +#+TITLE: Readability Audit Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-06-28 + +* Overview + +A pass over one file, a set of modules, or the whole tree that makes the code +*readable to a future maintainer*. It checks four things and fixes the cheap +ones in place: the file-top commentary, the inline comments, the names, and the +physical organization of the code. Structural changes that need a real refactor +(splitting a module, renaming a public symbol) are not done here — they are +filed as =:refactor:= tasks so they get their own design and test pass. + +This is language-agnostic. Where a step names a language-specific tool or +convention, it's stated as "the project's <X>, if it has one" — read the +project's =CLAUDE.md= / =notes.org= and the language bundle to resolve the +concrete tool. + +* Where it sits among the code-quality tools + +These tools are a pipeline, not duplicates. Knowing which to reach for: + +- *readability-audit* (this workflow) — prose and human-reader clarity: + comments, file headers, names, and physical organization. Judgment-driven + (does this comment lie? does this name reveal intent? can a newcomer place + this file in a minute?). +- =/refactor= — structure on measurable metrics: complexity, duplication, + dead-code, the =simplification= lens (behavior-preserving logic/size + reduction), and =rename= (executes a codebase-wide symbol rename). +- =/simplify= — behavior-preserving cleanup of the current diff, applied + directly. + +The link that keeps them from overlapping: when this audit finds a structural +problem too big for a comment/name fix — a module to split, a *public* symbol to +rename across call sites — it *files* a =:refactor:= task rather than doing it +here. =/refactor= (rename, simplification) or =/start-work= then executes that +filed task with a proper design and test plan. Readability finds and files; +=/refactor= transforms. + +* Problem We're Solving + +Source files drift toward two opposite failure modes, and both hurt the next +person to open the file: + +- *Documentation rot and noise.* Headers carry stale user-manual content + (quick-starts, full option matrices, setup walkthroughs) that belongs in user + docs; comments restate what the next line already says; comments go out of + date and start lying; placeholder =TODO=/=FIXME= stubs and conversational + asides accumulate. A blank summary or a missing file-top description leaves a + reader with no map. +- *Structural fog.* Names that don't reveal intent force the reader to decode + them; related functions scatter; a public entry point sits far from the + private helpers it calls; a file grows to hold several unrelated + responsibilities. + +Left alone, opening a file costs more every month. The fix is a repeatable audit +with a clear, checkable standard, run on demand or as files are touched. + +* Exit Criteria + +For the audited scope: + +1. *Every file has an accurate top section* that states what the file does and + how it fits the rest of the codebase — terse, no user-manual content, and + carrying the project's file-header convention where it has one. +2. *Every surviving comment earns its place* — it explains a *why* the code + can't (a constraint, a workaround and its reason, an ordering dependency, a + warning), it is accurate against the current code, and it is terse. Obvious + "describe the next line" comments are gone. +3. *Names reveal intent* — no cryptic abbreviations; the project's + public/private visibility convention is applied consistently. +4. *Related code is co-located* — a public function's private helpers sit right + after it; the file reads top-to-bottom by descending abstraction; sections + group what belongs together. +5. *Structural problems too big to fix in a comment pass are filed* as + =:refactor:= tasks, not left as a vague note and not half-done inline. +6. *Nothing broke* — the build is clean and the test suite is green + (comment/name edits are behavior-preserving, so this should always hold; it + is the proof, not a hope). See "Graceful degradation" for projects without a + suite. + +* When to Use This Workflow + +- "Let's run the readability-audit workflow." +- "Audit the comments and commentary in <file/area>." +- "Clean up the structure/organization of <module>." +- After landing a feature, on the files it touched, before moving on. +- On a single file you just found hard to read. +- As a tree-wide sweep: inventory all the source files, audit each, batch the + fixes. + +Do NOT use this to *perform* the structural refactors themselves (use +=/refactor= or =/start-work= against a filed task) or to hunt for bugs / +complexity / duplication (that is =/refactor=, not a readability pass). + +* Approach: How We Work Together + +** Phase 1 — Scope and inventory + +Pick the target: one file, a named module set, or the whole tree. For a sweep, +list the source files (honor =.aiignore=) and decide coverage. Lean on the +language's own doc linters as a first filter where they exist — many flag a +missing or blank file summary and malformed headers; run the project's lint +target first. + +** Phase 2 — Audit each file against the four dimensions + +Record findings as =file:line — issue — proposed fix=. The four dimensions: + +*** A. File-top commentary (the map) + +- Present, and *accurate* against what the file now does. +- States purpose, the file's role/architecture, and key entry points — + *tersely*. A reader should learn what this is and how it connects in a few + lines. +- Carries the project's file-header convention where it has one (a metadata + block, a module docstring, a standard header comment). If the project has no + header convention, skip this sub-check — don't invent one. +- Does *not* carry user-manual content — quick-starts, full option matrices, + step-by-step setup. That belongs in user docs; move it, don't keep it in the + source header. +- Mechanics are correct for the language: a filled summary line (not blank), the + expected section markers, the expected footer. + +*** B. Inline comments (why, not what) + +- Explains a *why* the code cannot: a workaround *and its reason*, an ordering + or load dependency, business-logic rationale, a real warning ("do not reorder + these — deadlock"). +- Is *accurate* — matches the current code. A wrong comment is worse than none; + fix or delete on sight. +- Is *terse and useful*. Delete the obvious "describe the next line" comment + unless it names a non-obvious constraint. Replace a stale placeholder or a + rambling aside with the real one-line reason, or remove it. +- Convert a comment that's only restating the code into a better *name* instead + (see C). + +*** C. Names (carry the what/how so comments don't have to) + +- Intention-revealing variable and function names; no cryptic single letters or + abbreviations outside tight local scopes. +- The project's public/private convention is applied consistently and correctly: + a helper only called within the file is private; a user-facing or + intentionally-reusable symbol is public. (Resolve the concrete convention from + the language and the project — a naming prefix, an export list, an + access modifier.) +- When a comment exists only to explain a name, rename instead. + +*** D. Organization (co-location and ordering) + +- Related functions sit together. A public function's private helpers come + *right after* it (stepdown / proximity / "reads like a newspaper"). +- The file reads top-to-bottom by descending abstraction. +- Sections group what belongs together. +- *Cohesion check:* if the file holds several unrelated responsibilities, or has + grown large enough that the top no longer describes one coherent thing, flag a + split into layered owners — but see Phase 4: that's a filed refactor, not an + inline fix. + +** Phase 3 — Apply the cheap, safe fixes inline + +Dimensions A, B, and C are *comment- and name-only* and *solo* (no design or +preference call): apply them directly. After each file (or a batch), verify with +the project's gates: parse/syntax check, a clean build (no new warnings), and a +green test suite. Comment/name edits can't change behavior, so green is the proof +the edit was clean, not a behavior check. + +For a tree-wide sweep, drive the uniform rewrites mechanically and verify the +whole batch at once: a *mechanical applier with a boundary assertion* that +replaces a well-defined header span is reliable and fast, then one suite run +covers the batch. Keep the varied cases (header-line fixes, summary fixes that +must preserve surrounding metadata, inline-comment surgery, generated-file +headers) as careful per-file edits. (The boundary markers are language-specific; +the principle — mechanical applier + assert + one suite run for uniform +rewrites, per-file judgment for varied cases — is not.) + +** Phase 4 — File the structural refactors, don't do them here + +Dimension D's bigger findings — split a module, rename a *public* symbol across +call sites, move a function to a different file — are real refactors with their +own risk and test surface. Do *not* slip them into a readability pass. File each +as a =:refactor:= task in =todo.org= with the specific finding, so it gets +=/refactor= or =/start-work= with a proper design and test plan. This is the +line between the cheap clarity win and the structural change; keeping it sharp is +what lets the audit stay safe and fast. + +** Phase 5 — Verify and commit in logical batches + +Full suite green, build clean. Commit the doc/comment changes as =docs:= (or +=refactor:= where a header/structure normalized) in cohesive batches — one +commit per coherent slice (a set of condensed commentaries, the +generated-file-header fixes, the obvious-comment prune), not one mega-commit and +not one-per-file. Generated files are fixed *in their generator* and then +regenerated, so the next regen stays compliant. + +* Graceful degradation + +The audit adapts to what the project provides: + +- *No file-header convention* → skip dimension A's metadata sub-check; still + check the summary/description for accuracy and terseness. +- *No test suite* → the green-suite proof in Phases 3 and 5 is unavailable. Fall + back to the strongest gate the project has (compile/byte-compile, parse check, + linters) and *flag the weaker proof as a known limit* — a behavior-preserving + edit is lower-risk, but say plainly that there's no suite to confirm it. +- *No doc linter* → do the Phase 1 first-filter by reading instead; the audit + still runs, just without the cheap pre-pass. + +* Principles to Follow + +- *Comments explain why; code explains what.* If a comment restates the code, + delete it or turn it into a better name. +- *Accuracy beats completeness.* A wrong or stale comment is worse than no + comment. When in doubt, delete. +- *Terse and useful.* Every comment and every header line earns its place. The + source header is not the user manual — move manuals to user docs. +- *Readable means the next person, fast.* The test of the top-section and the + organization is whether a maintainer who has never seen the file can place it + and navigate it in under a minute. +- *Keep the cheap pass cheap.* Comment/name fixes are solo and land inline. + Structural splits and public renames are not — they get filed, designed, and + tested separately. +- *Preserve legal and attribution headers verbatim.* Vendored / GPL / copyright + notices are never condensed away by a readability pass. +- *Manual validation is still Craig's.* Solo means no input is needed to *do* + the work; visual/behavior confirmation afterward is expected where relevant. + +* Living Document + +Update this with what real runs teach. Lessons worth keeping as the standard +sharpens: + +- *Interpretation default for "fix blank summary":* when a rewrite shows only a + header + summary and omits a metadata block the file already has, keep the + existing metadata and replace only the header line and the summary. Its + absence from the rewrite means "leave it," not "delete it." +- *Generated files:* fix the *generator*, then regenerate. Editing the generated + file directly is reverted on the next regen. +- *Vendored files:* preserve the copyright/attribution; do not auto-condense a + licensed header. +- *Mechanical applier + assert + one suite run* is the safe way to do a + many-file uniform rewrite; per-file judgment is for the varied cases. diff --git a/claude-templates/.ai/workflows/suspend.org b/claude-templates/.ai/workflows/suspend.org new file mode 100644 index 0000000..1c16bb9 --- /dev/null +++ b/claude-templates/.ai/workflows/suspend.org @@ -0,0 +1,112 @@ +#+TITLE: Session Suspend Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-06-28 + +* Overview + +This workflow captures the live state of a session when Craig must leave +abruptly, so a future session resumes with nothing lost. It is the fast, +capture-only workflow for departure: it writes down where every thread stands, +notes any uncommitted work, then STOPS — no cleanup, no archive, no teardown. + +Triggered by Craig saying "suspend the session," "suspend," "I need to go," +"stick a pin in everything," or similar. "I need to go" is broad — if it reads +as a conversational aside rather than a request to suspend, confirm before +running. + +* Where suspend sits among its neighbors + +Three workflows touch the session anchor (=.ai/session-context.org=); keep them +straight: + +- =flush= ([[file:../../flush/SKILL.md]] / =/flush=) — *stay and sharpen.* + Refreshes the anchor in place, prompts Craig to type =/clear=, and a hook + resumes the *same* logical session in a fresh context. Craig is still here. +- *suspend* (this workflow) — *leave.* Captures richly into the anchor, leaves + the file in place, and Craig walks away. The next session is a cold startup + that detects the present anchor and resumes from it. +- =wrap-it-up= ([[file:wrap-it-up.org][wrap-it-up.org]]) — *end.* Writes the + Summary, archives the anchor into =.ai/sessions/=, commits + pushes, and runs + the phrase-dependent teardown. + +Suspend and flush share one core — capture into the anchor, leave it in place. +They differ in the exit (leave vs clear-and-continue) and the resume path +(startup vs the =/clear= hook). Suspend reuses flush's capture discipline (its +Phase 1 anchor-refresh) rather than restating it, and adds a richer, +resume-weighted Session Log entry because it's written for a cold resume after a +gap, not a same-session reset. + +* Suspend vs wrap-up — the one structural difference + +=wrap-it-up= ARCHIVES =.ai/session-context.org= (renames it into +=.ai/sessions/=); its absence at the next startup is the signal that the last +session ended cleanly. + +Suspend does the opposite: it LEAVES =.ai/session-context.org= in place. Its +presence at startup is exactly the signal that the previous session was +interrupted, so the startup workflow reads it and resumes. Suspend provides only +the *capture* half — startup's existing interrupted-session path (Phase A checks +for the anchor, Phase B reads it, Phase C offers to resume) is the *resume* half, +already built. + +So: never archive, never rename the context file in a suspend. Capture into it +and leave it. + +* What gets captured + +The point is zero lost information, weighted toward RESUME. Into the +=* Session Log= of =.ai/session-context.org=, append one dated +=** YYYY-MM-DD ... — SUSPENDED= entry holding: + +1. *Open threads — resume here.* For each active or pending thread: the topic, + its status (ACTIVE / PINNED / SET ASIDE / DEFERRED), the immediate next + step, and the pointers needed to act on it cold (files + line numbers, + commit SHAs, the specific finding or decision). This is the core; spend the + most words here. Order newest / most-active first. +2. *Pending decisions / open questions* awaiting Craig — anything blocked on + his input, with enough context that the answer is actionable. +3. *Shipped this session* — a terse list of what landed, each with its commit + SHA, so the resume knows what is already done and need not re-derive it. +4. *Uncommitted work* — anything modified on disk but not committed, named + file by file, so the resume knows what state the tree is in. +5. *Key findings not yet recorded elsewhere* — anything learned this session + that isn't already in a commit, a file, or memory, so it survives. +6. *Background work* — any running task, agent, or job, and how to check it. +7. *Resume hint* — the single most likely "start here" next action. + +Also update the top of =* Summary= (Active Goal) with a one-line SUSPENDED +pointer to the entry, so startup reading the top sees the current state even +when the Summary body is from an earlier thread. + +* Steps + +1. *Write the SUSPENDED entry* into the Session Log, per "What gets captured" + above. Timestamp with =date "+%Y-%m-%d %a @ %H:%M:%S %z"=. +2. *Update the Active Goal pointer* at the top of =* Summary=. +3. *Record uncommitted work, don't force-commit it.* A suspend records state, it + does not tidy it. Name every uncommitted change in the SUSPENDED entry and + leave the tree as it is — on an abrupt departure, a dirty tree (like any + crash) is safer than a blind commit of arbitrary mid-work state. (If a + project defines a standing always-commit set in its own workflow, commit only + that set — but the default shared behavior is to leave the tree alone.) +4. *Leave =.ai/session-context.org= in place.* Do not archive it. +5. *Brief handoff* — one or two lines: what was captured, where the resume + pointer is, the most-active thread. End and let Craig go. + +* What suspend does NOT do + +Speed over completeness. A suspend deliberately skips everything wrap-it-up +does beyond capture: + +- No =* Summary= rewrite beyond the one-line Active Goal pointer. +- No todo.org cleanup / archive-done. +- No KB / memory promotion sweep. +- No Linear / board reconciliation. +- No session-record archive (the file stays live). +- No teardown (the ai-term buffer + tmux session stay up). It drops no + =Stop=-hook teardown sentinel, so the wrap-teardown hook stays dormant. +- No blind commit of working files (step 3). +- No valediction. A suspend is a pause, not a goodbye. + +If Craig later wants the clean end, he runs wrap-it-up, which picks up the +captured state and finishes the job. diff --git a/claude-templates/.ai/workflows/task-audit.org b/claude-templates/.ai/workflows/task-audit.org index 67ce496..94b99da 100644 --- a/claude-templates/.ai/workflows/task-audit.org +++ b/claude-templates/.ai/workflows/task-audit.org @@ -84,6 +84,21 @@ For every STALE task, edit it in the main thread: Follow =todo-format.md= for completion mechanics (depth-based DONE vs dated-rewrite) and the working-files / link-hygiene rules when moving artifacts. +** Phase C.5 — Consolidate related tasks (interactive) + +Phase C's *Consolidate duplicates* bullet folds tasks that track the *same* thing. This step is the broader case: tasks that aren't duplicates but are really *one effort* fragmented across the list. A spread-out effort — several tasks all circling "make the tooling agent-agnostic," say — is harder to see, plan, and finish as a whole than one task, or one parent with the pieces as children. + +After the Phase C edits, read the open-task set as a whole and look for *clusters*: tasks that share a goal, a subsystem, or an obvious sequence. Use judgment over the task bodies, not a keyword heuristic — adjacency is a semantic call, and a brittle title-match both misses real clusters and invents false ones. + +For each cluster, surface it to Craig (inline numbered options per =interaction.md=, no popup) with a recommendation, offering the two shapes: + +- *Merge* — fold the cluster into one task when the members are genuinely the same work split up (near-duplicates, or steps with no independent value). The merged task keeps the strongest priority, unions the type tags, and absorbs each member's body as a dated note or a short list; the absorbed tasks close per =todo-format.md= (a =**= task → =CANCELLED= + =CLOSED:= with a one-line "merged into <task>", or deletion if it carried nothing unique). +- *Parent with children* — when the members are related but distinct (each ships independently or has its own value), promote a parent task and re-home the members beneath it as sub-tasks, so the list shows the effort as a unit without losing the individual pieces. + +Never merge or re-parent autonomously — which tasks belong together, and whether they're one-work or related-distinct, is a judgment only Craig ratifies. Propose, don't apply, until he picks. A cluster he declines stays as separate tasks; don't re-surface it every audit (note the decline in the session log). + +When no clear cluster exists, say so in one line and move on — most audits won't find one, and forcing a merge fragments worse than it consolidates. + ** Phase D — Flag the judgment calls (interactive) Present the NEEDS-USER bucket as a short, scannable list — one line per task, naming the decision or the fact required. Adjudicate with the user one item at a time (inline numbered options per =interaction.md=, no popup). Apply the user's calls as they come (which may itself produce more autonomous updates, or new tasks). |
