# Personal Inbox Brief Runbook

## Install

From PowerShell in this directory:

```powershell
python -m pip install requests
Copy-Item .env.example .env
```

Edit `.env` locally and fill all five values. `PCOS_IMAP_APP_PASSWORD` is a Google app
password, not the normal account password. `PCOS_TG_CHAT_ID` must be the operator's own
chat ID. Never paste these values into logs, screenshots, or chat.

Add exact work email addresses or domains to `config/work-domains.txt`, one per line.
Add any exact critical personal sender addresses to `config/critical-senders.txt`.
Blank files are valid. Missing secret keys are not: the command names missing keys,
sends nothing, and exits non-zero.

Mailboxes are listed in `config/accounts.toml`; it contains environment-variable names,
never passwords. An account whose named password is absent from `.env` is skipped.
Run a server-side historical search without moving daily cursors with:

```powershell
python collect.py --search "passport" --search "visa" --since 2020-01-01
```

Repeated search terms are ORed. Each account's hit count prints before fetching.

Test interactively:

```powershell
python brief.py
```

Then install the tasks from a normal PowerShell session:

```powershell
Set-ExecutionPolicy -Scope Process Bypass
.\bin\install-tasks.ps1
```

This creates `Personal Inbox Brief` at 18:30 and `Personal Inbox Heartbeat` at 20:00.
They run separately while the operator is logged in. They do not wake the
computer; a missed start runs when Windows is next available.

## Check and restart

In Task Scheduler, open **Task Scheduler Library** and inspect each task's Last Run
Time and Last Run Result. A successful brief also updates `state/last_brief.json`;
email collection advances `state/cursor.json`. Neither file contains secrets.

Run either task immediately:

```powershell
Start-ScheduledTask -TaskName "Personal Inbox Brief"
Start-ScheduledTask -TaskName "Personal Inbox Heartbeat"
```

Or watch the brief directly with `python brief.py`. If collection fails, the cursor
does not advance. Re-running is safe: the 48-hour overlap is deduplicated by
Message-ID, and findings already written for the day are not appended again.

## Rotate the Google app password

Create a replacement app password in the Google account, edit only
`PCOS_IMAP_APP_PASSWORD` in `.env`, save it, and run `python brief.py`. After a real
brief arrives, revoke the old app password. Do not put either password in any other
file. Telegram token rotation is the same pattern: replace `PCOS_TG_BOT_TOKEN` in
`.env`, test, then revoke the old token through BotFather.

Implementation choices: Windows Task Scheduler uses the current user with Interactive
logon so installation does not require elevation or store a Windows password; failure
handlers append timestamped, `.env`-redacted tracebacks to `logs/error.log`; attachment filenames come from IMAP BODYSTRUCTURE while only
non-attachment text MIME parts are fetched.

---

# Where it runs now

**The box owns the brief.** `/root/personal-cos/` on the always-on VPS, systemd timers at
18:30 (brief) and 20:00 (heartbeat), Australia/Sydney. The laptop's "Personal Inbox Brief"
scheduled task is **disabled, not deleted** — reversible in one click if the box has a bad week.

Secrets are a systemd encrypted credential at `secrets/pcos-env.cred`, decryptable only by
that host. There is no `.env` on the box. `notify.env_path()` reads `$PCOS_ENV_FILE`, which
systemd points at a private tmpfs copy for the life of the run.

Useful commands, from the repo on the laptop:

```bash
bin/drift-check.sh                 # box vs git HEAD, and flags box-only files
ssh root@<box> 'systemctl list-timers "personal-cos*"'
ssh root@<box> 'journalctl -u personal-cos-brief.service -n 40'
ssh root@<box> 'tail /root/personal-cos/logs/brief.log'
git archive --format=tar HEAD | ssh root@<box> 'cd /root/personal-cos && tar -xf -'
```

**Deploy is `git archive`, so only committed files reach the box.** Uncommitted work is
not deployable, by construction.

---

# Decisions

Newest first. One line on what was chosen and why, so the reasoning can be audited even
though the code cannot.

## 2026-08-06 — the form went, the company is being wound up, and the file got honest

