Vibecode SaneBox
track this build4 phases, 12 steps, beginner friendly0%The visible email triage loop is buildable, but a credible replacement needs more than the first screen. SaneBox earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of SaneBox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # SaneBox · indie build An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits moving messages | sorting a mailbox does not require reading it | | Signals | Sender history, your reply behaviour and thread participation | who you actually answer is the strongest signal available | | Classification | A threshold tuned for precision on the leave-in-inbox side | the costs of the two errors are not symmetric | | Reversal | Every move recorded and undoable, with training from the correction | a correction is the most valuable signal you get | | Digest | A scheduled summary of what was filed | a mistake caught within a day is recoverable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with sender aggregation. - Get it: Docker or your package manager. - Verify: psql connects and aggregates two hundred thousand rows quickly - [ ] **A mail account with years of history** · free - Why: Sender profiles are built from your own replies over time, and a fresh account contains none of the signal the whole approach depends on. - Get it: Your own account, and read the provider's scope documentation to find the narrowest one that permits moving messages. - Verify: At least three years of history syncs as metadata - [ ] **Five hundred messages you have labelled yourself** · free, and it takes an evening - Why: The two error rates cannot be measured without ground truth, and only you know which of your messages mattered. - Get it: Take a random sample from your own mailbox and label each as should have stayed or could have been filed. Do not use a public dataset. - Verify: Five hundred labelled messages exist with both classes represented - [ ] **Your acceptable error rate on the expensive side** · free - Why: The threshold is set from it, and choosing after seeing the results means choosing whichever produces a flattering figure. - Get it: Decide how often you could tolerate something important being filed, expressed as a rate, before measuring anything. - Verify: A number is written down and the threshold is derived from it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. - push reliability across devices - team routing and shared inbox rules - abuse response and security operations - sender reputation, deliverability, and suppression handling - provider-grade deliverability If one of those is essential to you, that is the reason to keep paying for SaneBox, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SaneBox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me email triage that moves the unimportant out of the way and never hides the important to replace SaneBox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The asymmetry here is the whole design. Leaving an unimportant message in the inbox costs a second of attention. Filing an important one out of sight can cost a job offer, a deadline or a relationship, and the person will not know it happened. So the classifier is tuned deliberately toward leaving things in the inbox, every decision is reversible, nothing is ever deleted, and a digest of what was filed arrives on a schedule so that a mistake is caught within a day rather than never. The second constraint follows from what this tool sees. Triage needs headers, senders and your own reply behaviour; it does not need message bodies, and holding the full text of somebody's mailbox to sort it is a disproportionate amount of access for the job. Ask for the narrowest scope that works, store metadata, and assert in the suite that no body text is persisted. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits moving messages · sorting a mailbox does not require reading it - Signals: Sender history, your reply behaviour and thread participation · who you actually answer is the strongest signal available - Classification: A threshold tuned for precision on the leave-in-inbox side · the costs of the two errors are not symmetric - Reversal: Every move recorded and undoable, with training from the correction · a correction is the most valuable signal you get - Digest: A scheduled summary of what was filed · a mistake caught within a day is recoverable ### Data model (create this before Phase 1) - accounts(id, provider, address, scopes_granted, connected_at) - senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count) - messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only - decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by) - folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked - digests(id, account_id, period_start, period_end, sent_at, message_count) - A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined. ### Phase 1 · Sorting without reading Build: Connect with the narrowest scope that permits reading headers and moving messages. Sync metadata only. Assert in the suite that no body text is persisted anywhere, including in logs. Trap: Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. Done when: The narrowest scope permitting the operations is requested and documented. No message body is stored anywhere, asserted in the suite including log paths. A mailbox with two hundred thousand messages syncs metadata incrementally and resumably. Revoking access stops sync and leaves prior decisions intact. Do not build yet: No classification, folders, digest or reversal. ### Phase 2 · Signals from what you actually do Build: Build sender profiles from your own behaviour: replied, ignored, archived unread, filed by hand. Weight a reply far above any content signal. Handle a sender you have never seen and a sender whose address rotates. Trap: Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. Done when: Sender profiles derive from your own replies, archives and manual filings over a year of history. A sender you have replied to is never filed automatically. An unknown sender is treated as unknown and left in the inbox rather than filed. A sender rotating addresses within one domain is recognised. Do not build yet: No classification thresholds, folders or digest yet. ### Phase 3 · A threshold tuned for the expensive mistake Build: Classify with a confidence threshold set so that filing requires high confidence and anything uncertain stays in the inbox. Measure both error rates against a hand-labelled set. Report them separately rather than as one accuracy figure. Trap: Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. Done when: Filing requires confidence above a stated threshold and uncertain messages remain in the inbox. Both error rates are measured against a hand-labelled set of at least five hundred messages and reported separately. The threshold can be moved and its effect on both rates is shown. No single accuracy figure is presented. Do not build yet: No folders, digest or reversal yet. ### Phase 4 · Nothing is hidden and nothing is deleted Build: Move filed messages into visible folders rather than hiding them, never delete anything, and send a digest of what was filed on a stated schedule. Make reversing a decision one action that also trains the sender profile. Trap: Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. Done when: Filed messages go to visible folders and nothing is ever deleted. A digest of filed messages sends on the stated schedule and is skimmable in under a minute. Reversing a decision takes one action and updates the sender profile. A message reversed from a folder does not get filed again by the same rule. Do not build yet: No reminders or reporting yet. ### Phase 5 · Reminders and knowing whether it works Build: Follow-up reminders for messages you sent that received no reply. Report the two error rates over time from your own reversals. Support disconnecting cleanly with all messages left where the provider can see them. Trap: Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. Done when: Follow-up reminders fire for sent messages with no reply after a stated period, and are cancelled by a reply. Ongoing error rates are computed from reversals and displayed. Disconnecting leaves every message in a folder visible to the provider rather than in a hidden state. Export contains decisions and sender profiles. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or summarising message content, which would require a far broader permission than sorting needs. - Spam detection, which the provider already does with vastly more signal than you have. - Deleting anything, ever, which is excluded deliberately rather than omitted. - Shared or team triage, which changes both the permission model and whose behaviour the signals describe. ### README must contain - That filing something important is far worse than leaving noise, and every threshold reflects that - That the classifier works from metadata and no message body is stored, asserted in the suite - That whether you have replied to a sender is weighted above every other signal - That nothing is deleted and a digest of what was filed arrives on a short schedule - That the two error rates are reported separately, because a single accuracy figure hides the expensive one ===== AGENTS.md ===== # Agent instructions · SaneBox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits moving messages, Sender history, your reply behaviour and thread participation, A threshold tuned for precision on the leave-in-inbox side, Every move recorded and undoable, with training from the correction, A scheduled summary of what was filed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ===== BUILD_PLAN.md ===== # Build plan · SaneBox An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sorting without reading Triage works from metadata and no message body is stored. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Assert it in the suite, including log paths. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Two hundred thousand messages is a normal mailbox. Files: `src/sync.ts` ### Done when - [ ] The narrowest scope is requested and documented - [ ] No body text is stored, including in logs - [ ] A large mailbox syncs incrementally - [ ] Revoking access leaves prior decisions intact ### Watch out - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. ## Phase 2 · Signals from what you actually do The strongest signal is whether you reply, and it is used first. ### Steps 1. Weight your replies above everything It is the only signal that is genuinely about you. Files: `src/signals.ts` 2. Treat unknown as unknown Not as unimportant. The default must be to leave it alone. Files: `src/signals.ts` 3. Recognise rotating addresses within a domain Newsletters rotate constantly and fragment otherwise. Files: `src/senders.ts` ### Done when - [ ] Profiles derive from your own behaviour - [ ] A sender you have replied to is never auto-filed - [ ] Unknown senders stay in the inbox - [ ] Rotating addresses in one domain are recognised ### Watch out - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. ## Phase 3 · A threshold tuned for the expensive mistake The classifier is deliberately reluctant to file anything. ### Steps 1. Set the threshold for precision on filing Uncertain means leave it alone. Files: `src/classify.ts` 2. Hand label a real evaluation set Five hundred messages you judged yourself. There is no substitute. Files: `eval/labelled.json` 3. Report the two error rates separately A single accuracy figure hides the expensive one. Files: `src/evaluate.ts`, `tests/error-rates.test.ts` ### Done when - [ ] Filing requires confidence above the threshold - [ ] Both error rates are measured on a hand-labelled set - [ ] Threshold changes show their effect on both rates - [ ] No single accuracy figure is presented ### Watch out - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. ## Phase 4 · Nothing is hidden and nothing is deleted Filed messages are visible, reversible and summarised on a schedule. ### Steps 1. File to visible folders, delete nothing A folder nobody opens is a deletion with extra steps. Files: `src/folders.ts` 2. Digest on a short schedule A mistake caught within a day is recoverable. Files: `src/digest.ts` 3. Make reversal one action that trains The correction is the most valuable signal you will get. Files: `src/reversal.ts` ### Done when - [ ] Nothing is ever deleted - [ ] The digest is skimmable in under a minute - [ ] Reversal is one action and updates the profile - [ ] A reversed message is not refiled by the same rule ### Watch out - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ## Not in this build - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. ## After v1, if you want it - Per-sender rules you set explicitly, which beat any classifier for the cases you care about - A weekly view of who you never reply to, which is genuinely useful and slightly uncomfortable - Snoozing a message until a stated time, which is the other half of getting an inbox to zero ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata and sender behaviour. Revealing even without bodies. DATABASE_URL=postgres://localhost:5432/triage # Required. Present as a constant. Sorting a mailbox does not require reading it. STORE_MESSAGE_BODIES=never # Required. High. Uncertain means leave it in the inbox. FILE_CONFIDENCE_THRESHOLD=0.95 # Required. Short enough that a mistake is caught within a day. DIGEST_SCHEDULE=daily # Required. Present as a constant. Nothing is ever deleted. DELETE_MESSAGES=never # Required. Before reminding about an unanswered sent message. FOLLOWUP_DAYS=4
You are building a lean indie version of SaneBox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # SaneBox · indie build An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits moving messages | sorting a mailbox does not require reading it | | Signals | Sender history, your reply behaviour and thread participation | who you actually answer is the strongest signal available | | Classification | A threshold tuned for precision on the leave-in-inbox side | the costs of the two errors are not symmetric | | Reversal | Every move recorded and undoable, with training from the correction | a correction is the most valuable signal you get | | Digest | A scheduled summary of what was filed | a mistake caught within a day is recoverable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with sender aggregation. - Get it: Docker or your package manager. - Verify: psql connects and aggregates two hundred thousand rows quickly - [ ] **A mail account with years of history** · free - Why: Sender profiles are built from your own replies over time, and a fresh account contains none of the signal the whole approach depends on. - Get it: Your own account, and read the provider's scope documentation to find the narrowest one that permits moving messages. - Verify: At least three years of history syncs as metadata - [ ] **Five hundred messages you have labelled yourself** · free, and it takes an evening - Why: The two error rates cannot be measured without ground truth, and only you know which of your messages mattered. - Get it: Take a random sample from your own mailbox and label each as should have stayed or could have been filed. Do not use a public dataset. - Verify: Five hundred labelled messages exist with both classes represented - [ ] **Your acceptable error rate on the expensive side** · free - Why: The threshold is set from it, and choosing after seeing the results means choosing whichever produces a flattering figure. - Get it: Decide how often you could tolerate something important being filed, expressed as a rate, before measuring anything. - Verify: A number is written down and the threshold is derived from it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. - push reliability across devices - team routing and shared inbox rules - abuse response and security operations - sender reputation, deliverability, and suppression handling - provider-grade deliverability If one of those is essential to you, that is the reason to keep paying for SaneBox, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SaneBox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me email triage that moves the unimportant out of the way and never hides the important to replace SaneBox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The asymmetry here is the whole design. Leaving an unimportant message in the inbox costs a second of attention. Filing an important one out of sight can cost a job offer, a deadline or a relationship, and the person will not know it happened. So the classifier is tuned deliberately toward leaving things in the inbox, every decision is reversible, nothing is ever deleted, and a digest of what was filed arrives on a schedule so that a mistake is caught within a day rather than never. The second constraint follows from what this tool sees. Triage needs headers, senders and your own reply behaviour; it does not need message bodies, and holding the full text of somebody's mailbox to sort it is a disproportionate amount of access for the job. Ask for the narrowest scope that works, store metadata, and assert in the suite that no body text is persisted. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits moving messages · sorting a mailbox does not require reading it - Signals: Sender history, your reply behaviour and thread participation · who you actually answer is the strongest signal available - Classification: A threshold tuned for precision on the leave-in-inbox side · the costs of the two errors are not symmetric - Reversal: Every move recorded and undoable, with training from the correction · a correction is the most valuable signal you get - Digest: A scheduled summary of what was filed · a mistake caught within a day is recoverable ### Data model (create this before Phase 1) - accounts(id, provider, address, scopes_granted, connected_at) - senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count) - messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only - decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by) - folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked - digests(id, account_id, period_start, period_end, sent_at, message_count) - A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined. ### Phase 1 · Sorting without reading Build: Connect with the narrowest scope that permits reading headers and moving messages. Sync metadata only. Assert in the suite that no body text is persisted anywhere, including in logs. Trap: Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. Done when: The narrowest scope permitting the operations is requested and documented. No message body is stored anywhere, asserted in the suite including log paths. A mailbox with two hundred thousand messages syncs metadata incrementally and resumably. Revoking access stops sync and leaves prior decisions intact. Do not build yet: No classification, folders, digest or reversal. ### Phase 2 · Signals from what you actually do Build: Build sender profiles from your own behaviour: replied, ignored, archived unread, filed by hand. Weight a reply far above any content signal. Handle a sender you have never seen and a sender whose address rotates. Trap: Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. Done when: Sender profiles derive from your own replies, archives and manual filings over a year of history. A sender you have replied to is never filed automatically. An unknown sender is treated as unknown and left in the inbox rather than filed. A sender rotating addresses within one domain is recognised. Do not build yet: No classification thresholds, folders or digest yet. ### Phase 3 · A threshold tuned for the expensive mistake Build: Classify with a confidence threshold set so that filing requires high confidence and anything uncertain stays in the inbox. Measure both error rates against a hand-labelled set. Report them separately rather than as one accuracy figure. Trap: Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. Done when: Filing requires confidence above a stated threshold and uncertain messages remain in the inbox. Both error rates are measured against a hand-labelled set of at least five hundred messages and reported separately. The threshold can be moved and its effect on both rates is shown. No single accuracy figure is presented. Do not build yet: No folders, digest or reversal yet. ### Phase 4 · Nothing is hidden and nothing is deleted Build: Move filed messages into visible folders rather than hiding them, never delete anything, and send a digest of what was filed on a stated schedule. Make reversing a decision one action that also trains the sender profile. Trap: Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. Done when: Filed messages go to visible folders and nothing is ever deleted. A digest of filed messages sends on the stated schedule and is skimmable in under a minute. Reversing a decision takes one action and updates the sender profile. A message reversed from a folder does not get filed again by the same rule. Do not build yet: No reminders or reporting yet. ### Phase 5 · Reminders and knowing whether it works Build: Follow-up reminders for messages you sent that received no reply. Report the two error rates over time from your own reversals. Support disconnecting cleanly with all messages left where the provider can see them. Trap: Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. Done when: Follow-up reminders fire for sent messages with no reply after a stated period, and are cancelled by a reply. Ongoing error rates are computed from reversals and displayed. Disconnecting leaves every message in a folder visible to the provider rather than in a hidden state. Export contains decisions and sender profiles. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or summarising message content, which would require a far broader permission than sorting needs. - Spam detection, which the provider already does with vastly more signal than you have. - Deleting anything, ever, which is excluded deliberately rather than omitted. - Shared or team triage, which changes both the permission model and whose behaviour the signals describe. ### README must contain - That filing something important is far worse than leaving noise, and every threshold reflects that - That the classifier works from metadata and no message body is stored, asserted in the suite - That whether you have replied to a sender is weighted above every other signal - That nothing is deleted and a digest of what was filed arrives on a short schedule - That the two error rates are reported separately, because a single accuracy figure hides the expensive one ===== AGENTS.md ===== # Agent instructions · SaneBox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits moving messages, Sender history, your reply behaviour and thread participation, A threshold tuned for precision on the leave-in-inbox side, Every move recorded and undoable, with training from the correction, A scheduled summary of what was filed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ===== BUILD_PLAN.md ===== # Build plan · SaneBox An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sorting without reading Triage works from metadata and no message body is stored. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Assert it in the suite, including log paths. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Two hundred thousand messages is a normal mailbox. Files: `src/sync.ts` ### Done when - [ ] The narrowest scope is requested and documented - [ ] No body text is stored, including in logs - [ ] A large mailbox syncs incrementally - [ ] Revoking access leaves prior decisions intact ### Watch out - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. ## Phase 2 · Signals from what you actually do The strongest signal is whether you reply, and it is used first. ### Steps 1. Weight your replies above everything It is the only signal that is genuinely about you. Files: `src/signals.ts` 2. Treat unknown as unknown Not as unimportant. The default must be to leave it alone. Files: `src/signals.ts` 3. Recognise rotating addresses within a domain Newsletters rotate constantly and fragment otherwise. Files: `src/senders.ts` ### Done when - [ ] Profiles derive from your own behaviour - [ ] A sender you have replied to is never auto-filed - [ ] Unknown senders stay in the inbox - [ ] Rotating addresses in one domain are recognised ### Watch out - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. ## Phase 3 · A threshold tuned for the expensive mistake The classifier is deliberately reluctant to file anything. ### Steps 1. Set the threshold for precision on filing Uncertain means leave it alone. Files: `src/classify.ts` 2. Hand label a real evaluation set Five hundred messages you judged yourself. There is no substitute. Files: `eval/labelled.json` 3. Report the two error rates separately A single accuracy figure hides the expensive one. Files: `src/evaluate.ts`, `tests/error-rates.test.ts` ### Done when - [ ] Filing requires confidence above the threshold - [ ] Both error rates are measured on a hand-labelled set - [ ] Threshold changes show their effect on both rates - [ ] No single accuracy figure is presented ### Watch out - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. ## Phase 4 · Nothing is hidden and nothing is deleted Filed messages are visible, reversible and summarised on a schedule. ### Steps 1. File to visible folders, delete nothing A folder nobody opens is a deletion with extra steps. Files: `src/folders.ts` 2. Digest on a short schedule A mistake caught within a day is recoverable. Files: `src/digest.ts` 3. Make reversal one action that trains The correction is the most valuable signal you will get. Files: `src/reversal.ts` ### Done when - [ ] Nothing is ever deleted - [ ] The digest is skimmable in under a minute - [ ] Reversal is one action and updates the profile - [ ] A reversed message is not refiled by the same rule ### Watch out - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ## Not in this build - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. ## After v1, if you want it - Per-sender rules you set explicitly, which beat any classifier for the cases you care about - A weekly view of who you never reply to, which is genuinely useful and slightly uncomfortable - Snoozing a message until a stated time, which is the other half of getting an inbox to zero ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata and sender behaviour. Revealing even without bodies. DATABASE_URL=postgres://localhost:5432/triage # Required. Present as a constant. Sorting a mailbox does not require reading it. STORE_MESSAGE_BODIES=never # Required. High. Uncertain means leave it in the inbox. FILE_CONFIDENCE_THRESHOLD=0.95 # Required. Short enough that a mistake is caught within a day. DIGEST_SCHEDULE=daily # Required. Present as a constant. Nothing is ever deleted. DELETE_MESSAGES=never # Required. Before reminding about an unanswered sent message. FOLLOWUP_DAYS=4
You are building a production product version of SaneBox. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # SaneBox · product brief ## Problem The visible email triage loop is buildable, but a credible replacement needs more than the first screen. SaneBox earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A quieter inbox where everything filed is one folder away and summarised daily, nothing is ever deleted, and the classifier reports how often it got the expensive error wrong. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - IMAP or Gmail OAuth credentials where supported - secure local keychain access ## Explicit non-goals for v1 - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. - push reliability across devices - team routing and shared inbox rules - abuse response and security operations - sender reputation, deliverability, and suppression handling - provider-grade deliverability ## Success criteria - No body text is confirmed stored, including in logs - A sender you have replied to is confirmed never auto-filed - Both error rates are confirmed reported separately - Nothing is confirmed ever deleted - Reversal is confirmed to update the sender profile ===== BRIEF.md ===== # Build brief · SaneBox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me email triage that moves the unimportant out of the way and never hides the important to replace SaneBox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The asymmetry here is the whole design. Leaving an unimportant message in the inbox costs a second of attention. Filing an important one out of sight can cost a job offer, a deadline or a relationship, and the person will not know it happened. So the classifier is tuned deliberately toward leaving things in the inbox, every decision is reversible, nothing is ever deleted, and a digest of what was filed arrives on a schedule so that a mistake is caught within a day rather than never. The second constraint follows from what this tool sees. Triage needs headers, senders and your own reply behaviour; it does not need message bodies, and holding the full text of somebody's mailbox to sort it is a disproportionate amount of access for the job. Ask for the narrowest scope that works, store metadata, and assert in the suite that no body text is persisted. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits moving messages · sorting a mailbox does not require reading it - Signals: Sender history, your reply behaviour and thread participation · who you actually answer is the strongest signal available - Classification: A threshold tuned for precision on the leave-in-inbox side · the costs of the two errors are not symmetric - Reversal: Every move recorded and undoable, with training from the correction · a correction is the most valuable signal you get - Digest: A scheduled summary of what was filed · a mistake caught within a day is recoverable ### Data model (create this before Phase 1) - accounts(id, provider, address, scopes_granted, connected_at) - senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count) - messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only - decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by) - folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked - digests(id, account_id, period_start, period_end, sent_at, message_count) - A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined. ### Phase 1 · Sorting without reading Build: Connect with the narrowest scope that permits reading headers and moving messages. Sync metadata only. Assert in the suite that no body text is persisted anywhere, including in logs. Trap: Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. Done when: The narrowest scope permitting the operations is requested and documented. No message body is stored anywhere, asserted in the suite including log paths. A mailbox with two hundred thousand messages syncs metadata incrementally and resumably. Revoking access stops sync and leaves prior decisions intact. Do not build yet: No classification, folders, digest or reversal. ### Phase 2 · Signals from what you actually do Build: Build sender profiles from your own behaviour: replied, ignored, archived unread, filed by hand. Weight a reply far above any content signal. Handle a sender you have never seen and a sender whose address rotates. Trap: Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. Done when: Sender profiles derive from your own replies, archives and manual filings over a year of history. A sender you have replied to is never filed automatically. An unknown sender is treated as unknown and left in the inbox rather than filed. A sender rotating addresses within one domain is recognised. Do not build yet: No classification thresholds, folders or digest yet. ### Phase 3 · A threshold tuned for the expensive mistake Build: Classify with a confidence threshold set so that filing requires high confidence and anything uncertain stays in the inbox. Measure both error rates against a hand-labelled set. Report them separately rather than as one accuracy figure. Trap: Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. Done when: Filing requires confidence above a stated threshold and uncertain messages remain in the inbox. Both error rates are measured against a hand-labelled set of at least five hundred messages and reported separately. The threshold can be moved and its effect on both rates is shown. No single accuracy figure is presented. Do not build yet: No folders, digest or reversal yet. ### Phase 4 · Nothing is hidden and nothing is deleted Build: Move filed messages into visible folders rather than hiding them, never delete anything, and send a digest of what was filed on a stated schedule. Make reversing a decision one action that also trains the sender profile. Trap: Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. Done when: Filed messages go to visible folders and nothing is ever deleted. A digest of filed messages sends on the stated schedule and is skimmable in under a minute. Reversing a decision takes one action and updates the sender profile. A message reversed from a folder does not get filed again by the same rule. Do not build yet: No reminders or reporting yet. ### Phase 5 · Reminders and knowing whether it works Build: Follow-up reminders for messages you sent that received no reply. Report the two error rates over time from your own reversals. Support disconnecting cleanly with all messages left where the provider can see them. Trap: Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. Done when: Follow-up reminders fire for sent messages with no reply after a stated period, and are cancelled by a reply. Ongoing error rates are computed from reversals and displayed. Disconnecting leaves every message in a folder visible to the provider rather than in a hidden state. Export contains decisions and sender profiles. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or summarising message content, which would require a far broader permission than sorting needs. - Spam detection, which the provider already does with vastly more signal than you have. - Deleting anything, ever, which is excluded deliberately rather than omitted. - Shared or team triage, which changes both the permission model and whose behaviour the signals describe. ### README must contain - That filing something important is far worse than leaving noise, and every threshold reflects that - That the classifier works from metadata and no message body is stored, asserted in the suite - That whether you have replied to a sender is weighted above every other signal - That nothing is deleted and a digest of what was filed arrives on a short schedule - That the two error rates are reported separately, because a single accuracy figure hides the expensive one ===== ARCHITECTURE.md ===== # Architecture · SaneBox ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits moving messages | sorting a mailbox does not require reading it | | Signals | Sender history, your reply behaviour and thread participation | who you actually answer is the strongest signal available | | Classification | A threshold tuned for precision on the leave-in-inbox side | the costs of the two errors are not symmetric | | Reversal | Every move recorded and undoable, with training from the correction | a correction is the most valuable signal you get | | Digest | A scheduled summary of what was filed | a mistake caught within a day is recoverable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Access | Narrow scopes and metadata-only storage. | Any provider, provided bodies are never stored. | | Signals | Profiles from your own behaviour. | Any implementation, provided replies outweigh content signals. | | Classification | An asymmetric threshold. | Any classifier, provided both error rates are reported separately. | | Reversal | One-action correction that trains. | Any interface. The correction is the most valuable signal available. | | Digest | Scheduled visibility of what was filed. | Any transport, provided the schedule is short enough to catch a mistake. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Message metadata and sender behaviour. Revealing even without bodies. - `STORE_MESSAGE_BODIES` · required · Present as a constant. Sorting a mailbox does not require reading it. - `FILE_CONFIDENCE_THRESHOLD` · required · High. Uncertain means leave it in the inbox. - `DIGEST_SCHEDULE` · required · Short enough that a mistake is caught within a day. - `DELETE_MESSAGES` · required · Present as a constant. Nothing is ever deleted. - `FOLLOWUP_DAYS` · required · Before reminding about an unanswered sent message. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · SaneBox product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits moving messages, Sender history, your reply behaviour and thread participation, A threshold tuned for precision on the leave-in-inbox side, Every move recorded and undoable, with training from the correction, A scheduled summary of what was filed. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. - Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. ===== MILESTONES.md ===== # Delivery milestones · SaneBox Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sorting without reading Triage works from metadata and no message body is stored. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Assert it in the suite, including log paths. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Two hundred thousand messages is a normal mailbox. Files: `src/sync.ts` ### Done when - [ ] The narrowest scope is requested and documented - [ ] No body text is stored, including in logs - [ ] A large mailbox syncs incrementally - [ ] Revoking access leaves prior decisions intact ### Watch out - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. ## M2 · Signals from what you actually do The strongest signal is whether you reply, and it is used first. ### Steps 1. Weight your replies above everything It is the only signal that is genuinely about you. Files: `src/signals.ts` 2. Treat unknown as unknown Not as unimportant. The default must be to leave it alone. Files: `src/signals.ts` 3. Recognise rotating addresses within a domain Newsletters rotate constantly and fragment otherwise. Files: `src/senders.ts` ### Done when - [ ] Profiles derive from your own behaviour - [ ] A sender you have replied to is never auto-filed - [ ] Unknown senders stay in the inbox - [ ] Rotating addresses in one domain are recognised ### Watch out - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. ## M3 · A threshold tuned for the expensive mistake The classifier is deliberately reluctant to file anything. ### Steps 1. Set the threshold for precision on filing Uncertain means leave it alone. Files: `src/classify.ts` 2. Hand label a real evaluation set Five hundred messages you judged yourself. There is no substitute. Files: `eval/labelled.json` 3. Report the two error rates separately A single accuracy figure hides the expensive one. Files: `src/evaluate.ts`, `tests/error-rates.test.ts` ### Done when - [ ] Filing requires confidence above the threshold - [ ] Both error rates are measured on a hand-labelled set - [ ] Threshold changes show their effect on both rates - [ ] No single accuracy figure is presented ### Watch out - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. ## M4 · Nothing is hidden and nothing is deleted Filed messages are visible, reversible and summarised on a schedule. ### Steps 1. File to visible folders, delete nothing A folder nobody opens is a deletion with extra steps. Files: `src/folders.ts` 2. Digest on a short schedule A mistake caught within a day is recoverable. Files: `src/digest.ts` 3. Make reversal one action that trains The correction is the most valuable signal you will get. Files: `src/reversal.ts` ### Done when - [ ] Nothing is ever deleted - [ ] The digest is skimmable in under a minute - [ ] Reversal is one action and updates the profile - [ ] A reversed message is not refiled by the same rule ### Watch out - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ## M5 · Reminders and knowing whether it works (production only) You are reminded about what you sent, and you can see the classifier's record. ### Steps 1. Remind on unanswered sent messages Cancelled by a reply, which requires no body access. Files: `src/reminders.ts` 2. Compute error rates from reversals Free ground truth arriving continuously. Files: `src/metrics.ts` 3. Disconnect leaving everything visible No hidden state that only your application understands. Files: `src/disconnect.ts` ### Done when - [ ] Reminders fire and are cancelled by a reply - [ ] Ongoing error rates come from reversals - [ ] Disconnecting leaves all messages visible - [ ] Export contains decisions and profiles ### Watch out - Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. ===== OPERATIONS.md ===== # Operations · SaneBox ## Backup Sender profiles represent months of accumulated behaviour and are the expensive thing to lose, since they rebuild only as slowly as you reply to people. Message metadata resyncs. ## Restore Restore and confirm sender profiles and reversals survived, since losing reversals means the classifier will repeat every mistake the user already corrected. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The reversal rate is the health metric and it is ground truth arriving free. A rising rate means the classifier has drifted from how your correspondence actually works. ## Incident checklist An important message filed and missed is the failure this whole design is arranged around, and the digest is the control. If the digest is not being read, the schedule or the format is wrong rather than the classifier. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No body text is confirmed stored, including in logs - [ ] A sender you have replied to is confirmed never auto-filed - [ ] Both error rates are confirmed reported separately - [ ] Nothing is confirmed ever deleted - [ ] Reversal is confirmed to update the sender profile ## Launch constraint Do not market omitted SaneBox capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata and sender behaviour. Revealing even without bodies. DATABASE_URL=postgres://localhost:5432/triage # Required. Present as a constant. Sorting a mailbox does not require reading it. STORE_MESSAGE_BODIES=never # Required. High. Uncertain means leave it in the inbox. FILE_CONFIDENCE_THRESHOLD=0.95 # Required. Short enough that a mistake is caught within a day. DIGEST_SCHEDULE=daily # Required. Present as a constant. Nothing is ever deleted. DELETE_MESSAGES=never # Required. Before reminding about an unanswered sent message. FOLLOWUP_DAYS=4
# SaneBox · indie build An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits moving messages | sorting a mailbox does not require reading it | | Signals | Sender history, your reply behaviour and thread participation | who you actually answer is the strongest signal available | | Classification | A threshold tuned for precision on the leave-in-inbox side | the costs of the two errors are not symmetric | | Reversal | Every move recorded and undoable, with training from the correction | a correction is the most valuable signal you get | | Digest | A scheduled summary of what was filed | a mistake caught within a day is recoverable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with sender aggregation. - Get it: Docker or your package manager. - Verify: psql connects and aggregates two hundred thousand rows quickly - [ ] **A mail account with years of history** · free - Why: Sender profiles are built from your own replies over time, and a fresh account contains none of the signal the whole approach depends on. - Get it: Your own account, and read the provider's scope documentation to find the narrowest one that permits moving messages. - Verify: At least three years of history syncs as metadata - [ ] **Five hundred messages you have labelled yourself** · free, and it takes an evening - Why: The two error rates cannot be measured without ground truth, and only you know which of your messages mattered. - Get it: Take a random sample from your own mailbox and label each as should have stayed or could have been filed. Do not use a public dataset. - Verify: Five hundred labelled messages exist with both classes represented - [ ] **Your acceptable error rate on the expensive side** · free - Why: The threshold is set from it, and choosing after seeing the results means choosing whichever produces a flattering figure. - Get it: Decide how often you could tolerate something important being filed, expressed as a rate, before measuring anything. - Verify: A number is written down and the threshold is derived from it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. - push reliability across devices - team routing and shared inbox rules - abuse response and security operations - sender reputation, deliverability, and suppression handling - provider-grade deliverability If one of those is essential to you, that is the reason to keep paying for SaneBox, and the README should say so rather than pretend.
# Build brief · SaneBox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me email triage that moves the unimportant out of the way and never hides the important to replace SaneBox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The asymmetry here is the whole design. Leaving an unimportant message in the inbox costs a second of attention. Filing an important one out of sight can cost a job offer, a deadline or a relationship, and the person will not know it happened. So the classifier is tuned deliberately toward leaving things in the inbox, every decision is reversible, nothing is ever deleted, and a digest of what was filed arrives on a schedule so that a mistake is caught within a day rather than never. The second constraint follows from what this tool sees. Triage needs headers, senders and your own reply behaviour; it does not need message bodies, and holding the full text of somebody's mailbox to sort it is a disproportionate amount of access for the job. Ask for the narrowest scope that works, store metadata, and assert in the suite that no body text is persisted. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits moving messages · sorting a mailbox does not require reading it - Signals: Sender history, your reply behaviour and thread participation · who you actually answer is the strongest signal available - Classification: A threshold tuned for precision on the leave-in-inbox side · the costs of the two errors are not symmetric - Reversal: Every move recorded and undoable, with training from the correction · a correction is the most valuable signal you get - Digest: A scheduled summary of what was filed · a mistake caught within a day is recoverable ### Data model (create this before Phase 1) - accounts(id, provider, address, scopes_granted, connected_at) - senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count) - messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only - decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by) - folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked - digests(id, account_id, period_start, period_end, sent_at, message_count) - A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined. ### Phase 1 · Sorting without reading Build: Connect with the narrowest scope that permits reading headers and moving messages. Sync metadata only. Assert in the suite that no body text is persisted anywhere, including in logs. Trap: Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. Done when: The narrowest scope permitting the operations is requested and documented. No message body is stored anywhere, asserted in the suite including log paths. A mailbox with two hundred thousand messages syncs metadata incrementally and resumably. Revoking access stops sync and leaves prior decisions intact. Do not build yet: No classification, folders, digest or reversal. ### Phase 2 · Signals from what you actually do Build: Build sender profiles from your own behaviour: replied, ignored, archived unread, filed by hand. Weight a reply far above any content signal. Handle a sender you have never seen and a sender whose address rotates. Trap: Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. Done when: Sender profiles derive from your own replies, archives and manual filings over a year of history. A sender you have replied to is never filed automatically. An unknown sender is treated as unknown and left in the inbox rather than filed. A sender rotating addresses within one domain is recognised. Do not build yet: No classification thresholds, folders or digest yet. ### Phase 3 · A threshold tuned for the expensive mistake Build: Classify with a confidence threshold set so that filing requires high confidence and anything uncertain stays in the inbox. Measure both error rates against a hand-labelled set. Report them separately rather than as one accuracy figure. Trap: Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. Done when: Filing requires confidence above a stated threshold and uncertain messages remain in the inbox. Both error rates are measured against a hand-labelled set of at least five hundred messages and reported separately. The threshold can be moved and its effect on both rates is shown. No single accuracy figure is presented. Do not build yet: No folders, digest or reversal yet. ### Phase 4 · Nothing is hidden and nothing is deleted Build: Move filed messages into visible folders rather than hiding them, never delete anything, and send a digest of what was filed on a stated schedule. Make reversing a decision one action that also trains the sender profile. Trap: Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. Done when: Filed messages go to visible folders and nothing is ever deleted. A digest of filed messages sends on the stated schedule and is skimmable in under a minute. Reversing a decision takes one action and updates the sender profile. A message reversed from a folder does not get filed again by the same rule. Do not build yet: No reminders or reporting yet. ### Phase 5 · Reminders and knowing whether it works Build: Follow-up reminders for messages you sent that received no reply. Report the two error rates over time from your own reversals. Support disconnecting cleanly with all messages left where the provider can see them. Trap: Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. Done when: Follow-up reminders fire for sent messages with no reply after a stated period, and are cancelled by a reply. Ongoing error rates are computed from reversals and displayed. Disconnecting leaves every message in a folder visible to the provider rather than in a hidden state. Export contains decisions and sender profiles. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or summarising message content, which would require a far broader permission than sorting needs. - Spam detection, which the provider already does with vastly more signal than you have. - Deleting anything, ever, which is excluded deliberately rather than omitted. - Shared or team triage, which changes both the permission model and whose behaviour the signals describe. ### README must contain - That filing something important is far worse than leaving noise, and every threshold reflects that - That the classifier works from metadata and no message body is stored, asserted in the suite - That whether you have replied to a sender is weighted above every other signal - That nothing is deleted and a digest of what was filed arrives on a short schedule - That the two error rates are reported separately, because a single accuracy figure hides the expensive one
# Agent instructions · SaneBox indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits moving messages, Sender history, your reply behaviour and thread participation, A threshold tuned for precision on the leave-in-inbox side, Every move recorded and undoable, with training from the correction, A scheduled summary of what was filed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete.
# Build plan · SaneBox An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sorting without reading Triage works from metadata and no message body is stored. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Assert it in the suite, including log paths. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Two hundred thousand messages is a normal mailbox. Files: `src/sync.ts` ### Done when - [ ] The narrowest scope is requested and documented - [ ] No body text is stored, including in logs - [ ] A large mailbox syncs incrementally - [ ] Revoking access leaves prior decisions intact ### Watch out - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. ## Phase 2 · Signals from what you actually do The strongest signal is whether you reply, and it is used first. ### Steps 1. Weight your replies above everything It is the only signal that is genuinely about you. Files: `src/signals.ts` 2. Treat unknown as unknown Not as unimportant. The default must be to leave it alone. Files: `src/signals.ts` 3. Recognise rotating addresses within a domain Newsletters rotate constantly and fragment otherwise. Files: `src/senders.ts` ### Done when - [ ] Profiles derive from your own behaviour - [ ] A sender you have replied to is never auto-filed - [ ] Unknown senders stay in the inbox - [ ] Rotating addresses in one domain are recognised ### Watch out - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. ## Phase 3 · A threshold tuned for the expensive mistake The classifier is deliberately reluctant to file anything. ### Steps 1. Set the threshold for precision on filing Uncertain means leave it alone. Files: `src/classify.ts` 2. Hand label a real evaluation set Five hundred messages you judged yourself. There is no substitute. Files: `eval/labelled.json` 3. Report the two error rates separately A single accuracy figure hides the expensive one. Files: `src/evaluate.ts`, `tests/error-rates.test.ts` ### Done when - [ ] Filing requires confidence above the threshold - [ ] Both error rates are measured on a hand-labelled set - [ ] Threshold changes show their effect on both rates - [ ] No single accuracy figure is presented ### Watch out - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. ## Phase 4 · Nothing is hidden and nothing is deleted Filed messages are visible, reversible and summarised on a schedule. ### Steps 1. File to visible folders, delete nothing A folder nobody opens is a deletion with extra steps. Files: `src/folders.ts` 2. Digest on a short schedule A mistake caught within a day is recoverable. Files: `src/digest.ts` 3. Make reversal one action that trains The correction is the most valuable signal you will get. Files: `src/reversal.ts` ### Done when - [ ] Nothing is ever deleted - [ ] The digest is skimmable in under a minute - [ ] Reversal is one action and updates the profile - [ ] A reversed message is not refiled by the same rule ### Watch out - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ## Not in this build - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. ## After v1, if you want it - Per-sender rules you set explicitly, which beat any classifier for the cases you care about - A weekly view of who you never reply to, which is genuinely useful and slightly uncomfortable - Snoozing a message until a stated time, which is the other half of getting an inbox to zero
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata and sender behaviour. Revealing even without bodies. DATABASE_URL=postgres://localhost:5432/triage # Required. Present as a constant. Sorting a mailbox does not require reading it. STORE_MESSAGE_BODIES=never # Required. High. Uncertain means leave it in the inbox. FILE_CONFIDENCE_THRESHOLD=0.95 # Required. Short enough that a mistake is caught within a day. DIGEST_SCHEDULE=daily # Required. Present as a constant. Nothing is ever deleted. DELETE_MESSAGES=never # Required. Before reminding about an unanswered sent message. FOLLOWUP_DAYS=4
# SaneBox · product brief ## Problem The visible email triage loop is buildable, but a credible replacement needs more than the first screen. SaneBox earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A quieter inbox where everything filed is one folder away and summarised daily, nothing is ever deleted, and the classifier reports how often it got the expensive error wrong. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - IMAP or Gmail OAuth credentials where supported - secure local keychain access ## Explicit non-goals for v1 - Reading or summarising message content. - Spam detection. - Deleting anything. - Shared or team triage. - push reliability across devices - team routing and shared inbox rules - abuse response and security operations - sender reputation, deliverability, and suppression handling - provider-grade deliverability ## Success criteria - No body text is confirmed stored, including in logs - A sender you have replied to is confirmed never auto-filed - Both error rates are confirmed reported separately - Nothing is confirmed ever deleted - Reversal is confirmed to update the sender profile
# Build brief · SaneBox The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me email triage that moves the unimportant out of the way and never hides the important to replace SaneBox. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The asymmetry here is the whole design. Leaving an unimportant message in the inbox costs a second of attention. Filing an important one out of sight can cost a job offer, a deadline or a relationship, and the person will not know it happened. So the classifier is tuned deliberately toward leaving things in the inbox, every decision is reversible, nothing is ever deleted, and a digest of what was filed arrives on a schedule so that a mistake is caught within a day rather than never. The second constraint follows from what this tool sees. Triage needs headers, senders and your own reply behaviour; it does not need message bodies, and holding the full text of somebody's mailbox to sort it is a disproportionate amount of access for the job. Ask for the narrowest scope that works, store metadata, and assert in the suite that no body text is persisted. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits moving messages · sorting a mailbox does not require reading it - Signals: Sender history, your reply behaviour and thread participation · who you actually answer is the strongest signal available - Classification: A threshold tuned for precision on the leave-in-inbox side · the costs of the two errors are not symmetric - Reversal: Every move recorded and undoable, with training from the correction · a correction is the most valuable signal you get - Digest: A scheduled summary of what was filed · a mistake caught within a day is recoverable ### Data model (create this before Phase 1) - accounts(id, provider, address, scopes_granted, connected_at) - senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count) - messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only - decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by) - folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked - digests(id, account_id, period_start, period_end, sent_at, message_count) - A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined. ### Phase 1 · Sorting without reading Build: Connect with the narrowest scope that permits reading headers and moving messages. Sync metadata only. Assert in the suite that no body text is persisted anywhere, including in logs. Trap: Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. Done when: The narrowest scope permitting the operations is requested and documented. No message body is stored anywhere, asserted in the suite including log paths. A mailbox with two hundred thousand messages syncs metadata incrementally and resumably. Revoking access stops sync and leaves prior decisions intact. Do not build yet: No classification, folders, digest or reversal. ### Phase 2 · Signals from what you actually do Build: Build sender profiles from your own behaviour: replied, ignored, archived unread, filed by hand. Weight a reply far above any content signal. Handle a sender you have never seen and a sender whose address rotates. Trap: Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. Done when: Sender profiles derive from your own replies, archives and manual filings over a year of history. A sender you have replied to is never filed automatically. An unknown sender is treated as unknown and left in the inbox rather than filed. A sender rotating addresses within one domain is recognised. Do not build yet: No classification thresholds, folders or digest yet. ### Phase 3 · A threshold tuned for the expensive mistake Build: Classify with a confidence threshold set so that filing requires high confidence and anything uncertain stays in the inbox. Measure both error rates against a hand-labelled set. Report them separately rather than as one accuracy figure. Trap: Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. Done when: Filing requires confidence above a stated threshold and uncertain messages remain in the inbox. Both error rates are measured against a hand-labelled set of at least five hundred messages and reported separately. The threshold can be moved and its effect on both rates is shown. No single accuracy figure is presented. Do not build yet: No folders, digest or reversal yet. ### Phase 4 · Nothing is hidden and nothing is deleted Build: Move filed messages into visible folders rather than hiding them, never delete anything, and send a digest of what was filed on a stated schedule. Make reversing a decision one action that also trains the sender profile. Trap: Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. Done when: Filed messages go to visible folders and nothing is ever deleted. A digest of filed messages sends on the stated schedule and is skimmable in under a minute. Reversing a decision takes one action and updates the sender profile. A message reversed from a folder does not get filed again by the same rule. Do not build yet: No reminders or reporting yet. ### Phase 5 · Reminders and knowing whether it works Build: Follow-up reminders for messages you sent that received no reply. Report the two error rates over time from your own reversals. Support disconnecting cleanly with all messages left where the provider can see them. Trap: Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them. Done when: Follow-up reminders fire for sent messages with no reply after a stated period, and are cancelled by a reply. Ongoing error rates are computed from reversals and displayed. Disconnecting leaves every message in a folder visible to the provider rather than in a hidden state. Export contains decisions and sender profiles. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or summarising message content, which would require a far broader permission than sorting needs. - Spam detection, which the provider already does with vastly more signal than you have. - Deleting anything, ever, which is excluded deliberately rather than omitted. - Shared or team triage, which changes both the permission model and whose behaviour the signals describe. ### README must contain - That filing something important is far worse than leaving noise, and every threshold reflects that - That the classifier works from metadata and no message body is stored, asserted in the suite - That whether you have replied to a sender is weighted above every other signal - That nothing is deleted and a digest of what was filed arrives on a short schedule - That the two error rates are reported separately, because a single accuracy figure hides the expensive one
# Architecture · SaneBox ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits moving messages | sorting a mailbox does not require reading it | | Signals | Sender history, your reply behaviour and thread participation | who you actually answer is the strongest signal available | | Classification | A threshold tuned for precision on the leave-in-inbox side | the costs of the two errors are not symmetric | | Reversal | Every move recorded and undoable, with training from the correction | a correction is the most valuable signal you get | | Digest | A scheduled summary of what was filed | a mistake caught within a day is recoverable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Access | Narrow scopes and metadata-only storage. | Any provider, provided bodies are never stored. | | Signals | Profiles from your own behaviour. | Any implementation, provided replies outweigh content signals. | | Classification | An asymmetric threshold. | Any classifier, provided both error rates are reported separately. | | Reversal | One-action correction that trains. | Any interface. The correction is the most valuable signal available. | | Digest | Scheduled visibility of what was filed. | Any transport, provided the schedule is short enough to catch a mistake. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Message metadata and sender behaviour. Revealing even without bodies. - `STORE_MESSAGE_BODIES` · required · Present as a constant. Sorting a mailbox does not require reading it. - `FILE_CONFIDENCE_THRESHOLD` · required · High. Uncertain means leave it in the inbox. - `DIGEST_SCHEDULE` · required · Short enough that a mistake is caught within a day. - `DELETE_MESSAGES` · required · Present as a constant. Nothing is ever deleted. - `FOLLOWUP_DAYS` · required · Before reminding about an unanswered sent message. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · SaneBox product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits moving messages, Sender history, your reply behaviour and thread participation, A threshold tuned for precision on the leave-in-inbox side, Every move recorded and undoable, with training from the correction, A scheduled summary of what was filed. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. - Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them.
# Delivery milestones · SaneBox Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sorting without reading Triage works from metadata and no message body is stored. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Assert it in the suite, including log paths. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Two hundred thousand messages is a normal mailbox. Files: `src/sync.ts` ### Done when - [ ] The narrowest scope is requested and documented - [ ] No body text is stored, including in logs - [ ] A large mailbox syncs incrementally - [ ] Revoking access leaves prior decisions intact ### Watch out - Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only. ## M2 · Signals from what you actually do The strongest signal is whether you reply, and it is used first. ### Steps 1. Weight your replies above everything It is the only signal that is genuinely about you. Files: `src/signals.ts` 2. Treat unknown as unknown Not as unimportant. The default must be to leave it alone. Files: `src/signals.ts` 3. Recognise rotating addresses within a domain Newsletters rotate constantly and fragment otherwise. Files: `src/senders.ts` ### Done when - [ ] Profiles derive from your own behaviour - [ ] A sender you have replied to is never auto-filed - [ ] Unknown senders stay in the inbox - [ ] Rotating addresses in one domain are recognised ### Watch out - Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant. ## M3 · A threshold tuned for the expensive mistake The classifier is deliberately reluctant to file anything. ### Steps 1. Set the threshold for precision on filing Uncertain means leave it alone. Files: `src/classify.ts` 2. Hand label a real evaluation set Five hundred messages you judged yourself. There is no substitute. Files: `eval/labelled.json` 3. Report the two error rates separately A single accuracy figure hides the expensive one. Files: `src/evaluate.ts`, `tests/error-rates.test.ts` ### Done when - [ ] Filing requires confidence above the threshold - [ ] Both error rates are measured on a hand-labelled set - [ ] Threshold changes show their effect on both rates - [ ] No single accuracy figure is presented ### Watch out - Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately. ## M4 · Nothing is hidden and nothing is deleted Filed messages are visible, reversible and summarised on a schedule. ### Steps 1. File to visible folders, delete nothing A folder nobody opens is a deletion with extra steps. Files: `src/folders.ts` 2. Digest on a short schedule A mistake caught within a day is recoverable. Files: `src/digest.ts` 3. Make reversal one action that trains The correction is the most valuable signal you will get. Files: `src/reversal.ts` ### Done when - [ ] Nothing is ever deleted - [ ] The digest is skimmable in under a minute - [ ] Reversal is one action and updates the profile - [ ] A reversed message is not refiled by the same rule ### Watch out - Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete. ## M5 · Reminders and knowing whether it works (production only) You are reminded about what you sent, and you can see the classifier's record. ### Steps 1. Remind on unanswered sent messages Cancelled by a reply, which requires no body access. Files: `src/reminders.ts` 2. Compute error rates from reversals Free ground truth arriving continuously. Files: `src/metrics.ts` 3. Disconnect leaving everything visible No hidden state that only your application understands. Files: `src/disconnect.ts` ### Done when - [ ] Reminders fire and are cancelled by a reply - [ ] Ongoing error rates come from reversals - [ ] Disconnecting leaves all messages visible - [ ] Export contains decisions and profiles ### Watch out - Measuring accuracy only against the original labelled set means the reported figures stop reflecting reality as your correspondence changes, while your own reversals are a continuous stream of ground truth arriving for free. Compute the ongoing error rates from reversals and show them.
# Operations · SaneBox ## Backup Sender profiles represent months of accumulated behaviour and are the expensive thing to lose, since they rebuild only as slowly as you reply to people. Message metadata resyncs. ## Restore Restore and confirm sender profiles and reversals survived, since losing reversals means the classifier will repeat every mistake the user already corrected. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The reversal rate is the health metric and it is ground truth arriving free. A rising rate means the classifier has drifted from how your correspondence actually works. ## Incident checklist An important message filed and missed is the failure this whole design is arranged around, and the digest is the control. If the digest is not being read, the schedule or the format is wrong rather than the classifier. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No body text is confirmed stored, including in logs - [ ] A sender you have replied to is confirmed never auto-filed - [ ] Both error rates are confirmed reported separately - [ ] Nothing is confirmed ever deleted - [ ] Reversal is confirmed to update the sender profile ## Launch constraint Do not market omitted SaneBox capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata and sender behaviour. Revealing even without bodies. DATABASE_URL=postgres://localhost:5432/triage # Required. Present as a constant. Sorting a mailbox does not require reading it. STORE_MESSAGE_BODIES=never # Required. High. Uncertain means leave it in the inbox. FILE_CONFIDENCE_THRESHOLD=0.95 # Required. Short enough that a mistake is caught within a day. DIGEST_SCHEDULE=daily # Required. Present as a constant. Nothing is ever deleted. DELETE_MESSAGES=never # Required. Before reminding about an unanswered sent message. FOLLOWUP_DAYS=4
$ choose a build depth, inspect the files, then open the complete pack in your agent
SaneBox: Email is infrastructure. Customers pay to avoid lost messages, broken auth, spam problems, and the reputational cost of a flaky inbox.
xpush reliability across devices
xteam routing and shared inbox rules
xabuse response and security operations
xsender reputation, deliverability, and suppression handling
xprovider-grade deliverability
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
SaneBox pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| snack | $7 | $4.92 | 1 email account; choose any 2 SaneBox features and swap those selections at any time. |
| lunch | $12 | $8.25 | Up to 2 email accounts; choose any 6 SaneBox features and swap those selections at any time. |
| dinner | $36 | $24.92 | Up to 4 email accounts; access to all SaneBox features. |
free tierno free tier; 7-day full-feature trial
billingmonthly + annual + 2-year prepaid; plans auto-renew
hidden costsLower tiers cap both connected mailboxes and selectable features; more than 4 email accounts requires contacting support. The lowest advertised rates require a 24-month prepayment ($109/$179/$529).
verified 2026-08-12 · source ↗
Vibecode SaneBox
Kinda. The core of SaneBox is buildable in a weekend with the prompt on this page, but there are real gaps: push reliability across devices, team routing and shared inbox rules. Read the honest list above before committing.
How much does SaneBox cost?
SaneBox's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing SaneBox?
Honestly: push reliability across devices; team routing and shared inbox rules; abuse response and security operations; sender reputation, deliverability, and suppression handling; provider-grade deliverability. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to SaneBox?
Yes: Inbox Zero (Server-side AI rules, follow-up tracking and bulk cleanup for Gmail or Outlook; self-hosting starts with OAuth, Docker and a model.) The prompt is for when you want it exactly your way.