Inbox-to-issue agent: turn support email into draft issues you approve. Plus devin Desktop (formerly Windsurf)...
This week's blueprint: Inbox-to-issue agent: turn support email into draft issues you approve. 3 AI stories that matter if you build, plus the 6 tips from this week.
Written by Mara at Sig · Oct 26, 2026
This week's blueprint: Inbox-to-issue agent: turn support email into draft issues you approve. 3 AI stories that matter if you build, plus the 6 tips from this week.
Blueprint of the week
Inbox-to-issue agent: turn support email into draft issues you approve
A scheduled agent reads your support inbox or Slack channel, merges duplicate reports, writes repro steps and a severity guess, and queues draft GitHub or Linear issues. Nothing is filed until a human types y.
Connect one source, read-only (untested)
Pick one place reports land. Gmail: filter support mail into a "support" label and pull it with the Gmail API (users.messages.list, q="label:support newer_than:1d") on the gmail.readonly scope. Slack: a bot with only channels:history, invited to #support, calling conversations.history since the last run.
Save each message as {"id", "from", "subject", "body", "received"} in messages.json. Read-only means the agent can't reply to customers or close anything.
Write your label set
Create labels.md with your types, severity rules and area labels, plus agent-draft, which every draft gets. Both prompts read it, so the agent sorts reports your way. Create the same labels in GitHub or Linear.
Merge exact repeats with a script
Run python3 fingerprint.py messages.json > fingerprinted.json. It strips ids, numbers, emails and URLs from the first error line (or the subject) and gives identical reports one group number. Cheap and certain, so the model only judges the hard cases.
Cluster the rest with the dedupe prompt (untested)
Send fingerprinted.json with prompt 1. You get clusters with the shared symptom, a type, exact quotes and an "unsure" note. Bugs never merge with billing or feature requests. Read the unsure notes first; wrong merges show up there.
Draft each bug cluster with the repro prompt (untested)
For each bug cluster, send prompt 2 with labels.md and the cluster's messages. You get a title, a markdown body (summary, repro steps from the reports, expected, actual, report ids, what's missing), labels, a severity guess and a confidence. Collect them in drafts.json.
Approve by hand
Run python3 approve.py drafts.json --repo owner/name. It shows each draft and runs gh issue create only when you type y (e edits the title first). Use --dry-run the first week. Every decision goes to audit.jsonl.
On Linear, file approved drafts into your team's Triage with the issueCreate API instead (untested by us).
Schedule it
Run steps 1 to 5 once or twice a day: cron, a CI schedule, or a headless agent run (claude -p or codex exec). Keep step 6 manual. The agent fills the queue; a person empties it.
Audit once a week (untested)
Every Friday, send audit.jsonl and the issues you closed as duplicate or wrong with prompt 3. It lists skipped drafts, bad merges and at most 3 prompt changes. Apply one change at a time.
The prompts
Prompt 1: dedupe (untested)
Step 4. Paste fingerprinted.json at the end.
You are triaging support messages for <PRODUCT>. Below is a JSON list of messages. Each has an id, a subject, a body, and a "group" number from a script that already merged exact repeats.
Merge messages that describe the same underlying problem, even when the wording differs. Only merge when you can name the shared symptom. If you are not sure, keep them apart and say why.
Rules:
- Never invent details that are not in the messages.
- Never merge billing, account or feature requests with bugs.
- Quote the exact error text when there is one.
Return only JSON, no prose:
[{"cluster": 1, "ids": ["m1", "m2"], "symptom": "one sentence", "type": "bug|billing|account|feature|question", "evidence": ["exact quote"], "unsure": "empty, or why these might not belong together"}]
Messages:
<PASTE fingerprinted.json>Prompt 2: draft issue with repro steps and severity (untested)
Step 5, once per bug cluster.
Write a draft bug report for one cluster of support messages. You have not run the product. You only have what users wrote.
Our labels:
<PASTE labels.md>
Return only JSON, no prose:
{"title": "under 80 characters, names the symptom and the area",
"body": "markdown with these sections: Summary, Repro steps (from reports, not reproduced), Expected, Actual, Reports, Missing",
"labels": ["bug", "agent-draft", "<one area label>"],
"severity": "S1|S2|S3",
"confidence": "high|medium|low",
"reports": ["message ids"]}
Severity: S1 = people can't log in, pay, or reach their data. S2 = a main feature fails with no workaround. S3 = everything else.
Repro steps may only use actions a user described. If the reports don't give enough to reproduce it, write "Not enough detail to reproduce" and put the questions to ask under Missing.
Never include email addresses, names, invoice numbers, or anything else that identifies a customer.
Cluster:
<PASTE one cluster from prompt 1, plus the full text of its messages>Prompt 3: weekly audit (untested)
Step 8, Fridays.
Below is audit.jsonl from the last 7 days (every draft and whether a human created or skipped it), followed by the issues we later closed as duplicate, wrong, or won't fix.
List:
1. Drafts we skipped, and the most likely reason for each.
2. Clusters that should have been merged, or split.
3. At most 3 changes to prompt 1 or prompt 2 that would have prevented the most mistakes. Quote the draft titles that each change fixes.
Don't suggest new tools or new steps.
audit.jsonl:
<PASTE>
Closed issues:
<PASTE titles and close reasons>Script: fingerprint.py
Step 3. Python 3, standard library only. Ran on our 8-message sample.
#!/usr/bin/env python3
"""Group support messages that are obviously the same report before any model sees them.
Input: a JSON list of {"id", "from", "subject", "body", "received"} (one per email or Slack message).
Output: the same list with "fp" (fingerprint) and "group" added, sorted by group, to stdout.
The fingerprint is the first error-looking line (exception names, HTTP codes, "error:" lines) with
numbers, ids, emails and URLs stripped. Messages with no error line fall back to the normalized subject.
This only catches exact repeats. The dedupe prompt handles reports that describe the same bug differently.
Usage: python3 fingerprint.py messages.json > fingerprinted.json
"""
import hashlib, json, re, sys
ERROR_LINE = re.compile(r"(error|exception|traceback|failed|failure|\b[45]\d\d\b|timeout|crash)", re.I)
# Start the fingerprint at the error itself, so "Console says: TypeError ..." and "TypeError ..." match.
ERROR_START = re.compile(r"(\w*(Error|Exception)\b|\b(GET|POST|PUT|PATCH|DELETE)\b|\b[45]\d\d\b)")
def normalize(text: str) -> str:
t = text.lower()
t = re.sub(r"https?://\S+", "<url>", t)
t = re.sub(r"[\w.+-]+@[\w-]+\.[\w.]+", "<email>", t)
t = re.sub(r"\b[0-9a-f]{8,}\b", "<id>", t)
t = re.sub(r"\d+", "<n>", t)
t = re.sub(r"^(re|fwd?|fw):\s*", "", t)
return re.sub(r"\s+", " ", t).strip()
def fingerprint(msg: dict) -> str:
for line in (msg.get("body") or "").splitlines():
if ERROR_LINE.search(line) and len(line.strip()) > 8:
start = ERROR_START.search(line)
core = line[start.start():] if start else line
return "err:" + hashlib.sha1(normalize(core).encode()).hexdigest()[:10]
return "subj:" + hashlib.sha1(normalize(msg.get("subject") or "").encode()).hexdigest()[:10]
def main():
if len(sys.argv) != 2:
sys.exit(__doc__)
msgs = json.load(open(sys.argv[1]))
groups: dict[str, int] = {}
for m in msgs:
m["fp"] = fingerprint(m)
m["group"] = groups.setdefault(m["fp"], len(groups) + 1)
msgs.sort(key=lambda m: (m["group"], m.get("received", "")))
json.dump(msgs, sys.stdout, indent=2)
print(f"\n# {len(msgs)} messages, {len(groups)} groups", file=sys.stderr)
if __name__ == "__main__":
main()Script: approve.py
Step 6. Needs gh logged in. The dry run ran on our sample.
#!/usr/bin/env python3
"""Show each draft issue and create it only when a human types y. Nothing is created without a y.
Input: drafts.json, a list of {"title", "body", "labels": [...], "severity", "confidence", "reports": [ids]}
(the output of the draft-issue prompt).
Usage:
python3 approve.py drafts.json --repo owner/name # GitHub, via the gh CLI (gh auth login first)
python3 approve.py drafts.json --repo owner/name --dry-run # print the gh commands, create nothing
Approved, skipped and edited drafts are appended to audit.jsonl for the weekly review.
"""
import json, shlex, subprocess, sys, tempfile, datetime
def main():
args = sys.argv[1:]
if not args or "--repo" not in args:
sys.exit(__doc__)
path = args[0]
repo = args[args.index("--repo") + 1]
dry = "--dry-run" in args
drafts = json.load(open(path))
audit = open("audit.jsonl", "a")
for i, d in enumerate(drafts, 1):
print(f"\n--- Draft {i}/{len(drafts)} severity={d.get('severity')} confidence={d.get('confidence')} reports={len(d.get('reports', []))}")
print(d["title"])
print(d["body"])
answer = "y" if dry else input("Create this issue? [y]es / [n]o / [e]dit title: ").strip().lower()
if answer == "e":
d["title"] = input("New title: ").strip() or d["title"]
answer = "y"
decision = "created" if answer == "y" else "skipped"
if answer == "y":
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as f:
f.write(d["body"])
cmd = ["gh", "issue", "create", "--repo", repo, "--title", d["title"], "--body-file", f.name]
for label in d.get("labels", []):
cmd += ["--label", label]
if dry:
print("DRY RUN:", " ".join(shlex.quote(c) for c in cmd))
decision = "dry-run"
else:
subprocess.run(cmd, check=True)
audit.write(json.dumps({"at": datetime.datetime.now().isoformat(timespec="seconds"), "title": d["title"], "decision": decision, "severity": d.get("severity"), "reports": d.get("reports", [])}) + "\n")
audit.close()
if __name__ == "__main__":
main()What breaks
- Two different bugs share a generic error (a 500, a timeout) and get merged. Check the unsure notes and split by hand.
- Reports with no error text ("the button spins") don't fingerprint together, so they depend on the model.
- Repro steps can sound more certain than the reports. Keep the "from reports, not reproduced" heading.
- Customer details can leak into an issue body. Check before you type y on a public repo.
Skip this if: You get fewer than about 10 support messages a week. Reading them yourself is faster than setting this up.
Built on: Boris Cherny on having Claude run day-to-day app maintenance, Rowan Cheung's copy/paste routines
6 tips from this week
Plan before edits. Add one line to your CLAUDE.md (Claude Code) or AGENTS.md (Codex) so the agent lists its plan before it touches anything. Read the list before you say OK, and stop it if a file on it has nothing to do with the task.
Before editing, list every file you plan to change and why. Wait for my OK.Checkpoint before a long agent run. Commit everything before you hand an agent a long task, so the whole run becomes one diff you can read or throw away. Check git status as well as git diff: in our test, git diff HEAD showed the edited file but missed the new file the run created. Epic Games gives the same advice for its Unreal plugin for Claude Code, since one MCP call can move or delete tracked files. Careful with the last line: git clean -fd deletes every untracked file, so only use it right after a checkpoint.
git add -A && git commit -m "checkpoint before agent run"
# after the run
git status --short # lists new files too
git diff HEAD # edits to tracked files
# to throw the run away
git restore . && git clean -fdCredit: Epic Games: Unreal Engine skills for Claude Code (Security section)
Find what breaks before the Claude SDK upgrade. Claude's new API version (2026-10-21) returns a 400 for temperature, top_p and top_k, and the new SDK majors send your key as Authorization: Bearer instead of X-Api-Key. One ripgrep search finds every line to fix before you bump the package. The -i flag catches X-Api-Key in any casing. In our test it skipped a README that mentioned temperature, because the type filters limit it to Python and TypeScript files. Add --type go or --type java if you use those.
rg -n -i --type py --type ts 'temperature|top_p|top_k|anthropic-version|x-api-key' .Credit: Claude Platform docs: Migrating to API version 2026-10-21
The goldfish check: have a blank chat explain your spec back. Before you hand a feature to a coding agent, open a fresh chat with no history and paste only your spec. Ask it to explain the plan back and list every file it would change. Anything it gets wrong or has to guess is a gap the agent would guess at too. Google's Elephant-Goldfish team runs this as the first of three review steps in their open-source plugin. Fix the doc, then run it again until the explanation matches what you meant.
You have no other context. Read the design doc below. 1) Explain in your own words what we're building and why. 2) List every file you would create or change, and what changes in each. 3) List anything you had to guess or that's ambiguous. Don't write code.Credit: Google Open Source Blog: Introducing the Elephant-Goldfish Model plugin
Save your review prompt as a Codex preset. If you paste the same review prompt into Codex CLI every day, save it once. Codex 0.164.0 added /presets: ten numbered slots for prompts or slash commands. Type /presets, press n, paste the prompt and pick a slot. After that, /presets and the slot number runs it. Presets live in ~/.codex/config.toml under [tui.presets.N], so you can edit them there too. A slash command works as a preset as well, but write the flags out in full, like /model gpt-6.1-sol --reasoning=high.
[tui.presets.1]
input = "Review the current diff for regressions. For each finding, name the file and line, explain how it fails, and suggest the smallest fix. Do not edit files."
label = ""Ask Claude Code what's eating your usage, then press f. If you keep hitting Claude Code's limits, run /usage before you change plans. Since 2.1.299 it warns about the usual culprits: max effort, workflows, always-on context and large tool results. Since 2.1.305 each warning is one line that names where it comes from, and pressing f applies the suggested fixes. The same fixes show up in /doctor and in VS Code. Read the list before you press f, so you know which setting it changes.
/usage
(read the warnings, then press f to apply the fixes)Tool worth trying
mcpkit. Check your MCP server against 12 clients and see exactly what breaks, with a source for each finding.
npx @vrajved/mcpkit --helpGet your referral link: subscribe free and it comes in every issue. Refer 1 friend and you get the whole Sig blueprint archive. Subscribe.
Sig Weekly

