<!-- https://ios.app.promptugal.news/admin/briefs/2026-11-08__app-review-readiness/status/collab.html · PrompTugal for iOS 0.4.106 -->

# Status · Collaboration and AI (App Review readiness)

> Done and CI green: all seven items, with tests, on dev since 7d135e0. Follow-up done: Lisbon.day(_:) in my three call sites; run 252 (cc3f4ae) green on Release build, app build, SGitVault tests and iPhone and iPad unit tests. Ready to merge.

Branch: `night/collab` (merged from `origin/dev` 2bc718a, no rebase) · Last update: 17:08 UTC

## Done (pushed, with tests)

1. **R2/S2 consent before Ask sends anything** (`AI/ChatStore.swift`, `AI/AskView.swift`, `AI/AIContext.swift`, `AI/AITools.swift`)
   - A sheet before the first question names **OpenRouter** (openrouter.ai) and the **model's provider** (from the model id, e.g. Anthropic), lists each kind of context with its own switch and what it sends, and offers **Allow** / **Don't allow**.
   - The choice is stored (`ai.consent.v1`). Ask settings show it and offer "Withdraw permission" or "Ask me again". "Don't allow" is not asked again on every question; Ask shows that it is off.
   - No request is built without consent: checked in `send`, `ask` and again in `run`.
   - Context defaults to the **public agenda only** (new key `ai.context.v2`, so earlier wider choices are not carried over).
   - **Teammates' plans** (`where_is_everyone`) and **Current page** are separate, explicit switches, off by default.
   - Prepared prompts cannot widen what is shared; tools are offered and run only for switched-on data.
   - Tests: no request without consent (an injected `HTTPLog` stays empty), decline is stored and not re-asked, withdraw goes back to asking, default context is `[.agenda]`, prompts cannot widen, tools gated, provider names.
2. **R4 report and block on Messages** (`Lanes/LaneReport.swift`, `Lanes/LanesStore.swift`, `Lanes/LanesView.swift`, `Lanes/TeamNotes.swift`)
   - **Block** a lane, or the sender of an inbox message or a teammate's note: hidden everywhere on this device, persisted, and undone in a "Blocked" section.
   - **Report…** opens a prefilled mail (subject, sender, date, the text). The address is one constant, `LaneReport.contactAddress`, empty until Dinis gives it; the mail then opens with no recipient.
   - Context menus, swipe actions and VoiceOver actions ("Report", "Block sender").
   - Tests: block persists across stores and unblocks; report URL is prefilled and header-safe; works before the address is set.
3. **Q4 crypto off the main actor**: RSA-4096 generation (`makeMyLane`) and inbox decryption (`LanesStore.decryptAll`) run in `Task.detached`; the website inbox key is imported inside that task. Test: messages open through `decryptAll`, a bad one gets a readable error.
4. **S5 key only from my own vault** (`Coordination/CollabHooks.swift`, `Coordination/CollabVaultGlue.swift`, `AI/AIKeyStore.swift`)
   - The vault seam has `ownVaultId`: the account's reader vault. An imported key (a teammate's) is never "mine".
   - `loadFromVault` reads only that vault; saving the key, sharing my plan and posting team notes write only to it, and fail with a plain message otherwise.
   - Tests with a fake vault: a teammate's vault is never read for the key; my own is.
5. **S7**
   - Links in Ask answers: tapping one shows the full URL and asks first; only `https` and `mailto` can be opened, other schemes (including `promptugal://`) only copied. Test.
   - Copy from Ask: local only (no Handoff) and expires after two minutes. Test.
   - Lane endpoints: only `https://dev.send.sgraph.ai` (no port, no user); checked when parsing, for the website inbox record, and on every request. Tests (other host, http, port, credentials, a changed address at send time).
   - Append tokens: lane addresses moved from UserDefaults to the Keychain; the old copy is migrated once and deleted. Test checks no token remains in UserDefaults.
6. **Smaller fixes**
   - `isSending` is set before the Task is created, so a double tap sends once.
   - Conversation capped at 200 turns; at most 30 sent back with a question. Test (messages builder).
   - Messages on iPad: two columns only when the window is at least 700 pt wide; one list in narrower Split View. The fixed 400 pt is gone.
   - One Lisbon-day helper: my copy is deleted and I call team's `DayPlanner.lisbonDay`.
   - One set of team models: Ask's team context and "Where is everyone"'s people list now read `TeamStore`'s `TeamPerson` and `CommandCentre` (no separate fetch or parse). Tests decode fixtures through those models.
   - Debug literals in `AskView` and `LanesView` use `DebugMode.key` / `DebugMode.defaultOn`.
7. **Readable errors (Q7)**: `CollabErrorText.short` for every error shown in `AI/`, `Lanes/` and `Coordination/`: network errors in plain words, no `Error Domain=…`, no object paths. Test.

## CI

- Run 252 (workflow_dispatch, cc3f4ae, after team landed and the switch to `Lisbon.day(_:)`): **green**. Release build (device, unsigned), app build and SGitVault tests, iPhone and iPad unit tests.
- Run 233 (workflow_dispatch, 7ca0a00): **green**. App build and SGitVault tests, App unit tests on the iPhone and the iPad simulator (all suites, including AIConsentTests, AIKeyVaultOwnershipTests, AILinkTests, LanesReviewTests).
- Earlier build-only runs: 203, 209, 214 green. Run 226 failed to compile one test helper (a raw-string fixture at AITests.swift:183); fixed in 7ca0a00.

## In progress

- Nothing. `Lisbon.day(_:)` switch done (AIContext, AITools, CoordinationStore).

## Blocked

Nothing.

## Needs from others

- **Team:** done; I use `Lisbon.day(_:)`. `TeamStore` still fetches with `URLSession.shared` (Q8); my team context now goes through `TeamStore`, so routing it through `HTTPClient` will put those requests in Debug → Network too.
- **Vault:** my `ownVaultId` treats the reader vault (by `readerVaultId` or role "reader") as mine and "imported" as never mine. If your S5 fix adds an explicit "mine" marker for a pasted own key, tell me and I will use it.
- **Design:** `DebugMode.defaultOn` false in Release is yours; my views follow it.

## For Dinis

- **Contact address** for reports: set `LaneReport.contactAddress` (one line) once decided (pack decision 3).
