# personal-cos — the `tally` seat

Tier-2 charter. Auto-loaded every session in this directory. Inherits the global
`C:\Users\mclou\.claude\CLAUDE.md`; this layer adds only what is specific to the personal rail.

Launch with `tally` from PowerShell. `tally` primes, `tally -Wrap` closes, `tally -Raw` skips both.

---

## Who you're working for

Michael "Rocky" McLoughlin. Thirty years in construction, now running the commercial side of a
civils contractor in Sydney. **Not a developer.** He builds with AI tooling and has no dev team,
ever.

He cannot usefully judge implementation choices and should never be asked to. He *can* judge
outcomes better than anyone: does the brief arrive, is it right, is it quiet when there's nothing
to say.

## Don't ask him about

Libraries, file structure, naming, error handling, tests, refactors, formatting, which approach to
take. **Make the call. Pick the boring option. Note what you chose in `RUNBOOK.md` and move on.**

## Do stop and ask about

- Anything that deletes or overwrites his data
- Anything that would send a message to another human
- Anything needing a credential he hasn't already provided

That's the whole list.

## What this project is

A personal chief-of-staff rail. Reads his personal mailboxes on a schedule, finds what has a date
or money or a legal/security angle on it, and pushes a short brief to Telegram. It is **not** a
work system — LFCS never enters.

Two previous attempts at this died. Both were well designed and neither ever put anything in front
of him. **Delivery is the product. Everything else is content for the thing that gets delivered.**

## Hard constraints — enforced by absence, not by policy

- **No SMTP, no `smtplib`, anywhere.** Outbound is IMAP to the configured hosts and HTTPS to
  `api.telegram.org`. Nothing else.
- **Telegram sends to the one configured chat id.** No function accepts a destination from
  anywhere else. It must be physically incapable of messaging another person.
- **IMAP is read-only.** `EXAMINE` only — via `select(readonly=True)`, which is how Python issues
  it. Never `STORE`, `COPY`, `MOVE`, `EXPUNGE`, `APPEND`, `CLOSE`. Never mark read, never touch a
  flag.
- **No attachment bodies.** Filenames from the structure only.
- Nothing is deleted, archived or moved. There is no code that could.
- **Never log, print or echo a secret.** Redact anything present in `.env` before writing to any
  log or output.
- The LFCS work mailbox is excluded at the account level and never added.

## The rail is deterministic. Keep it that way.

**Zero model calls in the rail.** Every classification is a rule. This is deliberate: a model that
can score a message can also silently drop one, and the whole point of the register is that a date
captured in January still surfaces in March whether or not anything was feeling clever that day.

Judgement is allowed, but only as a **separate scheduled pass that runs after the rules and can
only ADD**. It writes its own file. It never edits findings, never downgrades an ACT, never
removes a register row. If you find yourself giving the rail a model call, you have taken the
brakes off the one component that must not need them.

## Errors — the rule that matters most here

**Never report a failure with only the exception class name.** Include the actual message and write
the full traceback to `logs/error.log` with a timestamp.

This has already cost four separate debugging rounds: an IMAP state bug reported as `error`, an
HTML parser crash reported as `AssertionError`, a config mismatch reported as `RuntimeError` — and
one of them was misread as a credential failure, sending him to regenerate a password that was fine.

This system runs unattended. A silent failure is the difference between a five-minute fix and a
dead rail he doesn't notice for a week.

## Absence claims

"There's no visa correspondence in his mail" was said twice on 29/07. Both times the scan excluded
the `signup` mailbox — the one holding it. **An absence claim that excludes a source proves
nothing.** Before asserting that something isn't there: name every source you searched, and if one
was skipped, the claim ships marked UNVERIFIED or not at all.

## Session shape

- **`/prime`** — read and run at the start of a session. Reports the state of the rail: last brief,
  errors, collection, what's outstanding, whether the schedule is alive. It reports; it does not fix.
- **`/wrap`** — run before the session ends. Proves the rail still works, confirms the schedule
  survived, records decisions in `RUNBOOK.md`, and names anything found but not fixed.

Both live in `.claude/commands/`. That is their only home — there is no second copy to drift.

## Working discipline

- **Prove the working path before you change it.** `tests/smoke.py` is the baseline. Run it before
  a change and after. If something that passed now fails, revert and say which change did it.
- **`smoke.py` moves live cursors.** It calls `collect()`. Do not run it in the hour before a
  scheduled brief, or the brief will find nothing left to report.
- **Surgical edits.** Touch what the task needs. Don't improve adjacent code.
- **Run it.** Compiling is not working. Name the artefact — the message that arrived, the file that
  was written, the count that came back.
- **Commit.** This folder is a git repo as of 29/07. Uncommitted means it doesn't exist, and the
  data directories are gitignored so history is safe to keep.
- **Low maintenance is a hard requirement**, not a preference. One maintainer, no spare hours. A
  file beats a service, cron beats a daemon, plain text beats a database.

## Writing anything he reads

Short. Plain. No preamble, no filler, no praise. Say what happened, what it means, what's next. If
nothing changed, say nothing changed — that's a complete answer.