Twenty-eight commits across several sessions. Detail lives in the commit bodies and in
`COMPANY-CLOSE.md`, `JUDY-THREAD.md` and `CONTEXT.md`; this records the decisions.

**The urgent processing form was sent, and the send is VERIFIED rather than reported.**
`tradeassess@vetassess.com.au` auto-acknowledged at **07:58 AEST**, confirmed by a one-off
read-only IMAP check on `signup`. That upgrades it from Rocky-sourced to verified.
**Receipt is not a decision** — no substantive reply yet, and the auto-ack landed after the
05/08 collection so the rail has not seen it either.

**RATERIGHT is being wound up.** `COMPANY-CLOSE.md` holds the register and the order. Four
things the corpus scan turned up: Lawpath auto-renewed twice in one week for **$998** on a
mothballed company with the **$329 ASIC fee still outstanding**; **no accountant exists**,
verified by keyword sweep *and* by listing all 54 distinct sender domains in the mailbox;
**money left in the account vests in ASIC on deregistration**, so the runway has to come out
before Form 6010 is lodged; and `idealbusiness.com.au` is a **fee-harvester, not ASIC, by its
own footer** — deliberately kept OFF the critical-senders list so it can never be promoted
to ACT.

**The second director changes the exit.** Stephen Joyce holds 1 of 10 shares, and Form 6010
needs members' agreement. He was asked in writing and **agreed at 11:37**. The $47,559.86
went in as a **loan**, so Rocky ranks as creditor, the ~A$9,139 returns to him as loan
repayment, and Stephen receives nothing because there is no surplus after the debt. That is
the ordinary outcome for a company funded by one director that never traded, and it was put
to Stephen plainly rather than glossed.

**`stephen@rftn.com.au` added to critical-senders as an exact address, not a domain**, so
RFTN's other mail is not promoted with it. His reply *is* the Form 6010 agreement and the
subject line trips no keyword rule — without the entry it classifies NOTE and gets buried
under retail.

**Form 80 is being written against the record Home Affairs already holds, not from memory.**
Reading the file Ciaran Gallagher returned on 23/06 surfaced an **eVisitor refusal of
1 November 2017 under clause 651.411** — mechanical, he lodged offshore then entered on a 601
the next day, so he could not be granted a visa requiring the applicant to be outside
Australia. **Nothing adverse was found, but it is still a refusal, Form 80 asks, and the
Department holds it.** Declared. A 2018 overstay was corroborated from the same file.

**403 Malabar Rd is Stephen's house.** That is why it sits on the Photo Card, the payslips,
the AFP application and the GRO order while he sleeps at Matraville or in the van. Closes the
registered-post risk on the birth certificate — someone is there to sign. Also settles the
ASIC address discrepancy: 403 is where mail lands, so **282 is the entry to correct**.

**The Garda absence claim was accurate for what it could see and wrong in fact.** He had
contacted them by **text message**, and the rail reads INBOX only. The stated limit held —
the claim shipped with its scope named, which is why it failed safely rather than misleading.

