# BUILD 01 — personal inbox brief. Ship it.

Write working code. No specification documents, no architecture essays. If you are
about to write a .md file that isn't the runbook, stop and write Python instead.

## What exists when you're done

Once a day my phone gets a Telegram message telling me which personal emails have a
date, money, or a legal/security keyword in them. If it stops running, something tells
me it stopped.

That's it. Nothing else in this build.

## Where it runs

Locally, on Windows 11, in this directory. Python 3, standard library plus `requests`.
No frameworks, no database, no web server, no daemon, no Docker.

Scheduling is Windows Task Scheduler, set up at the end. Until then everything runs
from the command line so I can watch it work.

## Secrets

Read from `.env` in this directory. Add `.env` to `.gitignore` before writing anything
else. Never log a value, never write one into any output file, never echo one to the
console.

```
PCOS_IMAP_HOST=imap.gmail.com
PCOS_IMAP_USER=<personal gmail address>
PCOS_IMAP_APP_PASSWORD=<16-char Google app password>
PCOS_TG_BOT_TOKEN=<telegram bot token>
PCOS_TG_CHAT_ID=<my own chat id>
```

Write a `.env.example` with these keys and empty values. If a key is missing at
runtime: print which one, send nothing, exit non-zero. Never invent a default.

## Hard constraints — enforced by absence, not policy

- **No SMTP. No `smtplib` anywhere in the tree.** The only outbound calls permitted are
  IMAP to `PCOS_IMAP_HOST` and HTTPS to `api.telegram.org`.
- **Telegram sends to `PCOS_TG_CHAT_ID` only.** Read once at startup, used as the sole
  destination. No function accepts a destination argument from anywhere else.
- **IMAP is read-only.** Use `EXAMINE`, never `SELECT` read-write. Never issue `STORE`,
  `COPY`, `MOVE`, `EXPUNGE`, `APPEND`, or `CLOSE`. Never mark anything read. Never
  touch a flag.
- **No attachment bodies.** List filenames from the message structure; never fetch a
  part body.
- Nothing is deleted, archived, or moved. There is no code that could.

## Build in this order. Do not reorder.

### Step 1 — make the phone buzz

`notify.py` — one function `send(text)` posting to the Telegram sendMessage API with
the bot token and the hardcoded chat id. Two retries on network error, then log and
give up.

`brief.py` — build a hardcoded three-line message, call `send()`.

Run it. **Do not go to step 2 until a real message has landed on a real phone.**
This step is the whole point; everything after it is just making the message true.

### Step 2 — collect

`collect.py`:

- IMAP over TLS, log in, `EXAMINE INBOX`.
- Cursor at `state/cursor.json`: `{uidvalidity, last_uid, last_run_utc}`.
- No cursor → collect the last 7 days, write one.
- `uidvalidity` changed → collect the last 7 days, set a flag so the brief says
  coverage was re-baselined.
- Otherwise fetch UIDs above `last_uid`.
- **Also re-search the last 48 hours every run** and merge, deduplicating on
  `Message-ID`. Keep this — it is the cheap half of catching missed mail.
- Per message pull: from, to, subject, date, plain-text body (convert HTML to text in
  memory, never load remote content).
- Write one JSON object per message to `data/messages/YYYY-MM-DD.jsonl` with a sha256
  of the body.
- Advance the cursor only after the file is written and read back successfully.

**Work exclusion:** drop any message whose sender, recipient, or reply-to matches an
entry in `config/work-domains.txt`. Count them. The brief reports the count only —
never a subject, never a sender.

### Step 3 — rules

`rules.py`, reading `config/rules.toml`. Three buckets, evaluated top down. Collect all
matching reasons, highest bucket wins.

**ACT** — any of:
- a date within the next 30 days in subject or body
- payment, overdue, invoice, arrears, suspended, declined, failed payment, final notice
- visa, immigration, passport, residency, citizenship, home affairs, solicitor, court,
  fine, tribunal, notice
- password, sign-in, security alert, suspicious, verify your account, new device
- sender listed in `config/critical-senders.txt`

**REVIEW** — any of:
- a date that is ambiguous, or further than 30 days out
- renewal, statement, policy, appointment, booking, confirmation, receipt
- message has an attachment
- a question mark in a body under 500 characters from a human-looking sender

**NOTE** — everything else.

**Dates are the part most likely to be wrong.** Extract date-shaped strings. Normalise
only when unambiguous (ISO, or a written month like "18 April"). **If it could be
day/month or month/day, do not guess** — keep the original text and drop the item to
REVIEW.

Write to `data/findings/YYYY-MM-DD.jsonl`: message id, bucket, reasons, normalised date
if any, original date text, subject, sender.

### Step 4 — the deadline register

`register.py` — append every ACT or REVIEW finding carrying a date to
`data/deadlines.jsonl`: date, what, source subject, source sender, first seen, last
seen.

Append only. Never rewrite a line. Same deadline seen again → append a new line with an
updated `last_seen`.

**This file is the point of the system.** A deadline seen in August must still surface
in October. The brief reads it for anything due in the next 30 days, regardless of when
the mail arrived.

### Step 5 — the real brief

Replace the hardcoded message. Plain text, under 20 lines:

```
PERSONAL — Wed 29 Jul

ACT
- Electricity payment failed, due 31 Jul
  Example Energy, 16:42
- Visa document request, respond by 12 Aug
  Home Affairs, 09:14

COMING UP
- 04 Aug  car rego renewal

REVIEW  3 items
NEW     7 others, nothing found

Covered to 18:30. 2 work emails excluded.
```

- Nothing in ACT and nothing due → say so in two lines and stop.
- Never pad. "Nothing needing you" is a correct and complete brief.
- No score, no rating, no encouragement, no comment about me.
- Always include the coverage line, even when everything else is empty.

### Step 6 — the heartbeat

`heartbeat.py` — reads `state/last_brief.json`, written by `brief.py` after every
successful send. If the last success is more than 26 hours old, send a Telegram message
saying so and naming the last successful time.

It must not import `brief.py` or `collect.py`. It reads a file and calls `notify.send()`.
If the brief is broken, the heartbeat still works.

### Step 7 — voice drop

Create `inbox-voice/`. `brief.py` counts files in it and adds one line when the count is
above zero: `Voice: 3 captures waiting.` Nothing processes them yet. The folder existing
and being counted is the whole feature.

### Step 8 — schedule it

Two Windows Task Scheduler entries, created by a PowerShell script in `bin/`:
- `brief.py` daily at 18:30
- `heartbeat.py` daily at 20:00, separate task

Both run whether or not I'm logged in, and don't wake the machine.

## Not in this build

No verifier process. No model calls of any kind. No web server. No dashboard. No
search. No approval queue. No drafts. No memory ledger. No self-evaluation framework.
No manifest hashing. No multi-account support. No calendar. No finance. No Docker.

If you find yourself building any of the above, you have gone outside the build.

## Done means

1. A real Telegram message arrived on a real phone from a scheduled run I didn't trigger.
2. Skipping the brief for a day produces a heartbeat warning the next evening.
3. A message with a clear deadline lands in ACT with the date extracted.
4. A message with an ambiguous date lands in REVIEW with the original text intact.
5. A work-domain message is excluded and counted, with no subject leaked.
6. `grep -r smtplib` over the tree returns nothing.
7. Running collection twice produces no duplicate findings.
8. A deadline seen today still surfaces on its date weeks later.

## Also produce

`RUNBOOK.md` — one page. Install, where secrets go, the two scheduled tasks, how to
check it ran, how to restart it, how to rotate the app password. Nothing else.
