diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-25 09:16:41 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-25 09:16:41 +0300 |
| commit | 9ac61087ecdd97fe67c8217c1a8c1b6d361ef2ef (patch) | |
| tree | b17000d8510ca1013a4832e30fbed53ac6c3de51 | |
| parent | b48ef4c3104b746e045e351eff1a50a68de1c609 (diff) | |
sync skills: add check-shopping-status, protonbridge-imap, sync-skills; add creating-cd-mixes scripts
| -rw-r--r-- | prompts/skills/check-shopping-status/SKILL.md | 203 | ||||
| -rwxr-xr-x | prompts/skills/creating-cd-mixes/scripts/build_cds.py | 176 | ||||
| -rw-r--r-- | prompts/skills/protonbridge-imap/SKILL.md | 126 | ||||
| -rw-r--r-- | prompts/skills/sync-skills/SKILL.md | 49 |
4 files changed, 554 insertions, 0 deletions
diff --git a/prompts/skills/check-shopping-status/SKILL.md b/prompts/skills/check-shopping-status/SKILL.md new file mode 100644 index 0000000..a0c03e9 --- /dev/null +++ b/prompts/skills/check-shopping-status/SKILL.md @@ -0,0 +1,203 @@ +--- +name: check-shopping-status +description: 'Audit the Proton Mail Shopping folders, find every shipment/order email, extract tracking numbers and carriers, and report current delivery status. Use when the user asks where their packages are, to check shopping status, the status of orders/parcels/deliveries, any updates on shipments, or wants a consolidated overview of recent purchases. Combines the protonbridge-imap skill (mailbox access) with headless-Chrome scraping for carrier tracking. Triggers on: shopping status, package status, delivery status, where is my package, check my shipments, parcel update, tracking status.' +--- + +# Check Shopping Status + +End-to-end workflow that turns "what's the status of my deliveries?" into a +single report. + +Prerequisites: + +- [`protonbridge-imap`](../protonbridge-imap/SKILL.md) skill loaded for + IMAP access; credentials live in `~/.protonbridge`. +- `google-chrome` (or `chromium`) available for JS-rendered carrier pages. +- `python3` with stdlib (`imaplib`, `email`, `ssl`). + +## Workflow + +### 1. Scan all Shopping folders + +Recurse `Folders/Shopping` and its subfolders (typically +`Folders/Shopping/Backing` and `Folders/Shopping/BackingButAddressIssues`). +For each message capture: id, From, Subject, Date, plain-text + decoded HTML +body. + +Folder names with spaces must be wrapped in double-quotes when selecting: +`M.select('"Folders/Shopping"', readonly=True)`. Always use `BODY.PEEK[...]` +so the seen flag is not changed. + +### 2. Classify each mail + +Use these signal categories (in order): + +| Category | How to detect | +|---|---| +| **Shipment with tracking** | Subject/body contains "shipped", "versandt", "on its way", "ausgeliefert", "Sendungsnummer", "tracking number" *and* a tracking code OR a recognisable carrier tracking URL. | +| **Order confirmation only** | "Order confirmation", "Bestellbestätigung", "Vielen Dank für Ihre Bestellung" without a tracking code. | +| **Payment / admin** | PayPal, PayU, Klarna, donation receipts. | +| **Pledge / pre-ship** | Kickstarter "You just backed…", "Pledge collected", project updates. | +| **Address / response needed** | "Response needed", "address issue", survey reminders. | +| **Marketing** | Newsletters from Proton/Amazon/PledgeBox with no real shipment context. | + +Mark only the first category as ✅ trackable; everything else as +informational. Filter out false positives: marketing mails often contain the +word "tracking" only in UTM links (`utm_source=tracking_email`). + +### 3. Extract tracking codes and carriers + +Carrier regexes (apply against the decoded body, dedupe matches): + +| Carrier | Pattern | +|---|---| +| DHL international piece | `\b[A-Z]{2}\d{9}DE\b` | +| DHL domestic | `\bJJD\d{12,20}\b` or `\b\d{12,14}\b` (filter `0+`) | +| UPS | `\b1Z[0-9A-Z]{16}\b` | +| USPS / S10 | `\b9\d{15,21}\b` or `\b[A-Z]{2}\d{9}US\b` | +| Asendia / Pirate Ship | `\bAHOY\w+\b` | +| Royal Mail | `\b[A-Z]{2}\d{9}GB\b` | +| FedEx | `\b\d{12}\b` or `\b\d{15}\b` | +| Generic numeric | `\b\d{10,30}\b` (only when carrier domain matches) | + +Also pull every `https://...track...|sendung|paket|versand|swiship|asendia| +17track|temu...` URL — it often contains the most reliable id (Amazon +`shipmentId=`, Temu `track_sn=`, Swiship `?id=`). + +Reject placeholder garbage: `6666666666...`, `3333333333...`, `0000000000`. + +### 4. Look up status per carrier + +Run lookups in parallel where possible. Quick reference for what works +without API keys: + +| Carrier | Reachable? | How | +|---|---|---| +| **Swiship / FAN Courier** (`swiship.co.uk/track?loc=de-DE&id=...`) | ✅ headless Chrome (`virtual-time-budget=20000`) | Plain text already in DOM. | +| **Asendia USA** (`a1.asendiausa.com/tracking/?trackingnumber=...&trackingkey=...`) | ✅ direct curl | First page render contains last event. | +| **Amazon DE/COM** (`progress-tracker?orderId=...&shipmentId=...`) | ❌ requires Amazon login | Report shipmentId + orderId and note "Amazon login required". | +| **Temu** (`api-euo.temu.com/callback/doha/open/track?...`) | ❌ needs Temu account | Use the rendered mail text instead ("Order shipped", "transferred to Bulgarian Post"). | +| **DHL** (`nolp.dhl.de`, `dhl.de/int-verfolgen`, `dhl.com/utapi`) | ❌ blocked | Akamai bot challenge + explicit HTTP 403 *"tracking attempt has been blocked"*. Aggregators (17track, parcelsapp, AfterShip, parcelmonitor) also fail without paid API keys. Fall back to the email body for context (sender hand-off date, delivery partner, expected days) and link the user to `https://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=<CODE>`. | +| **Bulgarian Post / Speedy** | ❌ Cloudflare challenge | Same fallback — quote what the mail says. | +| **Kickstarter / PledgeBox** | n/a | These don't carry carrier codes; status comes from the project update text. | + +Headless-Chrome incantation (works for sites without bot protection): + +```bash +UA='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36' +timeout 45 google-chrome --headless=new --disable-gpu --no-sandbox \ + --window-size=1400,2400 --user-agent="$UA" \ + --virtual-time-budget=20000 \ + --screenshot=/tmp/track.png --dump-dom "$URL" > /tmp/track.html +``` + +Then strip tags and grep for status keywords: +`deliver|delivered|in transit|out for delivery|zugestellt|unterwegs|abgeholt| +expected|arrived|departed|customs`. + +If the screenshot is < 60 KB or contains the words "Cloudflare", "security +verification", "Bot Manager", "Pardon Our Interruption", or "blocked" → +mark the lookup as **blocked**, do *not* keep retrying with the same client. + +### 5. Produce the report + +Always group by folder. Inside each folder produce a table with these +columns: # · Item · Carrier/Code · Status. Use these status badges so the +user can scan visually: + +- ✅ Delivered / clear good outcome +- 🚚 In transit (with date or ETA when known) +- 🟡 Handed to local carrier / pending pickup +- 🟠 Label created, not yet inducted +- ⏳ Pre-ship / awaiting production +- 🔒 Lookup needs login +- ❓ Carrier blocked automated lookup — manual check link +- 📋 Action needed from user (address, survey) + +End the report with a short "Things worth acting on" list: imminent +deliveries, address issues to answer, codes that haven't moved. + +## End-to-end one-shot script + +```bash +set -a; . ~/.protonbridge; set +a +python3 - <<'PY' +import imaplib, os, ssl, email, re, html +from email.header import decode_header + +ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE +M = imaplib.IMAP4(os.environ['IMAP_HOST'], int(os.environ['IMAP_PORT'])) +M.starttls(ssl_context=ctx); M.login(os.environ['IMAP_USER'], os.environ['IMAP_PASS']) + +def dh(s): + if not s: return '' + return ''.join(p.decode(c or 'utf-8','replace') if isinstance(p, bytes) else p + for p,c in decode_header(s)) + +CARRIERS = { + 'DHL-INT': re.compile(r'\b[A-Z]{2}\d{9}DE\b'), + 'DHL': re.compile(r'\bJJD\d{12,20}\b'), + 'UPS': re.compile(r'\b1Z[0-9A-Z]{16}\b'), + 'USPS': re.compile(r'\b9\d{15,21}\b'), + 'Asendia': re.compile(r'\bAHOY\w+\b'), + 'RoyalMail': re.compile(r'\b[A-Z]{2}\d{9}GB\b'), +} +SHIP_KW = re.compile(r'(track(ing)?[\s_-]*(number|code|id|link)?|sendungsnummer|sendungsverfolgung|shipped|versandt|on its way|delivered|geliefert|out for delivery|ausgeliefert|unterwegs)', re.I) +URL_RE = re.compile(r'https?://[^\s<>"\']+') +JUNK = {'00000000000000','0000000000','66666666666667','333333333333336'} + +def body_text(m): + parts=[] + if m.is_multipart(): + for p in m.walk(): + if p.get_content_type() in ('text/plain','text/html'): + try: parts.append(p.get_payload(decode=True).decode(p.get_content_charset() or 'utf-8','replace')) + except: pass + else: + try: parts.append(m.get_payload(decode=True).decode(errors='replace')) + except: pass + return '\n'.join(parts) + +for folder_q in ['"Folders/Shopping"', '"Folders/Shopping/Backing"', '"Folders/Shopping/BackingButAddressIssues"']: + typ,_ = M.select(folder_q, readonly=True) + if typ!='OK': continue + _, data = M.search(None, 'ALL') + ids = data[0].split() + print(f'\n=== {folder_q} ({len(ids)} messages) ===') + for num in ids: + _, msg = M.fetch(num, '(BODY.PEEK[])') + m = email.message_from_bytes(msg[0][1]) + subj = dh(m.get('Subject','')); frm=dh(m.get('From','')); date=m.get('Date','') + body = body_text(m) + codes=[] + for name,pat in CARRIERS.items(): + for c in set(pat.findall(body)): + if isinstance(c,tuple): c=c[0] + if c not in JUNK: codes.append(f'{name}:{c}') + urls = [u for u in URL_RE.findall(body) if re.search(r'track|sendung|paket|versand|swiship|asendia|temu|amazon.*progress-tracker', u, re.I)] + ship = bool(SHIP_KW.search(subj+'\n'+body)) or codes or urls + flag = '📦' if ship else ' ' + print(f'{flag} #{num.decode():>3} {date[:25]:25s} {frm[:38]:38s} {subj[:80]}') + for c in codes[:3]: print(f' code: {c}') + for u in urls[:2]: print(f' url: {u[:160]}') +M.logout() +PY +``` + +After this prints the candidates, run the headless-Chrome lookup loop for +each tracking code/URL and assemble the final markdown report following the +format in step 5. + +## Tips + +- Re-running is cheap and idempotent — IMAP is read-only and Chrome writes + to `/tmp/track/`. Wipe `/tmp/track/` between runs to avoid stale state. +- Cache the user's recent report under `~/.cache/check-shopping-status/` + (date-stamped JSON) so the next run can diff "what's new since last + time". +- When in doubt about a code being a real tracking number, link to the + carrier's tracking page so the user can verify in their own browser + (where they're logged in / not blocked). +- Never spam the same carrier endpoint with retries when it returns a bot + challenge — Akamai's `_abck` and Cloudflare's challenge cookies get + worse with each attempt. diff --git a/prompts/skills/creating-cd-mixes/scripts/build_cds.py b/prompts/skills/creating-cd-mixes/scripts/build_cds.py new file mode 100755 index 0000000..089d9ff --- /dev/null +++ b/prompts/skills/creating-cd-mixes/scripts/build_cds.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Pack FLAC tracks into N audio CDs, shuffled and deduplicated by title. + +Reads a JSON config from stdin or from --config FILE: + + { + "music_root": "/data/nfs/k3svolumes/navidrome/music", + "dest": "/home/paul/Desktop/Music", + "name_prefix": "CD", # output dirs become CD1-Mix, CD2-Mix, ... + "name_suffix": "Mix", + "title": "Focus / House Mix", + "num_cds": 3, + "max_seconds": 4380, # 73:00 — safe under 74-min CD limit + "seed": 42, + "source_dirs": [ + "Miscellaneous/Compilations/Deep House Study Mix_ Electronic Music for Studying, Concentration", + "Miscellaneous/Compilations/Chill House Cafe Playlist" + ], + "extra_files": [ + "Miscellaneous/David Guetta, Marten Hørger/The Freaks/01 The Freaks (Edit).flac" + ], + "exclude_titles": ["Eternal", "Tunnel"], # optional: skip these (case-insensitive) + "recursive": false, # optional: glob *.flac recursively + "start_index": 1 # optional: start CD numbering (e.g. 2) + } + +Disc-length presets (max_seconds): + 74-min CD: 4380 (73:00, ~1 min headroom) + 80-min CD: 4700 (78:20, ~1:40 headroom) + +Output per CD: + <dest>/<name_prefix><N>-<name_suffix>/01 - Title.flac ... + <dest>/<name_prefix><N>-<name_suffix>.txt (summary) +""" + +import argparse +import json +import random +import shutil +import subprocess +import sys +from pathlib import Path + + +def duration(p: Path) -> float: + out = subprocess.check_output( + ["ffprobe", "-v", "error", + "-show_entries", "format=duration", + "-of", "default=nw=1:nk=1", str(p)], + text=True, + ) + return float(out.strip()) + + +def mmss(secs: float) -> str: + m, s = divmod(int(round(secs)), 60) + return f"{m}:{s:02d}" + + +def clean_title(stem: str) -> str: + """Strip a leading 'NN ' track-number prefix.""" + if len(stem) > 3 and stem[:2].isdigit() and stem[2] == " ": + return stem[3:].strip() + return stem.strip() + + +def collect_pool(cfg: dict) -> list[dict]: + music_root = Path(cfg["music_root"]) + recursive = bool(cfg.get("recursive", False)) + glob_pat = "**/*.flac" if recursive else "*.flac" + paths: list[Path] = [] + for d in cfg.get("source_dirs", []): + base = music_root / d + if base.exists(): + paths.extend(sorted(base.glob(glob_pat))) + for f in cfg.get("extra_files", []): + p = music_root / f + if p.exists(): + paths.append(p) + + excluded = {t.lower().strip() for t in cfg.get("exclude_titles", [])} + seen: set[str] = set() + pool: list[dict] = [] + for flac in paths: + title = clean_title(flac.stem) + key = title.lower() + if key in seen or key in excluded: + continue + seen.add(key) + pool.append({ + "path": flac, + "title": title, + "source": flac.parent.name, + "duration": duration(flac), + }) + return pool + + +def pack(pool: list[dict], num_cds: int, max_seconds: int): + """Greedy fit into least-filled CD that still has room.""" + cds: list[list[dict]] = [[] for _ in range(num_cds)] + totals = [0.0] * num_cds + leftover: list[dict] = [] + for tr in pool: + placed = False + for i in sorted(range(num_cds), key=lambda x: totals[x]): + if totals[i] + tr["duration"] <= max_seconds: + cds[i].append(tr) + totals[i] += tr["duration"] + placed = True + break + if not placed: + leftover.append(tr) + return cds, totals, leftover + + +def write_cd(idx: int, tracks: list[dict], total: float, cfg: dict) -> None: + dest = Path(cfg["dest"]) + prefix = cfg.get("name_prefix", "CD") + suffix = cfg.get("name_suffix", "Mix") + title = cfg.get("title", "Mix") + max_s = cfg["max_seconds"] + start = int(cfg.get("start_index", 1)) + name = f"{prefix}{idx + start}-{suffix}" + out_dir = dest / name + out_dir.mkdir(parents=True, exist_ok=True) + + summary = [ + f"CD {idx + start} — {title}", + f"Total tracks: {len(tracks)}", + f"Total time: {mmss(total)} (limit {mmss(max_s)})", + "", + f"{'#':>2} {'TIME':>5} TITLE [source]", + "-" * 78, + ] + for i, tr in enumerate(tracks, 1): + num = f"{i:02d}" + dest_name = f"{num} - {tr['title']}.flac".replace("/", "_") + shutil.copy2(tr["path"], out_dir / dest_name) + summary.append( + f"{num} {mmss(tr['duration']):>5} {tr['title']} [{tr['source']}]" + ) + (dest / f"{name}.txt").write_text("\n".join(summary) + "\n", encoding="utf-8") + print(f" -> {name}: {len(tracks)} tracks, {mmss(total)}") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--config", "-c", help="JSON config file (default: stdin)") + args = ap.parse_args() + + raw = Path(args.config).read_text() if args.config else sys.stdin.read() + cfg = json.loads(raw) + + cfg.setdefault("num_cds", 3) + cfg.setdefault("max_seconds", 4380) + cfg.setdefault("seed", 42) + cfg.setdefault("title", "Mix") + + random.seed(cfg["seed"]) + Path(cfg["dest"]).mkdir(parents=True, exist_ok=True) + + pool = collect_pool(cfg) + print(f"pool: {len(pool)} tracks, " + f"{mmss(sum(t['duration'] for t in pool))} total") + + random.shuffle(pool) + cds, totals, leftover = pack(pool, cfg["num_cds"], cfg["max_seconds"]) + if leftover: + print(f" leftover (did not fit): {len(leftover)}") + for i, (tracks, total) in enumerate(zip(cds, totals)): + write_cd(i, tracks, total, cfg) + + +if __name__ == "__main__": + main() diff --git a/prompts/skills/protonbridge-imap/SKILL.md b/prompts/skills/protonbridge-imap/SKILL.md new file mode 100644 index 0000000..9b419e7 --- /dev/null +++ b/prompts/skills/protonbridge-imap/SKILL.md @@ -0,0 +1,126 @@ +--- +name: protonbridge-imap +description: 'Access ProtonMail mailboxes locally through Proton Bridge over IMAP (STARTTLS on 127.0.0.1 port 1143). Use when the user asks to read, list, search, count, or fetch emails from Proton Mail / ProtonMail / Proton Bridge, check inbox, list folders, or interact with the local IMAP bridge. Reads credentials from ~/.protonbridge. Triggers on: protonbridge, proton bridge, proton mail, protonmail, imap, list emails, read inbox.' +--- + +# Proton Bridge IMAP Access + +This skill lets you read mail from the local Proton Bridge IMAP server. + +## Credentials + +Credentials live in `~/.protonbridge` (chmod 600) as `KEY=VALUE` pairs: + +- `IMAP_HOST` (127.0.0.1) +- `IMAP_PORT` (1143) +- `IMAP_USER` (e.g. mail@paul.buetow.org) +- `IMAP_PASS` (bridge-generated password, NOT the Proton account password) +- `IMAP_SECURITY` (STARTTLS) + +Bridge also exposes SMTP on 127.0.0.1:1025 with the same user/password. + +Never echo `IMAP_PASS` to the terminal or commit it. Load it via shell: + +```bash +set -a; . ~/.protonbridge; set +a +``` + +## Connecting + +Bridge listens on plain TCP and upgrades via STARTTLS. The certificate is +self-signed by Bridge, so verification must be disabled. + +Use Python's `imaplib` — it is preinstalled and handles STARTTLS cleanly: + +```python +import imaplib, os, ssl +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +M = imaplib.IMAP4(os.environ['IMAP_HOST'], int(os.environ['IMAP_PORT'])) +M.starttls(ssl_context=ctx) +M.login(os.environ['IMAP_USER'], os.environ['IMAP_PASS']) +``` + +Do NOT use `imaplib.IMAP4_SSL` on port 1143 — Bridge requires STARTTLS, not +implicit TLS. + +## Common operations + +List folders: + +```python +typ, folders = M.list() +for f in folders: print(f.decode()) +``` + +Notable folders: `INBOX`, `Archive`, `Sent`, `Drafts`, `Spam`, `Trash`, +`All Mail`, `Starred`, and user folders under `Folders/...` (e.g. +`Folders/Newsletter`). Quote names with spaces: `M.select('"All Mail"')`. + +List recent message headers in INBOX: + +```python +M.select('INBOX') +_, data = M.search(None, 'ALL') +ids = data[0].split() +for num in ids[-20:]: + _, msg = M.fetch(num, '(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])') + print('---', num.decode()) + print(msg[0][1].decode(errors='replace').strip()) +``` + +Use `BODY.PEEK[...]` instead of `BODY[...]` so messages stay unread. + +Search examples (RFC 3501): + +- Unread: `M.search(None, 'UNSEEN')` +- From sender: `M.search(None, 'FROM', '"alice@example.com"')` +- Since date: `M.search(None, 'SINCE', '01-May-2026')` +- Subject text: `M.search(None, 'SUBJECT', '"invoice"')` + +Fetch a full message and decode subjects properly: + +```python +import email +from email.header import decode_header +_, msg = M.fetch(num, '(BODY.PEEK[])') +m = email.message_from_bytes(msg[0][1]) +subj = ''.join( + s.decode(c or 'utf-8', errors='replace') if isinstance(s, bytes) else s + for s, c in decode_header(m['Subject'] or '') +) +``` + +Always finish with `M.logout()`. + +## One-shot shell helper + +For quick "list latest N from a folder" tasks: + +```bash +set -a; . ~/.protonbridge; set +a +python3 - <<'PY' +import imaplib, os, ssl, sys +ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE +M = imaplib.IMAP4(os.environ['IMAP_HOST'], int(os.environ['IMAP_PORT'])) +M.starttls(ssl_context=ctx) +M.login(os.environ['IMAP_USER'], os.environ['IMAP_PASS']) +M.select('INBOX') +_, data = M.search(None, 'ALL') +for num in data[0].split()[-10:]: + _, msg = M.fetch(num, '(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])') + print('---', num.decode()); print(msg[0][1].decode(errors='replace').strip()) +M.logout() +PY +``` + +## Troubleshooting + +- `Connection refused` → Proton Bridge is not running. Start the Bridge app. +- `LOGIN failed` → password rotated in Bridge; regenerate and update + `~/.protonbridge`. +- `socket.gaierror` → don't use `localhost` if IPv6 is broken; stick to + `127.0.0.1`. +- `STARTTLS extension not supported` → you connected with `IMAP4_SSL`; use + plain `IMAP4` then `starttls()`. diff --git a/prompts/skills/sync-skills/SKILL.md b/prompts/skills/sync-skills/SKILL.md new file mode 100644 index 0000000..3de5b04 --- /dev/null +++ b/prompts/skills/sync-skills/SKILL.md @@ -0,0 +1,49 @@ +--- +name: sync-skills +description: "Check for untracked or changed skill files in the skills directory, summarize the changes, and commit and push them to git. Use when asked to sync skills, push skills, commit skills, or check for pending skill changes. Triggers on: sync skills, push skills, commit skills, skill changes, pending skills." +--- + +# Sync Skills + +Detect untracked and changed skill files in `~/git/dotfiles/prompts/skills/`, summarize the changes, then commit and push everything. + +## When to Use + +- User asks to **sync**, **push**, or **commit** skill files. +- User asks to check for **pending** or **uncommitted** skill changes. +- Triggers: *sync skills*, *push skills*, *commit skills*, *skill changes*, *pending skills*. + +## Instructions + +1. **Detect changes** — from `~/git/dotfiles`, run: + ``` + git status --short prompts/skills/ + ``` + This lists untracked (`??`) and modified/added/deleted (`M`/`A`/`D`) files. + +2. **If nothing to commit**, report that all skills are in sync and stop. + +3. **Summarize the changes** — for each changed or untracked file: + - For **new (untracked) files**: read the `SKILL.md` frontmatter (name + description lines) and note it as a new skill. + - For **modified files**: run `git diff -- prompts/skills/<path>` and summarize what changed (e.g., updated description, added instructions, new references). + - Present a concise human-readable summary before committing. + +4. **Stage and commit** — from `~/git/dotfiles`: + ``` + git add prompts/skills/ + git commit -m "sync skills: <brief summary>" + ``` + The commit message should start with `sync skills:` followed by a short phrase listing the key changes (e.g., `sync skills: add check-shopping-status, protonbridge-imap; update creating-cd-mixes scripts`). + +5. **Push**: + ``` + git push + ``` + +6. **Report** — confirm what was committed and pushed, including the commit hash. + +## Notes + +- Always work from `~/git/dotfiles` as the git root. +- Only touch paths under `prompts/skills/`. +- Keep the commit message concise but descriptive enough to know which skills were affected.
\ No newline at end of file |