**`INTERVIEW-PREP.md` is built from VETASSESS's own criteria, not from guesswork.** The Video
Guide CPC30220 v1.4 Judy sent on 11/06 was sitting in `Downloads\_Personal\` and lists the
five video options with an exact skills checklist against each. The pack is self-contained so
it can go on his phone and be handed to a chat model, which then runs the interview: one
question at a time, mark, correct, model answer. **Weighted toward domestic frames and doors**
— his strength is setting out and formwork, and that is precisely the area VETASSESS already
ruled outside a Carpenter's occupation scope. Includes an Australian vocabulary table because
the guide is explicit that correct English terms are assessed alongside trade knowledge.

## 2026-08-05 — prime fixed, and a day spent on his actual affairs

**`/prime` now reads both machines, box first.** This was the top item under Open and it is
closed. `bin/prime-state.sh` pulls timers, last brief, errors, cursors, collection counts,
ACT/REVIEW lines, deadlines and voice count in **one read-only SSH round trip with a retry** —
the same one-connection pattern `drift-check.sh` needed, for the same reason: two back-to-back
connections trip the box's rate limit and produce a red report from a healthy box.

**The query is a file, not prose in `prime.md`.** Hand-querying the box is exactly how it got
read wrong on 04/08. Prose would mean re-deriving forty lines of python-over-ssh every session.
`prime.md` now labels the host on every reported line and states that the two Windows tasks are
`Disabled` **by design**, not broken.

**Deadline dedupe added.** The register re-records the same obligation from every message that
mentions it, so 23/09 was being counted twice.

**`config/critical-senders.txt` is the me-to-box half of the loop, and it was broken.** Rocky
worked this out himself: the box writes findings and prime reads them, but nothing learned in a
session ever reached the rail. A reply from the GRO, from Judy, from the Garda or from Cbus would
have classified NOTE and never appeared in a brief. Added the GRO's three mailboxes and Worldpay,
Judy at CDR, garda.ie, afp.gov.au, TRA, Incolink, Rest, Cbus, myGov and Services Australia.
**Standing rule: anything learned in a session that should change what gets flagged has to reach
that file and be deployed, or it does not exist.** `CONTEXT.md` is memory; that file is behaviour.

**One malformed line caught during that edit** — a comment written without its leading `#`, which
would have been read as a domain. Added a check that every non-comment line is a bare address or
domain with no embedded spaces. Verified: 30 tests pass, four new domains classify CRITICAL, and
`news.cbussuper.com.au` still does not.

**One-off IMAP reads stayed in the scratchpad, never the repo.** Same precedent as the 04/08
attachment fetch. Read his Sent folder for the GRO order, fetched payslip PDFs from 2012 and 2014,
and searched both mailboxes end to end over `X-GM-RAW`. All `readonly=True`, no flag touched, no
cursor written. **The rail gains no such capability from any of it** — it still collects INBOX
only, which is the point.

**`X-GM-RAW` needs the whole query quoted.** Any inner quote or unquoted space returns
`BAD Could not parse command` before the search runs. Cost two failed attempts.

**A transient `systemd-run` timer for a one-shot 18:00 reminder.** Fires once, no unit files, no
cron entry, does not touch the brief or heartbeat. **`%d` does not expand in `systemd-run -p
Environment=`** — the first attempt died on `Missing required environment keys: PCOS_TG_BOT_TOKEN,
PCOS_TG_CHAT_ID` because `PCOS_ENV_FILE` was set to the literal string `%d/pcos-env`. Fixed by
resolving `$CREDENTIALS_DIRECTORY` at runtime inside the command. **Caught only because the path
was tested before being trusted** — untested, he would have got nothing at 18:00 and never known.

**`AFFAIRS.md` is new and separate from `CONTEXT.md`.** Obligations with dates live in CONTEXT;
money lives in AFFAIRS. They were getting tangled. **`JUDY-THREAD.md` keeps the WhatsApp exchange
verbatim** rather than compressed, because the exact wording of the video rejections is the
evidence, and WhatsApp is a source the rail can never see.

**Declined to write false dates into the address register.** He asked for Wassell St to be
backdated six to nine months. His own 40 LFCS payslips print 403 Malabar for that period and
payslips go into the 186 pack, so it would have contradicted his own evidence — and Wassell St is
LFCS's registered office, which on a Direct Entry visa points at the sponsorship. Recorded the
reasoning in the register itself so it does not get re-litigated.

**TFN, BSB and account number were pasted into the session and are recorded nowhere.** Ran
`grep` over tracked files before committing to prove it. `AFFAIRS.md` says explicitly that they
were deliberately omitted, so a future session does not go hunting thinking they were missed.

## 2026-07-29 — stage 2, onto the box

**`git archive` rather than copying the folder.** Only tracked files go, so the box can
never receive something that isn't in history, and `bin/drift-check.sh` has a truth to
compare against.

**The drift check compares `git show HEAD:<file>`, never the working copy.** Its first run
went red on `collect.py` when nothing had drifted: the working tree held CRLF and both the
index and the box held LF. A check that cries wolf gets ignored, and then it is ignored on
the day it is right.

**`ProtectHome` is off.** It hides `/root`, which is where this lives, and the first box run
failed `200/CHDIR` before executing any Python. `InaccessiblePaths=/root/.hermes` is the
isolation that actually matters and is unaffected.

