Private work ledger: Review my work — design spec
Status: in-progress · Author: North OS product engineering · Date: 2026-09-15 · Repo: north-os · Related: implementation plan, cross-source intelligence.
Current implementation scope: Plan §61 governs PR #444: foundation-only corrections, not the complete feature. It supersedes stale completion/limitation claims without authorizing a service, UI or MCP implementation. Identity mapping initially remains empty pending the sibling directory writer. Final corrected SHA/CI and fresh final plus independent-family review are outstanding; not merged or deployed. Broader spec status remains in-progress.
PR #444 R2 boundary correction: exact membership plus current direct-session/MCP-grant authority is required before the resolver signs and is revalidated through A5 by prepare and scan under both actor GUCs. Token-referenced sessions are locked in sorted/deduplicated order before clients/tokens and discovered links are rechecked; durable grants survive browser-session deletion. Pin expiry bounds admitted scan work and disclosure; cancellation prevents subsequent queries after started work settles. These are foundation corrections only; final independent-family review remains outstanding.
PR #444 R3 boundary correction: legacy stored actor/activity/user/matter keys are canonical positive safe integers, not fresh lossless int64 strings. Unsafe entries and differing-origin entries cannot enter projection or overflow; their coverage still refuses the named figure, while NULL-origin rows remain visible. A5 uses wall-clock expiry, revalidates after corpus work and supplies a transaction-local non-authorizing expiry bound for the final post-commit disclosure check. Ordinary refresh rotation preserves existing access authority; actual access/consent revocation denies. KMS runs outside authority locks; prepare rechecks authority and the locked connector/credential fence before signing. Exact Graph attempt/record-cap completion stays complete. The dedicated isolated-DB CI gate includes the existing real resolver test. Foundation only; no independent-family approval or release claim.
PR #444 R4 boundary correction: the recency seam applies the same positive safe-integer boundary to its stored actor link before attribution. Missing expected-origin provenance independently makes the named Clio figure unavailable, including for an empty corpus, without withholding NULL-origin rows. The personal Sent Items reader opts out of hidden SDK redirects as well as retries so its five-attempt budget bounds transport requests; other Graph consumers retain the SDK defaults. Neutral synthetic provider fixtures replace firm-specific literals without changing the existing ProviderContext ABI. This is still foundation-only scope pending fresh final and independent-family review.
In progress. Plan review CLOSED at §58. Plan §59 supersedes every personal-Clio-enrollment requirement here — a per-user Clio connection is not permitted to exist, and viewer identity comes from the organization directory joined on verified email. Plan §60 cuts the review UI: /matters is a visualisation owned by the sibling lane, MCP is where input, action, reading and requests happen, and every reference below to /tasks/chats/new?review=work is superseded — that route is going away. Preserve plan §26 staging-refuse (after green-lit reuse :604–609 and after existing [ -e "$BG_RELEASE_DIR" ] refuse :610–612, before set_phase :615). Introduce no new discriminator (no BG_RELEASE_REUSED); leave existing BG_RELEASE_PROMOTED at :55/:607/:723 untouched. J1 edit at bluegreen.sh:727–738: source="${BG_RELEASE_DIR}/deploy/preprod/bin/${name}.sh" (do not phrase $BG_RELEASE_DIR as current fact; price inside J1's 190). The hook refresh runs at :1511, before staging at :1515, so it cannot read $BG_RELEASE_DIR; skip is the only safe form. bg_refresh_post_receive_hook marker skip stays load-bearing, untouched. Hook skip (return 0, no copy) when the marker ≠ DEPLOY_SHA, or when the marker is absent and the active floor minimumVersion ≥ 1; do not abort; bg_stage_release owns the only refusal. Settled S/A, MCP IDs-only, exact generation, query-param /tasks/chats/new?review=work, and the durable assistant message marker. Digest guard: inside ContextDigestRepository.upsert's withRlsTransaction, mixed threadId throws before the write; the uniform-threadId construction is INSERT … SELECT … WHERE NOT EXISTS(marker) … ON CONFLICT DO UPDATE, correlated on the asserted single threadId; do NOT FOR SHARE/FOR UPDATE chat_thread from digest. B7 projection + overflow + null-date + null-user + non-visible + unstamped + below-epoch + unverified-origin + fingerprint share one REPEATABLE READ snapshot via existing packages/db/src/sync-tx.ts, committed after those reads and before any exact-entry validation call. Unstamped: shared population ∧ source_authorization_epoch IS NULL → refuse; nothing else inside that EXISTS. Below-epoch literal as on the B7 card. source_origin IS NULL is (11) only (complete figure unavailable; do not withhold rows). When non-null, source_origin <> $personalVerifiedOrigin is (6) identity-unverified. Delete source_origin IS NULL OR source_origin <> … as a single arm. Recorded-time projection, canonical/dismiss keys, and exact GET require source_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Existence of non-null differing origin is (6) coverage EXISTS that refuses the named figure; those rows are not keyed as personal and do not occupy the personal LIMIT 200. Matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows. Unseen is labelled only; delete the hard-incomplete arm of countEligibleNonVisibleCoverage. Ingest-connector set is type='clio' AND scope='organization' AND status='authorized'; revoked leftovers out; still no decrypt/refresh/who_am_i on firm ingest. Clio durable anchor is verified account (canonical regional origin + data.account.id) + activity ID; connector_id is a selection predicate and non-key provenance snapshot, not part of the unique/dismiss key. J stays three paths /610. With active floor minimumVersion ≥ 1, marker REQUIRED on the staging path: absence → bg_stage_release refuse, refresh skip; green-lit reuse unaffected; J3 absent-marker green-lit-reuse succeeds. No active floor → absence dormant. J3(e): run_deploy writes the marker for the SHA it materializes, simulating J2 (fixture-only; J2 stays the only production writer); (a)/(b)/(d)/(g) materialize a different SHA first; (e)'s absence arm removes or omits the seed. Complete map: 118 paths / 20,100 prospective lines. Recorded-time projection, canonical/dismiss keys, and exact GET require source_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Differing-origin rows are (6) coverage EXISTS, not keyed as personal, not in LIMIT 200; matching-origin and NULL-origin rows are not withheld. (11) NULL-origin complete-figure-unavailable do not withhold. Named key provider_metadata.personal_user_id. Enum stays 11 members. Do not add member (12).
Design for review: a private, saved review ledger, not an activity dashboard. Explicitly reconstruct bounded work from attributable personal sent mail; reconcile against the lawyer's locally ingested Clio time for eligible tracked matters, never a purported complete personal Clio week. Keep human corrections, uncertainty and evidence separate. One Home/matter review prepares copyable time records, involvement summaries and matter-update drafts.
1. Scope and lane authority
- Personal only, for the authenticated owner in the active organization. Linking a matter never shares or publishes an episode. A colleague or same-organization administrator has no ledger access.
- Home remains
/tasks/chats/new, with Today / This week / Review my work above LiveChat and the same structured review in query mode. This is F4, this lane's own surface, with NO NOS-384 hold. Matter Review stays independent of admin showActions/onAction; no chat-ui package or new dashboard route. Matters-only ownership gate: NOS-384 owns matters-page/table integration, so F5 and any matters-page wiring await explicit orchestrator handoff. Ordinary implementation gates still apply to Home. Cross-workspace messaging was blocked and the factory notified; no entrypoint capability is dropped. - Explicit, bounded reconstruction only. Page navigation, summary generation and saved-ledger reads never initiate reconstruction. Existing PR #431 source ingestion continues independently; no ledger worker, timer, cron or autonomous schedule.
- v1 reconstructs attributable personal sent mail and reconciles eligible tracked Clio TimeEntries only. It excludes meetings and North document drafts; F3's per-source panel labels that absence. This is not a complete personal-work claim.
- No automatic Clio writes, billing, email sending, monitoring, organizational aggregation/ranking, QuickBooks, new telemetry integration, NetDocs work reconstruction or adjacent refactors. Existing Clio write refusals stay intact.
- Immutable lane
private-work-ledger, branchfeat/private-work-ledger. The completed transplant and current merged-main base below define the review delta; the old dependency boundary is historical only. - Post-transplant queue — 2026-09-15: #431 squash merged as
c767e888b10f4e369e2a740f444b5e8fb85659ef; the orchestrator completed the ledger-only transplant and restored its documents. #432 now owns the exclusive schema-generation slot. This lane has NO slot and must not run db:generate until an explicit later factory grant. No product, merge/deploy or git authority follows.
2. Completed transplant and stale source evidence
Orchestrator-reported current base: PR #431 squash merged at c767e888b10f4e369e2a740f444b5e8fb85659ef. The explicit rebase --onto current origin/main completed; reported HEAD equals that squash commit with zero commits ahead and no squash-parent replay. Restored ledger docs/research are working-tree material; the safety stash remains with the orchestrator. Historical immutable old boundary b92e62b919790a79410c59dc307a74d2f6ba463d was used only for that transplant, NOT as the current review/migration delta base. Review the restored ledger delta against current origin/main, including untracked/deleted content; reuse the merged dependency contracts without replaying their history. This seat performs no git actions.
Evidence invalidation applies to this ENTIRE page and plan: ALL pre-transplant reviews, repository source-line/symbol/caller/lock/permission assertions, inspected-source tables and prior runtime/DB/gate/CI claims are stale against the new base, even where historical wording says “verified”, “current” or “source proof”. Retain them only as review history and intended contracts, not fresh evidence or permission to implement. Re-read current-main sources and all affected callers, then obtain a fresh independent pair and RLS re-review; rerun applicable gates on the eventual implementation head. The durable provider report remains unchanged: its primary documented facts retain that evidence level, but its repo citations also need a fresh pass and effective registered synthetic grants remain unproved.
Narrow merged-base refresh: plan §§14–15 record three Astra and eight Opus dispositions with fresh authority/FK, transport, registration, rollback/deployment, proxy/mint, Clio origin and MatterTable observations plus the installed checkpoint serialization trace. Only those cited source observations are refreshed; other historical claims are not re-certified. No SQL race, shell/runtime activation, OAuth or checkpoint persistence experiment has run.
| Observed source | Contract and ledger consequence |
|---|---|
packages/db/src/schema/clio_activity.ts, clioActivity; packages/db/scripts/provision-roles.ts, TABLE_GRANTS | Unique organization/connector/source ID; provider userSourceId, date, note, TimeEntry hours, visible/cleared/unseen states. Only sync_role reads/writes. There is no app_role SELECT and no agent reader. Retain that boundary. |
packages/clio-sync/src/activity-sync-state.ts, CLIO_AUTHORIZATION_EPOCH_SQL, parseActivitySyncState; apps/worker-clio/src/loops/activity-sync.ts, commitPage; packages/clio-sync/src/activity-events.ts, applyActivityPage | Page/checkpoint commits are epoch-fenced. Named CLIO_AUTHORIZATION_EPOCH_SQL is currently declared at apps/worker-clio/src/loops/activity-sync.ts:580; move into activity-sync-state.ts (A10), re-export from A11. Shared SQL returns text; B7 casts. Unstamped comparison numeric. B7 uses that named expression with numeric/bigint comparison. Add nullable source_authorization_epoch bigint (or text compared via ::bigint) and nullable source_origin text (ingest-side normalized origin, connector-clio origin helper). A new reader must not relabel old rows as current merely because the connector reauthorized. A6 stamps epoch and ingest-side normalized origin in the same delta. Keep A6 clear-writer NULLs of date/user/matter. cleared_reason is unused by this lane; no feeder edit. |
packages/connector-clio/src/auth.ts, completeAuth, bundleFromToken; apps/web/lib/connections/clio-oauth.ts, beginClioConnect, completeClioConnect | Existing Clio OAuth is organization-scoped. Metadata starts empty, then stores API host/token type. No proven app-user to Clio-user binding. Firm credential custody is not personal identity. |
apps/connectors-api/src/lib/connectors.ts, createConnector, persistCredentials; packages/db/src/schema/connectors.ts | Generic user-scoped connectors and encrypted provider metadata already exist. Reuse for a separately authorized personal Clio identity, without changing the firm's ingestion credential or adopting email/display-name matching. |
packages/agent-runtime/src/tools/mail-account.ts, resolveMailAccount; email-access.ts, resolveOutlookContext | Personal account resolves by organization + user; shared mailbox is an explicit alternative. Ledger reconstruction selects personal only. Current token result lacks account identity and lossless source epoch, so add a narrowly scoped verified source snapshot. |
packages/connector-auth/src/tokens/encrypt.ts, loadTokens/persistTokens; apps/connectors-api/src/lib/user-tokens.ts; apps/web/lib/connections/outlook-oauth.ts | loadTokens selects LIMIT 1 across the user's account rows; enrollment retains old rows, then the web caller separately PATCHes authorized. A connector/token join cannot prove which account is active. Bind selection, encrypted credential and grant epoch atomically at enrollment; strict ledger snapshot reads reject unbound legacy state. |
packages/provider/src/adapters/msgraph/email.ts, GraphMessage, graphMessageToMailMessage, fetchEmailsPage, getThread | Current projection keeps from/received time, not sender/sent time/changeKey/uniqueBody. General listing groups conversations, and getThread reads a first page. Do not mistake these for complete personal activity proof or reuse their coverage claims. |
packages/connector-netdocs-mcp/src/types.ts, NETDOCS_TOOL_NAMES, NetDocsSearchHit | Search/fetch/browse/attributes; hit has optional created_by and modified. No demonstrated editor identity, edit event stream, audit feeder or effort measure. Created_by is not proof of the caller's work this week. NetDocs remains live correspondence/search, not reconstruction input. |
packages/db/src/schema/working_profile.ts, workingProfileAnswer; schema/agent_memory.ts, agentMemoryCandidate; apps/web/lib/matters/hidden.ts, hideMatter | Reuse permissive organization + restrictive owner policies and withRlsTransaction(..., { userId }). Do not copy the working-profile admin SECURITY DEFINER reader, promotion workflow, or matter-shared memory scope. |
packages/agent-runtime/src/checkpointer.ts, getCheckpointer; packages/chat-runtime/src/message-visibility.ts, filterInternalMessages | Graph checkpoints persist full execution state. Message visibility currently removes a legacy marker-only message, not general source-tainted content. Existing NetDocs naming is not evidence that ledger privacy already works. |
packages/chat-runtime/src/run.ts/resumable.ts; tools/web-research.ts | Generic chat persists checkpoints/history and Redis SSE, while web_research sends model arguments externally. Therefore no ledger provider content enters generic product or MCP tool results at all. Test these real downstream seams unchanged; do not build a taint/checkpoint serializer framework. |
packages/agent-runtime/src/tools/summarize-matter-activity.ts, summarizeMatterActivityTool | Existing general matter synthesis gathers mail/documents and optionally saves a document. It is not a personal ledger summary and must not gain implicit access to these tables. |
Completed primary-source research: the durable provider capability preflight report at repo path docs/research/2026-09-15-private-work-ledger-provider-preflight.md (H4; not a docs-site URL — docs/superpowers/serve.ts roots at docs/superpowers and has no .md type) records the checked Clio OpenAPI/authorization/permissions sources [C1–C5,C10] and Graph message/list/get/attachment/identity contracts [M1–M15]. It establishes the documented Clio regional-origin + data.account.id discriminator and the precise Graph limitations below. It does NOT establish the registered app's effective permissions, nested-field visibility or any authenticated combined-query response: no account token/API calls were made. Existing test accounts are the chosen preflight route, but identifiers, fixture manifest, grant-setting evidence and secure runner inputs have not been supplied.
3. Source admission and reliable attribution
3.1 Personal sent mail first
- Resolve an atomic durable Outlook binding, not an arbitrary token row. Store nullable server-owned
connector_user_tokens.outlook_account_bindingon the selected sync-only credential row, NOT on the broadly readable connectors table. It contains version, connector_id, account_id, credential_id, enrollment_member_id, lossless grant_epoch and credential_revision; row identity/account/owner/org must match. A partial unique index permits at most one bound Microsoft row per organization/user/provider. Strict reads select this exact bound row and matching revision, never LIMIT 1 or a connector-column fallback. Retain credential_id's exact-row invariant. Grant epoch records enrollment selection; connector authorized_at remains a separate source epoch. Verify credential-specificGET /me?$select=id,mail,userPrincipalName,proxyAddresses: compare /me.id to the verified enrollment oid with directory tid context, never email/UPN/display name as account identity. Returned mail/SMTP proxyAddresses may attest aliases; otherMails and app-user domains do not. Actual alias visibility remains a grant-proof gate. - Add a specific provider reader, proposed
listPersonalSentWorkPage, besidefetchEmailsPage. Use delegated/me/mailFolders/sentitems/messages, the locale-independent Sent Items folder; never another mailbox as fallback. Selectid,conversationId,sender,from,sentDateTime,changeKey,uniqueBody,subject,webLink,isDraft,hasAttachments; sender is selected as a whole property, ImmutableId is a header preference, and attachment metadata is a separate collection. Filter sentDateTime by the half-open UTC range derived from the user's IANA timezone and order sentDateTime ascending, with that field first in the filter. RepeatPrefer: IdType="ImmutableId"on every page, exact GET and attachment request. Follow the entire @odata.nextLink unchanged, never manufacture $skip. Message-list default 10 and $top 1–1000 are provider contracts, not ledger budgets. IDs are case-sensitive and stable only within the same mailbox; archive-mailbox moves/export-reimport can change them. Pages are not a documented consistent snapshot, and changeKey is a version string, not a monotonic clock. - Require non-draft sent item and actual sender matching the provider-verified mailbox identity. Compare only addresses/aliases attested by that provider identity, never app-user email or names. Exclude delegate/send-on-behalf mismatches, shared mailboxes and unresolved aliases. Send As can make sender/from equal without identifying the delegate; sent copies may exist in either mailbox, both or neither. Label attribution as mailbox-sent evidence, neither audited human keystrokes nor exhaustive personal work; the lawyer can dismiss it as Not my work.
- Explicitly selected uniqueBody is documented as the part unique to the message, not guaranteed signature removal, human authorship or effort. The combined list projection is document-supported, not grant-tested. A missing uniqueBody is not empty authored text: P1 compares a known nonempty synthetic reply with exact
/me/messages/<immutable-id>?$select=id,changeKey,sender,from,sentDateTime,uniqueBody,isDraft. If list fails/omits it, record that result and revisit the request budget rather than add silent exact-read fanout. List prose/header documentation conflicts on text format; use HTML/default and parse itemBody.contentType, without assuming text conversion. Exact GET documents text/html and Preference-Applied. Extract substantive newly authored content, not quoted incoming text/signatures; empty acknowledgments, automatic replies and ambiguous text never imply effort. No message proves every assertion in it or a duration. - Existing personal mail classifications and matter associations can suggest matter IDs. They do not independently prove activity. Received mail, related threads and attachments are supporting context only, fetched on demand with current access. No inherited author attribution from a thread's owner, recipient or attachment.
- Attachment metadata is supporting context only: use separate
/me/messages/<immutable-id>/attachments?$select=id,name,contentType,size,isInline,lastModifiedDateTimefor file/item/reference base metadata. Never request contentBytes, $value or expanded item bodies; @odata.type is response metadata, not a selected base property. hasAttachments excludes inline attachments, so false cannot prove no attachments. Aggregate cap 100 records and 5 metadata HTTP attempts/pages including retries across the invocation, ≤ 4 concurrent requests, inside the shared source deadline. Graph documents no numeric attachment page-size/$top guarantee: follow nextLink only if returned; locally truncate/label an oversized page and stop scheduling at capacity. These are local admission limits, not a response-byte bound. Never use generic enrich fanout. Delegated Mail.Read covers these reads; Mail.ReadBasic excludes needed body/attachments, User.Read separately covers /me identity. No Mail.Send, Mail.ReadWrite, shared-mail or application-wide permission is required; actual consent remains untested.
Per-message and episode attachment coverage: use the existing coverage result, initialized to attachments_unchecked for every message not yet scheduled. Budget/deadline exhaustion before scheduling leaves it unchecked; hasAttachments=false or zero retained rows never establishes absence. A cut page, unfinished nextLink traversal or failed/incomplete listing is partial. Verified-empty requires that message's successful COMPLETE collection traversal, with no truncation and zero records; complete nonempty is distinct. C aggregates across every relevant message after combine/split without upgrading unchecked inputs. Saved reads lacking current coverage stay unchecked and do not trigger attachment requests. No schema field, new evidence row, detail-fetch endpoint or larger budget. F3 shows Attachments not checked/Attachment check incomplete and scoped complete-empty evidence in the existing web coverage display; MCP exposes safe enums/counts/owned IDs only, no labels, names or provider identifiers. G5/G10 prove scheduled-empty, cut-page and unscheduled-after-exhaustion cases, including inline/hasAttachments=false and mixed episode inputs. Plan §24.2 defines the bounded proof.
Bind enrollment at START: beginOutlookConnect resolves a live DIRECT session, active organization, current member.id and owned connector/original generation before provider work. Signed return-state binds session ID, actor/org, enrollment_member_id, connector/original generation and nonce/expiry; consume PKCE once. The callback and strict internal writer independently recheck that SAME direct session and membership incarnation, including after remote work. Remove/rejoin with a new member.id refuses the old START and stored enrollment even when user/org/provider IDs are unchanged. Never substitute callback-time membership/generation or reauthorize by refreshing an old token. Use the existing Outlook-local signed context, not a generic connector-interface rewrite. Error paths cannot mark a newer enrollment through fresh lookup.
Named START fence: A8 adds nullable connectors.enrollment_generation timestamptz (mode:string, no default/backfill). START reads/signs the exact nullable value without advancing it; callbacks and strict B9/B18 commits compare the original under lock using IS NOT DISTINCT FROM. Success atomically advances it with GREATEST(clock_timestamp(), COALESCE(previous, '-infinity'::timestamptz) + interval '1 microsecond'), returning the lossless value. All B9/B13/B16/B17/B18 and personal Clio B3/B4/B5 plus wire/receipt callers use this field, never updated_at/authorized_at, credential revision, source epoch or OAuth generation. First committed completion wins shared START snapshots. Scoped errors compare before invalidating; legacy Microsoft replacement/disconnect advances the fence with selection invalidation. Ordinary credential refresh preserves it. Selected Outlook binding stays sync-only JSON; authorized_at advances separately on real authorization. Plan §22.2 maps null/same-clock/ABA/rollback proof; same scheduled schema unit after #432.
Credential revision, not a connectors column: credential_revision remains the named lossless field inside the selected connector_user_tokens.outlook_account_binding JSON. B9, B11, B12 and legacy Microsoft write bump it under the selected-row lock; strict reads compare that exact revision. Distinct from A8 enrollment_generation. No extra A column.
Atomic strict Outlook commit: prepare provider verification and ciphertext before acquiring transaction locks. In the §3.4 root/membership lock order, persistTokens hooks then lock the owned connector and exact token rows in deterministic order, compare original generation/member ID, forbid ownership transfer, clear prior selected bindings and write the credential plus selected-row binding and authorized state atomically. No ordinary reader sees the authoritative binding; app_role/scheduler_role retain existing connectors access but no connector_user_tokens access. Legacy Microsoft persistence clears selection metadata in the credential transaction; canonical fenced refresh preserves membership/grant/selection and atomically advances only matching credential revision. NetDocs and ordinary mail callers keep their contracts, never gain strict authority by omission. Set generation/enrollment epoch strictly above locked prior values with GREATEST(clock_timestamp(), previous + interval '1 microsecond') and explicit null initialization. Capture credential ID/revision with the bearer from the same write/locked transaction, never a postcommit pairing. No separate authorized PATCH on acknowledged strict completion.
One-release expand/contract, not mandatory new fields on the old wire
Keep POST /connectors/outlook/user-tokens and its request/response operational for old web during release N. New API's legacy Microsoft persistence clears connector_user_tokens.outlook_account_binding and advances the owned connector generation atomically; a later old-web authorized PATCH cannot restore selection. Add GET /connectors/outlook/enrollment-capability returning protocol 2 and POST /connectors/outlook/enrollments returning a strict receipt with connector, original/new generation, membership incarnation, grant epoch and selected credential revision. Existing route/client/type files own these changes. New web requires the exact receipt; generic {ok:true} is never proof. Legacy enrollment remains unverified, not an impersonation bypass into strict ledger use.
New web against old API: missing capability or dedicated endpoint means bounded Unsupported enrollment version before any legacy credential write, no strict identity claim and no fallback to old persistence. Existing mailbox use remains operational; new enrollment requires the new API. Old web against new API: ordinary enrollment continues, but ledger source identity is deliberately unverified until strict enrollment. During mixed API/worker versions, source reconstruction remains disabled; a credential revision mismatch additionally catches old-binary writes that cannot clear new metadata. New reader never interprets an old token response as a strict snapshot. Tests exercise both version pairings and rollout/rollback, not only same-version mocks.
Deploy additive schema/API first, then new web/workers. Keep release-N legacy handling only for the one-release overlap; after all callers and the rollback floor are on protocol 2, release N+1 removes the old user-tokens enrollment endpoint/client and web's temporary legacy-state callback branch in these same files. Generic connector OAuth primitives remain ordinary, never a strict ledger-enrollment alias. No permanent compatibility alias, no rejection of old requests in N, and no completion claim for cutoff before its window has elapsed. Separately, ledger admission stays disabled until EVERY share-serving instance enforces marker-aware refusal. After any admission, every rollout/rollback build must retain D7 refusal and D9 visibility for admitted threads per §7; disabling capabilities never permits return to a vulnerable prior application binary.
Default one generated episode per attributable sent message. This boring identity rule prevents window-dependent regrouping. The review supports explicit combine/split; reconstruction never redoes human grouping. A long substantive reply can still have unknown effort.
3.2 Narrow personal Clio time reader
Proposed personal identity connection: connectors.type = clio, scope = user, owner_user_id = authenticated user, owner reach. The existing organization-scoped connector remains the only ingest feeder. Personal OAuth verifies identity and current read permission; it must never schedule initial sync or become an organization connector. Application permissions are configured in Clio's developer portal and fixed on grant, not selected by a personal-only runtime scope array. The data transport remains GET-only.
- Personal OAuth uses distinct signed app:connector_callback:clio-personal: state, binding org, owner, DIRECT session ID, current enrollment_member_id, connector/original generation, nonce/expiry and return target. Verify signature before error handling; reject impersonation, lost membership or remove/rejoin before exchange and again at commit. Forged/stale personal state cannot mark the firm connector. Direct imports from clio-oauth.ts; no assumed barrel. Identity verification precedes atomic strict personal commit, never a separate authorized PATCH.
- Proposed
readCurrentUserIdentityin publicly re-exportedconnector-clio/src/auth.tsuses unchanged ClioClient.json for extensionlessGET /api/v4/users/who_am_i?fields=id,account{id}, braces URL-encoded. Native Response.json does NOT preserve all int64 numbers. Validate decoded data.id and data.account.id before coercion: numbers require Number.isSafeInteger and positive value; strings require canonical positive ASCII decimal within signed int64 (§3.2 below). Otherwise identity stays UNVERIFIED. Account key is verified stored producing regional origin + validated account ID, never cfg/env/default. Use the existing allowed HTTPS regions. Personal who_am_i remains for personal identity. Do not call who_am_i on firm ingest credentials or decrypt/refresh firm ingest. Origin comparison is B7 against the A6/A7 stamped ingest origin, not a firm who_am_i. Exact activity user.id must match personal ID. Missing/redacted/unsafe IDs, unverified origin or denied identity stays unverified. No separate Account endpoint, guessed scalar, roster/email/name inference or cross-region numeric matching. No lifetime/migration stability or actual-grant proof is claimed. - Strict PERSONAL enrollment prepares provider results/ciphertext before locks, then follows §3.4 root/membership ordering and acquires the EXISTING refresh advisory
pg_advisory_xact_lock(hashtextextended(connector_id::text, 0))before connector/credential writes. Compare original member ID and START generation; commit encrypted credential, verified metadata and authorized epoch atomically. Reserved encrypted metadata includes enrollment_member_id andpersonal_user_id(who_am_idata.idpersisted at enrollment as a canonical decimal string); missing/old membership binding is unavailable and refresh cannot invent it. No new path, no new column. Generic persistCredentials and worker refresh fence remain unchanged: refresh locks before reread, preserves provider_metadata and holds the advisory through writes. No new row locks on every provider refresh or organization lock rewrite. - Reject legacy PERSONAL Clio credential writes at the sole generic HTTP writer before persistCredentials, using the stored connector type/scope/owner, never the caller's :type label. Personal writes require the strict endpoint; a later generic authorized PATCH cannot fabricate verified metadata. This is refusal without any credential/status write, not unsynchronized invalidation. Organization-scoped credentials keep their existing path. Enable strict personal enrollment only after every API writer runs this guard; during mixed old API versions it stays disabled. No broad org lock rewrite.
- Implement
readPersonalClioTimein connectors-api/lib/clio-activities.ts behind the narrow personal read route. The internal caller forwards its authenticated actor; the reader independently checks membership, ownership, both connector postures/epochs, account/actor and matter eligibility. Resolve current identity once per distinct connector per request, memoized only for that request; exact entry validation reuses that identity, with final epoch checks. - Under sync_role, select only that mapped actor's visible
TimeEntryrows with current row epoch, bounded date range and optional authorized matter, from the ingest-connector set: type='clio' AND scope='organization' AND status='authorized' (revoked leftovers out). Three org-Clio branches: (1) authorized exists → parse that row’s scan-state; (2) authorized empty AND any org Clio row EXISTS (no status filter) → ingest-not-authorized; status source: partial-unique live org Clio row when it exists, else any leftover; extend partial-reason enum to cover revoked/archived; (3) zero org Clio rows → missing-feeder. Do not parse scan-state of a non-authorized row. G5/G10 pending case. At most one authorized org Clio connector (connectors_org_scope_unique); isolation fixture is authorized vs revoked leftover. Do not decrypt or refresh firm ingest credentials, and do not call who_am_i on firm ingest for the personal ledger. Feeder stamps ingest-side normalized origin (A7 (apps/worker-clio, already depends on connector-clio) stamps origin: passes rawprovider_metadata.api_host(NULL if not a string) to the connector-clio string-returning origin helper exported frompackages/connector-clio/src/clio-client.ts(export-only; wrap/catch ClioApiError); A7/helper normalizes; stamps the helper return value, not.origin. A6 does not callnormalizeHost. Absent or throw → explicit NULL. Never rawapi_host. Never ClioClient.baseUrl/env default. Pre-B3 rows stamp NULL. A origin stamp ships only after B3 preserves verified origin; until thensource_originis NULL and B7 (11) null-origin refuses (complete figure unavailable; do not withhold rows)) ontoclio_activityin the same A6/A7 delta assource_authorization_epoch. When that stamp is absent, (11) null-origin (complete figure unavailable; do not withhold rows). When the stamp is non-null and differs from the personal verified origin, (6) identity-unverified refuse. B7 compares that same helper output to personal verified origin in the same REPEATABLE READ snapshot. No who_am_i. Do not filter on usable hours or quantity_redacted. B7's internal result is a bounded row projection, not keys+totals only: canonical key (verified account: canonical regional origin + data.account.id + activity ID;connector_idis a selection predicate and non-key provenance snapshot, not part of the unique/dismiss key; dismiss key is personal verified account + activity ID; no stored ingest account, no who_am_i; cross-account isolation rests on unproved global uniqueness of Clio user ids; drop ingest-same-account-as-personal comparison; dismissals survive disconnect+reconnect of that personal account), date/matter, visible-or-unknown minutes, lossless hours as the numeric string, redacted flag, authorized note/URL, row epoch, plus source population fingerprint (projection,countEligibleRecordedTimeOverflow, named coverage EXISTScountEligibleNullDateCoverage, named coverage EXISTScountEligibleNullUserCoverage, named coverage EXISTScountEligibleNonVisibleCoverage, named coverage EXISTScountEligibleUnstampedCoverage, named coverage EXISTScountEligibleBelowEpochSameOriginCoverage, NULL-origin arm (own enum member, same class as unstamped — awaiting feeder; user sentence is spec §10.1 null-origin): shared population ∧source_origin IS NULL→ null-origin refuse. Mismatch stays member (6): shared population ∧source_origin <> $personalVerifiedOrigin→ identity-unverified refuse. Recorded-time projection, canonical/dismiss keys, and exact GET requiresource_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Differing-origin rows are not keyed as personal and do not occupy the personal LIMIT 200; matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows,parseActivitySyncStateof the authorized ingest connector'sactivity_sync_statein that same REPEATABLE READ snapshot) and a separate round-once aggregate minutes from the same snapshot. Never rates, prices, expenses, tokens, arbitrary SQL or broad app-role SELECT. DROPlast_seen_atas a work-window predicate. Shared population (stated once; quoted only by unstamped / below-epoch / origin EXISTS): ingest-connector ∧observation_state='visible'∧ type='TimeEntry' ∧ (matter only when selected) ∧ date in window ∧ (user_source_id = $actor).$actoris this request's resolved who_am_idata.id, never the enrollment-stored personal user id. Order: single identity resolution → RR snapshot → commit → exact validations. Before the snapshot: if live who_am_idata.id≠ enrollment-storedprovider_metadata.personal_user_id, refuse identity-unverified (reuse (6) H string), zero rows, no snapshot. Request-level admission, not a row predicate. Do not add member (12). Named coverage EXISTS for noncanonical storeduser_source_idin the selected window (visible TimeEntry, ingest-connector, date/matter) that is not equal to$actorfires member (10). No complete-empty / silent omit. Identity GETs stay in the 20-second source phase, not C prepare. Non-visible, null-date and null-user keep their own literals. Unstamped NULL-user does not refuse (labelled via null-user). Cleared rows ignored everywhere. Named coverage EXISTScountEligibleNullDateCoverage: any currently eligible visible TimeEntry withdate IS NULLin the actor + ingest-connector set (and selected matter only when the request selected a matter) is hard incomplete. Narrowing matter can clear it; narrowing dates cannot. Out-of-connector/actor rows do not count. Matter predicate only when selected; week-wide uses the unfiltered eligible set. Named coverage EXISTScountEligibleNullUserCoverage: any currently eligible visible TimeEntry withuser_source_id IS NULLin the ingest-connector set (and selected matter only when the request selected a matter) is labelled incomplete, not org-wide refuse. Literal predicate: ingest-connector set ∧observation_state='visible'∧ TimeEntry ∧user_source_id IS NULL∧ (matter only when selected) ∧ (date BETWEENbounds ORdate IS NULL). No actor predicate. Windowdate BETWEENbounds ORdate IS NULL. NULL-user is labelled. Both-NULL row is labelled, not refuse. G5 and G6 assert visible redacted-user is labelled incomplete:countEligibleNullUserCoverage> 0 does not refuse the named figure. Do not change mapActivity in this pass; nested visibility still P1. Drop “awaiting feeder” as the clear path for redaction. G5: an out-of-window null-user row must not refuse. Partial reasons cite the B7 enumerated list. Named coverage EXISTScountEligibleNonVisibleCoverage(one literal, no cross-reference, no OR true; labelled, never refuse): ingest-connector ∧observation_state='unseen'∧ type='TimeEntry' ∧ (matter only when selected) ∧ (date BETWEENbounds ORdate IS NULL) ∧ (user_source_id = $actorORuser_source_id IS NULL) ∧matter_id IS NOT NULL. Other users’ unseen must not set the flag. G5 one labelled-flag assertion that bites, not refuse: an in-window actor-or-null unseen TimeEntry withmatter_id IS NOT NULLsets the flag; another user’s unseen does not. G5: clear a row, runmarkActivitiesUnseen, flag stays off for a different actor. Keep it in RR/fingerprint. Keep A6 NULLs. Drop thecleared_reasonCHECK from this lane.cleared_reasonis unused by this lane; no feeder edit. No generated CHECK, no hand-edited SQL, no deploy-path backfill. §8: redacted-matter entries leave the named figure with no signal. Named coverage EXISTScountEligibleUnstampedCoverage: shared population ∧source_authorization_epoch IS NULL→ refuse. Nothing else inside that EXISTS. Precedence: origin mismatch/NULL wins over unstamped when both true. Named coverage EXISTScountEligibleBelowEpochSameOriginCoverage: shared population ∧source_authorization_epoch IS NOT NULL∧source_authorization_epoch< (authorized ingest connector’sCLIO_AUTHORIZATION_EPOCH_SQLfromconnectors.authorized_at) ∧source_origin = $personalVerifiedOrigin. Labelled, sealed, not G5(b) refuse.source_origin IS NULLis (11) only (complete figure unavailable; do not withhold rows). When non-null,source_origin <> $personalVerifiedOriginis (6) identity-unverified. Deletesource_origin IS NULL OR source_origin <> …as a single arm. Recorded-time projection, canonical/dismiss keys, and exact GET requiresource_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Existence of non-null differing origin is (6) coverage EXISTS that refuses the named figure; those rows are not keyed as personal and do not occupy the personal LIMIT 200. Matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows. Drop “state epoch equals current” until a full scan completes under the new epoch. Seal unstamped/below-epoch/origin with the fingerprint. G5/G6/G10. G10 matter-filter affordance for null-date refuse. Null-user labelled flag/fingerprint only. Enumerated partial-reason list (implementation notes, sole technical authority on this B7 card for source-eligibility reasons; operational reasons (budget exhaustion, 20s deadline, cancellation, mid-request authorization loss) are owned by H with strings in spec §10.1; lawyer-facing source-eligibility sentences live in spec §10 H register; every other technical mention cites this list): (1) null-date — refuse; implementation note: matter-filter only; never narrow dates; (2) null-user — labelled flag/fingerprint only; implementation note: labelled incomplete for visible redacted-user; drop “awaiting feeder”; (3) non-visible — labelled, never refuse; implementation note: unseen labelled only; other users’ unseen must not set the flag; requiresmatter_id IS NOT NULL; (4) unstamped — a complete figure is unavailable; do not withhold rows; implementation note: awaiting feeder reobservation; ledger refresh cannot repair; origin mismatch/NULL wins over unstamped when both true; unstamped NULL-user does not refuse (labelled via null-user); (5) below-epoch-same-origin — labelled, sealed, not G5(b) refuse; implementation note: awaiting full scan under the current authorization epoch; (6) identity-unverified origin — refuse; not unscoped; not on cleared rows; implementation note: origin differs from personal verified origin; those rows are not keyed as personal and do not occupy the personal LIMIT 200; matching-origin and NULL-origin rows are not withheld; NULL-origin is its own member; mismatch wins over unstamped when both true; (7) ingest-not-authorized including revoked/archived — refuse; branch (2); implementation note: organization Clio ingest is not authorized; (8) missing-feeder — refuse; branch (3); implementation note: no organization Clio connector; (9) validation-denial/hours-redaction — refuse named figure; implementation note: validation-time whole-entry denial or hours redaction; already-known quantity_redacted does not refuse; (10) unsafe-ID identity-unverified — withhold entry, named figure unavailable; implementation note: unsafe or non-canonical identity; never equal-by-rounding. Coverage EXISTS for noncanonical storeduser_source_idin the selected window (visible TimeEntry, ingest-connector, date/matter) that is not equal to$actorfires (10). No complete-empty / silent omit. (11) null-origin — a complete figure is unavailable; do not withhold rows; same class as unstamped — awaiting feeder; user sentence is spec §10.1 null-origin; implementation note:source_origin IS NULL. Never authorization or full zero totals. Overflow/200-vs-201 measured on the same set as the projection. Delete the pre-epoch-filter overflow refinement. Visible NULLuser_source_idis labelled incomplete coverage, never evidence of “not mine”. B7 comparesclio_activity.dateto inclusive local date bounds; derived UTC range is for sentDateTime only. Overflow over dated-in-range rows does not see NULLs. - Before disclosing an entry, validate current personal credential access to that exact entry and unchanged actor/matter. Whole-entry denial withholds it; hours-only redaction preserves the authorized entry and association with duration null. Validation-time whole-entry denial or hours redaction refuses the named figure (partial reason). An already-known
quantity_redactedingestion row contributes zero minutes and one unknown-duration count and does not refuse. Do not publish the B7 snapshot aggregate as complete when validations withheld rows/hours. Never reuse cached hours or fall back from null/redacted quantity to another numeric field. Changed visible fields require current source data, not stale cached text. An entry with hidden hours is not an absent entry. - Add nullable
source_authorization_epochbigint (or text compared via::bigint) to clio_activity, stamped by applyActivityPage from commitPage's locked epoch together with the ingest-side origin string from A7’s connector-clio helper. Delete “already held in activities.ts”. A7 passes rawapi_host; origin helper returnsstring | null(export-only frompackages/connector-clio/src/clio-client.ts; wrap/catch URL/TypeError and ClioApiError); stamp the return value, not.origin. A6 does not callnormalizeHost. A7 stamps NULL on any throw. Never rawapi_host. Never ClioClient.baseUrl/env default. P3 A7 stamps NULL only. Enable the helper stamp in P4 after B3 preserves stored origin, same release. Unstamped: shared population ∧source_authorization_epoch IS NULL→ refuse. Nothing else inside that EXISTS. Named coverage EXISTScountEligibleBelowEpochSameOriginCoverage: shared population ∧source_authorization_epoch IS NOT NULL∧source_authorization_epoch< (authorized ingest connector’sCLIO_AUTHORIZATION_EPOCH_SQLfromconnectors.authorized_at) ∧source_origin = $personalVerifiedOrigin. Labelled, sealed, not G5(b) refuse.source_origin IS NULLis (11) only (complete figure unavailable; do not withhold rows). When non-null,source_origin <> $personalVerifiedOriginis (6) identity-unverified. Deletesource_origin IS NULL OR source_origin <> …as a single arm. Recorded-time projection, canonical/dismiss keys, and exact GET requiresource_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Existence of non-null differing origin is (6) coverage EXISTS that refuses the named figure; those rows are not keyed as personal and do not occupy the personal LIMIT 200. Matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows. Drop “state epoch equals current” until a full scan completes under the new epoch. Unstamped comparison numeric. Shared SQL returns text; B7 casts. Old/null-epoch observations are unavailable after rollout/reconnect until the DEPENDENCY feeder reobserves them. Explicit ledger refresh cannot restamp, repair or force that observation. The existing full scan may take seven days to revisit unchanged entries, or indefinitely if the feeder fails; show unknown/incomplete, never absence. Keep feeder scheduling unchanged; epoch and ingest-origin stamping are the ingestion delta in already-mapped A6/A7. B7 compares that same helper output to personal verified origin. No who_am_i. Namesource_authorization_epochandsource_originonapplyActivityPage'sON CONFLICT DO UPDATE SETlist (activity-events.ts:145–166).
Narrow ID boundary, not lossless JSON: B3 exposes one validator through the existing public auth export, reused by B7 before mapping fresh exact activity/user/matter IDs. Numbers must be positive safe integers; strings must match [1-9][0-9]* and be ≤ 9223372036854775807 without Number coercion. Reject signs, leading zeros, whitespace, exponents, zero, negatives, fractions, nonfinite/unsafe numbers and overflow. String/BigInt after Number decoding cannot repair lost precision. Unsafe identity is UNVERIFIED; unsafe entries are withheld with explicit coverage, never equal-by-rounding. Never authorization or full zero totals.
Existing ingestion has lost numeric provenance: ClioClient.json:98–103 and page:125–137 use Response.json; packages/connector-clio/src/activities.ts:47–66 maps decoded pages, :80/:101–104 stringifies activity/reference IDs; packages/connector-clio/src/matters.ts:98–99 stringifies matter IDs. applyActivityPage sees only strings. B7 requires canonical positive safe-range stored activity/user/matter keys, including matter.source_id; above9007199254740991 or malformed keys cannot establish exact correspondence even if a fresh string matches. Check actor/selected matter before narrowed SQL, URL/equality joins or disclosure; return identity-unverified/incomplete coverage and unavailable full figures, never omission/false absence. Fresh genuine strings may validate through int64; legacy large mapped keys remain unusable. These are exact source citations, not mapper/client/schema edit additions; epoch stamping remains the only ingestion delta. G5 uses adjacent unsafe-number HTTP JSON plus real mapActivity, with safe controls. Saved human work/offline exclusion remains usable.
Stable producing origin across refresh: merged-base auth.ts:116–123 constructs refresh transport from cfg, and bundleFromToken :189–193 overwrites stored api_host from cfg/env/default. B3 corrects that already mapped adapter: established verified origin controls refresh and identity/exact transport and is preserved in metadata; an injected client on a different origin refuses before sending a credential. Fresh enrollment stores the actual normalized producing client origin with verified identity. Legacy missing origin may retain ordinary refresh transport fallback but never gains ledger-verified provenance from configuration alone. A deliberate region change needs fresh verified enrollment/epoch, not relabelling existing evidence. G5 changes configuration across refresh and proves the same source still deduplicates to one anchor. No worker/global-client rewrite or duplicate anchors caused only by environment changes.
Affected auth suite: plan G15 adds existing packages/connector-clio/src/auth.test.ts. Its :56–87 fake exchange-only client has no baseUrl and asserts configuration origin. Replace that plumbing echo with real ClioClient synthetic HTTP proving actual producing origin at enrollment and stable origin through refresh after cfg/env changes, with no token sent to a mismatched region. Keep invalid-state/declined/revoked behavior. The extra existing path is necessary; no actual suite or provider call ran in this documentation pass.
Coverage and separate bounds: keep the local clio_activity reader; live provider calls authorize returned entries, not replace the corpus. Only ingested eligible tracked matters are covered; untracked, unassigned, disabled/ineligible activity and time outside observed coverage are explicitly excluded. Default label: No match in available eligible entries; never claim a complete personal week. Local selection: at most 200 canonical entries over up to 5 local keyset pages, default page size 40. Provider exact validation: at most 200 DISTINCT canonical entries per request, independently of local selection, with ≤ 4 concurrent exact validations and one identity resolution per distinct connector. Selecting a local row consumes no provider-validation slot; an attempted exact validation reserves its distinct-entry slot even if denied or failed. A retry of that entry is not a new distinct entry, but consumes the existing HTTP-attempt allowance and phase time. Preserve ClioClient's existing default three total HTTP attempts per GET, including the initial attempt, with no outer retry loop or spend increase. Exact-entry GETs and their retries are not local pages; five local pages never means five provider GETs. Existing endpoint-specific HTTP/page caps elsewhere remain separate and unchanged. Identity HTTP calls consume their existing attempt/time limits, not the distinct-entry counter. All source scanning/exact validation shares the same 20-second post-acquisition phase (§3.3), not an overall request deadline. Any operational budget exhaustion can yield partial/unavailable even below 200 entries, never absence.
Named overflow query: B7's countEligibleRecordedTimeOverflow is a separate COUNT/EXISTS over the eligible population, not a 201st materialized key and not “5 full pages”. Overflow/200-vs-201 measured on the same set as the projection. Delete the pre-epoch-filter overflow refinement. Coverage reasons before overflow: combined >200 AND null-date emits the coverage reason, never “narrow dates”. Projection uses LIMIT 200. 200 complete versus 201 too-many must both be expressible. Local keyset paging at default size 40 may fill the 200; overflow is that named query. G5 owns overflow query and 200-vs-201 under sync_role. G6/G10 own composed C/UI figures. One owner per case.
B7 snapshot and hours: projection, overflow, countEligibleNullDateCoverage, countEligibleNullUserCoverage, countEligibleNonVisibleCoverage, countEligibleUnstampedCoverage, countEligibleBelowEpochSameOriginCoverage, the NULL-origin arm (own enum member, same class as unstamped — awaiting feeder; user sentence is spec §10.1 null-origin): shared population ∧ source_origin IS NULL → null-origin refuse. Mismatch stays member (6): shared population ∧ source_origin <> $personalVerifiedOrigin → identity-unverified refuse. Recorded-time projection, canonical/dismiss keys, and exact GET require source_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Differing-origin rows are not keyed as personal and do not occupy the personal LIMIT 200; matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows, parseActivitySyncState of the authorized ingest connector's activity_sync_state in that same REPEATABLE READ snapshot. Three org-Clio branches: (1) authorized exists → parse that row’s scan-state; (2) authorized empty AND any org Clio row EXISTS (no status filter) → ingest-not-authorized; status source: partial-unique live org Clio row when it exists, else any leftover; extend partial-reason enum to cover revoked/archived; (3) zero org Clio rows → missing-feeder. Do not parse scan-state of a non-authorized row. Unstamped: shared population ∧ source_authorization_epoch IS NULL → refuse; nothing else inside that EXISTS. Below-epoch and origin predicates as on the B7 card (shared population includes type='TimeEntry' stated once; quoted only by unstamped / below-epoch / origin; user clause on those three is the literal (user_source_id = $actor); unstamped NULL-user does not refuse, labelled via null-user). Precedence: origin mismatch/NULL wins over unstamped when both true. Partial reasons cite the B7 enumerated list. Unseen is labelled only; delete the hard-incomplete arm of countEligibleNonVisibleCoverage. The population fingerprint run in one REPEATABLE READ transaction on the B7 connection, opened through existing packages/db/src/sync-tx.ts with optional isolationLevel forwarded to drizzle db.transaction so BEGIN ISOLATION LEVEL REPEATABLE READ precedes current_user. Default isolation is unchanged; existing callers stay READ COMMITTED. Do not drop the wrapper. When isolationLevel is requested, SELECT current_setting('transaction_isolation') must equal repeatable read; throw otherwise. That REPEATABLE READ transaction commits after projection/overflow/null-date/null-user/non-visible/unstamped/below-epoch/unverified-origin/fingerprint and before any exact-entry validation call. Seal countEligibleNullDateCoverage, countEligibleNullUserCoverage, countEligibleNonVisibleCoverage, countEligibleUnstampedCoverage, countEligibleBelowEpochSameOriginCoverage, the (11) NULL-origin and (6) mismatch origin arms and the fifth fingerprint-sealed precondition parseActivitySyncState (or a coverage digest) with the fingerprint. Refuse the named figure unless completed_updated_since IS NOT NULL and last_full_scan_completed_at is present. Drop “state epoch equals current” until a full scan completes under the new epoch. A zero-row fresh connector refuses, not 0 minutes. Cursor reuse refuses the full figure if the fingerprint OR those flags change. Sealed fingerprint, not an open transaction, revalidates later. Same as no provider under locks. G5 (b): dated overflow count unchanged; a null-date insert and a separate unstamped (source_authorization_epoch IS NULL) insert → figure refused. G5(b) does not refuse labelled unseen. G5 two cases, not one combined: stamped-epoch/NULL-origin is (11) — that fixture asserts the row IS in the projection, IS keyed under the actor’s verified origin, AND that the named figure is refused with (11); non-null origin differs is (6) — that fixture asserts the row is NOT keyed, NOT in the LIMIT 200, and coverage EXISTS. G5: an AU-origin row is not in the keyed set; no collision with a US source_id; no exact GET on US origin for the AU id. Matching-origin and NULL-origin rows are not withheld. Hours are lossless from the stored numeric onward. B7 returns lossless hours (numeric string) per row AND a separate round-once aggregate minutes from the same REPEATABLE READ snapshot. C uses that aggregate for the named figure; it does not sum per-row display minutes. Do not publish the B7 snapshot aggregate as complete when validations withheld rows/hours.
Remaining grant preflight: report §1 documents Users — Read + Activities — Read as the narrow developer-app configuration to verify, fixed at authorization and not expanded by refresh. OpenAPI lacks operation-level OAuth security declarations, so it cannot prove a who_am_i exemption, exact runtime scope strings or unconditional nested Account visibility. Observe both credential-specific current-user/account responses and an exact known synthetic TimeEntry; identity success alone proves neither Activities access nor visible hours. Preserve nullable hours/quantity_redacted and distinguish whole-entry denial. If settings need changing, record the owner-managed portal/reconsent prerequisite; never silently broaden firm grants. kwiss chose existing test accounts, not new account creation, but supplied no identifiers. The future authorized runner needs the report §3 fixture/account manifest, regional origins, observed uniqueness-scope recording on the two designated test accounts, registered settings/grant timing and secure credentials; do not invent them or run the mutating simulator preflight. No account token/API calls in this documentation task. Reconciliation remains gated on effective grant proof, not an unknown account discriminator; mocked fixtures prove parsing/denial only, never provider permission semantics or a complete mail-only substitute.
3.3 Phase-bound source budget and cancellation
Two phases, one source deadline. C first prepares ALL selected Outlook/Clio credential snapshots through canonical getValidGraphToken/getRefreshedBundle, including required refresh/KMS work, without source scanning. Recheck direct authority, membership incarnation, active org and all prepared bindings. Only then establish ONE absolute deadline, now + 20 seconds, for source scanning, identity GETs and exact validation across both providers. Internal prepare calls have no source deadline; scan calls propagate this same deadline unchanged. B6/B7/B10/B20/B21 carry request-local prepared snapshots on authenticated internal RPCs; B8 is the credential writer, not a scan carrier. Secrets never reach web JSON, generic tools, logs or a stored capability cache. Scan handlers validate snapshots against current authoritative rows and use prepared bearers without reacquisition. Expired/changed/refresh-needed snapshot stops that source with partial/reprepare-required; a fresh explicit request prepares again, never resets this deadline. No KMS cancellation framework or overall latency guarantee.
Request cancellation stops ledger work. Check before scheduling each source page/validation, after every await, before source disclosure/model input and before ledger mutation/commit. Abort supported HTTP fetch/body consumption with the caller signal and the shared absolute deadline. postConnectorsApiJson forwards it without resetting; internal scan routes bind c.req.raw.signal and reject missing/expired deadlines rather than start their own phase. Recheck live authority at the same boundaries (§3.4). No resource factory or detached Promise.race. Cancellation withholds late source results and prevents further ledger commits; it never permits background reconstruction.
Canonical maintenance may settle after cancellation. Already-started shared credential maintenance, Redis coordination and KMS RPCs are not guaranteed cancellable. Await canonical settlement where attached; an aborted ledger continuation then schedules no source work, disclosure or ledger write. Personal enrollment uses the SAME existing refresh advisory before writes; its original-generation CAS and the existing refresh metadata preservation prevent stale A from corrupting B. No worker-fence/global persistCredentials rewrite, scoped Redis/cipher factories, global crypto cancellation or new recovery queue.
Bound database waits where this lane owns the transaction. Keep RLS wrappers and existing pool-acquisition limits. Ledger/admission and strict-enrollment transactions use positive transaction-local lock/statement timeouts and bounded advisory acquisition, checking cancellation after waits and before commit; never compute zero, which PostgreSQL treats as unlimited. A cancelled pre-commit operation rolls back and settles started queries before a terminal response. If COMMIT was already submitted before cancellation, settle and resolve its actual receipt/outcome rather than claim a rolled-back timeout. No timed-out response followed by detached ledger writes. These local controls do not promise interruption of an in-flight KMS RPC or rewrite shared maintenance timeouts.
Latency limitation and proof: external credential refresh, shared coordination and KMS latency can prolong connection preparation and cancellation settlement. UI explains this phase before the bounded source scan; it must not advertise a 20-second total countdown. Test slow acquisition followed by a full bounded source phase, cancellation during acquisition with no subsequent source calls, cancellation between pages/before ledger commit, supported HTTP abort and no new disclosures. Separately prove that maintenance settling after cancellation preserves newer enrollment. Remove global KMS/Redis cancellation tests and the fictional all-in SLA; this deletes our speculative promise, not any user date-range, refresh, privacy or transactional behavior.
3.4 Live authority, membership incarnation and custody
Existing platform defect, not a ledger fix claim: auth.ts:81–98 revokes browser sessions before member removal, not Outlook tokens/subscriptions. worker-mail/loops/outlook-subscription-renewal.ts:281–301 selects by subscription/connector state without a member join; connector_user_tokens remains globally unique on provider/account (schema/connectors.ts:669–671). Global notification/renewal offboarding and safe account release/transfer therefore need a separately authorized platform remediation covering removal, leave, renewal/notification admission and credential ownership. This lane does NOT rewrite that system or claim it solved. Strict ledger access instead requires live membership and a fresh member-bound enrollment; a former member cannot restore source authority by remove/rejoin or refreshing a retained token. A cross-org account conflict fails closed pending platform remediation, never repoints another organization's credential.
Membership identity: member.id is the enrollment incarnation, not user ID, role or timestamp. schema/auth.ts:168–187 provides an immutable row key and unique org/user pair; domain-autojoin.ts:212–224 creates a fresh UUID. Store this ID in both strict source enrollments and immutable per-field/evidence authorization snapshots, NOT in the stable source anchor key and NOT as a membership FK. Removal retains saved rows; rejoin can restore owned human records through a new membership, but old source dependencies stay withheld until a new strict enrollment and deliberate current-source validation. Never backfill old membership provenance from whoever happens to be a member now.
Direct actor requirement: every ledger web read/write, product tool, strict personal START/callback/internal commit and MCP ledger call must prove a live direct user or an immutable directly authorized MCP grant. Global support impersonation is NOT outside this rule. session-resolver.ts:114–177 currently drops impersonatedBy; an absent field on that projection is not direct-user proof. Carry a server-derived session ID to C and reread the authoritative session (matching user/active org, unexpired, impersonated_by IS NULL), user ban state, active organization and exact member row. Public tool/API arguments cannot supply origin, session or grant identity. Strict internal endpoints independently verify the trusted server actor, not just a user/org pair in JSON.
Active organization and executable recovery: ordinary session-resolver/resolveActiveMembership keeps sole-member fallback, including stale active org. Ledger authority still requires an explicitly selected authoritative session org matching request/current membership. Stale non-null or null/absent active org yields select-organization-required—even with one membership—not a silent session write or permanent error. Fresh session creation normally supplies its sole/default org; legacy null sessions show one explicit choice. After authentication/password/direct-origin checks, F4 renders only F3's organization recovery, with eligible choices and no ledger/source data. Use existing authClient.organization.setActive({ organizationId }); the real Better Auth operation validates membership and updates session/cookie. Refresh the same canonical selection and recheck all owner/org/matter/episode references before reads or personal connect. C/A5 rechecks after awaits; ordinary non-ledger resolution remains unchanged. G10 proves stale/null→real selection→reload/read with Outlook absent; plan §19.3 records source and boundaries.
Chosen MCP policy A — new explicit direct-authorization scope: mcp:private-work-ledger gates all five ledger tools; mutations also require mcp:write. Choose least privilege: ledger-only reads do NOT require mcp:read or broad firm consent. Token ∩ live consent must contain a COMMON read or directly issued ledger scope for principal admission; ledger calls still require ledger specifically. All non-ledger families/catalogues retain effective read, and ordinary mutations read+write. Real OAuth principals always carry explicit scopes, so ledger-only/empty arrays cannot become legacy read defaults. Direct issuer/consent/code checks, old-grant no-upgrade, live member/org/client/consent checks and valid durable new grants surviving browser-session deletion remain. Catalogue/direct/chassis/discovery/prompt guards change together; no general firm-tool bypass. Preserve S+A and all existing non-ledger grants.
Client ceilings across every mutation surface: existing auth.ts hook already matches register, authenticated create/update and server-only admin create/update. Apply the same allowed explicit scope policy on body.scope or body.update.scope, preserving existing privilege/ownership/redirect/grant guards. New public DCR MAY explicitly request ledger and a direct user explicitly consent; REJECT universal operator approval for new clients. Creation omissions get existing non-ledger registration defaults. Omitted updates preserve present ceilings; legacy null/absent stored scopes are bounded to the configured non-ledger remainder at authorization, never treated as implicit ledger permission. Null/malformed wire scope refuses under existing schemas. Existing trusted Claude upgrade still uses the mapped operator opt-in script/cache replacement; ordinary owner updates/re-registration stay available. Neither a ceiling nor later consent upgrades an old access/refresh token.
Explicit request is distinct from ceiling and consent: pinned provider1.6.23 defaults absent scope to client.scopes ?? opts.scopes. Existing auth.ts before-hook normalizes original/reentered ctx.query.scope to the effective non-ledger remainder before signing; explicit ledger against a null ceiling refuses. All five client mutation surfaces use the same policy. This supported adapter/context override solves default scope only; §3.5's narrow provider patch is separately necessary for persisted consent provenance.
Login/consent continuation: inspect original/reentered ctx.query, not the underlying login URL. Provider :2963–2969 dispatches configured before hooks and carries trusted authorizeSettings; login :3026–3036, consent :3086 and continue :3113 reenter it. Preserve signed-query validation and direct-session/issuer guards. The selected patch adds the accepted consent binding ONLY to server-local authorizeSettings, never public query parameters; after concurrent regrant, reentry cannot substitute the newer generation. I real-provider tests retain omitted-scope/login/skip-consent controls and prove explicit direct ledger authorization.
Advertised availability and unsupported request_uri: internal scope recognition is not public availability. Pin issuer advertisedMetadata.scopes_supported to existing non-ledger scopes until the SAME guarded-fleet activation flag used by C/D/I and both MCP protected-resource metadata URLs is active. Pinned metadata defaults to opts.scopes otherwise (:2827/:3047/:4074); existing AS export already delegates to it, no route wrapper. Test actual AS/RS responses before/after activation. request_uri remains unsupported: no resolver is configured and provider :3839–3841 refuses it. Test an actual request's refusal/no new code or grant; reject a speculative future-resolver finding and source-text/config-only proof.
Chosen support policy S — activation transaction across the fleet: deployment-wide suspension is part of ledger activation, not this documentation edit or dormant rollout. Keep ledger admission disabled; first prohibit new impersonation and reject existing impersonated sessions on every raw-auth/web/native entrypoint. Then retire ALL relevant preactivation SSE/WS connections and in-flight/resumable/background runs before enabling any ledger access. Current streams captured authority at opening; changing the resolver or waiting a WS cache TTL is not retirement proof. Use controlled ingress quiescence and existing process shutdown/drain/replacement, not a transport revocation framework. If precise impersonated-run classification is unavailable, drain all runs/connections on those processes. Account for every serving instance and run owner, close old sockets, settle or terminate outstanding continuations, and fence queued/resumable old-origin work from resuming. Drain timeout/unknown instance/remaining run leaves ledger OFF; do not enable and hope it expires. Clear trusted issuer caches in the same replacement barrier after an authorized client-ceiling change. Once all old execution contexts are gone and fresh direct sessions work, enable ledger. D7/D9, issuer guards, suspension and old-session denial survive rollback; no reintroduction of old processes/connections. Normal direct sessions reconnect and existing non-ledger MCP grants remain usable. G9 opens actual impersonated SSE, resumable SSE and WS plus a paused run before activation, then proves retirement and no postactivation private event/result, including rollback.
Proxy/mint closure and in-flight streams: plan §15.2 retains clicky-proxy/src/auth.ts and web clicky-mint.ts plus its existing test. Proxy authentication rereads authoritative impersonation state before vendor work; an absent bearer-plugin field is not direct proof. Mint refuses old impersonated raw bearer/cookie handoff before API or loopback returns credentials. Preserve ordinary direct/non-org-scoped behavior. G9 proves old application streams/runs retired, including a held-open proxy body: its timeout cleared after headers is not drain proof. Previously issued vendor credentials/direct vendor connections do not automatically grant ledger/history access; their revocation, expiry or erasure is explicitly OUTSIDE this requirement, not a condition keeping ledger OFF.
Narrow executable rollback floor: only existing bluegreen.sh, post-receive and bluegreen-sim.test.sh (J: three paths /610 prospective lines, including §23's exact-tree race closure, §25's materialized-SHA marker, §26's relocated stage-release refuse after green-lit reuse, skip (return 0, no copy) inside bg_refresh_post_receive_hook when the marker ≠ DEPLOY_SHA, or when the marker is absent and the active floor minimumVersion ≥ 1; do not abort; bg_stage_release owns the only refusal. J1 edit at bluegreen.sh:727–738: source="${BG_RELEASE_DIR}/deploy/preprod/bin/${name}.sh" (do not phrase $BG_RELEASE_DIR as current fact; price inside J1's 190); no BG_RELEASE_REUSED; leave existing BG_RELEASE_PROMOTED at :55/:607/:723 untouched. With active floor minimumVersion ≥ 1, marker REQUIRED: absence → bg_stage_release refuse, refresh skip. No active floor → absence dormant. J3(e) sets the floor). Operator durably sets/restores minimumVersion and verifies compatible active/fallback, NOT a per-SHA approval map. Every future candidate carries one static declaration in its existing library, read as DATA via git show target:deploy/preprod/lib/bluegreen.sh; retained releases use the same declaration. No candidate source/eval before admission. Version1 preserves D7/D9, permanent memory exclusion, activated S/direct issuance and J1/J2/floor continuity. Missing/malformed/duplicate/below-minimum declaration or malformed state refuses; dormant/compatible rollback remains supported. Shared checks cover start/swap/automatic-return/recovery/route/current repair. J3: hook bytes unchanged; bg_reconcile still runs. J3(a): no RELEASES_ROOT/<B>, no .deploy-release=B, no swap, hook/runtime-script bytes unchanged. J3 retains compatible-push/refreshed-hook continuity, the real A/B race, post-abort direct-deploy refuse, stale-marker ordinary-push success, green-lit reuse with stale marker still succeeding, and retained rollback/recovery/skip-swap.
J totals, missing json and restore: current J is 190/60/360 =610, not 530. Historical 190/60/320 =570 is not current. Leftover 570 remains historical. Version 1 also preserves worker-context/digest (D16/D17) with D7/D9 and permanent memory exclusion. Missing compatibility json is unimplementable as post-vs-pre by the json itself. J1: if the live controller's static declaration is ≥ 1, missing json refuses; if the live controller has no declaration or declaration 0, missing json remains dormant. J3 deletes only the json after a declaring controller is live and still refuses. H names the operator restore obligation. Stay in three J files; no second file. No deploy.sh/rollback.sh edit.
Trusted scope and exact hook order: after existing flock, J2's current-controller check runs in an isolated subshell UNCONDITIONALLY before the :107 .deploy-mode branch, :128 candidate-library source and :148 checkout. Active-floor absent/corrupt mode cannot bypass it or replace the controller. Add standard-hook leaked-test-mode refusal before mkdir/lock/status. Candidate declarations are trusted maintainer statements, not hostile-code attestation; operator owns minimum/restore and initial handoff, not routine approvals. No new manifest/helper/installer/daemon, digest proof, arbitrary privileged-operator defense or vendor erasure. Deployment orchestrator owns install/activation/execution; plan §§15.1/19 keep the supported current-controller paths bounded.
Admission and materialization use the SAME immutable target: post-receive captures target_sha from receive input but currently :148 checks out mutable main, and :167–173 refuses only after controller replacement. Choose GIT_WORK_TREE="$DEPLOY_DIR" git read-tree --reset -u --no-sparse-checkout "$target_sha"; update tracked checkout/index without detaching bare HEAD, changing refs or adding a linked worktree. After every successful read-tree, write a non-secret materialized-SHA marker beside .deploy-mode; preserve untracked runtime files. J2 writes that marker during the deployment owner's initial handoff install (same barrier as the durable minimum). Keep J1 bg_init_config a side-effect-free assignment as today. Move the marker≠DEPLOY_SHA refuse into bg_stage_release AFTER the green-lit reuse early-return (:604–609) and immediately after the existing [ -e "$BG_RELEASE_DIR" ] refuse (:610–612), before set_phase :615 (and thus before rsync of DEPLOY_DIR :637–646) (bluegreen.sh:602; BG_RELEASE_DIR="${RELEASES_ROOT}/${DEPLOY_SHA}" at :53), the only path that turns DEPLOY_DIR into a new release. post-receive:110 sets DEPLOY_SHA=target then :137 calls bg_init_config BEFORE materialization, so a marker A ≠ incoming B on every ordinary push; putting the refuse in bg_init_config deadlocks those pushes and also breaks rollback.sh (DEPLOY_SHA from prior record/argument at :54–58, bg_init_config at :126–127, target reset only at :192) and sim reconcile-only entries. Direct deploy.sh "$DEPLOY_DIR" after superseded abort (marker A, main B) must refuse and never stage RELEASES_ROOT/B from A's tree. Ordinary later compatible push must still read-tree, rewrite the marker and deploy. Reuse of an existing green-lit release with a stale checkout marker still succeeds. Rollbacks reuse RELEASES_ROOT/<sha> and must not consult the checkout marker. J2 is the only marker writer. Retain the post-materialization main comparison, existing static-map handling and failure status. A superseded hook aborts; never claim B will deploy when B's hook may already have failed flock after updating the ref. No automatic retry/rewind/latest-main substitution. Do not edit deploy.sh/rollback.sh: :690–696 derives DEPLOY_SHA from rev-parse main, so the stage-release refuse is what stops a supported direct deploy.sh "$DEPLOY_DIR" from staging A's tree as RELEASES_ROOT/B. J3 pauses admitted A, advances main to incompatible B and awaits B's lock refusal, then resumes A: only A's tree may materialize, bare HEAD/main remain unchanged by A, and unchecked B never replaces controller or affects routes/services; after that abort, direct deploy.sh must refuse. J3(a) asserts no phase change and no $BG_STAGE_DIR on refusal. Real Git is required for this fixture; runtime proof remains pending, no Git command executed here.
Production floor identity: bluegreen.sh:12–38 lets BLUEGREEN_TEST_MODE alter RELEASES_ROOT. J1's production compatibility path remains pinned independently of environment roots. Fixture state requires a copied script with baked isolated root, matching canonical location and fixture-created marker; environment alone never selects it. Standard J2 refuses leaked test mode before side effects. J3 preserves active-floor/empty-override refusal and valid fixture control alongside §23's exact-tree race and §26's marker proofs: (a) superseded abort then direct deploy.sh refuses, with no phase change and no $BG_STAGE_DIR; (b) marker present and stale, ordinary push still succeeds; (c) retained rollback/recovery/skip-swap still work; (d) reuse of an existing green-lit release with a stale marker still succeeds; (e) after the first successful J2 materialization, an absent-marker direct-deploy refuses. Marker absence is dormant only until that first successful J2 materialization. Operator recovery: J2 is the only marker writer; a manual git checkout -f main in DEPLOY_DIR can leave a stale marker that then blocks the next staging deploy until J2 rewrites it on a successful admitted materialization or the operator re-pushes so J2 rewrites it. Same three files; leftover J 190/60/320 =570 is historical; current J is 190/60/360 =610. With active floor minimumVersion ≥ 1, marker REQUIRED on the staging path: absence → bg_stage_release refuse, refresh skip; green-lit reuse unaffected; J3 absent-marker green-lit-reuse succeeds. No active floor → absence dormant. J3(e) sets the floor. No hostile-root claim or deployment framework. Version1 preserves this separation.
Request-local origin, never checkpoint authority: name it context.work_ledger_actor on each I run.ts LangGraph invocation; D8/C consume only that fresh server context and revalidate authority. Existing configurable.ledger_block/ledger_coverage at :2114/:2121 hold coverage content, not ledger-user provenance; leave them unchanged. This is naming clarity, not a new security finding. Preserve §15.4's actual saver trace, no graph-default/state/metadata/write/tool-argument copies or restored-ID fallback. G9 inspects real persisted records/events and proves no-context resume refusal versus fresh direct context. No serializer/taint rewrite.
Fresh checks and complete authority locking: retain entry/postremote/pre-model/post-model/precommit/preresponse checks of direct actor/grant, exact member.id and active org. For each local mutation/admission/strict enrollment, A5 discovers the exact authoritative row IDs without locking, then acquires: user FOR SHARE → organization FOR SHARE → member FOR KEY SHARE → referenced session rows FOR SHARE → OAuth client FOR SHARE → referenced refresh row FOR SHARE → exact access row FOR SHARE → exact authorizing consent row FOR SHARE. Skip absent branches; direct web requires its live direct session, MCP locks any surviving access/refresh session references only for FK ordering, never as a browser-liveness requirement. Sort/deduplicate IDs within each class; never take a joined query's unspecified lock order. Use NOWAIT for ALL authority-row acquisitions, including roots: lock conflict/missing or changed discovery links aborts the whole local transaction with content-free retry-required, no SKIP LOCKED, fallback row, out-of-order addition or hidden retry. This avoids a wait cycle with native cascades/revocation whose child order is outside this lane. Once the full set is locked, revalidate identity/expiry/active-org/ban/client-enabled state and token ∩ exact live consent required scopes from locked rows. Then owner/provider advisory and sorted thread/episode/credential locks; hold authority locks through actual commit/rollback. FOR SHARE conflicts with DELETE and non-key UPDATE of session validity, client disabled/scopes and consent scopes; KEY SHARE alone would NOT block those updates. Membership KEY SHARE protects the incarnation, not a mutable role authorization. Refresh/session FK coordination must not turn rotated refresh or expired/deleted browser sessions into new MCP authority requirements. No provider/KMS/model call under local locks; final wall-clock expiry/cancellation is rechecked before write/commit.
Narrow privileged authority seam and revocation semantics: A5 stays fixed-search-path/GUC-bound, with no secrets/source/ledger-row output, PUBLIC execute revoked and required app/sync EXECUTE only; no broad auth grants. Locking just roots is insufficient. Direct-session deletion, access deletion, client disable/delete and consent narrow/delete cannot commit between the relevant locked final check and ledger commit. If an authority-revoking change wins first, the locked reread refuses; if ledger holds locks first, it may commit before the competing revoke completes. Deleting an MCP grant's originating browser session is NOT grant revocation: after FK re-discovery the otherwise valid direct grant remains usable. This is transaction ordering, not revoking already-sent bytes or automatic cancellation at revoke invocation. Current FKs cascade user→session/client/grants, client→refresh/access/consent, refresh→access, and SET NULL session→refresh/access. Provider revocation deletes access; refresh revocation updates refresh then deletes access, while consent narrowing updates only consent. NOWAIT handles incompatible cascade order without modifying those global paths. G5/G6 drive both orders for direct-session revoke, client disable, consent narrow/delete and access/family revoke, plus MCP session-deletion positive controls, FK cascades and ordinary refresh. Real SQL must prove the contract; plan §14's narrow source audit is not runtime proof.
A5 authority and executable ownership: retain app_role SECURITY DEFINER, existing auth lock privileges, qualified relations/fixed pg_catalog, pg_temp, both actor GUCs, PUBLIC EXECUTE revoked/app+sync only. Plan §22.1 preflights actual provisioner schema/SET rights independently of DB_APP_LOGIN_USER. Pass ONE simple-protocol unparameterized statement list with no caller values to runLockBoundedStatement: the helper ALONE owns BEGIN, local timeouts, COMMIT/ROLLBACK. No duplicate transaction delimiters/timeouts. Temporary CREATE→owner transfer/ACL→REVOKE and assertions remain inside that transaction; use SET LOCAL ROLE app_role and transaction-local reset to the verified unassumed installer. Nested BEGIN only warns; inner COMMIT would end the outer transaction, not create an independently nested one. No permanent CREATE, role-membership auto-grant, broader auth DML or helper rewrite. G6's actual-entry nonsuperuser/rollback proof remains pending. Strict sync actor restoration and refresh's browser-independent authority remain.
3.5 Exact consent generation, persisted through issuance and refresh
Selected minimum: five nullable fields on existing OAuth tables: consentGeneration on consent; consentId/consentGeneration on access and refresh. No defaults/backfill or consent FK. Retain both token pair CHECKs and consent-generation-implies-ledger CHECK. Add THREE ledger-only identity CHECKs, one per consent/access/refresh table: ledger in scopes requires nonnull user_id AND reference_id even when generation is null. Ordinary scopes retain nullable semantics; null token provenance still denies ledger. The partial ledger tuple index now covers every ledger-bearing consent without NULL identity gaps; unique nonnull generation remains. Exact CHECK names/SQL and isolated invalid-row proof are in plan §22.5. One schema unit after #432, no new table/version switch.
Actual extension boundary: pinned consentReferenceId receives no client/consent identity; customTokenResponseFields receives no refresh row and only adds response JSON before token writes; custom claims do not persist opaque-token provenance. OAuth schema options rename fields but cannot add them; raw provider adapter writes bypass normal database hooks. Therefore extend the existing1.6.23 patch's schema/types and consent/code/token seams with a new, explicitly patch-defined consentGeneration: { scope, run } option. Existing auth.ts supplies real db.transaction, bounded root/consent locks and a transaction-bound drizzleAdapter to the provider continuation; no global adapter switch or async-local fallback. Plan §20.1 records exact source locations, arguments and regeneration procedure.
Algorithm: explicit signed direct ledger acceptance atomically writes scopes and a fresh random UUID, even on same-row/same-scope reauthorization. Carry that exact accepted pair through trusted authorizeSettings; stale reentry refuses, never relinks. Existing active consent may authorize a new code without rotating; null legacy/skip-consent cannot initialize provenance. Store the pair inside the same authorization-code verification JSON as its existing identity/query. Code consumption remains single-use and outside token transaction, so invalid PKCE/stale consent/DB failure burns the code. A code written after competing revocation may be unusable, never rebound. Exchange locks/revalidates exact ID/generation/tuple/scopes and atomically inserts access/refresh with that pair before releasing any token response.
Exact verification source: @better-auth/oauth-provider@1.6.23/dist/index.mjs:556–585 implements checkVerificationValue and calls the consumer at :557. better-auth@1.6.23/dist/db/internal-adapter.mjs:640–720 implements consumeVerificationValue and expiry checks, outside the new token transaction. Preserve database-backed one-use code burn; do not misattribute the core consumer to provider index.mjs or add a core-adapter patch. Real configured-provider/SQL proof remains pending; the core's non-atomic secondary-storage fallback is not a cross-process guarantee.
Rotation/revocation: reread and lock the original refresh before revoked CAS; sequentially revoke old/insert new refresh/insert access in one transaction, copying the original pair, never minting a generation. Failure rolls back all token writes; original refresh remains usable. Any changed scope set on a ledger-bound consent clears generation atomically; update-consent cannot add ledger in place of direct consent. Delete removes the exact row; regrant gets a new UUID. Old codes/tokens/descendants never regain ledger on same-ID or same-tuple replacement. Replay invalidates only its original generation before generation-filtered cleanup, never a newer grant. Null/stale provenance is always ledger-denied. Existing ordinary rights and explicitly downscoped ordinary refresh remain governed by existing scope rules; descendants retain their old pair but cannot upscope to ledger. Browser logout/session expiry alone is not grant revocation.
All family cleanup is partitioned: bound cleanup requires exact consentId/generation plus client/user; ordinary cleanup requires BOTH fields NULL plus client/user, including final refresh delete and child-access selection. Never retain the pinned unfiltered ordinary path, which would delete a fresh ledger grant. I's real-provider regression replays a revoked ordinary refresh, confirms ordinary-family cleanup, then successfully uses and refreshes a fresh ledger grant for the same client/user. Existing code-burn/atomic-rotation/regrant semantics remain.
Concurrency and rollout proof: tuple try-advisory serializes missing-row creation; partial uniqueness backs it. Root/member/session/client/refresh/consent locks use fixed order/NOWAIT, no remote work or hidden retries. Every consent write seam, even ordinary-scope narrowing, participates so a concurrent ledger acceptance cannot escape invalidation. Real provider/SQL tests cover first-consent races, accept→authorize replacement, code burn, token rollback after refresh CAS, concurrent rotation/replay, delete/regrant, stale/null/forged binding, both revocation lock orders, browser-session deletion and ordinary OAuth positives. Issuer, RS/A5 and patched rollback builds deploy together before scope availability; no old binary may mint or relink ledger provenance. These are required future tests, not proof executed by this docs seat.
Ledger MCP rotation compatibility: mcp-app.ts captures its init principal; complete live/recovery keys include the authoritative nonsecret access-row ID, while ordinary-only keys retain their old bytes. Compare stored/current keys even across ledger scope loss/gain. Same-scopes replacement B intentionally cannot reuse A:404 requires explicit reinitialization, including routine hourly rotation (auth.ts:451). Valid authenticated B initialization succeeds; no promise that arbitrary external clients transparently retry or that open streams/elicitation are uninterrupted. A's in-flight ledger work keeps immutable A authority and original cancellation; B never replaces it. A may finish only while valid/uncancelled, and expiry/revocation/cancellation forbids later ledger disclosure/commit. Completed commits retain existing receipt/idempotence semantics. G23 executes two real calls across rotation, live/recovered sessions, open stream, pending elicitation and explicit same-UUID retry with no double write; I proves actual grant validity. Plan §24.1; all runtime proof pending.
4. Two-table ledger model
New tables: work_episode and work_episode_evidence. Drizzle row types are authoritative. No third ledger table, event bus, graph, vector index, aggregate view or organizational reporting endpoint.
| work_episode group | Proposed fields and invariant |
|---|---|
| Identity | id, organization_id, owner_user_id; unique composite organization/owner/id. Owner/org immutable. Both user and organization FKs explicitly ON DELETE CASCADE. Every child references this composite identity. |
| Review record | Nullable matter_id/matter_organization_id: CHECK (matter_id IS NULL AND matter_organization_id IS NULL) OR (matter_id IS NOT NULL AND matter_organization_id IS NOT NULL AND matter_organization_id = organization_id). Composite matter FK uses only that nullable pair, ON DELETE SET NULL. work_date NOT NULL, IANA timezone; observed_start_at/observed_end_at are both null OR both nonnull with end ≥ start. work_type = correspondence / recorded_time / other. review_status is nonnull needs_review / reviewed / dismissed; dismissal_reason is required and in not_my_work / not_useful exactly when dismissed, otherwise NULL. A point observation does not imply elapsed work. Coverage labels episodes excluded solely by timezone difference between stored episode timezone and the request; do not change the stored-work_date predicate. |
| Authorship | Separate nullable user_description, derived_description and retained source_edited_description; proposed_fields holds fresh matter/date/type proposals. Immutable field_provenance contains evidence navigation ID, source key/account, connector/identity-connector IDs, enrollment_member_id, all authorization/grant epochs, provider revision and visibility basis. Evidence updates never rewrite retained snapshots. Human origin is server-owned, not a caller flag. |
| Duration | Nonnull duration_state. CHECK a disjunction of complete branches: unknown requires estimated_min_minutes, estimated_max_minutes and confirmed_minutes ALL NULL; estimated requires both bounds nonnull, min ≥ 0, max ≥ min, max > 0 and confirmed NULL; confirmed requires positive nonnull confirmed_minutes and BOTH estimate bounds NULL. No SQL-UNKNOWN loophole, automatic confirmation or inference from association. |
| Refresh and concurrency | Monotonic version, created_at/updated_at, last source observation time, last_mutation_id and canonical payload hash. No reconstruction_key: the unique activity anchor is the sole source-idempotence key. Provider proposal changes increment version but cannot overwrite human edits/dismissal. |
| Structure | lifecycle = active / superseded; immutable bounded lineage records operation UUID/payload hash/parent/result IDs. Superseded parents are tombstones. Nullable staged_restructure on one coordinator episode stores at most one opaque receipt with expiry, complete combine/split payload, immutable source dependencies and exact input versions. It is owner-private staging metadata, never part of the generic projection. |
| work_episode_evidence group | Proposed fields and invariant |
|---|---|
| Identity / ownership | id, organization_id, owner_user_id, episode_id; composite owner/org/episode FK explicitly ON DELETE CASCADE. Unique episode/source-key/relation. Partial unique owner/source-key for activity_anchor; one surviving episode owns each anchor after combine/split. |
| Source key | source_kind = outlook_message / outlook_attachment / clio_time_entry; source_key is a versioned, length-delimited canonical tuple, not concatenated display strings. Outlook: provider account ID + immutable message ID, attachment ID when applicable. Clio durable anchor is verified provider account (canonical regional origin + data.account.id validated under §3.2) + provider activity ID with proven exact correspondence under §3.2's conservative mapped-key boundary. connector_id is a selection predicate and non-key provenance snapshot, not part of the unique/dismiss key. Dismiss key is personal verified account + activity ID; no stored ingest account, no who_am_i; drop ingest-same-account-as-personal comparison; dismissals survive disconnect+reconnect of that personal account. G5 does not assert reconnect-different-account. Unsafe numeric or ambiguous ingested IDs never become anchors. Include org/owner in uniqueness, never email/name, date, note hash, mutable REST ID or matter guess. Membership incarnation stays an authorization snapshot, not an anchor-key component. |
| Authorization and version | Immutable source connector/account IDs, enrollment_member_id and lossless source epoch; Outlook exact selected credential ID/grant_epoch; Clio personal identity-connector ID/epoch and activity-row epoch. These are scalar snapshots, NOT connector/credential/member FKs. Source revision = Graph changeKey or Clio sourceUpdatedAt + etag; fingerprint grants no authority. observed_at/availability describe the current observation only; updating it cannot mutate a retained field snapshot. |
| Relation | relation = activity_anchor / supporting / clio_candidate / clio_associated; candidate and associated require clio_time_entry. association_origin = proposed / explicit; relation_decision = pending / accepted / excluded. Explicit exclusion persists, rather than deletion that refresh can undo. Supporting Clio evidence is not an associated time entry. |
| Content | Nullable bounded cached title/excerpt/provider URL and structured duration snapshot. All are source-derived. No raw mail bodies or attachment bytes in the ledger. Provider URLs are validated, never synthesized from guessed routes; absent URL gives a guarded in-app read, not a fabricated deep link. |
Null-safe SQL and lifecycle proof: encode the complete matter, duration, observation and dismissal branches above in schema TS CHECKs, rejecting unknown truth values where necessary; G6 issues actual invalid INSERT/UPDATE permutations, not only Zod tests. Follow working_profile.ts:175–180 for explicit user/org CASCADE; membership removal retains inaccessible ledger rows, user/org deletion cascades episodes and their evidence, evidence-parent deletion cascades, and matter deletion only SET NULLs its two-column pair. No membership/source-connector FK, accidental NO ACTION or cascade from source disconnect. Human descriptions survive matter/source loss. Generate and inspect these FKs/CHECKs via bun db:generate after the slot; no hand-written migration or global deletion-path rewrite.
RLS: both tables retain permissive org isolation plus restrictive owner AND live membership/positive-active organization predicates in USING/WITH CHECK. Require owner GUC and matching member JOIN organization with organization.status = 'active'. NULL status is unknown and denies ledger; do NOT COALESCE to active. C/A5/I return structured organization_inactive_or_unverified before ledger/source work, preserving separate organization-selection recovery and ordinary resolver behavior. Current resolver resolves memberships, not positive status. G6/I/G8 test valid member/direct-session + NULL denial and explicit-active success; no status rewrite or NOT NULL migration. Use .enableRLS(), pgPolicy(), withRlsTransaction and only needed app-role ledger grants; no ordinary sync/scheduler/reporting read. Direct origin/session active-org remains independently enforced.
Schema-slot constraint — post-transplant: #431 is squash merged and this lane's own replay is complete. #432 owns the exclusive schema-generation slot NOW. Do not run bun db:generate, initial or regenerated, until the factory explicitly releases a later slot to this lane. A completed transplant or successful review is not that grant. Independent authorized documentation/reviews may continue; no schema or product work is implied.
5. Reconstruction, corrections and atomic structure
5.1 Explicit refresh
Input: inclusive start/end local dates, explicit IANA timezone, optional matter. Maximum 31 days; default current week to today. Resolve once to a half-open UTC range, rejecting inverted/future-only ranges and invalid timezones. Bound each invocation to 200 primary source items and 5 pages; source scanning/exact validation has the 20-second phase budget after canonical credential acquisition (§3.3), not an overall request limit. Returned continuation is opaque and bound to owner/org, timezone, exact window/filter, connector identities/epochs and fixed scan cutoff. Each next batch requires another explicit request; no unattended resume.
- Authorize owner and selected matter. Admit available feeders independently; show per-source coverage and missing identity/connection reasons. If none can reconstruct, hide
review_my_work; leave saved reads and corrections available. - Read bounded source pages outside a DB transaction; recheck cancellation, epochs and individual access after remote work. Gather proposals with source anchors and revisions. Exhausting a page/source-phase budget stops scanning and yields explicit partial coverage, never absence. A still-active request may atomically finalize already validated partial results before its response; cancellation instead prevents further ledger commits or disclosures.
- In one owner-scoped transaction per batch, take the org/owner advisory lock, recheck local epochs, and look up each unique activity anchor before inserting an episode. Existing anchors route to the surviving active episode, including dismissed ones; only an absent anchor creates an episode plus anchor atomically. No reconstruction-key upsert against a superseded parent and no grouping by note similarity.
- Existing anchors route to the surviving episode. Refresh only fresh proposals and current evidence observations. Human descriptions, matter corrections, confirmed duration, explicit unknown, dismissal, Clio exclusions and lineage survive. Retained source-edited text also stays stored with its original immutable dependencies; preservation is not permission to display it. R1-edited text followed by redacted R2 remains bound to R1 and withheld; a fresh proposal can bind R2 independently, never relabel the old edit.
- New sent messages create new episodes even after a thread was combined/split; dismissed source keys stay dismissed across overlapping windows and same-account reconnects. Different provider account means different identity. Fresh exact access is necessary but cannot repair old/null Clio observation epochs: explicit ledger refresh must wait for dependency reobservation and report incomplete coverage.
- Missing source in a bounded/incomplete scan does not delete work. Removed/denied exact evidence becomes unavailable. Return counts, coverage, persisted IDs and versions; do not claim a complete workweek.
Sent-mail episodes anchor on the immutable message key; a Clio-only recorded-time episode anchors on its canonical entry key. Combine/split moves each anchor exactly once, so later reconstruction finds the survivor and never recreates a parent. Other episodes may associate the same Clio entry without duplicating its anchor or counted duration. Explicit manual/anchorless children are created only by versioned human operations and UUID receipts, never by reconstruction; retries use lineage/result IDs, not a source key.
5.2 Explicit edits and retries
Ordinary mutation retry: updateWorkEpisode, including exclusion, requires owned ID, expected version and mutation UUID. Under the owner lock, first resolve an identical latest UUID/payload to its saved safe receipt; a UUID/payload mismatch conflicts. Otherwise require current expected version and commit once. After an intervening mutation overwrites the bounded latest receipt, an old retry is stale and must never reapply. This differs from structural retry: combine/split retains immutable operation UUID/hash/result IDs on parent lineage, so an identical retry still resolves the same result IDs after child edits. Neither retry skips fresh membership/ownership or discloses now-hidden source fields.
Review status and duration are independent: reviewing an episode does not confirm an estimate. Confirm 35 minutes records an explicit numeric human decision. Effort unknown records an explicit unknown decision. Proposed numbers never silently enter confirmed totals.
5.3 Combine and split
- Keep the transactional combine/split union. Direct web preview is client-local; Save executes one transaction under the owner lock, with stable row-lock order and all expected versions checked. Optional explicit web staging supports committing the same complete operation from product chat/MCP using an opaque owned receipt, without exposing anchor/source identities.
- Combine: 2–20 active owned episodes; explicit resulting date/timezone, matter or no matter, description and duration choice. Conflicting matters cannot be silently chosen. Create one result; move unique anchors; deduplicate supporting evidence; preserve distinct Clio links; supersede parents atomically. Confirmed durations are not automatically summed because work may overlap.
- Split: one active episode into 2–10 children. Partition anchors exactly once; supporting evidence may be associated with multiple children. A single-message episode can be split, but only one child keeps its activity anchor; the others are explicit human-created records with that source as supporting evidence. All children receive explicit date/matter/duration decisions. Do not divide an estimate, confirmed duration or shared Clio entry automatically.
- Assign result IDs deterministically from owner-scoped mutation UUID and child index. Parents retain immutable operation ID/payload hash/result IDs. A transport retry returns the same result IDs even after a child changes; a different payload under that ID conflicts. Superseded inputs refuse other edits; the UI offers the surviving records.
- A failure after any insert/move/supersede rolls back everything. Refreshed anchors never recreate parents. No generic undo log; correcting structure uses another explicit combine/split.
5.4 Safe references and explicit handoff
Association is not exclusion. The strict update union has associate_clio and exclude_clio_relationship, both using owned opaque evidence ID/link_reference plus episode version and UUID. Adding or restoring association requires fresh exact provider authorization, actor/account/matter checks and unchanged source epochs; disconnected access refuses. Excluding an EXISTING owned candidate/association requires only fresh membership, owner/relationship ownership, version and UUID: no token lookup, provider GET or source hydration. Persist relation_decision=excluded as a local tombstone; reconnect/reconstruct must not restore it. Return only owned IDs/version/operation receipt, no provider fields. Generic reads expose owned relationship references with local decision enum so an owner can exclude while disconnected; guessed foreign references remain indistinguishable not-found. This adds no source disclosure or new tool.
For source-dependent partitioning, stageWorkRestructure uses one coordinator that MUST belong to the operation's input episode set. Stage, replace and receipt commit take the §3.4 authority locks, then the owner advisory, then the deduplicated union of ALL inputs/coordinator in ascending episode-ID order, each row exactly once. Never lock the coordinator first as a special case. For commit, an initial nonlocking receipt read may discover input IDs; after sorted locking require the same receipt UUID/payload/input-set/versions or refuse and restart explicitly, never append new locks from a replaced receipt. Store one 30-minute stage with random UUID, complete payload/dependencies and exact versions; stage creation requires expected prior receipt NULL, replacement requires its exact expected UUID. Staging does not change content versions; UUID CAS prevents two replacements from succeeding on the same stage. Response remains coordinator ID/receipt/kind/versions/expiry/action link only.
Product/MCP receipt references resolve to the SAME transactional combine/split union. Complete required remote source validation before local locks; under the locks recheck direct authority, membership, ownership, expiry, UUID CAS, local source epochs and versions, then consume stage and write completed lineage atomically. No provider/KMS call while holding these locks. Receipt UUID becomes mutation UUID; identical completed retry returns original result IDs. Wrong-owner/expired/replaced/stale receipts refuse without partial edits. Missing owned link/partition references return Review required with the authenticated episode/action link; supplying the reference or staging preserves every correction capability. G6 PostgreSQL crosses stage(A,B, coordinator A), stage(B,A, coordinator B), replacement and commit: overlapping stages may coexist, concurrent replacements of the same receipt have one UUID-CAS winner, and overlapping structural commits cannot both consume the same input versions. No deadlock, double lock, partial structure or stage consumed by another receipt.
6. Human ownership is not provider-text laundering
| Action | What remains after source loss |
|---|---|
| Confirm episode / accept suggested description unchanged | Review decision remains. Suggested description, provider dates/labels/links and supporting excerpts remain source-derived and hide. Confirmation is not a copy-to-human operation. |
| Edit a suggested description | Retain the edit and its immutable per-field dependency snapshot, including paraphrases. UI labels this Edited from source. R1 text edited then refreshed against R2 stays R1-bound and is withheld unless its exact dependencies remain currently authorized and unchanged. A new R2 proposal does not replace or reauthorize the retained R1 edit. No text-diff threshold proves independent authorship. |
| Write an independent personal statement | A separate blank field, never prefilled from provider text, allows My own description. Explicit human submission stores user_description; this stays available independently. The ordinary editor does not silently invoke this path. Deliberate human copy/paste cannot be prevented; the application must not automate it. |
| Confirm duration/date/type/matter | The specific explicit scalar decision survives; it does not confer human authorship on adjacent text. On lost matter access show Matter unavailable, retaining the user's decision internally without disclosing its provider label. |
| Explicit MCP correction | Save normally through the same service and expected-version contract. Numeric decisions persist as explicit decisions. Text editing a source proposal retains that proposal's source dependencies. MCP/model arguments cannot assert origin = human; a host cannot prove independent authorship merely by setting a flag. |
| Model summary / time description | Generated text remains derived from the union of its inputs. No model-generated summary becomes permanent human text through a confirm button. |
Authorization evaluates each field's stored snapshot, not evidence IDs dereferenced to the newest row. Combine/split copies or unions original dependency snapshots for inherited source-bound text; it cannot rebind that text to current evidence. A deliberate replacement written from currently authorized R2 is a new source-bound value with its own snapshot. Independent human statements and confirmed scalars retain their separate ownership rules.
7. Source-free generic chat; authorized web evidence
Named transport projection distinction: the user's explicit IDs and review links only decision defines mcp_ledger_transport_projection: opaque owned IDs/versions, structured operation receipts/status or reason enums/counts, and authenticated review/action links only. No free-text ledger output, including independently human-authored descriptions, labels/titles, time descriptions, input-echoing errors or receipts, provider identity/excerpts/proposals, evidence URLs or model prose. Link targets carry only authorized opaque references and validated navigation parameters, never embedded descriptions. Apply this projection to every ledger MCP list/read/mutation, direct and chassis response, including failures; structured correction/combine/split capabilities remain. This is an output boundary, not a taint/declassification framework.
Projection includes pre-handler SDK and every ordinary chassis exit: SDK1.29 validates before the callback and may reflect an unexpected argument key; D14/D15 normalize non-task ledger results/errors into strict receipts without arbitrary fields. D15 reuses the existing installed/boot-checked SDK closure, not public registerTool or a second interceptor. Task-present ledger requests are a DIFFERENT protocol outcome: reject before inner validation/callback with a fixed data-free JSON-RPC error, never a CallToolResult or invented CreateTaskResult. Plain chassis registerTool is task-forbidden (SDK server/mcp.js:699–704); global task capability belongs only to the existing spike and is not expanded. Preserve earlier already-safe capability errors; do not pass through raw SDK validation text. A task refusal remains protocol-shaped even if audit settlement fails. Preserve request correlation/L1/L2 audit and ordinary non-ledger results/errors. Reflected caller text is a contract breach, not proof of stored-ledger leakage; fixed generic prose is not itself a privacy leak. Plan §§21.2/24.3 own the source-grounded distinction.
product_chat_ledger_projection retains independently human-owned text under permanent private-thread admission and memory exclusion; the MCP reduction does NOT remove that product-chat behavior. Provider identity/title/excerpt, source-edited or proposed fields, source time quantity, evidence URL and generated source summary remain absent from BOTH generic transports. Product errors/receipts stay content-free. Dedicated authenticated web review retains authorized full descriptions and copy drafts. One service supplies the shared mutations; adapters enforce the named transport distinction, never a caller-controlled disclosure switch.
- Generic list/read are local saved projections: no tokens, provider scans, evidence hydration, ledger writes or queues. The product adapter first enforces the separate private-thread admission transaction below; that content-free marker is not a ledger mutation. Explicit reconstruction/corrections remain available through the same service but return only safe owned IDs/versions/receipts/links.
- Only dedicated authenticated Hono review routes call
hydrateWorkEpisodesand source-bearing copy/summary helpers for WorkLedgerReview. Batch visible episode IDs (up to 20), sharing identity checks and the 20-second post-acquisition source-phase budget rather than one who_am_i per row. Credential acquisition has no new all-in latency guarantee. Owner/org checks precede existence details; query episode IDs and filters are never authority. - Hydration causes no ledger inserts/updates, reconstruction, observation stamps, version or lineage changes. Existing trusted credential maintenance is permitted: getValidGraphToken may rotate credentials and the access-token route may request canonical recovery; Clio uses in-process getRefreshedBundle after ownership checks. No ledger feeder/reconstruction queue is authorized by a read. Test credential, ledger and queue effects separately.
- Evaluate every source-derived field against its immutable identity/grant/source-epoch/revision/visibility snapshot. Disconnect, exact denial and reconnect ABA with unchanged IDs withhold old fields; an R1 edit cannot be relabelled by R2 evidence. Recheck before web response/copy/model input and after model output. No claim of instantaneous detection of unobserved provider ACL changes.
- Web evidence stays request-local/in-memory: no-store responses, no persisted query/localStorage cache. On foreground/resume reauthorize; clear prepared copy/derived rows on owner/org change or authorization failure. Do not send hidden source fields in JSON. Copy is a deliberate user export; this server cannot erase text a user has already copied.
- Permanent private-thread admission: C locks the owned chat_thread, refuses existing shares, checks for the marker and inserts it only if absent: one assistant chat_message with a server-generated message ID, senderUserId/coverageReceipt null, and exactly
parts = [{"type":"data-private-work-ledger","data":{"version":1}}]. Idempotence is serialized by that thread lock; never treat a caller-chosen ID collision as proof of admission. No text, source/episode IDs, provider identity or free-form payload. grantThreadShare checks the persisted marker under the same lock and refuses with not_shareable even if the admitted turn fails before reply. Share-first refuses admission; admission-first permanently refuses sharing. Same permanence class asdata-netdocs-taint. First ledger tool use admits the thread. Admission ACCEPTS a matter-filed thread; do not refuse, do not unfile as a side effect of admission. D20 same discriminated rule as D21: allowtargetMatterId === null(unfile); refuse a non-null target on a marked thread.updateThreadMattertakes the samechat_threadFOR UPDATE as C/grantThreadShare, then marker check, then mutate. Non-null + marker → refuse;updateThreadMatter(..., null)unfile still allowed. Unfile setsmatter_idnull only; marker stays. Prompt/tool-help explain permanence. Unfiling an admitted thread is permanent and re-filing is refused. Do not add a HITL interrupt. No thread schema change,chat_threadcolumn, or automatic marker removal. - Marker visibility: D9 changes only filterInternalMessages' internal-only assistant marker predicate. buildPriorTurnSummary already receives listMessages' filtered transcript; leave lastVisibleAssistantText and its real blank/mixed-message behavior unchanged. G9 interrupts after persisted admission and verifies actual history reload plus the earlier meaningful summary through that caller chain, not an isolated unfiltered-helper test. Explain permanent private-chat restriction before use; standalone web avoids marking an unrelated chat. MCP has no product-thread marker.
- No memory extraction from admitted threads, including human-only text. Check the exact raw marker before extraction and after remote work; filtering it out is not exclusion. D10's final create/supersede/guarded salience transactions discover and lock relevant user roots → organization → optional target matter before chat_thread FOR UPDATE, all NOWAIT with positive local timeouts. Revalidate parent identity/eligibility and freshly read the marker AFTER the thread lock, before duplicate success, predecessor retirement or insert; hold through commit. Missing/deleted/foreign roots or contention roll back with a content-free policy-skip/retry reason, never partial memory effects. No remote work under locks. Admission-first prevents persistence; memory-first may finish before admission, not historical erasure. Independent threadless Settings/MCP memory retains its contract. Plan §20 S4 specifies parent-delete races; no global lock framework or memory schema rewrite.
- Permanent rollout/rollback floor: the existing C/D product-thread admission path fails closed until activation is explicitly authorized after EVERY share-serving instance enforces D7 marker-aware refusal; a compatible local instance alone is insufficient during a mixed-version rollout. This is a deployment activation guard in the mapped admission wiring, not a new fleet-discovery service or table. Once any ledger admission occurs, D7 refusal and D9 marker visibility remain mandatory independently of ledger capability/entry/tool switches. Rollback may disable those capabilities, but must use an explicit rollback-compatible build retaining this privacy patch, never the vulnerable prior binary. Preserve markers and refusal for existing admitted threads, including independently human-authored private text; source-free is not permission to share. G9 admits that text, disables capabilities, verifies sharing still returns not_shareable and history/prior-turn lookup remains correct; its mixed-version activation case refuses admission until the whole share-serving fleet is compatible.
- The same permanent floor retains policy S's deployment-wide impersonation suspension and old-session denial from ledger activation, plus policy A's direct-origin checks. Disabling ledger capabilities cannot restore impersonation or expose admitted private history. Every issuer must enforce direct-user ledger-scope authorization before scope availability and retain that guard on rollback; old issuer binaries cannot mint it without direct-user proof.
- No provider content reaches generic transport: graph model input, checkpointer, digest/history, runChat and resumable SSE/Redis receive only safe ledger projection. The only graph change is actor-scoped async tool admission below; no source rehydration, serializers or tainted-turn allowlist. Actual transport/alternate-tool tests share a positive authorized web fixture and remain source-free after revoke.
- Product catalogue seam: do not register ledger tools in synchronous global allDefaultTools. legal-assistant/index.ts supplies an async ledger-tool resolver to buildLegalAssistantGraph; graph llmCall awaits it with authenticated config immediately before each binding. Combine a fresh local array with existing non-ledger tools; retain the exact advertised snapshot for dispatch. Resumed dispatch lacking a snapshot awaits the current actor-scoped resolver. The service checks membership/feeder identity again on invocation, including stale calls; no global mutable list or cross-actor cache. Missing/unknown feeder hides reconstruction, while saved human reads/corrections remain available. MCP tools/list and tools/call use the same admission decision with its existing refresh sequencing. No host-certification exception.
Worker-context/digest admission floor: D16's EXISTS over chat_message parts inside findDigestThread's existing withRlsTransaction, and D17's digest guard inside ContextDigestRepository.upsert's withRlsTransaction — mixed threadId throws before the write; the uniform-threadId construction is INSERT … SELECT … WHERE NOT EXISTS(marker) … ON CONFLICT DO UPDATE, correlated on the asserted single threadId, ONE statement that skips insert AND ON CONFLICT update when the admission marker exists. Pre-model window and context_digest_attempt rows are deliberately out of scope (ids/spend only). Do not add a D13-style recheck. Do NOT FOR SHARE/FOR UPDATE chat_thread from digest (that blocks C's FOR UPDATE and invents a gap G24 cannot observe). Drop marker-row lock wording. Upsert asserts uniform threadId beside organizationId. G24 two-thread mixed threadId throws, zero rows written, not a mixed write; admitted thread's existing (thread_id, tool_call_id) digest/content_sha256/created_at unchanged; digestRowCount alone is insufficient. runContextDigestJob cannot hold a lock across repository.upsert; keep job-body cancellation. Checkpoint/graph.getState does not contain the marker. thread-loader alone is insufficient; digest.ts is already mapped. Every activation and rollback-compatible build after admission retains this floor. G24: concurrent C admission committed before that write → digestRowCount unchanged (no new row, no conflict-update of existing toolCallId). If admission commits after the write, later runs skip; that is the existing memory-first-may-finish rule.
Memory rollout and proof: all relevant memory consumers must carry the shared guard before first thread admission and retain it on capability rollback; old consumers are replaced/drained using the existing activation barrier. Queues/nightly sweep dispatch only IDs and stay unchanged because every consumer/direct miner and final writer checks. The memory export already re-exports access.ts; product save/update-memory already pass threadId into the shared writes. Existing service integration tests cross real C admission and real memory transactions in both lock orders, with a human private-text sentinel, unchanged enrichment predecessor and no retrievable new personal/matter/firm memory. Worker suites cover raw-marker exclusion before filtering/model calls and normal import/incremental positives. No SQL/worker test was run by this documentation seat.
Audit/log allowlist: opaque internal actor/org/thread/episode/operation IDs where already needed, tool/operation/outcome enums and counts only. Exclude ALL free text, including independently human-owned descriptions, as well as provider titles, URLs, notes, email, body, prompts/results, bearer tokens and cursors. Source-free generic tools are not permission to audit human prose. No knowledge ingestion, new telemetry or employee monitoring. Adapters project the allowlist before audit; tests submit human/source text sentinels and verify neither enters audit arguments.
Combined-scope MCP proof: G8/G23 use actual registered ledger calls with stored private human/source sentinels and separately a caller-only sentinel in a malformed extra argument KEY; verify strict receipt shape and sentinel absence across SDK validation, limiter false/throw, scope denial, thrown refusal, consent exits and audit failure. The malformed case must never invoke the handler. Then ordinary memory_save with unrelated input under the same combined grant persists normally; threadId:null is why product markers cannot protect MCP prose. No arbitrary-text detector, external-host erasure claim or unrelated-memory denial. Fixed generic prose is normalized for the agreed contract, not called a stored-data leak.
Task protocol proof: G23 sends real HTTP JSON-RPC ledger tools/call with valid params.task and malformed task metadata/keys containing private sentinels, both without global task capability and through the existing spike-capable fixture. Assert a correctly correlated data-free error envelope, no result/task handle/receipt/private text and no handler/provider/elicitation/ledger/task-creation effects. The same ordinary ledger request without task returns a strict CallToolResult; ordinary tools stay unchanged. No fake task-shaped refusal, task registration, SDK patch or client-side-only rejection proof. Plan §24.3 specifies the early wrapper branch and pending runtime check.
8. Reconciliation without false precision
| Visible state | Meaning / arithmetic |
|---|---|
| Recorded in Clio | Current authorized TimeEntry, even when its hours are unknown/redacted. Count the canonical entry once across episodes/pages; add minutes only for currently visible usable hours and report unknown-duration entries separately. Shared links never allocate time. Hours-only redaction retains the entry/link with null duration, never a cached numeric fallback. |
| Confirmed, not associated | User explicitly confirmed duration; no explicit Clio association. It can still be possibly covered by a candidate. Show separately, not as an invoice amount or guaranteed additional time. |
| Estimate awaiting review | Optional defensible range with evidence basis; no email-count × minutes, timestamp-gap arithmetic, model confidence converted to minutes, or automatically confirmed midpoint. In this source set, unknown is usually the honest result. |
| Effort unknown | Blank numeric duration with explicit Unknown label. Never zero, omitted from arithmetic and counted as unknown records. |
| Possibly covered | Candidate entry matching the verified actor and relevant date/matter, with readable supporting note where authorized. Similarity proposes a relationship only. Linking requires the user's explicit selection. |
| No matching entry found | Default No match in available eligible entries, with tracked eligible-matter/local coverage and excluded untracked/unassigned activity explicit. Only fully observed, specifically scoped eligible coverage permits scoped absence; never a complete personal Clio week. Hidden hours are still an entry, and partial validation cannot establish absence. Never unbilled. |
No single falsely precise total of recorded + confirmed + estimated time. Show separately: distinct recorded Clio time, confirmed-not-associated minutes (with possibly-covered count), estimate ranges by episode, unknown count. Do not subtract a many-episode entry or combine estimates into a purported weekly effort total. Hours are lossless from the stored numeric onward; never Number() that stored numeric string. Decoded Number at ingest is the same class as IDs (lossy JSON Number), tolerated for short decimals; no ingest/mapper code path. B7 returns lossless hours (numeric string) per row AND a separate round-once aggregate minutes from the same REPEATABLE READ snapshot; C uses that aggregate for the named figure and does not sum per-row display minutes. Display conversion never implies precision not in the source. Redacted-matter entries leave the named figure with no signal: cleared rows, including cleared_reason='matter_redacted', are ignored for counts, flags and fingerprint. Keep A6 NULLs.
A Clio-only record may be shown as Recorded time, not newly inferred work. Once associated with substantive episodes, its entry still has one recorded-time identity. Dismissal excludes an episode from work summaries but does not erase the source's recorded time; an explicit time-section filter explains the distinction.
8.1 Server-owned web range reconciliation
readWorkRangeReconciliation(actor, {startDate, endDate, timezone, matterId?, cursor?}, signal) lives in the existing ledger service, exposed only by dedicated POST /api/work-ledger/reconciliation in the existing Hono router. Same 31-day/date/timezone/owner rules; navigation reads, never reconstructs. Return scope/cutoff/version, separately named ledger and recorded-time populations, currently authorized reconciliation rows, known minutes plus unknown/unavailable counts, coverage reasons and opaque next cursor. No generic tool registration and no client-provided totals.
Population: saved-work figures use a separate app_role query over active, non-dismissed episodes by stored work_date compared to the request's local date bounds and effective matter selection, excluding superseded parents. Episode timezone is display metadata, not the range predicate. Mail-only episodes count. A dismissed-row browsing toggle never silently changes those figures. Recorded-time population is the verified caller's eligible local Clio TimeEntries by the entry's OWN current authorized date/matter in the same range, independent of episode date/matter, association or dismissal. Include authorized unlinked and dismissed-only entries; excluding a relationship does not delete provider time. Label these two filters explicitly. An associated entry outside the recorded-time range may explain an episode but is not added to that range's recorded minutes. Shared/excluded links never allocate or duplicate its duration.
Paging and dedup: B7 (sync_role) returns the bounded row projection in §3.2 plus source population fingerprint, LIMIT 200. Seal countEligibleNullDateCoverage, countEligibleNullUserCoverage, countEligibleNonVisibleCoverage, countEligibleUnstampedCoverage, countEligibleBelowEpochSameOriginCoverage, the NULL-origin arm (own enum member, same class as unstamped — awaiting feeder; user sentence is spec §10.1 null-origin): shared population ∧ source_origin IS NULL → null-origin refuse. Mismatch stays member (6): shared population ∧ source_origin <> $personalVerifiedOrigin → identity-unverified refuse. Recorded-time projection, canonical/dismiss keys, and exact GET require source_origin IS NULL OR source_origin = $personalVerifiedOrigin; NULL-origin rows stay in the projection and are keyed under the actor’s own verified origin — (11) refuses the named figure only, it never withholds a row (§58 Opus 1). Differing-origin rows are not keyed as personal and do not occupy the personal LIMIT 200; matching-origin and NULL-origin rows are not withheld. Not unscoped. Not on cleared rows and the fifth fingerprint-sealed precondition parseActivitySyncState (or a coverage digest) with the fingerprint. Unstamped: shared population ∧ source_authorization_epoch IS NULL → refuse; nothing else inside that EXISTS. Below-epoch and origin predicates as on the B7 card (shared population includes type='TimeEntry' stated once; quoted only by unstamped / below-epoch / origin; user clause on those three is the literal (user_source_id = $actor); unstamped NULL-user does not refuse, labelled via null-user). Precedence: origin mismatch/NULL wins over unstamped when both true. Partial reasons cite the B7 enumerated list. Unseen is labelled only; delete the hard-incomplete arm of countEligibleNonVisibleCoverage. Refuse the named figure unless completed_updated_since IS NOT NULL and last_full_scan_completed_at is present. Drop “state epoch equals current” until a full scan completes under the new epoch. A zero-row fresh connector refuses, not 0 minutes. Cursor reuse refuses the full figure if the fingerprint OR those flags change. Named overflow query countEligibleRecordedTimeOverflow is a separate COUNT/EXISTS, not a 201st key and not five full pages, measured on the same set as the projection. It does not join ledger tables. C (app_role) runs saved-work independently of Clio keys; bounded VALUES join only decorates B7's recorded-time projection against work_episode_evidence for association/exclusion. Unlinked TimeEntries still appear with empty relation. C binds a sealed opaque cursor to owner/org, exact filters, fixed scan cutoff, local population version and last canonical key. Version fingerprints the relevant episode/relationship versions and eligible source rows/epochs; source or local mutation invalidates the cursor and replaces prior results, never splices two populations. No historical reconstruction of changed rows, proof-carrying cache or third table. Per request, the independent budgets are 200 canonical local selections and 200 distinct provider exact validations at ≤ 4 concurrency; local selection never consumes the provider counter. Retries/identity calls follow §3.2's separate HTTP limits and all source work shares the same 20-second post-acquisition phase. Attachment caps remain separate unchanged aggregate limits within that phase. No app_role SELECT on clio_activity; no sync_role ledger grants.
Fresh figures, not hydration-page sums: B7 computes recorded-time aggregates as sync_role over clio_activity (provision-roles.ts:829–830 grants that table to sync_role only), returning lossless hours per row and a separate round-once aggregate minutes from the same REPEATABLE READ snapshot. C computes the saved-work aggregate as app_role over ledger tables, which deny sync_role, and uses B7's aggregate minutes for the named recorded-time figure; it does not sum per-row display minutes. C may combine those bounded server-computed results in process. Never sum visible hydrateWorkEpisodes pages. Each response rechecks membership, feeder/grant epochs, row eligibility and population version after remote work. A full eligible-range recorded figure requires the WHOLE population ≤ 200, every required exact validation in this request and sufficient feeder observation coverage, carving out labelled coverage (2) null-user, (3) non-visible and (5) below-epoch-same-origin — those members stay label-only and do not refuse the named figure. Earlier browsed batches must be revalidated within the same independent limits, never reused as proof. Above 200, return null range total and too-many-entries for the CURRENT scope; guidance: Narrow dates or apply a matter filter; a range figure is available only if source checks complete. Continuation browses, never promises an eventual total. Narrowing MAY enable completeness, not guarantee it. Even ≤ 200 can exhaust attempts/page limits/deadline or lack authorization/observation: return null with the specific partial reason, not too-many-entries. Only explicitly batch-scoped verified subtotals in incomplete cases; cursor exhaustion is not completeness. Fully validated scoped figures use B7's round-once aggregate minutes and separately count unknown-duration entries; hidden hours never become zero, cached numeric time or claimed complete numeric time. No larger budget, proof cache or complete-personal-week claim.
The review consumes only C's named figures and coverage, never sums visible hydrateWorkEpisodes pages or adds page subtotals. C may combine B7's bounded recorded-time aggregate with its saved-work aggregate in process. On cursor/version/authorization failure clear dependent figures. G5/G6/G10 retain cross-batch shared keys, dismissed-only/unlinked/out-of-range entries, redacted hours, offline exclusion, old/null observation epochs and mutation/permission loss between batches. G6/G10 add positive boundary controls with current observed coverage and fast accessible one-hour entries: 199 entries returns a non-null 11,940-minute figure; 200 returns non-null 12,000 minutes; 201 returns null with too-many-entries/narrow-filter guidance. Narrow the SAME 201-entry fixture by dates or matter to 200 entries and obtain the full 12,000-minute figure. Those 199/200/201/narrowed boundary fixtures contain zero null-date eligible rows; the null-date fixture is disjoint from them. Five local pages do not spend the provider counter or limit exact GETs to five. Also exhaust an operational budget below 200 and verify explicit partial coverage. An always-null implementation must fail these controls.
No production latency claim: neither success nor impossibility for 200 validations at concurrency four within the shared 20-second source phase has been measured. Fast boundary fixtures above prove that a successful full-figure path exists, not an SLA. Retain operationally partial cases below 200; optional injected latency is only a scheduling/abort/late-disclosure regression, not provider performance evidence or a budget increase.
9. One service, UI and tools
One server-only service in packages/agent-runtime/src/work-ledger/service.ts, exported through @workspace/agent-runtime/work-ledger. Generic adapters retain safe projections and the same transactional mutations; only dedicated Hono review routes call hydrateWorkEpisodes, readWorkRangeReconciliation and source-bearing copy/summary. Range figures are independent of visible-row hydration and may be incomplete. Keep the service outside browser-safe /model and /coverage exports.
| Product tool / service method | Contract |
|---|---|
review_my_work / reconstructWork | Explicit bounded local write; dates/timezone/matter/opaque continuation. Generic product/MCP result contains only safe owned IDs/versions, operation receipt and review link; detailed source coverage/proposals are web-only. |
list_work_episodes / listWorkEpisodes | Persisted-only safe projection; cursor/limit ≤ 100. No source fields/scans/tokens/queues. Product private-thread admission precedes return. |
read_work_episode / readWorkEpisode | Owned ID; use §7's named transport projections: product chat may retain independent human text, ledger MCP may not. No evidence hydration. Dedicated web hydrateWorkEpisodes is not registered as a generic tool. |
update_work_episode / updateWorkEpisode | ID/version/UUID; strict correction union including associate_clio (fresh exact provider authorization) versus exclude_clio_relationship (existing owned relation, local-only even offline). Explicit unknown persists. Reject lifecycle/owner/provenance overrides and generic patches; return only safe receipt and owned references. |
restructure_work_episodes / restructureWorkEpisodes | Shared transactional combine/split union with expected versions and deterministic retries. An adapter may resolve an explicit owned staged receipt to the same union. Missing source-partition reference yields Review required plus authenticated action link, not guessed anchors or permanent refusal. |
MCP names retain the existing north__ prefix. Three mutation tools require mcp:write and write-rate limiting; normal explicit personal corrections remain silent-write. Every mutation uses §7's named transport projection for its adapter, not a shared human-text result. No user_id, actor, organization, source-disclosure switch or provenance override in public tool schemas. Clio links use owned opaque link_reference, never guessed source IDs.
Time-copy, involvement and matter-update previews are dedicated web requests to prepareWorkCopy/composeWorkSummary. Hydrate at most 20 selected episodes/100 distinct evidence items; use the post-acquisition source-phase budget in §3.3. When synthesis is needed, reuse request-local callAnthropic with no tools or graph/checkpointer, at most 20,000 input characters/2,048 output tokens and a 30-second synthesis-phase deadline including retries, not an overall request SLA. Recheck cancellation and exact source dependencies before model input and response; persist no model prose or prompts/results. Each factual sentence cites authorized episode/evidence IDs. A matter update means my involvement, not all firm activity. Failure returns unavailable, not invented text; generic adapters link to the full preview under §7's transport distinction.
10. Structured review experience
Scene: a lawyer at a desk reconciling a week before leaving, needing confidence and a short path to correcting the record. Product register, restrained semantic color, existing user-selected theme, Early Sans body and Kepler headings per DESIGN.md. No metric hero, nested card grid, calendar heatmap or productivity score.
- Enter — Home is lane-owned; matters integration awaits handoff. F4 owns Today / This week / Review my work above LiveChat and WorkLedgerReview query mode with Back to chat, with no NOS-384 hold. F5 personal Matter Review remains independent of admin showActions/onAction with aligned header/cell/menu. Fresh c767 guards remain :138/:168/:243; NOS-384 owns that matters-page/table boundary until orchestrator handoff, then recheck its delivered version. Links stay
/tasks/chats/new?review=work&period=week, optional authorized matter_id andepisode=<opaque-owned-id>; server verifies owner/org and loads deep-linked episodes outside the default range. Navigation never reconstructs; neither entrypoint is dropped. - Saved review does not require Outlook. Keep the canonical /tasks/chats/new?review=work URL. The parent mailbox gate and F4 use the SAME trusted server mode: exact pathname plus exactly one decoded review=work query value, derived by middleware and overwritten on every gated request. User-supplied headers, duplicates, missing context or a plain chat URL cannot claim the exception. F4 branches on that mode rather than independently parsing review; invalid review filters never fall back to ungated chat. Parent password enforcement and I's live direct-session/current-member/active-org checks remain. Only the mailbox prerequisite is waived, never authentication or source authorization. Back to chat explicitly rechecks Outlook before ordinary LiveChat, including same-path query navigation.
- Connect personal Clio here. F3 WorkLedgerReview owns Connect/Reconnect for unverified/disconnected personal Clio and invokes I's direct-session action, not the organization Settings flow. Preserve the selected review/window/timezone/matter in the validated signed return target through B4/B5; return after success/cancel/failure and recheck matter authority. G10 proves unverified → authorize → usable reconciliation on that selected review with current feeder observations, and the same-connector Reconnect flow. Denial leaves identity unverified and organization credentials untouched. Reauthorization alone cannot restamp stale ingestion; show its actual coverage instead.
- Choose range. Show dates/timezone/matter, Only you and explicit Reconstruct this range / Refresh evidence. Fetch server reconciliation separately from row hydration; label saved non-dismissed work versus entries by provider date/matter, including dismissed-only time. Never sum client pages or label partial as weekly total. Above 200, the current range figure is unavailable: Narrow dates or apply a matter filter; a range figure is available only if source checks complete. Continuation only browses. Even ≤ 200 can remain operationally/authorization/observation partial; show its specific reason, with known versus redacted hours distinct. Old/null observation epochs await the existing feeder, potentially seven days/indefinitely; ledger refresh cannot repair them. Explain permanent private-chat sharing restriction before product-tool use; standalone review does not mark an unrelated chat.
- Wait or cancel. Pending review explains Preparing connections, then reviewing sources: credential acquisition may take longer; only the source scan/exact-validation phase has a 20-second budget. No overall countdown or new progress-stream framework. Cancel stops new ledger work and clears pending derived previews; already-started credential maintenance may safely settle without creating later ledger work.
- Review rows. Dense day-grouped list with description, editable matter, effort state and Clio relation. Keep unknown visible. Click or keyboard-open an inline detail region, not a first-choice modal. Show supporting evidence separately from associated time.
- Correct. All existing matter/description/duration/dismissal corrections remain explicit. Adding/restoring a Clio link needs fresh exact provider access; excluding an owned existing candidate/association remains enabled while disconnected, writes only its local decision and returns a safe receipt. Preserve that exclusion on reconstruction. Show source-unavailable labels without hidden titles or hours; failures retain unsaved inputs.
- Restructure and hand off. Inline Combine/Split preview shows anchor assignments/conflicts only on the web. Save executes atomically; cancelling an unstaged preview writes nothing. Optional explicit Stage for chat/MCP persists the bounded receipt described in §5.4; Copy action reference exposes only opaque IDs/versions/receipt, not source payload. Clio association similarly offers an owned link reference. Stale inputs require reload without losing local form data.
- Prepare copy. Secondary modes Time record / My involvement / Matter update. Preview includes date, matter, concise description, confirmed minutes or Unknown/estimate label, and recorded/possibly-covered status. Copy excludes unavailable text and dismissed/superseded records. Reauthorize immediately before preparing payload; never reuse a stale clipboard payload. No Send, Bill or Log in Clio button.
Full reload and OAuth-return proof: G10 reloads the review through the actual parent layouts with Outlook absent, revoked and requires_reauth, persists saved human corrections/offline exclusion, and reloads to observe them. With both sources lost, personal Clio Connect/Reconnect still returns to the selected review/window/matter while Outlook remains unavailable; current Clio observations can reconcile, stale observations remain unavailable. Ordinary chat and /matters full reloads still redirect to /connect-outlook. Forged headers/duplicate-query controls cannot render ordinary chat ungated; unauthenticated, password-reset, impersonated and lost-membership controls remain refused. G6/G10 combined fixture: >200 AND null-date emits the coverage reason, never “narrow dates”. G10 cites the B7 enumerated partial-reason list. G10: on a null-date refusal the “narrow dates” string is absent. G10 exposes the matter-filter affordance for null-date refuse. Null-user labelled flag/fingerprint only. This is required future browser proof, not a runtime check executed here.
Accessibility and responsiveness: semantic table/list controls, labelled date/timezone/matter inputs, aria-expanded detail region, focus returned after cancel/save, visible focus ring, keyboard combine/split without drag, announced errors and saved state, 44px touch targets. On narrow screens stack row cells in their reading order and expand editor below the selected row. Text labels accompany status color. Use existing UI primitives, border-border, focus-visible:ring-ring and radius pyramid. Reduced motion respected; no custom token additions planned.
Required states: no saved work; no attributable source; partial/truncated reconstruction; Clio identity unverified; source disconnected; exact evidence denied; reconnect-stale evidence; source changed; unknown effort; dismissed view; optimistic conflict; atomic operation failure; summary unavailable; organization_inactive_or_unverified; already_shared_thread. A provider failure leaves saved human work usable, not an empty page.
10.1 Partial-reason user register (H)
H owns the source-eligibility sentences, the operational-reason sentences (including share-first admission refuse), organization_inactive_or_unverified, and already_shared_thread. B7 remains sole technical authority only for source-eligibility reasons. Implementation notes for source-eligibility stay on the B7 card. Every other user-facing mention cites this register.
- null-date: Some recorded time has no date. Filter by matter; narrowing dates will not clear this.
- null-user: Some recorded time has no identifiable lawyer.
- non-visible: Some recorded time we previously ingested no longer appears in Clio's scan.
- unstamped: Recorded time is waiting for Clio ingest to reobserve it, so a complete figure is unavailable. Refreshing this review cannot repair that.
- below-epoch-same-origin: Some recorded time was observed under an earlier Clio authorization.
- identity-unverified origin: Recorded time could not be matched to your verified Clio identity.
- ingest-not-authorized: Organization Clio ingest is not authorized.
- missing-feeder: This organization has no Clio connection.
- validation-denial/hours-redaction: Clio denied or redacted hours for some entries, so a complete figure is unavailable.
- unsafe-ID identity-unverified: Some Clio identities could not be verified, so a complete figure is unavailable.
- null-origin: Recorded-time reconciliation stays unavailable until Clio ingest has completed a full scan under the current authorization epoch and origin on this organization, so a complete figure is unavailable.
Operational reasons are owned by H, not B7. Lawyer-facing strings:
- budget-exhaustion: Some source checks could not finish within this review's request budget.
- source-deadline: Source review stopped when the 20-second source deadline ended.
- cancellation: You cancelled this review before source checks finished.
- mid-request-authorization-loss: Mailbox or Clio authorization was lost during this review.
- share-first admission refuse: This chat is already shared, so Review my work cannot start here.
organization_inactive_or_unverified: This organization is inactive or unverified. Review my work cannot run until an operator restores active status.
11. Acceptance gates, all pending
- Owner/org RLS and composite FK tests deny peers, same-org admins, cross-org actors, missing user GUC and alternate MCP calls without revealing existence.
- Reliable mailbox sender and Clio user/account binding; delegate/received/colleague/unverified identity cases do not become personal activity.
- Overlapping reconstruction and retries create no duplicate anchors; edited/dismissed/unknown decisions and combine/split survive refresh.
- Concurrent changes conflict predictably; combine/split failure rolls back, retry returns the same lineage, anchors and shared Clio entries are not duplicated.
- Fixed date/item/page caps and post-acquisition 20-second source phase hold; cancellation during acquisition, scanning or before commit produces no subsequent source scheduling/disclosure or ledger commit. Owned DB waits are bounded; settled credential maintenance cannot corrupt newer enrollment. No all-in credential/KMS cancellation guarantee is asserted.
- Server-owned range reconciliation deduplicates canonical Clio keys across batches and counts dismissed-only/unlinked entries under explicit provider date/matter scope. Fresh authorization, fixed cutoff/version and bounded revalidation prevent page sums or partial weekly totals. Redacted hours and stale observation epochs remain unknown. Offline relationship exclusion persists while adding a link requires exact access.
- Same fixture first hydrates a provider sentinel on authorized web; generic product/MCP tools, actual graph/model input, checkpoints, history, SSE/Redis replay and alternate-tool arguments never contain it. After revoke/R2 redaction the web withholds it; independent human fields remain usable.
- Exact admission marker permanently blocks sharing AND memory extraction/persistence, even with independently human-authored text, interrupted admission or capability rollback. Real SQL races cover candidate creation/enrichment/salience versus admission AND user/org/matter deletion, with root-first then bounded thread locking and post-lock revalidation. Admission-first refuses writes; memory-first settles before admission; deleted parents produce deterministic content-free outcomes without partial effects. All share-serving/memory consumers retain compatible guards through rollback. G9 history/prior-turn rendering stays correct; workers exclude whole marked threads. Combined-scope MCP real responses exclude private human/source sentinels while ordinary unrelated memory input still works. Source-free handoff/per-bind admission and IDs/enums/counts-only audit remain.
- Actual Home flows (this lane; still gates G/P6/P8) open the review and persist every named correction; ui-test on a permitted existing runtime, both themes, mobile and keyboard. No dev-server start. NOS-384-owned (does not gate this lane's completion): /matters matter-row (
showActionsfalse /onActionabsent / header/cell/menu Review). F5 stays paused. Do not edit matter-table.tsx in this lane. - Isolated database clone/migrate/provision with every DSN consumer overridden; schema generation serialized after token coordination; root lint/typecheck, changed-export suites, cheap eval and independent reviews pass.
- Packet I consent provenance:
auth-oauth.test.ts/oauth-auth.integration.test.tsprove exact consent generation through issuance and refresh; ordinary-family cleanup cannot delete a fresh ledger grant. - Packet J deployment floor: J3 proves the compatibility floor on the three existing shell files, including hook skip and
bg_stage_releaserefuse. - Policy S activation/stream retirement: G9 proves preactivation SSE/WS connections and in-flight/resumable/background runs are retired before ledger admission.
- Operator-attested: query
activity_sync_state.last_full_scan_completed_atplus stampedsource_originon the target org before the reconciliation surface is enabled. - Mailbox-gate exception: G16/G17 prove canonical review=work exemption versus forged/duplicate/ordinary-chat denial.
- G5/G8/G9/G18/G21: gate record names the executed cases, not a green exit: G5 overflow 200-vs-201 / REPEATABLE READ snapshot / origin-stability / unsafe-ID / AU-origin keyed-set / noncanonical stored user_source_id; G8 MCP IDs-only projection and combined-scope memory_save; G9 interrupt-reload-marker / share refusal / preactivation stream retirement; G18 admission-order and delete-first/memory-first races; G21 admitted-thread exclusion on direct/post-turn/nightly jobs.
REQUIRE_DB_INTEGRATION=1 && !RUNthrow so those suites cannot skip. G18/G21 use the theREQUIRE_DB_INTEGRATION=1 && !RUNthrow only, keeping their plaindescribe(§58 Opus 2): both already run unconditionally, anddescribeIntegrationwould convert them to opt-in suites. G5/G8/G9 stay new files with the throw. G19 is mocked; G18 owns SQL races — no REQUIRE throw on G19. - Worker-context digest floor: gate record names the executed G24 cases, not a green exit: admitted thread produces no new
context_digestrows (unmarked positive control still writes); concurrent C admission committed before upsert leaves existing (thread_id,tool_call_id) digest/content_sha256/created_atunchanged; mixedthreadIdthrows, zero rows written; D7/D16/D17/D21 refuse the same seeded marker row.REQUIRE_DB_INTEGRATION=1 && !RUNthrow so the suite cannot skip. - NOS-384-owned (does not gate this lane's completion): firm-wide listings that omit owner (
listChatsForMatter) exclude admitted threads, including after ledger capability disablement. Explicitly exclude ownerlistThreads/ GET /threads?matterId=. Do not hide the owner's Review-my-work chat from the matter sidebar. Delivery oflistChatsForMattermarker-EXISTS exclusion remains the NOS-384 handoff at plan:92. Do not add a G27 assertion that imports apps/web.
12. Decisions and implementation readiness
Earlier review dispositions: keep accepted Astra/Opus2 marker visibility, range ownership/bounds, offline exclusion, configured writer, source-free handoff and per-bind admission fixes. The obsolete global-KMS expansion and optional generic-title/receipt replacements remain rejected. Sol-max has now returned eight findings; its authority/custody/lifecycle corrections below supersede the former claim that review was merely pending. No clean pair or implementation proof.
Permanent sharing fix retained: all share-serving instances must enforce D7 before admission, and every post-admission rollback build retains D7/D9, permanent memory exclusion and worker-context/digest (D16/D17). Sol's direct-origin findings add required caller/policy closure; they do not undo this floor or claim the existing shares.ts owner/origin guard was marker-aware.
Independent Opus2 — eight individual dispositions: recorded in plan §12. Preserve separate local/provider counters, explicit > 200 unavailable scope and positive 199/200/201/narrowed controls; all three MatterTable guards; no separate schema merge without kwiss; G6 real SQL role-denial proof, not check-access Phase B's hot-table list. Use extensionless who_am_i with fields=id,account{id} and regional account comparison without allowlist widening; retain credential_id's exact-row invariant. Primary research is complete, effective synthetic grant proof remains pending. No proof cache or larger provider spend; Opus2 dispositions are retained, not a clean review.
Chosen privacy contract: §7's named transport distinction supersedes the former shared human-text projection. The user explicitly chose IDs and review links only for ledger MCP output; product-chat human text remains protected by admission/memory guards, and authenticated web review retains full authorized descriptions/copy drafts. Structured corrections/combine/split stay shared-service operations. Source-dependent references/staging remain opaque; no host certification, ordinary source-bearing MCP disclosure or taint/declassification framework.
Complete prospective estimate: plan §15.5 owns current unique/line/output caps (118 unique with I=22; 20,100 lines; 123 expected outputs).
Sol-max dispositions: eight individually recorded in plan §13: pre-existing offboarding versus member-bound enrollment; chosen support/MCP policies; fresh authority and membership locks; sync-only custody; null-safe CHECKs; deletion lifecycle; prepare-all deadline; coordinator/input locking and UUID CAS. Global offboarding remains separate and unsolved. This frozen revision requires a fresh independent review pair and RLS re-review; no clean review, schema slot or product work is claimed.
Fresh Astra — three dispositions: complete provenance-row locking and FK-safe contention refusal; retirement of preexisting impersonated streams/runs; explicit client-ceiling upgrade/cache replacement before new direct consent. See plan §14. Opus has now returned eight additional dispositions below; no clean pair or new RLS/runtime result.
Fresh independent Opus — eight dispositions: plan §15 retains narrow supported-operation rollback enforcement, proxy/mint denial and application stream retirement; rejects unmeasured absolute latency claims; fixes stored Clio regional identity; keeps explicit-only DCR scope; corrects carriers to B6/B7/B10/B20/B21; preserves the three verified MatterTable guards with a matters-only handoff; and chooses fresh request-local authority after tracing checkpoint serialization. The orchestrator rejected J's expanded deployment trust model and vendor erasure, not D7/D9/S. All authority/member/RLS/new-scope invariants and three Astra findings remain. Deployment code handoff and synthetic/runtime/review proof are still pending.
Fresh narrowed Astra: four ACCEPT dispositions. Plan §16 records checker/floor continuity, fail-closed Clio IDs with conservative ingested correspondence, review-owned personal Connect/Reconnect and existing Clio auth suite replacement. All earlier authority/member/RLS/new-scope invariants and dispositions remain.
Independent narrowed Opus: three dispositions. Plan §17: O1 ACCEPT existing authorize-hook normalization including original login/consent continuation, not DCR defaults or consent alone; O2 DUPLICATE of Astra's version-1 continuity; O3 ACCEPT pinned production floor identity and standard-hook leaked-test-mode refusal with isolated J3 proof. Review returned against the prior snapshot; the integrated revision is not clean and actual runtime/grant/RLS proof remains pending.
Complete Astra and Opus history: plan §18 records Astra's accepted mailbox-independent canonical review P2; §19 preserves all ten separate Opus dispositions covering scope ceilings, static deployment declaration, memory closure, ledger-only admission, org recovery, metadata, pre-checkout ordering, unsupported request_uri, cap arithmetic and context naming. Neither is a clean review of this revision.
Sol FINAL RLS: four dispositions. Plan §20: S1 ACCEPT, exact-generation mechanism now selected and fully allocated; S2 ACCEPT, MCP output policy resolved by explicit user decision; S3 ACCEPT app_role A5/both GUCs and strict sync restoration; S4 ACCEPT root-first memory locks/parent-delete races. The former OAuth hold was ordinary engineering work, not a user decision. Design decisions are complete; implementation and fresh adversarial/runtime/RLS proof remain pending. NOT CLEAN; #432 retains schema generation.
Fresh fullpass pair — eight separate dispositions: Astra3 accepts unbound cleanup isolation, complete SDK/chassis projection and access-row-keyed sessions. Opus5 accepts executable owner installation, named connector generation, removal of redundant summary-helper work and ledger-only identity CHECKs; accepts nullable-status fact but REJECTS fail-open COALESCE. Opus confirmed prior provider-hook/patch mechanics and arithmetic, not cleanliness. Both reviews completed against the previous snapshot; integrated revision remains planned/NOT CLEAN, real owner/SQL/provider/SDK/protocol proof and re-review pending.
Latest complete Astra — one P1 retained, then a later clean pass: plan §23 accepts admitted-A/mutable-main-B materialization, selecting exact read-tree with bare HEAD/refs preserved and truthful race refusal. A subsequent Astra pass against that integrated revision returned clean. Latest Opus then returned three findings in §25; independent Astra+Opus then returned four findings after §25, separately dispositioned in §26; neither reviewer is running. No J3 runtime or clean pair claimed.
Latest completed Opus — five separate dispositions: plan §24: (1) PARTIAL ACCEPT single helper-owned transaction; REJECT the claim nested BEGIN commits early. (2) ACCEPT rotation compatibility/proof gap, retaining intentional404/reinitialize and immutable A authority; no transparent-client guarantee. (3) ACCEPT unchecked/partial/complete-empty attachment coverage using existing B/C/F/G paths. (4) ACCEPT unsupported-task response gap; REJECT fake task result/capability/raw-error passthrough, require safe protocol error before effects. (5) ACCEPT qualified Clio mapper and Better Auth core-consumer citations only, no added product path. All prior consent/privacy/S/A/§23 decisions remain; actual owner/SQL/provider/SDK/lifecycle/coverage/browser/J proof and re-review are pending. Both reviews of that snapshot completed, NOT CLEAN.
Latest independent Opus — three separate dispositions after Astra clean: plan §25: (1) ACCEPT P2 deploy SHA mismatch: after exact read-tree, DEPLOY_DIR can be A while main is B; J2 writes a non-secret materialized-SHA marker beside .deploy-mode, J1 bg_init_config refuses when it exists and differs from DEPLOY_SHA, J3 proves direct deploy.sh refuses and never stages RELEASES_ROOT/B from A's tree; no deploy.sh/rollback.sh edit. (2) ACCEPT P2 unmapped research report and 404 hrefs: keep H4 at docs/research/2026-09-15-private-work-ledger-provider-preflight.md; cite the repo path, never a docs-site URL; do not expand serve.ts. (3) ACCEPT P3 unqualified citation: chassis elicitation is apps/mcp-server/src/chassis.ts:390–455, not mcp-app.ts SSE. NOT CLEAN.
Independent Astra+Opus after §25 — four separate dispositions: plan §26: (1) ACCEPT same root cause as (2): post-receive:110 sets DEPLOY_SHA=target then :137 bg_init_config BEFORE materialization, so marker A ≠ incoming B on every ordinary push; keep bg_init_config side-effect-free; move marker≠DEPLOY_SHA refuse into bg_stage_release. (2) ACCEPT: direct deploy.sh "$DEPLOY_DIR" after superseded abort must refuse; ordinary later compatible push must still read-tree, rewrite marker and deploy; rollbacks reuse RELEASES_ROOT/<sha> and must not consult the checkout marker; J3 proves (a)(b)(c); J2 is the only marker writer. (3) ACCEPT B7 sync_role / C app_role split with no new grants. (4) REJECT re-litigation of §24.2's 100-record/5-attempt cap and attachments_unchecked tail; no user question. Map stays 102/16,080. NOT CLEAN.
Independent Astra three + Opus eight after §26 — eleven separate dispositions: plan §27. Astra: (1) ACCEPT B7 bounded row projection (canonical key, date/matter, visible-or-unknown minutes, redacted flag, authorized note/URL, row epoch, population fingerprint); C joins for association/exclusion only; unlinked TimeEntries appear with empty relation; no app_role SELECT on clio_activity. (2) ACCEPT named countEligibleRecordedTimeOverflow COUNT/EXISTS, LIMIT 200, not a 201st key or five full pages; 200 complete vs 201 too-many both expressible. (3) ACCEPT saved-work as a separate app_role query over work_episode by stored work_date/matter; VALUES join only decorates B7's projection; mail-only episodes count. Opus: (1) CLARIFY credential_revision inside binding JSON, not a connectors column. (2) ACCEPT named indexes in existing A files, no db:generate now. (3) ACCEPT stored work_date vs request local date bounds; timezone is display metadata; clio_activity.date IS NULL forbids the complete-figure claim. (4) ACCEPT D10 predicate in apps/worker-context/src/thread-loader.ts and assertion in existing digest.integration.test.ts; admitted threads produce no new digest rows; no taint framework. (5) REJECT a new pathname; query-param /tasks/chats/new?review=work stays. (6) REJECT a chat_thread column; admission remains a durable assistant marker, same permanence class as data-netdocs-taint. (7) ACCEPT v1 excludes meetings and North document drafts; F3 labels that absence. (8) ACCEPT post-activation missing compatibility state file refuses, not silent dormant-permissive; pre-activation absence remains dormant; H names restore; J3 asserts the case. Preserve §26 staging-refuse. Neither reviewer running. NOT CLEAN.
Independent Opus ten + Astra two after §27 — twelve separate dispositions: plan §28. Opus: (1) ACCEPT map digest.ts; re-read raw-marker after last remote await and immediately before repository.upsert, hold to commit (superseded by §30); G24 asserts admission between load and upsert produces zero new rows; thread-loader alone is insufficient. (2) ACCEPT EXISTS over chat_message parts inside findDigestThread's existing withRlsTransaction; checkpoint/graph.getState does not contain the marker; drop inspect-raw-messages wording. (3) ACCEPT worker-context/digest on spec §7 admission floor and plan §8 activation + rollback-compatible-build list, same class as D7/D9/memory. (4) ACCEPT B7 projection + overflow + fingerprint in one REPEATABLE READ transaction; sealed-cursor fingerprint; G5 feeder-commit interleave never a complete 200. (5) ACCEPT ingest-connector set and index (organization_id, connector_id, user_source_id, date); G5 two connectors same user_source_id → zero cross-connector disclosure. (6) ACCEPT never Number() hours; aggregate unrounded exact decimal hours, round once at the named minute figure. (7) ACCEPT overflow/200-vs-201/narrowed proof in G5; G6 only C composition against a real connectors-api fixture. (8) ACCEPT spec J 190/60/320=570 and missing-state refuse + restore in §3.4. (9) ACCEPT named evidence index including source_kind and owner_user_id. (10) ACCEPT work_date NOT NULL; timezone-difference coverage label without changing the stored-work_date predicate. Astra: (1) ACCEPT missing json judged from the live controller's static declaration, not post-vs-pre by the json; J3 deletes only the json after a declaring controller is live. (2) ACCEPT named null-date EXISTS on the same snapshot. Neither reviewer is running. NOT CLEAN.
Independent Astra+Opus after §28 — eight separate dispositions: plan §29. Astra+Opus: (1) ACCEPT digest guard inside ContextDigestRepository.upsert, same withRlsTransaction as the insert, SELECT … FOR SHARE on chat_thread / marker or INSERT…SELECT WHERE NOT EXISTS (superseded by §30); runContextDigestJob cannot hold a lock across repository.upsert; keep job-body cancellation; G24 admits between guard read and insert, not only load vs upsert; digest.ts already mapped. (2) ACCEPT map existing packages/db/src/sync-tx.ts; optional isolationLevel forwarded to drizzle db.transaction so BEGIN ISOLATION LEVEL REPEATABLE READ precedes current_user; default unchanged; existing callers stay READ COMMITTED; do not drop the wrapper. (3) ACCEPT split G5: (a) same-request feeder interleave → projection/overflow/null-date/fingerprint all agree (RR property; READ COMMITTED must fail); (b) cross-request sealed cursor after feeder → fingerprint differs, full figure refused; drop “never a complete 200” from same-request. (4) ACCEPT null-date EXISTS scoped to actor + ingest-connector + selected matter and bounded by last_seen_at overlapping the window (superseded by §30); out-of-overlap null-date rows are a labelled coverage count, not hard incomplete. (5) ACCEPT null-date fixture disjoint from 199/200/201/narrowed controls; boundary fixtures contain zero null-date eligible rows. (6) ACCEPT stage-release refuse AFTER green-lit reuse early-return (:604–609) and BEFORE rsync (:637); J3: reuse of existing green-lit release with stale marker still succeeds. (7) ACCEPT remaining spec 530 / 190/60/280 replaced with 190/60/320 =570. Astra 3 ACCEPT: B7 returns lossless hours (numeric string) per row AND a separate round-once aggregate minutes from the same RR snapshot; C uses that aggregate for the named figure and does not sum per-row display minutes; G5 asserts both on the B7 result. Neither reviewer is running. NOT CLEAN.
Independent Astra+Opus after §29 — eleven separate dispositions: plan §30. Astra: (1) ACCEPT inside upsert's withRlsTransaction, ONE statement that skips insert AND ON CONFLICT update when the admission marker exists; do NOT FOR SHARE/FOR UPDATE chat_thread from digest; drop marker-row lock wording; G24 concurrent C admission committed before that write → digestRowCount unchanged; admission after the write is memory-first-may-finish; keep job-body cancellation; REJECT Opus 2's lock recipe. (2) ACCEPT DROP last_seen_at as a work-window predicate; any currently eligible visible TimeEntry with date IS NULL in the actor + ingest-connector set (and selected matter only when selected) is hard incomplete; narrowing matter can clear it, narrowing dates cannot; out-of-connector/actor rows do not count. (3) ACCEPT matter predicate only when selected; week-wide uses the unfiltered eligible set. Opus: (1) ACCEPT visible NULL user_source_id is labelled incomplete coverage, never evidence of “not mine”; same eligible set as null-date, optional matter; disjoint G5 fixture. (2) REJECT digest FOR SHARE/FOR UPDATE chat_thread. (3) ACCEPT any whole-entry denial or hours redaction refuses the named figure; do not publish B7 snapshot aggregate as complete when validations withheld rows/hours; G5/G6 one denied and one redacted inside otherwise complete 200. (4) ACCEPT RR transaction commits after projection/overflow/null-date/fingerprint and before any exact-entry validation call; sealed fingerprint, not an open transaction, revalidates later. (Refined: no provider work inside the open transaction; identity GETs stay in the 20-second source phase.) (5) ACCEPT B7 compares clio_activity.date to inclusive local date bounds; derived UTC range is for sentDateTime only; G5/G6 boundary-day non-UTC fixture. (6) ACCEPT J3(a) claims only no RELEASES_ROOT/<B> staging, no .deploy-release=B, no swap; hook refresh and reconcile may run from admitted A first. (7) ACCEPT G5 owns overflow query and 200-vs-201 under sync_role; G6/G10 own composed C/UI figures; one owner per case. (8) ACCEPT mark stale 105/17,100 and 102/16,080 plan lines historical; then-current 106/17,440 is historical; §15.5 is sole current authority. Neither reviewer is running. NOT CLEAN.
Astra CLEAN after §30; independent Opus eight — separate dispositions: plan §31. Astra returned CLEAN against the §30 integrated revision. Opus: (1) ACCEPT null-date and null-user each own a partial-reason enum and guidance; neither uses the “narrow dates” string; null-date guidance is matter-filter only, null-user is awaiting feeder; G10: “narrow dates” string absent on a null-date refusal. (2) ACCEPT null-user predicate literally: ingest-connector set ∧ observation_state='visible' ∧ TimeEntry ∧ user_source_id IS NULL ∧ (matter only when selected); no actor, no date; G5 disjoint fixture includes one row NULL on both date and user_source_id (Superseded by §39 Opus 2: both-NULL row is labelled, not refuse). (3) ACCEPT split: validation-time denial/redaction refuses the named figure; an already-known quantity_redacted ingestion row contributes zero minutes and one unknown-duration count and does not refuse; split the G5/G6 fixture. (4) ACCEPT stage-release refuse immediately after existing [ -e "$BG_RELEASE_DIR" ] refuse (:610–612) and before set_phase :615; J3(a) asserts no phase change and no $BG_STAGE_DIR on refusal. (5) ACCEPT marker absence is dormant only until the first successful J2 materialization; J2 writes the marker during the deployment owner's initial handoff install (same barrier as durable minimum); replace “or an operator deletes the marker” with “or the operator re-pushes so J2 rewrites it”; J3 absent-marker direct-deploy after first materialization (live predicate deleted in §39 Opus 5; floor ≥ 1 ⇒ marker required; no floor ⇒ absence dormant; J3(e) sets the floor) refuses. (6) ACCEPT mark superseded live recipes in §28/§29 (hold-through-commit, FOR SHARE chat_thread, last_seen_at) with “superseded by §30” in place. (7) ACCEPT mark plan:556/:576 J “350 lines” and plan:788 H 200 as historical; current J 570, H 220. (8) ACCEPT digest upsert asserts uniform threadId beside organizationId, or INSERT…SELECT correlated on v.thread_id; G24 two-thread batch (INSERT…SELECT alternative deleted by §33; G24 two-thread mixed threadId is throw, not mixed write). Opus has not re-run. NOT CLEAN.
Independent Astra 1 + Opus 1–6 after §31 — seven separate dispositions: plan §32. Astra: (1) ACCEPT seal null-date and null-user EXISTS (or a coverage digest) with the fingerprint; cursor reuse refuses the full figure if fingerprint OR those flags change; G5 (b): dated overflow count unchanged, null-date insert and a separate null-user insert → figure refused. (Superseded by §40 Astra 2: G5(b) keeps overflow-unchanged + null-date refuse; null-user/both-NULL only labelled flag/fingerprint, do not refuse.) Opus: (1) ACCEPT G24 observables: admitted thread's existing (thread_id, tool_call_id) digest/content_sha256/created_at unchanged; two-thread batch: unmarked thread's row IS written, admitted thread's is not (superseded by §33: G24 two-thread mixed threadId is throw, zero rows written); digestRowCount alone is insufficient. (2) ACCEPT null-user is hard incomplete, same sentence shape as null-date; G5 and G6 assert the named figure is refused when countEligibleNullUserCoverage > 0. (3) ACCEPT drop ingest origin+account; eligible ingest set is owned org Clio connector rows already visible to sync_role; do not decrypt/refresh firm ingest or call who_am_i for personal ledger; G5 two connectors same user_source_id isolate on connector_id; ingest identity unverified is not a path. (4) ACCEPT price §30/§31 in §2 delta and §15.5 line list (B7 predicates, G5/G6/G10 fixtures, guidance); do not claim +0/0. (5) ACCEPT restore the two outlook-automatic-recovery files from HEAD; one sentence in plan §1: generated STATUS/index only; do not ship another lane's pages. (6) ACCEPT uniform-threadId / two-thread batch on this §7 floor paragraph beside the one-statement guard. Neither reviewer is running. NOT CLEAN.
Independent Astra 1 + Opus 2–7 after §32 — seven separate dispositions: plan §33. Astra: (1) ACCEPT ingest-connector set is type='clio' AND scope='organization' AND status='authorized'; revoked leftovers out; still no decrypt/refresh/who_am_i; G5 two-connector isolation includes revoked leftover vs live authorized. Opus: (2) ACCEPT null-user keeps no actor predicate; add window date BETWEEN bounds OR date IS NULL so NULL-date+NULL-user stay hard incomplete (Superseded by §39 Opus 2: both-NULL row is labelled, not refuse); G5 out-of-window null-user row must not refuse. (3) ACCEPT spec §8.1 cursor seal includes null-date/null-user flags (or coverage digest), same wording as §3.2. (4) ACCEPT pick assertion: upsert throws on mixed threadId, zero rows written; G24 two-thread case is throw, not mixed write; delete INSERT…SELECT as the mixed-threadId ALTERNATIVE only (uniform-threadId D17 construction remains INSERT … SELECT … WHERE NOT EXISTS(marker) … ON CONFLICT DO UPDATE; mixed threadId still throws before that write). (5) ACCEPT mark plan:848 origin+account recipe superseded by §32 in place. (6) ACCEPT mark leftover 106/17,440 sentences historical; §15.5 is sole current authority. (7) ACCEPT when isolationLevel requested, SELECT current_setting('transaction_isolation') must equal repeatable read, throw otherwise; G5 (a) nested call throws. Neither reviewer is running. NOT CLEAN.
Independent review after §33 — seven separate ACCEPT dispositions: plan §34. (1) ACCEPT gate bg_refresh_post_receive_hook and bg_refresh_runtime_scripts on the same marker predicate as bg_stage_release; stay in 3 J files; J3 hook bytes unchanged when staging refuses after superseded abort and on stale-marker green-lit reuse. (2) ACCEPT scope §33 Opus 4 deletion to the mixed-threadId ALTERNATIVE only; name D17 construction INSERT … SELECT … WHERE NOT EXISTS(marker) … ON CONFLICT DO UPDATE, correlated on the asserted single threadId; mixed threadId still throws before that write. (3) ACCEPT third coverage EXISTS countEligibleNonVisibleCoverage on the same RR snapshot (ingest-connector set, observation_state IN ('cleared','unseen') OR coverage='unknown' (Superseded by §39 Astra 2 / Opus 3: delete cleared from this EXISTS.), window date BETWEEN OR date IS NULL, actor predicate where user_source_id non-null); seal with fingerprint; own partial-reason + guidance; G5/G6/G10. (4) ACCEPT null-user guidance names labelled incomplete for visible redacted-user; drop “awaiting feeder” as the clear path for redaction; distinct enum; no “narrow dates”; G10 matter-filter affordance; G5/G6 matter-scoped request clears week-wide null-user refusal. (Superseded by §44 Astra 2: null-user labelled flag/fingerprint only; G5/G6 matter-scoped request does not clear week-wide null-user refusal.) (5) ACCEPT restore the two outlook-automatic-recovery HTML files from HEAD. (6) ACCEPT hours lossless from stored numeric onward; decoded Number at ingest is the same class as IDs, tolerated for short decimals; qualify §3.2/plan §16.2; no code path. (7) ACCEPT qualify packet B preflight origin+account to personal-grant successive enrollments, not ingest comparison. Neither reviewer is running. NOT CLEAN.
Independent Astra 1–3 + Opus 1–5 after §34 — six separate dispositions: plan §35. Astra 1 / Opus 3 ACCEPT skip not abort: inside both bg_refresh_post_receive_hook and bg_refresh_runtime_scripts, skip (return 0, no copy) when the marker is absent after first materialization or ≠ DEPLOY_SHA (Superseded by §39 Opus 5: delete “after first materialization” as live predicate; floor ≥ 1 ⇒ marker required; no floor ⇒ absence dormant; J3(e) sets the floor.) (Superseded by §46 Opus 2: skip-both half; sole live J contract is hook skip only; runtime-scripts copy from $BG_RELEASE_DIR with no discriminator.); do not abort; bg_stage_release owns the only refusal; J3 hook bytes unchanged; bg_reconcile still runs; J3(a) no RELEASES_ROOT/<B>, no .deploy-release=B, no swap, hook/runtime-script bytes unchanged; mark §30.2 Opus 6 superseded in place. Astra 2 / Opus 2 ACCEPT reshape countEligibleNonVisibleCoverage: TimeEntry ∧ ingest-connector ∧ (cleared/unseen OR coverage='unknown') ∧ (date BETWEEN OR date IS NULL) ∧ (actor if user_source_id non-null else incomplete) ∧ matter only when selected; PARTITION unseen/coverage='unknown' and matter_redacted hard incomplete, matter_ineligible/matter_absent labelled coverage count; name affordances; G5 Expense / other-matter / null-user-non-visible cases. Astra 3 ACCEPT mark §30.2 Opus 6 superseded in place. Opus 1 ACCEPT fourth EXISTS countEligibleUnstampedCoverage: ingest-connector ∧ visible TimeEntry ∧ (source_authorization_epoch IS NULL or below current authorization epoch) ∧ same window/actor; seal fingerprint; own partial-reason + “awaiting feeder reobservation; ledger refresh cannot repair”; overflow/200-vs-201 measured on the same set as the projection; G5/G6/G10. (Superseded by §41/§42: three separately named literal predicates; unstamped EXISTS is shared population ∧ source_authorization_epoch IS NULL only.) Opus 4 ACCEPT Clio durable anchor is verified account (canonical regional origin + data.account.id) + activity ID; connector_id is selection predicate and non-key provenance snapshot, not unique/dismiss key; dismiss key is personal verified account + activity ID; no stored ingest account, no who_am_i; drop ingest-same-account-as-personal comparison; dismissals survive disconnect+reconnect of that personal account; G5 live-authorized vs revoked leftover still isolates on connector_id for selection, not for dismiss keys. Opus 5 REJECT as generated-index mtime residue of HANDOVER-clicky-surface.html, not a ledger contract; do not fight status.ts mtime sort; do not ship other-lane content edits. Neither reviewer is running. NOT CLEAN.
Independent review after §35 — eight separate ACCEPT dispositions: plan §36. (1) ACCEPT fifth fingerprint-sealed precondition: parseActivitySyncState in the same RR snapshot; refuse named figure unless completed_updated_since IS NOT NULL, state epoch equals current authorization epoch, last_full_scan_completed_at present; zero-row fresh connector refuses, not 0 minutes; G5/G6/G10. (2) ACCEPT split unseen vs cleared: hard incomplete only unseen/coverage='unknown' WITH user_source_id=actor AND date in window; demote every cleared_reason including matter_redacted to labelled coverage count; do not change A6 clear-writer (it NULLs date/user/matter); name column cleared_reason with exact three literals; non-NULL only when observation_state='cleared'; default arm for NULL/unknown reasons; G5 week-wide ineligible does not refuse; matter-scoped redacted does not refuse the named figure (labelled only). (3) ACCEPT name cleared_reason not coverage for the partition. (4) ACCEPT move CLIO_AUTHORIZATION_EPOCH_SQL into packages/clio-sync; Shared SQL returns text; B7 casts. Unstamped comparison numeric. B7 uses that named expression with numeric/bigint comparison; map existing packages/clio-sync/src/activity-sync-state.ts. (5) ACCEPT coverage reasons before overflow; combined >200 AND null-date emits coverage reason, never “narrow dates”; G6/G10 combined fixture. (6) ACCEPT at most one authorized org Clio connector (connectors_org_scope_unique); isolation fixture is authorized vs revoked leftover; state the invariant. (7) ACCEPT with active floor minimumVersion ≥ 1, marker REQUIRED on the staging path: absence → bg_stage_release refuse, refresh skip; green-lit reuse unaffected; J3 absent-marker green-lit-reuse succeeds; no active floor → absence dormant; J3(e) sets the floor. (8) ACCEPT spec J 190/60/360 =610; mark leftover 570 historical. Neither reviewer is running. NOT CLEAN.
Independent Astra 1–2 + Opus 4–10 after §36 — nine separate ACCEPT dispositions: plan §37. Astra 1 ACCEPT drop labelled cleared coverage that needs actor/date/matter; keep A6 clear-writer NULLs; hard incomplete only unseen/coverage='unknown' WITH user_source_id=actor AND date in window; do not window labelled coverage on cleared rows. Astra 2 ACCEPT CHECK: (observation_state='cleared' AND cleared_reason IN three literals) OR (observation_state <> 'cleared' AND cleared_reason IS NULL); markActivitiesUnseen must NULL cleared_reason — absence-writer change in A6/clio-sync. Opus 4 ACCEPT measure overflow on the same set as the projection; delete pre-epoch-filter overflow refinement. Opus 5 ACCEPT visible redacted-user is labelled incomplete, not org-wide refuse; do not change mapActivity; nested visibility still P1; drop “awaiting feeder” as the clear path for redaction. Opus 6 ACCEPT named partial reason ingest connector present but not authorized (error/requires_reauth); empty projection shows that, not no-match; G5/G10. Opus 7 ACCEPT source_authorization_epoch bigint (or text compared via ::bigint); unstamped comparison numeric; shared SQL returns text; B7 casts. Opus 8 ACCEPT map packages/clio-sync/src/index.ts as A11. Opus 9 ACCEPT dismissals apply only while ingest is the same Clio account as the personal verified account; G5 reconnect ingest under a different account → prior dismissals do not apply; no who_am_i. Opus 10 ACCEPT marker absence refuses on the staging path only; green-lit reuse unaffected; J3 absent-marker green-lit-reuse succeeds. Superseded in part by §38. Neither reviewer is running. NOT CLEAN.
Independent Astra 1–4 + Opus 6–9 after §37 — seven separate dispositions: plan §38. Astra 1 ACCEPT drop remaining labelled cleared coverage; hard incomplete only unseen/coverage='unknown' WITH user_source_id=actor AND date in window; keep A6 NULLs; G5 does not count labelled cleared rows. Astra 2 ACCEPT Writer markActivitiesUnseen NULLs cleared_reason. Drop the cleared_reason CHECK from this lane. Throwaway backfill for P3 clones only. No generated CHECK, no hand-edited SQL, no deploy-path backfill. (Superseded by §42 Opus 6 / §43 Astra 2 / Opus 1: drop the writer/backfill; cleared_reason is unused by this lane; no feeder edit; keep Drop the CHECK.) Astra 3 ACCEPT before no-match/scan-state on empty authorized set, EXISTS org Clio status IN ('error','requires_reauth','pending') or status <> 'authorized' emits ingest-not-authorized with the status enum; do not parse scan-state of a non-authorized row. Astra 4 / Opus 9 ACCEPT drop different-account G5; dismiss key is personal verified account + activity ID; no stored ingest account, no who_am_i; drop ingest-same-account-as-personal comparison. Opus 6 REJECT HITL first-admission; first ledger tool use admits the thread; prompt/tool-help explain permanence; do not add interrupt. Opus 7 ACCEPT restore outlook-automatic-recovery HTML after regen; CDN-script-drop clause withdrawn in place — tags are the house head template, not ledger content. Opus 8 ACCEPT any org Clio row with status <> 'authorized' emits ingest-not-authorized; only complete absence of org Clio is missing-feeder; G5/G10 pending case. Neither reviewer is running. NOT CLEAN. (Astra 3 / Opus 8 superseded by §39 Astra 1 / Opus 1 / Astra 3: ingest-not-authorized only on empty authorized set; exclude revoked/archived; leftover only if none live. Astra 1 labelled-cleared drop superseded by §39 Astra 2 / Opus 3: delete cleared from non-visible; cleared ignored for counts/flags/fingerprint.)
Independent Astra 1–3 + Opus 1–6 after §38 — seven separate ACCEPT dispositions: plan §39. Astra 1 / Opus 1 ACCEPT ingest-not-authorized only on empty authorized set; exclude revoked/archived from that EXISTS; if authorized org Clio exists, ignore leftovers and parse scan-state of the authorized row; if authorized empty, remaining live org Clio (status NOT IN revoked/archived) is ingest-not-authorized; zero org Clio rows is missing-feeder. Astra 2 / Opus 3 ACCEPT delete observation_state='cleared' from countEligibleNonVisibleCoverage; non-visible is unseen or coverage='unknown' only; hard incomplete unseen/unknown WITH user_source_id=actor AND date in window; cleared rows ignored for counts/flags/fingerprint; §8 redacted-matter entries leave the named figure with no signal; keep A6 NULLs. Astra 3 ACCEPT empty authorized: status from unique live org Clio (status NOT IN revoked/archived); leftover only if none. Opus 2 ACCEPT NULL-user is labelled; delete stale both-NULL hard-incomplete sentence; both-NULL row is labelled, not refuse; fix G5. Opus 4 ACCEPT feeder stamps ingest-side normalized origin (connector-clio origin helper) onto clio_activity in the same A6/A7 delta as source_authorization_epoch; refuse/label identity-unverified when absent or differs from personal verified origin; no who_am_i. Opus 5 ACCEPT delete “after first materialization” as live predicate; floor ≥ 1 ⇒ marker required; no floor ⇒ absence dormant; J3(e) sets the floor. Opus 6 ACCEPT spec:357 arithmetic; §15.5 sole authority. Neither reviewer is running. NOT CLEAN.
Independent Astra 1–4 + Opus 1–5 after §39 — eight separate ACCEPT dispositions: plan §40. Astra 1 / Opus 2 ACCEPT A7 passes bundle raw provider_metadata.api_host (NULL if not a string); A6 runs existing normalizeHost on that string only; never construct ClioClient to learn origin; stamp NULL when absent; B7 compares that same url.origin to personal verified origin in the same RR snapshot; absent/mismatch refuses the named figure (drop “label”); never stamp ClioClient.baseUrl or env default. (Superseded by §41 Opus 1 / Astra 4 / Opus 10.) Astra 2 ACCEPT mark §32/:945 and §15.5 G5(b) null-user refuse superseded; G5(b) keeps overflow-unchanged + null-date refuse; null-user/both-NULL only labelled flag/fingerprint, do not refuse. Astra 3 / Opus 3 ACCEPT same A unit: change markActivitiesUnseen to NULL cleared_reason; throwaway packages/db/scripts/ backfill SET cleared_reason=NULL WHERE observation_state <> 'cleared' run before migrate (P3 records row count); delete “already NULLs it”; CHECK after backfill. (Superseded by §41 Astra 3 / Opus 4: drop CHECK; P3-clone backfill only.) Astra 4 ACCEPT empty authorized still refuses; status from unique live org Clio when one exists; otherwise leftover revoked/archived is ingest-not-authorized, never 0 minutes; missing-feeder only zero org Clio rows; G5/G10 revoked-only fixture. (Superseded by §41 Opus 5 three org-Clio branches.) Opus 1 ACCEPT hard-incomplete unseen only while fresh against last completed scan (last_seen_at vs last_full_scan_started_at); a row that survives a second completed full scan as unseen is a deletion → labelled coverage, same class as cleared; no schema. (Superseded by §41 Astra 1 / Opus 3: drop second-scan; unseen labelled.) Opus 4 ACCEPT green-lit reuse copies drain/probe from $BG_RELEASE_DIR/deploy/preprod/bin/ (admitted tree), not skip; J3(g) asserts helper bytes match the promoted release. (Superseded by §41 Opus 6 discriminator.) Opus 5 ACCEPT unstamped refuse is source_authorization_epoch IS NULL only; below-epoch-same-origin is labelled; origin mismatch refuses; status recovery that bumps authorized_at must not refuse the figure until a full scan. (Superseded in part by §41 Astra 2 / Opus 7 named predicates.) Seat note ACCEPT product code and prose workers are omp Astra seats per the 2026-09-08 standing rule, not new Claude panes; dual reviewers stay two models (Astra + Opus).
Independent Opus 1–2 / Astra 1–4 / Opus 3–10 after §40 — ten separate ACCEPT dispositions: plan §41. Opus 1 / Astra 4 ACCEPT: do not call normalizeHost from A6; A7 (apps/worker-clio, already depends on connector-clio) stamps origin; export a string-returning origin helper from connector-clio (map clio-client.ts export-only; wrap/catch ClioApiError); stamp the return value, not .origin; absent or throw → explicit NULL; B7 compares that same helper output to personal verified origin; never raw api_host; never ClioClient.baseUrl/env default. Opus 2 ACCEPT: Pre-B3 rows stamp NULL; A origin stamp ships only after B3 preserves verified origin; until then source_origin is NULL and B7 identity-unverified refuses. Astra 1 / Opus 3 ACCEPT: drop second-scan/freshness split; no extra schema; unseen rows are labelled coverage (deletion/redaction class), not hard-incomplete; hard-incomplete remains coverage='unknown' with observation_state='visible' AND user_source_id=actor AND date in window; split the EXISTS; G5(b) does not refuse labelled unseen. Astra 2 / Opus 7 ACCEPT: three named predicates: countEligibleUnstampedCoverage = source_authorization_epoch IS NULL (refuse); countEligibleBelowEpochSameOriginCoverage labelled, sealed in fingerprint, not G5(b) refuse; origin mismatch = existing identity-unverified refuse; rewrite spec:146; drop “state epoch equals current” until a full scan completes under the new epoch (Astra 5). Astra 3 / Opus 4 ACCEPT: drop the cleared_reason CHECK from this lane; writer markActivitiesUnseen NULLs cleared_reason; throwaway backfill for P3 clones only; no generated CHECK, no hand-edited SQL, no deploy-path backfill. Opus 5 ACCEPT: three org-Clio branches: (1) authorized exists → parse that row’s scan-state; (2) authorized empty AND any org Clio row EXISTS (no status filter) → ingest-not-authorized; status source: partial-unique live org Clio row when it exists, else any leftover; extend partial-reason enum to cover revoked/archived; (3) zero org Clio rows → missing-feeder. Opus 6 ACCEPT: discriminator once: on reuse (BG_RELEASE_PROMOTED=1 after bg_stage_release early return) helpers copy from $BG_RELEASE_DIR; marker skip applies only off that path; bg_refresh_post_receive_hook keeps skipping. Opus 8 ACCEPT: Headers 19,620. Opus 9 ACCEPT: reconcile packet table to §15.5; one H value (240). Opus 10 ACCEPT: delete “already held in activities.ts”; A7 passes raw api_host; A7/helper normalizes. Neither reviewer is running. NOT CLEAN.
Independent Opus 1 / Astra 1 / Astra 3–5 / Opus 2–8 after §41 — twelve separate ACCEPT dispositions: plan §42. Opus 1 ACCEPT option B: delete the hard-incomplete arm of countEligibleNonVisibleCoverage and its G5/G6/G10 assertions; unseen is labelled only; do not keep a predicate no writer can produce; rewrite G5/G6/G10 (Astra 2). Astra 1 ACCEPT: unstamped EXISTS is source_authorization_epoch IS NULL only; below-epoch is a separate labelled EXISTS; origin compare is helper-output vs personal verified origin refuse, not inside unstamped. Astra 3 ACCEPT: add countEligibleBelowEpochSameOriginCoverage to the RR read set, G5(a) agreement list, and B7 fingerprint; commit after that read, before provider calls. Astra 4 ACCEPT: P3 A7 stamps NULL only; enable the helper stamp in P4 after B3 preserves stored origin, same release. Astra 5 ACCEPT: origin helper returns string | null and catches URL/TypeError and ClioApiError; A7 stamps NULL on any throw. Opus 2 ACCEPT: distinct BG_RELEASE_REUSED=1 only at bluegreen.sh:607; discriminator keys on that; marker skip on bg_refresh_runtime_scripts is dead on the ordinary path; keep skip load-bearing on bg_refresh_post_receive_hook; J3 restated. (Superseded by §46 Opus 2: delete the reuse discriminator; unconditional copy from $BG_RELEASE_DIR; hook skip stays load-bearing.) Opus 3 ACCEPT: fourth named predicate source_origin IS NULL OR source_origin <> $personalVerifiedOrigin → identity-unverified refuse; seal it in fingerprint (spec:154/303, plan:144/164); G5 stamped-epoch/NULL-origin row. Opus 4 ACCEPT: map apps/mcp-server/src/tool-import-boundary.ts + its test as D-packet; admitting server-only ledger subpath means IDs-only is adapter discipline from that point. Opus 5 ACCEPT: map packages/connector-clio/src/transport-policy.test.ts as affected existing suite; do not delete it as plumbing; price inventory extension. Opus 6 ACCEPT: drop markActivitiesUnseen cleared_reason NULL change; drop enum/default-arm/partition-uses-cleared_reason residue; no feeder edit for a dead column. Opus 7 ACCEPT: mark §39 ingest-not-authorized exclude-revoked sentence superseded by §41 Opus 5 (no status filter). Opus 8 ACCEPT: plan:127 names GRANT ON work_episode and work_episode_evidence TO app_role in TABLE_GRANTS. Neither reviewer is running. NOT CLEAN.
Independent Astra 1 / Opus 2, Astra 2 / Opus 1, Opus 3–10 after §42 — ten separate ACCEPT dispositions: plan §43. Astra 1 / Opus 2 ACCEPT: delete every “Hard-incomplete remains coverage='unknown' with observation_state='visible'…” sentence from plan:144 and spec:144; strike hard-incomplete G10/G5/G6 assertions at spec:336 and plan:212; keep only “non-visible is unseen or coverage='unknown' only; cleared rows ignored; unseen labelled only.” Astra 2 / Opus 1 ACCEPT: delete Writer markActivitiesUnseen NULLs cleared_reason and throwaway-backfill from plan:144 and spec:144; spec:101 cleared_reason is unused by this lane; no feeder edit; keep Drop the CHECK. Opus 3 ACCEPT: copy plan:164 fingerprint list onto plan:144 word for word including countEligibleBelowEpochSameOriginCoverage and the origin-null/mismatch predicate. Opus 4 ACCEPT: §15.5 packet table sole authority; plan:97 chain matches 19,820; §41 +0/0 reconciliation; do not invent a 20,020 cap. Opus 5 ACCEPT: replace plan:115 Fable Claude launch with the omp Astra seat line; owner table already historical. Opus 6 ACCEPT: G25 and D19 are closed-inventory guards that must be updated, never removed (plan:227/:569/:190). Opus 7 ACCEPT: restate G25 as classify the new helper in the closed call inventory and update the pinned token-request text; GET-only and no-unclassified-transport remain; catch/destination proof stays G15/G5. Opus 8 ACCEPT: branch (2) status source is partial-unique live org Clio row when it exists, else any leftover; extend partial-reason enum to cover revoked/archived. Opus 9 ACCEPT: spec:101 cites apps/worker-clio/src/loops/activity-sync.ts:580 as current declaration; move into activity-sync-state.ts (A10), re-export from A11. Opus 10 ACCEPT: CDN-script-drop clause withdrawn in place — tags are the house head template, not ledger content. Neither reviewer is running. NOT CLEAN.
Independent Astra 1 / Opus 3, Opus 1–2, Opus 4, Astra 2, Opus 5–8 after §43 — nine separate ACCEPT dispositions: plan §44. Astra 1 / Opus 3 ACCEPT: on plan:144 and spec:144 ONLY, drop the “three named predicates” parenthetical; split into three separately named literal predicates; mark spec:380 / plan:996 superseded by §41/§42; unstamped is shared population ∧ source_authorization_epoch IS NULL → refuse, nothing else inside that EXISTS. Opus 1 ACCEPT: countEligibleNonVisibleCoverage labelled never-refuse: ingest-connector ∧ observation_state='unseen' ∧ (coverage='unknown' OR true) ∧ same window/actor as null-user; G5 one labelled-flag assertion that bites, not refuse; keep in RR/fingerprint. Opus 2 ACCEPT: countEligibleBelowEpochSameOriginCoverage literal on plan:144 and spec:146. Opus 4 ACCEPT: one origin form quoted everywhere: shared population ∧ (source_origin IS NULL OR source_origin <> $personalVerifiedOrigin) → identity-unverified refuse; not unscoped; not on cleared rows. Astra 2 ACCEPT: delete G5/G6 matter-scoped week-wide null-user refusal from plan:144, spec:144, plan:209, plan:220; null-user labelled flag/fingerprint only; keep G10 matter-filter for null-date refuse. Opus 5 ACCEPT: refuse move_chat_to_matter on a marked thread under the same chat_thread lock C/grantThreadShare already take; G9/G10 admitted thread never appears on the matter page for a second member; map move-chat-to-matter.ts and listChatsForMatter. Opus 6 ACCEPT: one enumerated partial-reason list on the B7 card; every other mention cites it. Opus 7 ACCEPT: mark plan:385 and plan:412 Fable-worker rows superseded in place with the omp Astra seat line. Opus 8 ACCEPT: plan:241 the adopted 73 / 8,700 baseline (historical; §15.5 is the current map). Neither reviewer is running. NOT CLEAN.
Independent Astra 1 / Opus 9, Astra 2, Opus 10, Astra 3 / Opus 4–6, Astra 4 / Opus 7, Opus 1–3, Opus 8 after §44 — nine separate ACCEPT dispositions: plan §45. Astra 1 / Opus 9 ACCEPT drop D21: listChatsForMatter has no production caller (sidebar uses listThreads/threadVisible); drop the G9/G10 matter-page second-member assertion; do not add userId to an unused helper. Astra 2 ACCEPT: same chat_thread FOR UPDATE + marker refuse C/grantThreadShare already use, on updateThreadMatter (covers router + ChatRow); keep D20; map packages/chat-runtime/src/threads.ts; router stays a caller. Opus 10 ACCEPT: admission ACCEPTS a matter-filed thread (matter-table “Review my work” is the product); do not refuse, do not unfile; G9 asserts D20 still blocks subsequent moves in the same fixture. Astra 3 / Opus 4–6 ACCEPT: one literal, no cross-reference, no OR true: countEligibleNonVisibleCoverage = ingest-connector ∧ observation_state='unseen' ∧ type='TimeEntry' ∧ (matter only when selected) ∧ (date BETWEEN bounds OR date IS NULL) ∧ (user_source_id = $actor OR user_source_id IS NULL) ∧ matter_id IS NOT NULL; labelled, never refuse; other users’ unseen must not set the flag. Astra 4 / Opus 7 ACCEPT: shared population includes type='TimeEntry' stated once; precedence origin mismatch/NULL wins over unstamped when both true; guidance strings for every enum member; add validation-denial/hours-redaction and unsafe-ID identity-unverified as members with strings. Opus 1 ACCEPT: §15.5 expected outputs 116→118. Opus 2 ACCEPT: cite remaining dispositions by §/packet label, not stale line numbers. Opus 3 ACCEPT: mark §34 finding 4 / spec §34 “G5/G6 week-wide null-user refusal” superseded by §44 Astra 2. Opus 8 ACCEPT: map packages/agent-runtime/src/tools/move-chat-to-matter.test.ts as affected existing G suite; name bg_gate_cross_matter as a gate that must stay green. Neither reviewer is running. NOT CLEAN.
Independent Opus 1–2, Astra 2 / Opus 3, Opus 4–5, Astra 1 / Opus 6, Opus 7–11 after §45 — ten separate ACCEPT dispositions: plan §46. Opus 1 ACCEPT: add ∧ matter_id IS NOT NULL to the non-visible literal; G5: clear a row, run markActivitiesUnseen, flag stays off for a different actor. Opus 2 ACCEPT: delete the reuse discriminator; bg_refresh_runtime_scripts copies helpers from $BG_RELEASE_DIR unconditionally; no BG_RELEASE_REUSED; hook skip stays load-bearing; supersede plan:513/520/809 PROMOTED sentences. Astra 2 / Opus 3 ACCEPT: updateThreadMatter returns a discriminated result; router maps it to 422 not_shareable-class like grantThreadShare; price existing I router.ts + router.test.ts; ChatRow already has !res.ok; keep D20. Opus 4+5 ACCEPT: restore D21 as marker EXISTS exclusion in listChatsForMatter (no userId); same class as D7/D9/D16/D17; one biting G9/G10 second-member; keep spec:336. Astra 1 / Opus 6 ACCEPT: “every coverage EXISTS quotes shared population” applies only to unstamped / below-epoch / origin; non-visible, null-date, null-user keep their own literals; quoting three use (user_source_id = $actor); unstamped NULL-user does not refuse (labelled via null-user). Opus 7 ACCEPT: fold into spec:272 refuse move_chat_to_matter/updateThreadMatter on a marked thread; admission accepts a matter-filed thread. Opus 8 ACCEPT: map move-chat-to-matter.test.ts as G 21→22; total 113→114 with lines; name bg_gate_cross_matter. Opus 9 ACCEPT: plan:97 → 112 authored. Opus 10 ACCEPT: split each partial-reason enum member into an implementation note and a lawyer-facing user sentence; H owns the ten user strings under spec §10 register. Opus 11 ACCEPT: invert plan:229 default; worker’s final report enumerates every removed assertion; reviewers approve the list; keep G25/D19 carve-out.
Independent Astra 1 / Opus 1, Astra 2, Opus 2–7 after §46 — seven separate ACCEPT dispositions: plan §47. Astra 1 / Opus 1 ACCEPT: map apps/web/lib/matters/chats.ts as D22 (D 21→22, +1 path, +40); keep threads.ts as the updateThreadMatter slot; do not put listChatsForMatter on threads.ts. Astra 2 ACCEPT: mark §35 Astra 1 skip-both half and Astra 3 superseded by §46 Opus 2; sole live J contract is hook skip only; runtime-scripts copy from $BG_RELEASE_DIR with no discriminator. Opus 2 / Opus 4 ACCEPT: map packages/chat-runtime/src/threads.test.ts and apps/web/lib/matters/chats.test.ts as existing G paths; biting second-member assertion lives in chats.test.ts calling listChatsForMatter, not G9; drop the biting G9/G10 claim. Opus 3 ACCEPT: map packages/connector-clio/src/clio-client.ts as A12 export-only origin helper. Opus 5 ACCEPT: expected outputs = unique paths + 5 generated = 123. Opus 6 ACCEPT: B enrollment 11/1,170 + source 8/1,750 + refresh 2/200 =21/3,120; packet row B 21/3,120 left alone. Opus 7 ACCEPT: terminate item (10) with “never equal-by-rounding.” identically on both B7 cards; start the following sentence separately.
Independent Opus 1, Astra 1–2, Opus 2–8 after §47 — ten separate ACCEPT dispositions: plan §48. Opus 1 ACCEPT: keep D22 as an explicitly inert forward guard (no production caller today; NOS-384 may wire it); delete “one biting G9/G10”, “named biting suite”, and “admitted thread never appears for a second member” from plan:194/:218 and spec:336; do not drop the helper guard. Astra 1 ACCEPT: end item (10) with “never equal-by-rounding.” identically on both B7 ID cards (spec:148 / plan:579); start the authorization/full-zero sentence separately, identical wording. Astra 2 ACCEPT: rewrite G28 as real SQL like threads.test.ts — seed marker chat_message, assert the admitted id is absent from listChatsForMatter; do not add userId; the suite proves the helper, not a page. Opus 2 ACCEPT: three more §11 checkboxes for packet I consent provenance, packet J deployment floor (J3), and policy S activation/stream retirement (G9). Opus 3 ACCEPT: narrow B7 “sole technical authority” to source-eligibility reasons; H owns operational reasons (budget exhaustion, 20s deadline, cancellation, mid-request authorization loss) with strings. Opus 4 ACCEPT: add organization_inactive_or_unverified to required-states and a §10.1 sentence; one P0/P6 line preflighting status on target orgs plus the operator fix. Opus 5 ACCEPT: move appended existing A/G paths to the end of each list so ordinal and label agree. Opus 6 ACCEPT: one reason-keyed string in ChatRow’s !res.ok branch for marked-thread 422; map chat-row.tsx (I22). Opus 7 ACCEPT: map packages/connector-clio/src/read-only.test.ts as affected existing G29 with lines. Opus 8 ACCEPT: replace “dead column” with “unused by this lane; no feeder edit” on plan:125 and :128.
Independent Opus 1–4, Astra 1 / Opus 5, Opus 6–7, Astra 3 / Opus 8, Astra 2 after §48 — nine separate ACCEPT dispositions: plan §49. Opus 1 ACCEPT: name source_authorization_epoch and source_origin on applyActivityPage's ON CONFLICT DO UPDATE SET list (activity-events.ts:145–166); G4 asserts restamping of an existing row after a second observation, not only a new insert. Opus 2 ACCEPT: J1 edit at bluegreen.sh:727–738: source="${BG_RELEASE_DIR}/deploy/preprod/bin/${name}.sh"; price inside J1's 190; J3(g) asserts helper bytes on a stale-marker green-lit reuse where DEPLOY_DIR and $BG_RELEASE_DIR deliberately differ; do not phrase $BG_RELEASE_DIR as current fact. Opus 3 ACCEPT: on D16/D17 cards, pre-model window and context_digest_attempt rows are deliberately out of scope (ids/spend only); stop claiming “same class as D7/D9/memory”; do not add a D13-style recheck. Opus 4 ACCEPT: keep G28 mocked; put the biting marker-exclusion assertion in packages/chat-runtime/src/threads.test.ts (already live Postgres); do not add apps/web DB integration. Astra 1 / Opus 5 ACCEPT: headers and §8 completion: 120 paths / 20,180. Opus 6 ACCEPT: §15.5 expected outputs 123→125. Opus 7 ACCEPT: H non-visible user string “Some recorded time we previously ingested no longer appears in Clio's scan.”; labelled-never-refuse unchanged. Astra 3 / Opus 8 ACCEPT: P3 “no feeder edit; cleared_reason is unused by this lane.” Astra 2 ACCEPT: copy the two item-(10) sentences identically onto plan §16.2 and the B7 map card if still missing: “never equal-by-rounding.” then “Never authorization or full zero totals.”
Astra CLEAN after §49; independent Opus 1, Opus 1+2, Opus 3–6 — six separate ACCEPT dispositions: plan §50. Opus 1 ACCEPT: updateThreadMatter(..., null) is allowed on a marked thread (unfile); refuse only a non-null target (cannot file/refile after admission). Opus 1+2 ACCEPT drop D22 from this lane; name the listChatsForMatter marker-EXISTS guard + test as a NOS-384 handoff condition at plan:92; reclaim chats.ts / chats.test.ts paths and lines; do not leave an untested helper; do not add a cross-workspace import. Opus 3 ACCEPT seed-per-SHA: the fixture seeds the marker per run_deploy SHA (fixture-only; J2 stays the only production writer). Opus 4 ACCEPT: one P6/P8 line plus a §11 checkbox requiring an observed completed full Clio scan under the new epoch and origin on the target org before the reconciliation surface is enabled; H sentence in spec §10.1 for the interval. Opus 5 ACCEPT: J1 card: the hook refresh runs at :1511, before staging at :1515, so it cannot read $BG_RELEASE_DIR; skip is the only safe form. Opus 6 ACCEPT: two more §11 checkboxes: mailbox-gate exception (G16/G17) and worker-context digest floor (G24).
Independent Astra 1–2 + Opus 1–5 after §50 — seven separate ACCEPT dispositions: plan §51. Astra 1 ACCEPT: D21 same chat_thread FOR UPDATE as C/grantThreadShare, then marker check, then mutate; non-null + marker refuse; null unfile allowed. Astra 2 ACCEPT: unfile sets matter_id null only; marker stays; G27 unfile then non-null still refuses; G9 share still not_shareable after unfile. Opus 1 ACCEPT: add the invariant to the §8 floor and a §11 checkbox at capability level: no admitted thread is returned by any matter-scoped thread listing; keep plan:92 handoff sentence as the delivery note. Opus 2 ACCEPT: D20 same discriminated rule allow targetMatterId === null, refuse non-null; propagate to spec:272, plan:192, plan:214, G26. Opus 3 ACCEPT: restate J3(e): run_deploy writes the marker for the SHA it materializes, simulating J2; (a)/(b)/(d)/(g) materialize a different SHA first; (e)'s absence arm removes or omits the seed. Opus 4 ACCEPT: NULL-origin own enum member carrying the spec:362 sentence (same class as unstamped — awaiting feeder); leave mismatch on member (6); §11 checkbox operator-attested with named observable activity_sync_state.last_full_scan_completed_at plus stamped source_origin. Opus 5 ACCEPT: I = 21, add “stable labels skip I19”; recalculate total/outputs; do not invent a path. (Superseded by §52 Astra 2: I=22; unique 118; outputs 123.) D22 stays dropped (NOS-384 handoff).
Independent Astra 1–2 + Opus 1–12 after §51 — fourteen separate ACCEPT dispositions: plan §52. Astra 1 ACCEPT: scope the §8/§11 floor to firm-wide listings that omit owner (listChatsForMatter, NOS-384); explicitly exclude owner listThreads / GET /threads?matterId=; do not hide the owner's Review-my-work chat from the matter sidebar. Opus 1 ACCEPT in that scope: mark the §11 box NOS-384-owned so it does not gate this lane's completion; keep plan:92 handoff; do not add a G27 assertion that imports apps/web. Astra 2 ACCEPT: keep all 22 I files; I = 22; unique 118; outputs 123; undo the I=21 correction. Opus 2 ACCEPT: copy plan:145 members (6)+(11) verbatim onto spec:144; plan:145 is the copy of record. Opus 3 ACCEPT: cite user sentences by name (“spec §10.1 null-origin”), not stale line numbers. Opus 4 ACCEPT: keep the string on the null-origin member; delete the duplicate operator-interval sentence. Opus 5 ACCEPT: member (5) below-epoch user string is an observation fact: “Some recorded time was observed under an earlier Clio authorization.” Keep “waiting” only where it refuses (unstamped, null-origin). Opus 6 ACCEPT: on both B7 cards, cross-account isolation rests on unproved global uniqueness of Clio user ids; P1 records the observed uniqueness scope on the two designated test accounts; align plan:291; no firm who_am_i. Opus 7 ACCEPT: introduce no new discriminator (no BG_RELEASE_REUSED); leave existing BG_RELEASE_PROMOTED at :55/:607/:723 untouched. Opus 8 ACCEPT: export D10's predicate as the single SQL fragment/TS helper; D7/D16/D17/D20/D21 cite it; one assertion that every guard refuses the same seeded marker row, hosted in G24. Opus 9 ACCEPT: defer plan:97/spec:392 to §15.5; with I=22 the chain is 118. Opus 10 ACCEPT: plan:545 expected outputs 123 (118+5). Opus 11 ACCEPT: J3 case: main advances between post-receive's ref check and deploy.sh's SHA derivation → refuse, no staging, later push deploys; one H sentence. Opus 12 ACCEPT: one sentence in D20 tool description, prompt.ts, and F3/ChatRow unfile affordance: unfiling an admitted thread is permanent and re-filing is refused; assert in G26/G27.
Astra CLEAN after §52; independent Opus 2–6 — five separate ACCEPT dispositions: plan §53. Astra returned CLEAN. Opus 2 ACCEPT: host the D20 arm in G26 move-chat-to-matter.test.ts (already imports the tool); scope G24's unified assertion to D7/D16/D17/D21; do not add a ./tools/move-chat-to-matter subpath. Opus 3 ACCEPT: add to §7 worker-context RUN_DB_INTEGRATION=1 REQUIRE_DB_INTEGRATION=1 bunx vitest run src/digest.integration.test.ts src/thread-loader.test.ts and worker-memory RUN_DB_INTEGRATION=1 bunx vitest run src/backfill.test.ts src/mine.test.ts src/synthesize.integration.test.ts; name both packages in plan:339 changed-exports with the isolated all-DSN map. Opus 4 ACCEPT: G24 card require REQUIRE_DB_INTEGRATION=1 && !RUN throw so the suite cannot skip under the gate; §11 gate record names the executed G24 cases, not a green exit. Opus 5 ACCEPT: terminate member (11) at “implementation note: source_origin IS NULL.”; restore “Never authorization or full zero totals.” as the list's own closing sentence, identical on spec:144 and plan:145; card prose starts separately. Opus 6 ACCEPT: add “stable labels skip dropped G28” to plan:207 and the §15.5 slice note, beside B19.
Independent Astra 1 + Opus 1–6 ACCEPT, Opus 7 NOTE after §53 — eight separate dispositions: plan §54. Astra 1 ACCEPT: qualify G10, spec §11 “Actual Home and /matters flows”, and the usable-UI matrix the same way as the listing box: Home is this lane and still gates G/P6/P8; /matters matter-row (showActions false / onAction absent / header/cell/menu Review) is NOS-384-owned and does not gate this lane; F5 stays paused; do not edit matter-table.tsx in this lane. Opus 1 ACCEPT: reword J2/J3 “preserve leaked-test-mode refusal” to “add” on plan:523/:524/:526/:601 and spec:176; do not re-price J. Opus 2 ACCEPT: add to §7 packages/agent-runtime RUN_DB_INTEGRATION=1 REQUIRE_DB_INTEGRATION=1 bunx vitest run src/memory/access.integration.test.ts; G18 card REQUIRE_DB_INTEGRATION=1 && !RUN throw. Opus 3 ACCEPT: apply REQUIRE_DB_INTEGRATION=1 plus && !RUN throw uniformly to G5/G8/G9/G18/G19/G21 (Superseded by §57 Opus 3 for G19 — mocked, G18 owns the SQL races — and by §58 Opus 2 for G18/G21, which keep plain describe and take the throw only); §11 names executed case ids, not a green exit; add oauth-auth.integration.test.ts §7 command (existing test:oauth-integration already has the throw). Opus 4 ACCEPT: drop firm-wide omit-owner listings from the §8 rollout floor; keep as the plan:92 NOS-384 prerequisite only; do not add a C/D activation guard; do not re-map D22. Opus 5 ACCEPT: mark §51 Opus 5 superseded in place by §52 Astra 2: I=22; unique 118; outputs 123 (plan:1299, plan:1302, spec:430). Opus 6 ACCEPT: restate enum members (4) and (11) in the “a complete figure is unavailable” shape used by (1) and (9); do not withhold rows; align H unstamped / null-origin; identical on spec:144 and plan:145. Opus 7 NOTE only: STATUS.md vs index.html document-count off-by-one is a pre-existing generator discrepancy (HEAD 279/24 vs 280/29); this lane does not hand-edit the generator.
Astra CLEAN after §54; independent Opus 1–2 — two separate ACCEPT dispositions: plan §55. Astra returned CLEAN. Opus 1 ACCEPT: replace “before any provider call” with “before any exact-entry validation call” on spec:154, spec:303, plan:165, plan:226; state the order once on the B7 card: single identity resolution → RR snapshot → commit → exact validations; mark plan:933 refined in place; keep no-provider-work-inside-the-open-transaction; identity GETs stay in the 20-second source phase, not C prepare. Opus 2 ACCEPT: one sentence on the B7 card: $actor is this request’s resolved who_am_i data.id, never the enrollment-stored personal user id; G5: live who_am_i data.id ≠ enrollment-stored personal user id → identity-unverified refuse, zero rows disclosed; do not revive the dropped different-account dismiss-key G5. No new path. Do not re-price a packet.
Independent Astra 1 + Opus 1–2 after §55 — three separate ACCEPT dispositions: plan §56. Astra 1 ACCEPT: split the combined fourth predicate; source_origin IS NULL is (11) only (complete figure unavailable; do not withhold rows); source_origin <> $personalVerifiedOrigin when non-null is (6) identity-unverified; delete source_origin IS NULL OR source_origin <> … as a single arm; G5 two cases, not one combined: stamped-epoch/NULL-origin is (11) — that fixture asserts the row IS in the projection, IS keyed under the actor’s verified origin, AND that the named figure is refused with (11); non-null origin differs is (6) — that fixture asserts the row is NOT keyed, NOT in the LIMIT 200, and coverage EXISTS. G5: an AU-origin row is not in the keyed set; no collision with a US source_id; no exact GET on US origin for the AU id. Matching-origin rows are not withheld. Opus 1 ACCEPT: on the B7 card, before the snapshot: if live who_am_i data.id ≠ enrollment-stored personal user id, refuse identity-unverified (reuse (6) H string), zero rows, no snapshot; do not add member (12); $actor remains this-request who_am_i data.id; request-level admission, not a row predicate; G5 case stays. Opus 2 ACCEPT: mark spec:408 refined in place like plan:933: no provider work inside the open transaction; identity GETs stay in the 20-second source phase. B7 order stays identity GET → RR snapshot → commit → exact validations. Enum stays 11 members. No new path. Do not re-price a packet.
Independent Astra 1 + Opus 1–5 after §56 — six separate ACCEPT dispositions: plan §57. Astra 1 ACCEPT: recorded-time projection, canonical/dismiss keys, and exact GET require source_origin = $personalVerifiedOrigin; existence of non-null differing origin is (6) coverage EXISTS that refuses the named figure; those rows are not keyed as personal and do not occupy the personal LIMIT 200; matching-origin rows are not withheld; G5: AU-origin row is not in the keyed set; no collision with US source_id; no exact GET on US origin for the AU id; (11) NULL-origin complete-figure-unavailable do not withhold. Opus 1 ACCEPT: coverage EXISTS for noncanonical stored user_source_id in the selected window (visible TimeEntry, ingest-connector, date/matter) that is not equal to $actor; member (10) fires; no complete-empty / silent omit; G5: stored user_source_id = '9007199254740992' with live data.id = '9007199254740993' must not publish a complete figure. Opus 2 ACCEPT: persist who_am_i data.id at enrollment inside existing B18 encrypted provider_metadata as personal_user_id; state it on B3/B4/B5/B18; no new path, no new column; keep request-level admission, reuse (6) H string, no member (12). Opus 3 ACCEPT: drop REQUIRE throw from G19 (mocked; G18 owns SQL races); G18 and G21 get the G24 skip idiom (Superseded by §58 Opus 2 for G18/G21: both already run unconditionally, so they keep plain describe and take the throw only; describeIntegration stays G24’s alone.) G5/G8/G9 stay as new files with the throw. Opus 4 ACCEPT: add operational required-state already_shared_thread and a §10.1 operational sentence for share-first admission refuse (already-shared thread); not an enum member; F3/prompt cite that sentence. Opus 5 ACCEPT: spec:304 and the twin C figure-gate sentence carve out labelled coverage (2)(3)(5); do not reverse those label-only members. Enum stays 11 members. No new path. Do not re-price a packet. B7 order stays identity GET → RR snapshot → commit → exact validations.
Independent Opus after §57 — five separate ACCEPT dispositions; plan review CLOSED: plan §58. Opus 1 ACCEPT: the recorded-time projection, canonical/dismiss keys and exact GET require source_origin IS NULL OR source_origin = $personalVerifiedOrigin — NULL-origin rows stay in the projection, are keyed under the actor’s own verified origin, and (11) refuses the named figure only; member (6) (non-null differing origin) is unchanged. Opus 2 ACCEPT: G18/G21 keep plain describe and take only the REQUIRE_DB_INTEGRATION=1 && !RUN throw — both run unconditionally today and describeIntegration would convert them to opt-in suites; the idiom stays G24’s alone. Opus 3 ACCEPT: mark §49’s 120/20,180/125 superseded by §50. Opus 4 ACCEPT: drop G19 from the live uniform-throw preserve-lists and mark the §54 restatements in both pages. Opus 5 ACCEPT: cite deploy.sh:690–696, not :690. CLEAN; kwiss closed plan review on 2026-09-16 and the next artifact is code. The Grok deletions seat and the Codex-backed seats are unavailable, so §7 convergence runs Astra + Opus only — recorded, not absorbed.
Evidence boundary: the completed provider report at repo path docs/research/2026-09-15-private-work-ledger-provider-preflight.md proves documented contracts, not effective registered app/grant permissions. Existing test accounts are chosen but no identifiers were supplied. No token/API calls were made; P1 remains unchecked pending a designated manifest, secure read-only runner and actual grant observations. A successful documentation build proves only pages/indexes, not OAuth, RLS, UI or source access. All implementation checkboxes stay unchecked.
Review history remains factual: Fable exhausted before substantive R2 review; Devin Fusion startup failed the then-required shared-ledger registration, with no substantive review. Kwiss later removed that registration requirement; no binder/registry step applies to this bounded fixer. Dual reviewers stay two models (Astra + Opus); their dispositions above are not a clean result. Product code and prose workers are omp Astra seats per the 2026-09-08 standing rule, not new Claude panes. Tests use configured writer @write Astra high. No agent is launched by this documentation task.
R5 implementation correction: B7 computes its named aggregate from exact stored numeric hours over the same bounded eligible projection, then rounds once with the retained JavaScript half-boundary semantics; floating per-row display minutes are not an aggregate input. Fresh Clio authorization records the normalized origin of the producing client, while unknown legacy provenance and established-origin refresh mismatch guards remain fail-closed. The clio_user_link comment states that one candidate is admitted regardless of enabled status, a disabled candidate starts unlinked, and ambiguity spans all candidates; it does not claim a future writer implementation. No schema, migration, grant, service, UI or MCP capability is added. Neutral organization/account naming applies to new comments and option types; existing wire ABI names remain intact.