chore(plan): T-9 and T-13 done in 1.14.0; T-14, and a note on T-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:58:36 +02:00
co-authored by Claude Opus 5.5
parent 44c5facc65
commit 7c0ff2bfe0
3 changed files with 104 additions and 0 deletions
+55
View File
@@ -107,3 +107,58 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
Live in core-api 1.13.0; production migrated to 008 — jpmschweitzer/office-claude → claude-ai, llm_tester none; /tenants/me answers it.', NULL, '2026-10-19 20:16:05', '2026-10-19 20:16:05.428', '2026-10-19 20:16:05.428', NULL, 'e1691631641e5a8a37743c8a53fe6c75', 2) ON CONFLICT(hash) DO NOTHING; Live in core-api 1.13.0; production migrated to 008 — jpmschweitzer/office-claude → claude-ai, llm_tester none; /tenants/me answers it.', NULL, '2026-10-19 20:16:05', '2026-10-19 20:16:05.428', '2026-10-19 20:16:05.428', NULL, 'e1691631641e5a8a37743c8a53fe6c75', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBAZ6KW704AF07ZBR3YC868', 'status', 'backlog', 'done', NULL, '2026-10-19 20:16:05', '2026-10-19 20:16:05.567', '2026-10-19 20:16:05.567', NULL, 'd4fb4422de5f1d47f6ab52904623cd0d', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBAZ6KW704AF07ZBR3YC868', 'status', 'backlog', 'done', NULL, '2026-10-19 20:16:05', '2026-10-19 20:16:05.567', '2026-10-19 20:16:05.567', NULL, 'd4fb4422de5f1d47f6ab52904623cd0d', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNA8JCWVP31DKSE7F4EMF6G4', 'description', '`GET /scribe/mailbox` sorts unread first, then by id descending, behind one `before_id` cursor — two sort keys, one cursor. No cursor choice is safe in every case: paging below the last row is correct whenever a page holds a read entry, but a page of only unread entries (more than 200 unread) skips read entries with higher ids. Both clients that turn a message number into a mailbox entry — the Office (`src/systems/scribe.py` `_resolve_entry_id`) and `toj scribe mark` — page the mailbox to do it.
**Fix:** a `message_id` query parameter on `GET /scribe/mailbox` that answers the caller''s own entry for that message (zero or one), so neither client pages. Optionally, a cursor that matches the sort (`before` over `(read, id)`) for the listing itself.
Done when: both clients drop their paging for a single call, and a test seeds an old unread entry ahead of more than a page of read ones and finds a read entry beyond the first page.', '`GET /scribe/mailbox` sorts unread first, then by id descending, behind one `before_id` cursor — two sort keys, one cursor. No cursor choice is safe in every case: paging below the last row is correct whenever a page holds a read entry, but a page of only unread entries (more than 200 unread) skips read entries with higher ids. Both clients that turn a message number into a mailbox entry — the Office (`src/systems/scribe.py` `_resolve_entry_id`) and `toj scribe mark` — page the mailbox to do it.
**Fix:** a `message_id` query parameter on `GET /scribe/mailbox` that answers the caller''s own entry for that message (zero or one), so neither client pages. Optionally, a cursor that matches the sort (`before` over `(read, id)`) for the listing itself.
Done when: both clients drop their paging for a single call, and a test seeds an old unread entry ahead of more than a page of read ones and finds a read entry beyond the first page.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', NULL, '2026-10-19 20:58:36', '2026-10-19 20:58:36.211', '2026-10-19 20:58:36.211', NULL, 'dd28daf8baefbf8c60d5842445ac6672', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNA8JCWVP31DKSE7F4EMF6G4', 'status', 'backlog', 'done', NULL, '2026-10-19 20:58:36', '2026-10-19 20:58:36.314', '2026-10-19 20:58:36.314', NULL, '83fae52bf6b40a1dcef4a89b9d8285d9', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'description', 'The user wants a way to get rid of a message that is no longer useful. A message is retained and shared by every recipient (workspace D-66 — history is how agents rebuild context), so deletion is **per mailbox**, in D-70''s vocabulary:
- **archive / unarchive** — the entry leaves the default mailbox listing; still in `history`, still findable.
- **trash / restore** — the entry leaves the mailbox, restorable; trashed entries are listed only when asked for.
- True erasure of a message''s content stays `redact` (write, audited) — not this ticket.
**core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.', 'The user wants a way to get rid of a message that is no longer useful. A message is retained and shared by every recipient (workspace D-66 — history is how agents rebuild context), so deletion is **per mailbox**, in D-70''s vocabulary:
- **archive / unarchive** — the entry leaves the default mailbox listing; still in `history`, still findable.
- **trash / restore** — the entry leaves the mailbox, restorable; trashed entries are listed only when asked for.
- True erasure of a message''s content stays `redact` (write, audited) — not this ticket.
**core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', NULL, '2026-10-19 20:58:36', '2026-10-19 20:58:36.431', '2026-10-19 20:58:36.431', NULL, 'be754e2e77e34e20c3b7bf511adefdce', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'status', 'backlog', 'done', NULL, '2026-10-19 20:58:36', '2026-10-19 20:58:36.539', '2026-10-19 20:58:36.539', NULL, '1673ea85b251b578be2248e3d3fe6f54', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GN6XMY61H4P86X0TKGD3D37W', 'description', 'The rest of the Book''s verbs (workspace D-70), and two corrections to what T-1 built — all before the Book''s first deploy. Driven by workspace T-166/T-167.
**Corrections, found reviewing T-1 (2026-10-19):**
- **Outward ids.** Records use UUIDs, but the Office names items in words — "Task 12", "Note 40" (workspace D-67). Add a short sequential `number` per record (unique, never reused) and accept it wherever a UUID is accepted. Migration 005 has never run against production `core_api` (still at 004), so **amend 005** rather than adding 006 — and re-prove its round trip on `core_api_test`.
- **`work` and `personal` as tags.** `tag`/`untag` treat them as free text; they must map onto the `scope` column (tagging `work` sets scope work; untagging the only scope is refused with a code that names the fix), so the outward "baked-in tags" of D-69 hold.
**The remaining verbs:** `append`, `remind`, `reschedule` (batch), `undo` (this identity''s last change, from `book_events`), `purge` (from trash only), `redact` (scrubs a value from the record and its events — the one audited exception to the append-only trigger), `pin`/`unpin`, `check`/`uncheck`, `snooze`, `archive`/`unarchive`, `trash`/`restore`, `pause`/`resume` (a project and its descendants through `parent`; statuses saved in `paused_statuses` and restored exactly), `due`, `next` (filters energy, estimate, context tags, scope); batch forms for every note- and write-class verb; an idempotency key on `add`.
**Add an owner (D-73).** Book records need an `owner` — the tatlock tenant id from the token''s signed `tatlock_user` claim (production `jpmschweitzer`), forwarded by the Office — and every query scoped by it. Not in migration 005 as written; add it while amending 005. The Scribe''s tables (workspace T-176) carry the same owner on addresses.
**Isolation (D-73).** Every Book query and write filters on the owner tenant; a test proves an `llm_tester` caller cannot read, change, link to or close a `jpmschweitzer` record (and the reverse) — the error is `not_found`, so another tenant''s ids are not even confirmed to exist.
**Split 2026-10-19:** the short outward ids and the owner tenant (amending 005), and the Book''s tenant filter, moved to **T-7**, so core-api can release safely. T-2 keeps the remaining verbs and work/personal as tags.', 'The rest of the Book''s verbs (workspace D-70), and two corrections to what T-1 built — all before the Book''s first deploy. Driven by workspace T-166/T-167.
**Corrections, found reviewing T-1 (2026-10-19):**
- **Outward ids.** Records use UUIDs, but the Office names items in words — "Task 12", "Note 40" (workspace D-67). Add a short sequential `number` per record (unique, never reused) and accept it wherever a UUID is accepted. Migration 005 has never run against production `core_api` (still at 004), so **amend 005** rather than adding 006 — and re-prove its round trip on `core_api_test`.
- **`work` and `personal` as tags.** `tag`/`untag` treat them as free text; they must map onto the `scope` column (tagging `work` sets scope work; untagging the only scope is refused with a code that names the fix), so the outward "baked-in tags" of D-69 hold.
**The remaining verbs:** `append`, `remind`, `reschedule` (batch), `undo` (this identity''s last change, from `book_events`), `purge` (from trash only), `redact` (scrubs a value from the record and its events — the one audited exception to the append-only trigger), `pin`/`unpin`, `check`/`uncheck`, `snooze`, `archive`/`unarchive`, `trash`/`restore`, `pause`/`resume` (a project and its descendants through `parent`; statuses saved in `paused_statuses` and restored exactly), `due`, `next` (filters energy, estimate, context tags, scope); batch forms for every note- and write-class verb; an idempotency key on `add`.
**Add an owner (D-73).** Book records need an `owner` — the tatlock tenant id from the token''s signed `tatlock_user` claim (production `jpmschweitzer`), forwarded by the Office — and every query scoped by it. Not in migration 005 as written; add it while amending 005. The Scribe''s tables (workspace T-176) carry the same owner on addresses.
**Isolation (D-73).** Every Book query and write filters on the owner tenant; a test proves an `llm_tester` caller cannot read, change, link to or close a `jpmschweitzer` record (and the reverse) — the error is `not_found`, so another tenant''s ids are not even confirmed to exist.
**Split 2026-10-19:** the short outward ids and the owner tenant (amending 005), and the Book''s tenant filter, moved to **T-7**, so core-api can release safely. T-2 keeps the remaining verbs and work/personal as tags.
**When the Book''s archive/trash verbs ship:** its `view=archive` filter (`src/domains/book/service.py`) matches `archived_at IS NOT NULL` alone, so a record both archived and trashed would show there. The Scribe''s (1.14.0) is archived and not trashed — trash wins. Make the Book agree. Found in round core-api-T-13-001.', NULL, '2026-10-19 20:58:36', '2026-10-19 20:58:36.679', '2026-10-19 20:58:36.679', NULL, 'ab2af45b461fbd0e84d422e625afaff2', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -11,3 +11,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBAZ6KW704AF07ZBR3YC868', 'T-11', '2026-10-19 19:38:11.488', '2026-10-19 19:38:11.488', NULL, '8150ae921a6b2682265917fb669c67e0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBAZ6KW704AF07ZBR3YC868', 'T-11', '2026-10-19 19:38:11.488', '2026-10-19 19:38:11.488', NULL, '8150ae921a6b2682265917fb669c67e0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBHXYDJEEDQEADCP3JTR7BR', 'T-12', '2026-10-19 20:08:36.204', '2026-10-19 20:08:36.204', NULL, '087d8b4674d3e12ca3b274fc4e255ade', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBHXYDJEEDQEADCP3JTR7BR', 'T-12', '2026-10-19 20:08:36.204', '2026-10-19 20:08:36.204', NULL, '087d8b4674d3e12ca3b274fc4e255ade', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'T-13', '2026-10-19 20:35:21.053', '2026-10-19 20:35:21.053', NULL, 'aab7f828f5f01bd8310c85f53ff0dc19', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at; INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'T-13', '2026-10-19 20:35:21.053', '2026-10-19 20:35:21.053', NULL, 'aab7f828f5f01bd8310c85f53ff0dc19', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBXC7H3JBESKFS20BV71BJG', 'T-14', '2026-10-19 20:58:36.809', '2026-10-19 20:58:36.809', NULL, '65086d233f5bc652cec0aa79dcaec2c4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
+48
View File
@@ -150,3 +150,51 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
- True erasure of a message''s content stays `redact` (write, audited) — not this ticket. - True erasure of a message''s content stays `redact` (write, audited) — not this ticket.
**core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.', 'backlog', 'high', NULL, NULL, NULL, '2026-10-19 20:35:21.053', '2026-10-19 20:35:21.053', NULL, 'd4fe2d5d06329b627a04dec089ca8a8b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; **core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.', 'backlog', 'high', NULL, NULL, NULL, '2026-10-19 20:35:21.053', '2026-10-19 20:35:21.053', NULL, 'd4fe2d5d06329b627a04dec089ca8a8b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNA8JCWVP31DKSE7F4EMF6G4', 'task', NULL, 'The mailbox answers by message number, so no client pages to find an entry', '`GET /scribe/mailbox` sorts unread first, then by id descending, behind one `before_id` cursor — two sort keys, one cursor. No cursor choice is safe in every case: paging below the last row is correct whenever a page holds a read entry, but a page of only unread entries (more than 200 unread) skips read entries with higher ids. Both clients that turn a message number into a mailbox entry — the Office (`src/systems/scribe.py` `_resolve_entry_id`) and `toj scribe mark` — page the mailbox to do it.
**Fix:** a `message_id` query parameter on `GET /scribe/mailbox` that answers the caller''s own entry for that message (zero or one), so neither client pages. Optionally, a cursor that matches the sort (`before` over `(read, id)`) for the listing itself.
Done when: both clients drop their paging for a single call, and a test seeds an old unread entry ahead of more than a page of read ones and finds a read entry beyond the first page.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', 'backlog', 'medium', NULL, NULL, NULL, '2026-10-19 17:07:53.702', '2026-10-19 20:58:36.211', NULL, '464c90f9c8ff84163446b97f2e5fd2ad', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNA8JCWVP31DKSE7F4EMF6G4', 'task', NULL, 'The mailbox answers by message number, so no client pages to find an entry', '`GET /scribe/mailbox` sorts unread first, then by id descending, behind one `before_id` cursor — two sort keys, one cursor. No cursor choice is safe in every case: paging below the last row is correct whenever a page holds a read entry, but a page of only unread entries (more than 200 unread) skips read entries with higher ids. Both clients that turn a message number into a mailbox entry — the Office (`src/systems/scribe.py` `_resolve_entry_id`) and `toj scribe mark` — page the mailbox to do it.
**Fix:** a `message_id` query parameter on `GET /scribe/mailbox` that answers the caller''s own entry for that message (zero or one), so neither client pages. Optionally, a cursor that matches the sort (`before` over `(read, id)`) for the listing itself.
Done when: both clients drop their paging for a single call, and a test seeds an old unread entry ahead of more than a page of read ones and finds a read entry beyond the first page.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', 'done', 'medium', NULL, NULL, NULL, '2026-10-19 17:07:53.702', '2026-10-19 20:58:36.314', NULL, 'd49072b54cd82406d0e4cc7e999ff331', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'story', NULL, 'Archive and trash a message in your own mailbox', 'The user wants a way to get rid of a message that is no longer useful. A message is retained and shared by every recipient (workspace D-66 — history is how agents rebuild context), so deletion is **per mailbox**, in D-70''s vocabulary:
- **archive / unarchive** — the entry leaves the default mailbox listing; still in `history`, still findable.
- **trash / restore** — the entry leaves the mailbox, restorable; trashed entries are listed only when asked for.
- True erasure of a message''s content stays `redact` (write, audited) — not this ticket.
**core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', 'backlog', 'high', NULL, NULL, NULL, '2026-10-19 20:35:21.053', '2026-10-19 20:58:36.431', NULL, 'c21eed83c0a16a0affce7a5d53935efc', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBR1VBM8SZP8DDK93SFS608', 'story', NULL, 'Archive and trash a message in your own mailbox', 'The user wants a way to get rid of a message that is no longer useful. A message is retained and shared by every recipient (workspace D-66 — history is how agents rebuild context), so deletion is **per mailbox**, in D-70''s vocabulary:
- **archive / unarchive** — the entry leaves the default mailbox listing; still in `history`, still findable.
- **trash / restore** — the entry leaves the mailbox, restorable; trashed entries are listed only when asked for.
- True erasure of a message''s content stays `redact` (write, audited) — not this ticket.
**core-api:** `scribe_mailbox_entries` gains `archived_at` and `trashed_at`; `PATCH /scribe/mailbox/{entry_id}` takes `archived` and `trashed` booleans beside `read`; `GET /scribe/mailbox` takes `view` (`inbox` default: neither archived nor trashed · `archive` · `trash` · `all`). Paired with T-9 (look an entry up by message number) in one round, since both touch the same route.
Live in core-api 1.14.0 (2026-10-19); production migrated to 009. Round core-api-T-13-001: 566 passed; three mutation checks each failed their test.', 'done', 'high', NULL, NULL, NULL, '2026-10-19 20:35:21.053', '2026-10-19 20:58:36.539', NULL, 'd12ebcdf5e52795a05a51c0620565172', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GN6XMY61H4P86X0TKGD3D37W', 'task', NULL, 'The Book''s remaining verbs, short outward ids, and work/personal as tags', 'The rest of the Book''s verbs (workspace D-70), and two corrections to what T-1 built — all before the Book''s first deploy. Driven by workspace T-166/T-167.
**Corrections, found reviewing T-1 (2026-10-19):**
- **Outward ids.** Records use UUIDs, but the Office names items in words — "Task 12", "Note 40" (workspace D-67). Add a short sequential `number` per record (unique, never reused) and accept it wherever a UUID is accepted. Migration 005 has never run against production `core_api` (still at 004), so **amend 005** rather than adding 006 — and re-prove its round trip on `core_api_test`.
- **`work` and `personal` as tags.** `tag`/`untag` treat them as free text; they must map onto the `scope` column (tagging `work` sets scope work; untagging the only scope is refused with a code that names the fix), so the outward "baked-in tags" of D-69 hold.
**The remaining verbs:** `append`, `remind`, `reschedule` (batch), `undo` (this identity''s last change, from `book_events`), `purge` (from trash only), `redact` (scrubs a value from the record and its events — the one audited exception to the append-only trigger), `pin`/`unpin`, `check`/`uncheck`, `snooze`, `archive`/`unarchive`, `trash`/`restore`, `pause`/`resume` (a project and its descendants through `parent`; statuses saved in `paused_statuses` and restored exactly), `due`, `next` (filters energy, estimate, context tags, scope); batch forms for every note- and write-class verb; an idempotency key on `add`.
**Add an owner (D-73).** Book records need an `owner` — the tatlock tenant id from the token''s signed `tatlock_user` claim (production `jpmschweitzer`), forwarded by the Office — and every query scoped by it. Not in migration 005 as written; add it while amending 005. The Scribe''s tables (workspace T-176) carry the same owner on addresses.
**Isolation (D-73).** Every Book query and write filters on the owner tenant; a test proves an `llm_tester` caller cannot read, change, link to or close a `jpmschweitzer` record (and the reverse) — the error is `not_found`, so another tenant''s ids are not even confirmed to exist.
**Split 2026-10-19:** the short outward ids and the owner tenant (amending 005), and the Book''s tenant filter, moved to **T-7**, so core-api can release safely. T-2 keeps the remaining verbs and work/personal as tags.
**When the Book''s archive/trash verbs ship:** its `view=archive` filter (`src/domains/book/service.py`) matches `archived_at IS NOT NULL` alone, so a record both archived and trashed would show there. The Scribe''s (1.14.0) is archived and not trashed — trash wins. Make the Book agree. Found in round core-api-T-13-001.', 'backlog', 'high', NULL, NULL, NULL, '2026-10-19 09:20:45.104', '2026-10-19 20:58:36.678', NULL, 'fb33d075391ff2771a3361218f0035b7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GNBXC7H3JBESKFS20BV71BJG', 'task', NULL, 'Migration round-trip tests leave core_api_test behind head', 'Each migration file''s round-trip test re-upgrades to its own revision, not to head (tests/test_tenants_migration.py ends on 008), so since 009 a full suite run leaves core_api_test one behind head and the next manual check starts from a stale schema. End every round-trip test on head. Found in round core-api-T-13-001.', 'backlog', 'low', NULL, NULL, NULL, '2026-10-19 20:58:36.809', '2026-10-19 20:58:36.809', NULL, '52f32db6c5ddcdd77f3eb995a64e8ba5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;