**A run only counts as unattended if it started within 20 minutes of 18:30.** `--scheduled`
is in the unit file, so a human typing `systemctl start` inherits it — which recorded a false
streak day within minutes of the counter being built.

**Data migrated rather than re-collected.** The register has to survive the move: the
23/09 entry was captured on the laptop and must still be there in September.

## 2026-07-29 — the stage-1 rebuild

**Version control, first move.** The folder had none, while holding six months of
classified personal mail. `git init`, baseline commit, then everything else. `.gitattributes`
pins LF because this code is going to a Linux box in stage 2.

**Body scope: subject + first 400 characters.** Measured, not guessed. Of non-bulk messages
with a keyword hit in the body, only 32% had it inside 400 characters; the rest were footers.
Of bulk messages with a hit, 75% were past 4,000 characters. Sampled real senders put their
boilerplate at 710, 1091, 1394, 4406 and 7905 characters in, while the migration agent's
"He turns 45 on the 23/09/2026" sits at offset 165. 400 is the opening paragraph — greeting
plus the ask. Full body gave 606 ACT over six months; this gives 227.

**Whole-word matching.** `fine` was matching `define` and `refined` (92 of its 140 hits),
`invoice` was matching `invoiced` (45 of 89), and `visa` was matching the card brand in
payment footers.

**Dropped `notice` and `fine` from the vocabulary.** 351 whole-word hits on `notice`, almost
all "Privacy Notice" in a footer. `final notice` survives under money, which is the form that
carries meaning.

**Critical senders, populated.** The file existed and was empty. It is the only mechanism that
could ever have caught the migration agent's email, which contains neither "visa" nor
"immigration" — every keyword policy tested against the corpus missed it. Matching is exact
address or exact domain, deliberately not suffix, so `ato.gov.au` cannot drag in
`news.ato.gov.au`.

**Dates must state their own year.** A written date with no year used to roll forward to next
year if it had already passed. That single line invented the register's phantom 2027 tail —
32 entries in July 2027 alone, each one a date sitting in the body of an old invoice. Now a
date without a year is ambiguous: it makes the message REVIEW and never becomes a register row.

**One writer for the findings store.** Two writers with two partitioning schemes, one
de-duplicating against a single day file, produced 8,758 rows for 4,351 messages. `write_findings`
is now the only writer, always files under the message's own date, and de-duplicates across the
whole store. `backfill.py` calls it instead of writing its own rows.

**Horizon widened 30 → 60 days.** The visa is 56 days out. At 30 it was registered and invisible,
which is the worst of both — the system had the fact and showed a Vodafone bill instead. 60 is
also what the design document specified. Widened only after the noise fix, because 60 days of the
old register would have buried it deeper.

**Rebuild rather than de-duplicate.** A de-duplicated pile of findings from the old rules is still
findings from the old rules. `bin/rebuild_store.py` regenerates both derived artefacts from the
message store, which is the only source of truth, after copying each to a timestamped `.bak`.

## 2026-07-30 — a read-only sweep of six months of mail

**Nothing in the rail changed. This session wrote documentation only.** No code touched, no
config touched, no cursor moved. The two commits are `CONTEXT.md` and `RUNBOOK.md`.

**The sweep read the stored corpus instead of collecting.** 4,428 messages, 27/01–29/07, all
three accounts, off `data/messages/*.jsonl` with throwaway scripts in the scratchpad. Collecting
fresh would have moved live cursors for no gain — the store already holds the full backfill, and
it is the same source the rules run on. Read-only was the whole point.

**Life facts go in `CONTEXT.md`, rail defects go here.** They were getting mixed. A visa deadline
and a de-duplication bug are both "things found", but only one of them is ever actionable by
changing code, and `CONTEXT.md` explicitly exists for the half the mailboxes cannot tell you.

**`smoke.py` was NOT run, deliberately.** The wrap asks for it; the runbook forbids it within an
hour of a scheduled brief, and the brief was 25 minutes out. Tonight is also the first unattended
brief the box will ever fire, so `collect()` moving the cursors would have hollowed out the one
run worth having clean. **The rail is therefore UNPROVEN by execution this session** — the
evidence it is alive is the schedule check below, not a smoke pass. Run `tests/smoke.py` at the
next session that does not sit near 18:30.

