Skip to main content

skill-email-cleanup

Ad-hoc wrapper-only email triage - census, classify, review, confirmed archive/delete/unsubscribe-extract. Default 50-step mode, --all whole-mailbox mode, --archive scope to the account's archive folder. Gmail and Logos accounts supported. Invoke for /email command.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
benbrastmckie/nvim
آخر نشاط في المصدر
٢٨ يوليو ٢٠٢٦ في ١٨:٣٥
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٤٤٤
التفرعات
٤٥٩

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
skill-email-cleanup
description
Ad-hoc wrapper-only email triage - census, classify, review, confirmed archive/delete/unsubscribe-extract. Default 50-step mode, --all whole-mailbox mode, --archive scope to the account's archive folder. Gmail and Logos accounts supported. Invoke for /email command.
allowed-tools
Bash, Read, AskUserQuestion
# Email Cleanup Skill (Direct Execution) Direct-execution skill for email triage, invoked by `/email`. Runs a census -> classify -> review -> confirmed-execute pass without dispatching to a subagent. This skill is wrapper-only: it may invoke ONLY the five named nix-built binaries below, by name, and must NEVER call raw `himalaya`, `notmuch`, `msmtp`, or `secret-tool`, and must NEVER run `rm` against a Maildir path. The one sanctioned exception is the index-only `email-reindex` operator helper (staleness remediation) — non-mutating, exempt exactly as the `mbsync` reconcile is; see the Staleness Remediation section. An account selector, two decision-granularity modes, and an orthogonal folder scope (parsed by `/email`, passed in as args): | Arg | Values | Meaning | |-----|--------|---------| | `account` | `gmail` (default, no flag) / `logos` (`--account logos` or `--logos`) | which mailbox account BASE_QUERY, the pilot gate, and every wrapper `--account` call resolve against. Resolved ONCE and threaded unchanged through the whole invocation. `account=logos` is a live, accepted account subject to a light liveness check: see "Account Liveness Check" below. | | `mode` | `default` (no flag) / `all` (`--all`) | bounded 50-step pass vs. whole-mailbox sweep + one bucket approval + sub-50 drain | | `scope` | `inbox` (no flag) / `archive` (`--archive`) | classify QUERY base — account-dependent, see Stage 0 | | `focus_hint` | free text | optional extra narrowing (sender/domain/topic) | **MANDATORY INTERACTIVE REQUIREMENT -- DO NOT SKIP**: - The human review gate is NEVER skipped in any mode. In `mode=default` it is Stage 3 (per-message candidate review). In `mode=all` it is Stage 2.5 (one consolidated bucket approval). - Do NOT construct or pass `--execute --confirm-manifest <sha256>` to any binary until the user has explicitly approved the reviewed candidate set in this conversation. - The review-gate AskUserQuestion and ALL execute calls run in the ROOT SESSION via direct execution — never inside a background job or subagent. The ONLY thing that may ever run in the background is the read/tag-only classify sweep of `mode=all` Stage 2. ## $PATH Precondition (contract §9 — check before Stage 1) ```bash command -v email-census email-classify email-archive-confirmed email-delete-confirmed \ email-unsubscribe-extract ``` If any binary is missing, stop and tell the user to run `home-manager switch --flake .#<user>` to activate the generation containing `modules/home/email/agent-tools.nix`. Do not fall back to a raw `himalaya`/`notmuch` call. ## Account Liveness Check (`account=logos` only — check before Stage 1) The five wrapper binaries accept `--account <gmail|logos>` as a live enum (wrapper-contracts.md §2, verified 9/9 against the `.dotfiles` wrapper suite); unknown values are rejected loudly. This is not a permanent gate on `logos` — it is a light read-only liveness check that guards against a transient/environmental problem (e.g. a stale `$PATH` generation predating multi-account support). When `account=logos` is resolved (from `/email`'s `--account logos`/`--logos`), this skill confirms the wrapper accepts it BEFORE any binary call by running `email-census --account logos` (read-only, side-effect-free per the Five Binaries safety-class table below) and checking its exit code and stderr — NOT `email-census --account logos --help`, which short-circuits before flag validation (wrapper-contracts.md line 41: `--help` prints verb/safety-class/flags unconditionally) and would never actually test account acceptance. If the probe exits non-zero or reports an account-rejection error, STOP immediately and report: > `/email --logos` failed its liveness check — `email-census --account logos` did not succeed. > Run `home-manager switch --flake .#<user>` to activate the generation with multi-account > support, then retry. This is an ACTIONABLE, LOUD failure — never a silent continuation against `gmail`. Do not construct or run any `folder:Logos*` query, and do not touch Gmail either, if the liveness check fails. `account=gmail` is entirely unaffected by this check (it is today's already-accepted default value) and proceeds straight to the `$PATH` check above. ## The Five Wrapper Binaries (the ONLY binaries this skill may invoke) | Binary | Safety class | Mutates? | |--------|--------------|----------| | `email-census` | read-only | no | | `email-classify` | local-tags-only (notmuch tags, never maildir/IMAP) | no (tags only) | | `email-unsubscribe-extract` | read-only | no | | `email-archive-confirmed` | mutation | yes (maildir move) | | `email-delete-confirmed` | mutation | yes (maildir move + optional `--expunge-trash`) | ## Execution Flow ### Stage 0: Mode and Scope Dispatch Resolve the branch before any binary call: 0. **Account liveness check**: if `account=logos`, run the Account Liveness Check above FIRST; do not proceed to step 1 until it passes. `account` is resolved exactly ONCE here and threaded unchanged (same value) to every wrapper call, the pilot gate, and both `BASE_QUERY` branches below for the rest of this invocation — never re-resolved mid-flow. 1. **Base query from account + scope** (folder: tokens ONLY — never `tag:<account>`, which is live but deliberately not relied on for wrapper scoping; no fallthrough to a Gmail token for a non-gmail account): | `account` | `scope=inbox` | `scope=archive` | |-----------|---------------|------------------| | `gmail` (default) | `BASE_QUERY="folder:Gmail"` | `BASE_QUERY="folder:Gmail/.All_Mail"` | | `logos` | `BASE_QUERY="folder:Logos"` (bare root = INBOX) | `BASE_QUERY="folder:Logos/.Archive"` (the real Proton Archive folder — Logos has no `.All_Mail`/`.Spam`) | These are the exact tokens from wrapper-contracts.md §11 (gmail) and the Logos ground-truth folder census — folder scoping is ONLY ever expressed as (part of) the `email-classify` QUERY positional; no wrapper flag exists for it. The verified `folder:`-syntax forms (live-verified; see `domain/index-architecture.md`): | Form | Example | Live behavior | |------|---------|---------------| | Glob (broken, never in wrapper source) | `folder:Gmail*` | 0 matches — notmuch `folder:` does not glob | | Bare exact-match (the `/email` wrappers, by design) | `folder:Gmail` | INBOX-only exact maildir-folder match | | Regex (aerc querymap) | `folder:/Gmail/` | Whole-account match across all folders | A `tag:<account>` scheme (`tag:gmail`/`tag:logos`) is live (populated by the `postNew` hook, exactly matching `folder:/Gmail/` / `folder:/Logos/`) but wrapper scoping deliberately never relies on it. 2. **Focus terms**: translate `focus_hint` (if any) into additional notmuch query terms (e.g. `from:github.com`) appended to `BASE_QUERY` with `and`. 3. **Branch on mode**: `mode=default` -> Default Mode flow below; `mode=all` -> `--all` Mode flow below. 4. **Archive gates**: if `scope=archive`, apply the extra-caution gates in the "Archive Scope (`scope=archive`)" section to whichever mode flow runs, including the PER-ACCOUNT pilot gate check BEFORE any sweep or classify pass. --- ## Default Mode (`mode=default`): Bounded 50-Step Pass The safer default. Each pass classifies at most 50 messages, is reviewed per-message, and by construction can never trip the wrapper's `enforce_batch_size` cap — so this mode never needs classify chunking (input is bounded to 50) and never needs manifest splitting (the approved set is ≤ 50 per action). Repeated bare `/email` runs make real forward progress via the cursor rule in Stage 2. ### Stage 1: Census Run `email-census --account <account>` (dry-run/read-only by nature) to summarize senders, folders, and date ranges. Present a brief summary to the user. ### Stage 2: Classify (with cross-invocation cursor) Run `email-classify --account <account> --limit 50 "<CURSOR_QUERY>"` to produce a candidate manifest (JSONL keyed on Message-ID) with `proposed_action` (`delete|archive|keep|unsure`) and `confidence` per message, following the recall-on-keep-bias standard (near-100% recall on `keep`; delete auto-proposed only at confidence `>= 0.90`, otherwise `unsure`). **Cursor rule** — once a mailbox has had at least one default pass, exclude already-classified messages so each re-run advances by construction: ``` CURSOR_QUERY = <BASE_QUERY + focus terms> and not tag:proposed-delete and not tag:proposed-archive and not tag:proposed-unsure and not tag:proposed-keep ``` Why this advances: `email-classify` tags every message it processes with exactly one durable `+proposed-*` notmuch tag (wrapper-contracts.md §10). Executed messages leave the folder entirely; declined-but-seen messages keep their durable tag and are excluded by the cursor. So a second bare `/email` classifies a different 50 than the first, and successive runs step through the mailbox oldest-remaining-first (newest-first within each pass). The cursor is expressed purely as an `email-classify` QUERY argument — the skill never issues a raw `notmuch tag`/`notmuch search` command. On a fresh mailbox (no `+proposed-*` tags), the cursor query is equivalent to the plain scoped query. To deliberately revisit previously-declined messages, use `/email --all` (the whole-mailbox sweep re-surfaces them); the default cursor intentionally skips them. **Cursor rule stays account-agnostic by construction**: `CURSOR_QUERY` is built on top of the now-account-aware `BASE_QUERY` (Stage 0), which already confines every account to its own disjoint folder subtree (exact-match `folder:Gmail`/`folder:Gmail/.<Sub>` vs `folder:Logos`/`folder:Logos/.<Sub>` tokens — notmuch `folder:` does not glob). The `+proposed-*` tags themselves are per-message notmuch tags, not account-scoped, but because the two accounts' folders never overlap, a Gmail pass and a Logos pass can never tag, cursor-exclude, or collide on the same message — no additional account qualifier is needed in the tag-exclusion terms. ### Stage 3: Review (mandatory stop) Present the candidate manifest to the user via AskUserQuestion. Allow the user to approve some, all, or none of the proposed actions. Do not proceed without an explicit response. ### Stage 4: Confirm Once the user approves a set of actions, an approved manifest (git-tracked) is produced/updated and its sha256 (over the raw manifest bytes) is computed. ### Stage 5: Execute Invoke `email-archive-confirmed --account <account>` and/or `email-delete-confirmed --account <account>` with `--execute --confirm-manifest <sha256>` for the approved manifest only — the SAME `account` resolved in Stage 0, never re-resolved. Optionally run `email-unsubscribe-extract --account <account>` (read-only) to surface `List-Unsubscribe` candidates for senders the user flagged. ### Stage 6: Verify Diff the wrapper's own execution-state output (never re-derived) against the approved manifest to confirm which IDs were actually mutated. Report this diff to the user. ### Stage 7: Harvest (opt-in, never-silent) An opt-in step that routes wrapper-confirmed decisions from this pass into the memory vault as sender/domain-aggregated `email/preferences/{account}/{key}` preference memories. Grounded in the authoritative design at `context/project/email/design/email-to-memory-preferences.md` — see that document for the full rationale; this section is the executable prose. Fires ONLY after Stage 6, reading ONLY the Stage 6 *executed* diff — never the Stage 2 candidate manifest (`proposed_action`, unconfirmed) and never an approved-but-not-yet-executed manifest. 1. **Key derivation (default mode)**: for every Message-ID the Stage 6 diff confirms as `executed` this pass, look up its `sender` field in the approved manifest and derive its identity via `.claude/scripts/email-preference-harvest.sh identity "<sender>"` (add `--rollup` only for a caller-chosen non-freemail domain rollup — never for freemail/shared domains). Group executed IDs by the resulting `.key`, prefixed by the resolved account: `email/preferences/${ACCOUNT}/${KEY}`. Never key on Stage 2/3/4 unconfirmed lines. 2. **Mixed-sender handling (design §1.5)** is a first-class branch, not an edge case: when a group's confirmed actions this pass are heterogeneous with no uniform majority, either split by a subject/category token into distinct keys, or decline to aggregate that portion this pass (leave it as unresolved tally noise) — never average into a false scalar action. 3. **Per-key dedup + tally update**: for each key's group, determine this round's action-count breakdown (e.g. `{archive: 6}` for a uniform group, or split per mixed-sender handling), then: ```bash EXISTING=$(bash .claude/scripts/email-preference-harvest.sh dedup .memory/memory-index.json \ "email/preferences/${ACCOUNT}/${KEY}") EXISTING_TALLY=$(echo "$EXISTING" | jq -c '.tally // null' 2>/dev/null || echo null) RESULT=$(bash .claude/scripts/email-preference-harvest.sh tally-op "$EXISTING_TALLY" \ "$ACTION" "$COUNT" "$(date -u +%Y-%m-%d)") OPERATION=$(echo "$RESULT" | jq -r '.operation') # CREATE | EXTEND | UPDATE NEW_TALLY=$(echo "$RESULT" | jq -c '.tally') ``` (Note: a real memory file's tally block, not `.tally` on the raw index entry, is the ground truth — read it from the memory file body if the index entry alone is insufficient; the `dedup` subcommand's index lookup is for the exact-key hit/miss decision, per `skill-learn/SKILL.md`'s "Exact-Key Dedup for Reserved Namespaces".) 4. **Evidentiary threshold gate**: only keys/operations meeting `.claude/scripts/email-preference-harvest.sh threshold "$NEW_TALLY" "$UNIFORM"` (uniform-batch this round, or rolling N>=3 at >=80% against the post-update tally) become Tier 1 candidates below; a single isolated confirm below both bars is still tallied (Step 3) but is not presented as a strong preference this round. 5. **Archive-scope isolation**: if this pass's scope is `scope=archive`, record the tally delta in the memory's `### Archive-scope tally` sub-section (per `skill-learn/SKILL.md`'s Namespace-Scoped Tally-Arithmetic subsection) instead of the inbox-scope tally — never merge the two. 6. **One consolidated, never-silent gate** (`AskUserQuestion`, root session), mirroring `skill-todo`'s harvest -> dedup -> tiered-gate -> batch-regen *logic only* (never its `state.json`/`project_number` substrate): - **Tier 1** (pre-selected): keys meeting the evidentiary threshold outright this round (Step 4). - **Tier 2** (shown, not pre-selected): keys newly crossing the rolling-N threshold this round. - **Fuzzy near-miss suggestions** (skill-learn's retained fuzzy path, §4.2): surfaced as a labeled option, never auto-selected. - Skip the gate entirely (no prompt) if this pass produced zero candidates in any tier — harvest is opt-in per-round, not a mandatory stop when there is nothing to offer. 7. **Write on confirm — Bash/jq file-write path** (this skill's `allowed-tools` is `Bash, Read, AskUserQuestion`; memory files are written via Bash heredoc/`jq`, never via a `Write`/`Edit` tool call or a `skill-learn`/`/learn` dispatch): for each user-confirmed key, write or update `.memory/10-Memories/MEM-email-pref-{key-slug}.md` using the body template from `skill-learn/SKILL.md`'s Namespace-Scoped Tally-Arithmetic subsection (frontmatter incl.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub