Sig Weekly

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.

  • For: Indie hackers and small dev teams who answer their own support
  • Setup time: 1 to 2 hours, our estimate (not measured yet)
  • Cost: Two model calls per bug cluster per run, plus one weekly audit call. We haven't measured token cost on a real inbox yet.
  • You need: Read-only access to one support source: a Gmail label or one Slack channel; Python 3 (the two scripts use only the standard library); A model you can call from a script or a coding agent CLI; The GitHub CLI (gh) logged in to your repo, or a Linear API key
  • Partly tested: We ran fingerprint.py and the dry run of approve.py on an 8-message sample inbox we wrote: the script merged the exact repeats (8 messages into 6 groups) and printed the gh commands without creating anything. We have not run the three prompts or the inbox pull on a real inbox yet, so those parts are marked untested.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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).

  7. 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.

  8. 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

The week in AI, for builders

Devin Desktop (formerly Windsurf) rebuilds its Agent Window and turns diff zones off by default

Devin Desktop 3.11.212 (October 23) makes the Agent Window run the same interface as the Devin web app, with up to 8 sessions side by side. It also changes defaults: agent edits now go straight into files with no diff zones and no format-on-save. 3.11.216 followed on October 24 with fixes.

Why it matters: Applying edits directly and reviewing in the Changes tab is a fair default for agent-heavy work. We'd still flip format-on-save back on if your CI fails on lint.

Try this: Update to 3.11.216, then open Devin Settings, Editor, User Interface and decide on three switches: Agent diff zones, Run save actions on agent edits, and Auto-open edited files. If CI checks formatting, turn on save actions so agent edits get formatted again.

Source: Devin Desktop changelog (3.11.212 and 3.11.216)

Cursor CLI now applies a folder's AGENTS.md the moment the agent edits there

In Cursor CLI 2026.10.22, when the agent edits or creates a file in a subdirectory, it sees that directory's AGENTS.md and nested always-apply rules, even if it never read a file there first. Pinned subagents whose model can't run now fall back to the parent's model.

Why it matters: The nested rules fix is the change we'd care about. A rule that only loads after the agent happens to read a file was never reliable.

Try this: Update the Cursor CLI, add an AGENTS.md in one package folder with a rule you can spot (for example, "every new file in this folder starts with a comment naming the owner team"), then ask the agent to create a new file in that folder without reading anything first. Check the rule was followed.

Source: Cursor CLI changelog (v2026.10.22)

Claude Code 2.1.310 stops failing behind gateways that reject its effort beta

Released October 23, Claude Code 2.1.310 retries requests without the effort-2025-11-24 beta value or mid-conversation system messages when a gateway or compatible server rejects them. It also fixes cleanup deleting recent files when cleanupPeriodDays is set very high.

Why it matters: Small release, but the gateway retry and the Windows hook fix are the kind of bugs that waste an afternoon. Update before you open a ticket.

Try this: Run claude --version. If you're behind a company gateway or on Windows with hooks and you're below 2.1.310, update (claude update), then rerun one request that failed this week. If you set cleanupPeriodDays to a huge number to keep history, update before your next session.

Source: Claude Code changelog (2.1.310)

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 -fd

Credit: 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 = ""

Credit: OpenAI Codex 0.164.0 release notes

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)

Credit: Claude Code changelog (2.1.299 and 2.1.305)

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 --help

Get 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

Get the next issue on Monday.