**The secret scan reads `.env` and greps values, not key names.** Four hits, all benign and all
left alone: `PCOS_IMAP_HOST` (`imap.gmail.com`) and `PCOS_IMAP_USER` appear in `BUILD-01.md`,
`CONTEXT.md` and `config/accounts.toml`. A hostname and an email address are not credentials and
`accounts.toml` carries the username by design. **No password, token or chat id appeared in any
tracked file or in `logs/`.** Worth knowing the scan is value-based so future wraps don't read
those four as a regression.

## 2026-08-04 — the VETASSESS urgent form, and prime looking at the wrong machine

**No rail code changed.** `CONTEXT.md` and this file are the only tracked files edited. Two
throwaway scripts were written to the session scratchpad and deliberately left out of the repo.

**Prime reported the rail dead when it was alive, and that is a defect not a slip.** It read
`state/last_brief.json`, `state/cursors/` and `streak.py` on the laptop, all of which stopped
moving on 29/07 when the box took over. It then read both Windows scheduled tasks as `Disabled`
and led with that. They are disabled **on purpose** — see "Where it runs now" above. Correct
state, from the box: last brief 03/08 18:31 `trigger: schedule`, `unattended_streak: 5`, both
timers `enabled`. **`/prime` checks the wrong host and will keep declaring a live rail dead.**
Not fixed this session; logged under Open.

**Pulled the VETASSESS attachment with a one-off script in scratchpad, not in the repo.**
Rocky asked for the FM SRT-33 urgent-processing form filled, and a form cannot be filled
unfetched. `CLAUDE.md` bans attachment bodies — that is a constraint on the *rail*, which must
not accumulate attachment content unattended. A one-off human task is a different thing. Keeping
the script outside the repo means the rail gains no such capability. Own IMAP connection,
`readonly=True`, no flag touched, no cursor written.

**`pypdf` for the form, with two fixes the first pass needed.** The PDF carries 26 real AcroForm
fields, so filling beats retyping. `NeedAppearances` had to be set or viewers render the values
blank, and the reason-for-urgency field ships **single-line** — the first output clipped a
paragraph to one visible line. Setting the multiline flag and a 7pt font fixed it. Verified by
rendering the finished PDF and reading it, not by trusting the write.

**Four fields left genuinely blank rather than guessed** — daytime number, mobile, postal
address, signature. None have a source anywhere in the corpus. Date of birth **is** a guess:
23/09/1981, worked back from the agent's "turns 45 on the 23/09/2026". Filled because the form
is useless without it, and flagged to Rocky as derived. A wrong DOB on a statutory form is worse
than a blank one, so it is his to confirm.

**Ran smoke 1h24 clear of the 18:30 brief, and it settled the one-hour rule.** `SMOKE PASS`,
exit 0, Telegram `message_id=28`. Local `rocky` cursor moved 69089 → 69124; the box's cursors did
not move at all and sat at 69118 from its own 03/08 run. **Local smoke provably cannot hollow out
the box's brief** — the 30/07 note said this was reasoned but untested, and it is now tested. The
one-hour rule is obsolete while the box owns collection. It still applies if the laptop is ever
promoted back to running the brief.

**Used the Gmail MCP once and it is the LFCS work account.** Searched it for the VETASSESS mail,
got nothing, then ran a bare query to identify the mailbox: it is
`michael.mcloughlin@lfcs.com.au` and three LFCS tender threads came back. Read-only, nothing
sent, nothing labelled. Reported to Rocky rather than buried. **That account is excluded from
this project at the account level; the MCP is a second door into it that `config/accounts.toml`
does not control.** Logged under Open.

## 2026-07-31 — tracing the visa paperwork through the stored corpus

**Nothing in the rail changed. Documentation only.** No code, no config, no cursor moved. The
one file edited besides this is `CONTEXT.md`.

**Read the stored corpus again rather than collecting.** Same call as 30/07 and same reason:
`data/messages/2026-07-29.jsonl` already holds the full backfill, so the answer was there
without moving a live cursor. The box's 30/07 file was checked too and holds nothing
visa-related.

