Contracts for the overnight push
The interfaces every session codes against, so six sessions can work in parallel without waiting on each other. Change one only through the conductor, and note it here.
1. Project layout (platform owns)
- iOS 18.0 minimum, Swift 6 language mode if the CI Xcode supports it, otherwise Swift 5 with strict concurrency warnings.
- The Xcode project moves to folder-synchronized groups (Xcode 16+,
PBXFileSystemSynchronizedRootGroup). That way, adding a Swift file to a folder needs noproject.pbxprojedit, and only the platform session edits the project file. - Until the platform session says the layout is ready (in its status page), other sessions put code in new files in their own folders and keep going. The platform session folds those folders into the project.
Packages/SGitVaultis a local Swift package (swift-tools-version: 6.0, platforms iOS 18 and macOS 15) with its own tests, run withswift teston macOS CI. The app depends on it.- Test targets:
PrompTugalTests(unit, XCTest or Swift Testing) andSGitVaultTests(in the package).
2. CI (platform owns)
.github/workflows/ios.ymlruns on every push tonight/**anddev, and on pull requests. Three jobs:swift testforPackages/SGitVault;xcodebuild buildfor the app (simulator,CODE_SIGNING_ALLOWED=NO);xcodebuild testfor the app’s unit tests on an iPhone simulator.- Each failure must show the compiler error in the job summary, so a session can read it through the GitHub tools.
3. Schedule (team owns, everyone reads)
The wire format is the website’s, unchanged:
{ "v": 1, "picks": [ { "id": "ai-doom-or-boom", "at": "2026-11-08T02:45:00.000Z" } ] }
idis the bare event slug, for exampleai-doom-or-boom: the website takes it fromevents/<slug>/index.htmlwith the pattern(?:^|/)events/([^/]+)/index\.html$inassets/schedule/schedule.js.atis the time the pick was made, as an ISO 8601 string (the website writesnew Date().toISOString()inassets/schedule/schedule.js). The app writes ISO 8601 and reads both ISO 8601 and Unix milliseconds.- The website mirrors picks to the reader vault when it is open:
window.__vaultpathagenda/picks, stored asapps/agenda/picks.jsonin the vault, merged as a union byid. - Merge rule (the website’s): union by
id. A removal wins only if it is later than the pick. - The Swift API in
PrompTugal/Schedule/:
@MainActor @Observable final class ScheduleStore {
var picks: [SchedulePick] // sorted by event start, Lisbon time
func isPicked(_ id: String) -> Bool
func toggle(_ id: String)
func merge(_ incoming: ScheduleDocument) // from the web view or the vault
var document: ScheduleDocument { get } // nr.schedule.v1
}
4. Bridge (team owns)
- Message handler name
pt, in its ownWKContentWorld, only for origins on the allow-list. - The app injects
window.ptApp = { version, ops: [...] }at document start. - Phase-1 ops:
schedule.get,schedule.set,identity.get,notify.plan,meter.topup. - Until the website calls the bridge itself, the app syncs the schedule by reading and writing
localStorage['nr.schedule.v1']withevaluateJavaScriptin the page’s world, and dispatching thenr:scheduleevent. No website change is needed tonight.
5. Vault (vault owns, everyone reads)
public struct VaultAddress: Sendable { public let vaultId: String; public let server: URL }
public actor SGitVault {
public init(address: VaultAddress, readKey: SymmetricKey, cache: URL, log: VaultTrafficLog)
public func pull() async throws -> VaultCommit // fetch the ref, walk new objects only
public func files() async throws -> [VaultFile] // path, size, blob id
public func read(_ path: String) async throws -> Data
public func history(limit: Int) async throws -> [VaultCommit]
}
@MainActor @Observable public final class VaultTrafficLog { // feeds the debug panel
public private(set) var events: [VaultTrafficEvent] // newest first, capped at 2,000
}
public struct VaultTrafficEvent: Identifiable, Sendable {
public let id: UUID; public let at: Date
public let kind: Kind // .request, .cacheHit, .decrypt, .verify, .keyDerivation, .error
public let path: String // e.g. "bare/refs/ref-pid-muw-…" — never a key
public let bytes: Int; public let durationMs: Double; public let status: Int?
public let detail: String // e.g. "AES-256-GCM, read key, 547 B → 512 B"
}
- Server default:
https://dev.send.sgraph.ai. Reads areGET /api/vault/read/<vault_id>/<url-encoded path>with no auth. - The format spec is
briefs/2026-11-07__native-vs-web/evidence/vault_read_probe.py. - Keys never appear in logs, the debug panel, analytics or git.
- App glue in
PrompTugal/Vault/:VaultKeychain(keys behind Face ID with.biometryCurrentSet, this device only),VaultUnlock(read-key import from a pastedsgit_private_read_…:<vault_id>or a QR code, then passkey PRF when the domain is ready),VaultSession(the open vaults, as an@Observablethe UI uses).
6. Team data (team owns the models, design owns the screens)
Public JSON, read without keys and cached for offline use:
| Source | URL | Used for |
|---|---|---|
| Team people and counts | https://web-summit.sgit.ai/me.json | badge, "only you", decide, overdue |
| Command centre | https://web-summit.sgit.ai/people/<id>/app/data.json | tasks, decisions, named-for-me, today |
| Meetings | https://web-summit.sgit.ai/meetings/… (check llms.txt) | stand-ups and briefings |
| Agents | https://web-summit.sgit.ai/wakes/, /reports/, /pulse/, /log/ (find their JSON via llms.txt) | what the agents are doing |
| Newsroom | https://promptugal.news/api/v1/newsroom/{desks,stories,dashboard,decisions,status}.json | newsroom status |
| News | /feed.xml, /api/v1/newsletter.json, /api/v1/story.json, /api/v1/search/docs.json | the news in every form |
| Agenda | /api/v1/live/agenda.json, /api/v1/places.json | schedule and planning |
These are data sources only. Product copy says PrompTugal.
7. Shell (design owns)
PrompTugal/Shell/AppShell.swiftis the root view. On iPad it is aNavigationSplitViewsidebar. On iPhone it is tabs plus a "More" list.- Each feature exposes one entry view and registers it in
Shell/Destinations.swift(design merges these registrations):
struct Destination: Identifiable { let id: String; let title: String; let symbol: String
let section: Section; let kind: Kind /* .native, .web(URL), .hybrid */; let view: () -> AnyView }
- Every screen gets a small "native / web / hybrid" badge in debug mode, so we learn the balance.
8. Debug mode (design owns the UI, everyone feeds it)
- Settings → Debug (or a triple tap on the version) turns on debug mode.
- Panels: Vault traffic (
VaultTrafficLog), Network (everyURLSessionrequest the app makes, through one sharedHTTPClientwith a log, owned by platform inPrompTugal/App/HTTPClient.swift), Bridge (every bridge message), Storage (files, sizes, cache ages), Keys (which keys exist and where, never values), Build (app version, build, git commit, site release).
9. Secrets
- OpenRouter key: entered by the user, stored in the Keychain (
.biometryCurrentSetoptional), optionally saved to the user’s vault underapps/ai/openrouter.jsononce vault writes exist. Sent only toopenrouter.ai. - No key of any kind in git, logs, fixtures, screenshots or status pages.
Changes
- 03:15 UTC, §3 Schedule: a pick’s
atis an ISO 8601 string, not Unix milliseconds (checked against the website’sassets/schedule/schedule.js). The app writes ISO and reads both. Added the vault mirror pathapps/agenda/picks.json. Requested by the conductor. - 03:45 UTC, §3 and §6: a pick’s
idis the bare slug, notevents/<slug>; the places file is/api/v1/places.json(83 places, no coordinates), notplace.json. Both found by the team session and checked against the live site.