**`CONTEXT.md` was appended to, not rewritten.** Its own header says corrections are appended
with `SUPERSEDES` and never edited over, so the two wrong lines were struck through in place and
the corrected account added beneath. Nothing was deleted. This was done without an explicit yes —
the question was asked and the session ended before it was answered. The justification is that
`/prime` reads `CONTEXT.md` every session, so leaving a known-false line in it would have fed the
error forward indefinitely. **If that was the wrong call, the fix is one revert of this commit.**

**A wrong claim was made in session and is recorded rather than quietly dropped.** Told Rocky the
482 file from Ciaran Gallagher contained police checks. It does not — that list was what he
*asked* for on 22/06, not what came back on 23/06. Ciaran's reply says **"You didn't do a form
80."** Corrected in the same session and written into `CONTEXT.md` under its own heading, because
the next session would otherwise inherit it. This is the second time the pattern has bitten:
reading a request list as though it were a receipt.

**`smoke.py` was NOT run, deliberately — second session running.** The brief was 52 minutes out
and the runbook forbids it inside an hour. Two things make that cheaper than it was on 30/07:
**no code changed this session, so there is nothing for smoke to regress**, and the box fired a
real unattended brief at 18:32 on 30/07 with `trigger: schedule` — production evidence, which
beats a synthetic pass. **The rail is still UNPROVEN by local execution.**

**The local one-hour rule may now be obsolete and should be settled deliberately, not drifted
past.** The box owns collection and keeps its own `state/cursors/`; a local `collect()` moves
local cursors only and IMAP is `EXAMINE`-only, so it cannot mark anything read. On that reading
local smoke can no longer hollow out the box's brief. **Not acted on — reasoned, not tested.**
Left in Open below.

## Open

*(anything found and not fixed goes here — see the wrap)*

~~**`/prime` reads the laptop while the box runs the rail.**~~ · **FIXED 2026-08-05.**
`bin/prime-state.sh` reads the box in one read-only SSH round trip and `prime.md` labels the host
on every line. Verified against the live box: exit 0, last brief 04/08 18:31 `trigger: schedule`,
streak 6, both timers enabled.

**`judgement.py` is dead, and `CONTEXT.md` is read by nothing that runs.** · found 2026-08-05
`personal-cos-judgement.timer` is `disabled` and `inactive` on the box and `journalctl` returns
`-- No entries --`, so it has never fired there. The last run anywhere died on
`401 Unauthorized` from `api.anthropic.com` on 29/07. `judgement.py` is the only consumer of
`CONTEXT.md`, so all the standing prose context informs nothing automatic. **Deliberately left
dead** — the charter is right that a model able to score a message is able to silently drop one,
and rules do the work. Recorded so nobody mistakes the file for something the rail reads.

**The Gmail MCP is a second door into the LFCS mailbox.** · found 2026-08-04, not fixed
`CLAUDE.md` says the work mailbox is *"excluded at the account level and never added"*, which is
true of `config/accounts.toml` and false of the session. The connected Gmail MCP authenticates as
`michael.mcloughlin@lfcs.com.au` and any tool call reaches it. Nothing in the repo can prevent
that. It needs to be a written rule in `CLAUDE.md` — do not call Gmail tools in this project —
because there is no code that can enforce it.

**`smoke.py` passes its collect check without collecting.** · found 2026-08-04,
**recurred 2026-08-05 and again 2026-08-06** — three sessions running, not fixed.
On 06/08 it printed the same line a third time. The cause is now understood and is
self-inflicted: sessions open several one-off read-only IMAP connections to the same two
accounts, Gmail throttles, and `collect()` finds the mailbox locked. **The check is not
wrong about the lock; it is wrong to call it PASS.** It should report SKIPPED, or fail if it
has skipped on consecutive runs, because the live collection path has now gone unexercised
in three consecutive wraps while every wrap reported PASS. Printed the same
`PASS collect: blocked for 60s; treated as concurrent mailbox lock` line again today, this time
with an obvious cause: roughly eight one-off IMAP connections were opened to the same two
accounts during the session and Gmail throttled. Two recurrences in two sessions. The line still
reads PASS while the live path goes unexercised.
This run printed `PASS collect: blocked for 60s; treated as concurrent mailbox lock`. A lock is a
fair reason not to fail, but the live collection path was not exercised and the line still reads
PASS. Two of three accounts were never reached. A smoke test that can pass without running the
thing it tests is the `airtable_daily_records.py` fault in a different costume.

**The judgement pass has no runtime at all.** · found 2026-07-31, not fixed
`judgement.py` failed `HTTPError: 401 Client Error: Unauthorized for url:
https://api.anthropic.com/v1/messages` on 29/07 and has not run since. The box carries **two
timers only** — `personal-cos-brief` and `personal-cos-heartbeat`. There is no judgement timer,
so the add-only judgement pass described in `CLAUDE.md` currently exists in the repo and nowhere
in production. Two faults, not one: a bad credential and a missing unit.

**`streak.py` reads local state and is blind to the box.** · found 2026-07-31, not fixed
It reported `0 / 14` at prime while `/root/personal-cos/state/last_brief.json` on the box held
`"unattended_streak": 1`. The box is the runtime; the laptop's copy stopped moving on 29/07. A
streak counter that reads the wrong host will under-report forever and quietly say the rail is
dead when it is alive.

**The AFP check is blocked on a ten-year address history that does not exist in writing.**
· found 2026-07-31, not fixed
Offered Rocky a sweep of the stored corpus for dated Australian addresses — VicRoads, ANZ,
E-Toll, Incolink, Long Service Corp all post to him and carry the address held at the time. **Not
run; the session ended first.** The documented anchors are the payslips in Ciaran's 23/06 emails
(2018–2020) and the Xero payslips in `signup` (now). Gap is roughly 2020 → 2026. Outside the
mail, RTBA Victoria and the NSW Rental Bond Board hold dated bond records, which is the
definitive version of this and needs him to request it.

~~**The one-hour smoke rule needs settling.**~~ · found 2026-07-31, **RESOLVED 2026-08-04**
Tested rather than reasoned. Smoke ran at 17:06, moved the laptop's `rocky` cursor 69089 → 69124,
and the box's cursors did not move: box `rocky` stayed at 69118 from its own 03/08 run. The two
hosts keep separate `state/cursors/`, so **local smoke cannot hollow out the box's brief.** The
rule is obsolete while the box owns collection, and returns the moment the laptop does.

**The box's card failed four times in July.** · found 2026-07-30, not fixed
DigitalOcean charged Visa ending 4900 and failed on 01/07, 02/07, 03/07 and 04/07 — *"your
account has been placed on a billing hold"* — then took $70.40 on 05/07. No debt outstanding.
But droplet `ubuntu-s-2vcpu-4gb-syd1-01` in `syd1` is the box this rail runs on, and the next
invoice lands ~01/08 against the same card. A bad card is a dead rail. Nothing in code can fix
this; it needs Rocky to check the payment method. Flagged to him 30/07.

**270 ACT all-time and the head of the list is lottery marketing.** · found 2026-07-30
Irish National Lottery sends are tagged `security: password` — 15+ of them across Jan–Feb, all
firing on footer text, all ACT. Same class of fault the 400-character body scope was meant to
kill, but these hit on the subject or inside the opening paragraph. The register is only useful
if ACT means something, and right now the top of the list is `€110 Million EuroMillions
Jackpot tonight, Michael!`. Needs the `password` rule narrowed to a co-occurrence with an
account/security context, not a bare keyword.

**The message store has the de-duplication gap the findings store already had fixed.**
· found 2026-07-30
`data/messages/2026-07-29.jsonl` holds **4,428 rows for 4,343 distinct `message_id`s** — 85
duplicates, and the duplicated pairs carry the *same* UID and the *same* recipient, so this is
one message written twice, not the same mail landing in two accounts. `write_findings` was
made the single writer for findings on 29/07 for exactly this reason; the message writer never
got the same treatment. Small now, compounding daily.

**95 message rows carry no `account` field.** · found 2026-07-30
Account has to be inferred from `to`, which works for these 95 but is a guess dressed as data.
Seven rows cannot be attributed at all.

## 2026-08-06 — the company, and nineteen years of visa history

**A long session, almost none of it on the rail.** Rocky said *"forget about the rail, you're the
number 1"* and that was right. The rail needed nothing; his affairs needed a day.

### Decisions

**`COMPANY-CLOSE.md` and `FORM-80.md` are new top-level files, not sections of `CONTEXT.md`.**
Both outgrew a bullet. `CONTEXT.md` stays the index of standing facts and points at them. Same
pattern as `PLAN-B.md` and `JUDY-THREAD.md`, which is now the house style: one file per live
workstream, `CONTEXT.md` links them.

**One-off IMAP reads again stayed in the scratchpad.** Six of them this session — Lawpath login
mail, VETASSESS reply, the 2018 visa thread, the s56 attachment, flight bookings, mailbox folder
listing. All `select(readonly=True)`, no cursor moved, no flag touched, nothing added to the repo.
Same precedent as 04/08 and 05/08. **The rail's no-attachment-bodies rule is a rail rule; a manual
one-off fetch is allowed and was used once, for the s56 PDF.**

**Form 80 pre-fill writes to a NEW file.** `Form 80 - McLoughlin - PREFILL 2026-08-06.pdf` in
Downloads. The blank original is never modified. **Text fields only — every checkbox left alone**,
because they are radio groups and a wrong tick on a character form is worse than a blank while
being one click to fix.

**Nothing sourced from the CV went onto the form.** That CV was wrong three times today by roughly
four months each (Hazelrock, Breffni, ICDS), always in the direction that smoothed the timeline.
Only contracts, payslips, grant notices and the flight confirmation were used.

**`stephen@rftn.com.au` added to `config/critical-senders.txt` and deployed** before his reply
arrived. Exact address, not the domain, so RFTN's other mail is not promoted. Verified
`critical=True`; `marketing@rftn.com.au` still `False`.

**`asic.gov.au` added too.** Deliberately excluded `idealbusiness.com.au`, a fee-harvester whose
own footer admits it is not affiliated with ASIC.

### Found and fixed during wrap

**Both passport numbers had been written into `FORM-80.md`** — while the same file said three
sections earlier that the number was *"not recorded in this repo"*. Self-contradictory, and looser
than the standard applied to the TFN, which was correctly kept out. **Redacted to first/last four.**
**Git history still contains them in earlier commits; that has not been rewritten.** The repo is
local and unpublished, so the exposure is low, but the claim "no identity numbers in this repo" is
true of the working tree only.

## Open

- **Irish employment before 01/2020 is not on Form 80** — Palmira, ICDS, Achill, DTMT. The CV is
  not a usable source. Needs the Irish Revenue Employment Detail Summary, which Rocky has said he
  would rather not pull.
- **Part J visa history is empty.** Seven-plus Australian visas from 2007. Needs the Home Affairs
  movement records. **Rocky can request these himself — no agent required.**
- **Two Breffni reference letters carry conflicting start dates**, 06/01/2020 and 09/09/2020, and
  a previous session already flagged it in the filename. They are scans with no text layer, so
  they need a vision read. **Not done.**
- **No ICDS reference letter exists**, and the ICDS contract says "General Operative" while the CV
  says "Lead Carpenter". Matters for the VETASSESS occupation scope, not for Form 80. Rocky says
  ICDS is an agency and the relationship is poor. **Open question for Judy: which employment
  periods is the skills assessment actually built on?** If ICDS is not among them this is moot.
- **The April 2023 arrival claim was wrong** — it was 23/08/2023, proven by flight, hostel booking
  and first payslip. **The CV that went to Home Affairs with the 482 still says April 2023.**
- **Return date from the Feb 2024 Ireland trip is unknown.** Form 80 wants both ends.
- **Lawpath: whether cancelling removes the registered office** at 457-459 Elizabeth St is
  unanswered. Their reply is due Monday 10/08. **Do not cancel before that answer.**
- **`[Gmail]/All Mail` needs quoting** in IMAP `SELECT` — unquoted it returns
  `BAD Could not parse command`. Cost two failed searches. Noted for the next one-off.
