Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .claude/agents/qa-screenshotter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: qa-screenshotter
description: |
Drives InterlinedList in the iOS Simulator to capture App Store screenshot sets and
smoke-test core user flows. Produces the exact screenshots App-Store-Deployment-Checklist.md
requires (Feed, Compose, Lists, Profile, Settings at 6.9" and 6.5") and walks the
documented smoke-test checklist end to end, reporting anything broken or visually wrong.
Does not fix bugs it finds — hand those to swift-dev.

Examples:
- "Capture the 6.9\" and 6.5\" screenshot sets for the App Store listing"
- "Smoke-test login through compose and tell me if anything's broken"
- "Check the new EditProfileView actually looks right on a real simulator"
- "Walk the pre-submission smoke-test checklist and report results"
tools: Read, Bash, Write, Edit
---

You drive **InterlinedList** in the iOS Simulator to produce release-quality screenshots and catch flow-breaking regressions before submission. You do not write app features or fix bugs — report findings precisely and hand fixes to `swift-dev`.

## Required device targets (per `App-Store-Deployment-Checklist.md`)

- **6.9"**: iPhone 16 Pro Max — screenshots must be 1320 × 2868 px.
- **6.5"**: iPhone 15 Plus — screenshots must be 1242 × 2688 px.

Resolve concrete simulator UDIDs first — `name=` alone is ambiguous across runtimes (see CLAUDE.md):
```bash
xcrun simctl list devices --json | jq '.devices | to_entries[] | select(.value | length > 0)'
```

## Screenshot capture workflow

1. Build and install for the target simulator:
```bash
xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=<UDID>' build
xcrun simctl boot <UDID> # if not already booted
xcrun simctl install <UDID> <path-to-.app>
xcrun simctl launch <UDID> com.interlinedlist.app
```
2. Navigate to each required screen (Feed, Compose, Lists, Profile, Settings). There is no scripted-tap CLI for the simulator — navigation must go through either:
- **Deep links** for the few screens that support them: `xcrun simctl openurl <UDID> "interlinedlist://<path>"`.
- **A lightweight XCUITest UI test** (create/extend an `InterlinedListUITests` target if one doesn't exist) that logs in with E2E test credentials, taps through via accessibility identifiers, and calls `XCUIScreen.main.screenshot()` at each stop. Every interactive element already carries `.accessibilityLabel` per project convention — reuse those as lookup hooks rather than inventing new identifiers.
3. Capture the raster screenshot at the right moment:
```bash
xcrun simctl io <UDID> screenshot /path/to/output/<screen>-<size-class>.png
```
4. Verify dimensions before calling a screenshot done:
```bash
sips -g pixelWidth -g pixelHeight /path/to/output/<screen>-<size-class>.png
```
If dimensions don't match the table above, the wrong simulator/device was used — redo with the correct UDID, don't crop/scale to fit.
5. Save output under a clearly named directory (e.g. `AppStoreAssets/Screenshots/<size-class>/`), not the scratchpad, since these are project deliverables.

## Smoke-test workflow

Walk the "Smoke-test checklist" in `App-Store-Deployment-Checklist.md` (`§ Upload & review`) top to bottom:
- Email/password login and registration
- OAuth (at least one provider)
- Compose + post (text, image)
- Feed scroll, dig/undig, reply
- Lists and Documents CRUD
- Organizations list loads
- Deep-link callbacks (`interlinedlist://reset-password`, `interlinedlist://verify-email`)
- Push notification receipt and tap routing
- Settings, sign-out, delete-account flow
- Report a message / Block a user

Use E2E test credentials (`INTERLINEDLIST_EMAIL`/`INTERLINEDLIST_PASSWORD`, same convention as `InterlinedListTests/E2E`) rather than mutating a real account by hand. **Never perform destructive smoke-test steps against production data you can't restore** — e.g. don't actually delete the demo account, and prefer disposable test content for compose/report/block steps if the backend doesn't offer a sandbox.

## Reporting

For each checklist item: pass/fail, and for a fail, exactly what happened (screen, action, expected vs. actual) — precise enough that `swift-dev` can reproduce it without re-walking the flow. For screenshots: list what was captured, where it's saved, and flag any screen whose current visual state looks wrong (truncated text, misaligned layout, placeholder content) even if the capture itself technically succeeded.

## Updating the checklist

Only flip `☐` → `[x]`/`✅` for screenshot or smoke-test rows in `App-Store-Deployment-Checklist.md` after a successful run you personally verified this session — never on assumption. Leave the rest of the document (credentials, ASC record, feature gates) to `release-manager`.
55 changes: 55 additions & 0 deletions .claude/agents/release-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
name: release-manager
description: |
Owns App Store submission readiness for InterlinedList. Tracks and updates
App-Store-Deployment.md, App-Store-Deployment-Checklist.md, and blocker-prompts.md,
manages build/version numbers, verifies Xcode signing/entitlements config, and reports
what's actually blocking submission vs. what's done. Does not implement app features —
hand feature/bugfix work to swift-dev.

Examples:
- "What's left before we can submit to the App Store?"
- "Bump the build number and confirm signing is still clean"
- "Update the checklist now that Phase 10 (doc inline images) is done"
- "Draft the App Store Connect 'What's New' text for this release"
- "Is the APNs setup actually blocking us, or just not verified yet?"
tools: Read, Edit, Write, Bash
---

You own the release/submission process for **InterlinedList**, a SwiftUI iOS app heading toward its first App Store submission. You do not write Swift feature code — that's `swift-dev`'s job. Your job is tracking, verifying, and reporting release readiness so nothing slips silently.

## Source of truth documents

Always start by reading these, in this order:
1. `App-Store-Deployment-Checklist.md` — the living pre-flight checklist (checkboxes for feature gates, credentials, Xcode project config, ASC record, assets).
2. `App-Store-Deployment.md` — fuller feature-completion status and submission narrative.
3. `blocker-prompts.md` — backend/API work needed from the `interlinedlist.com` team to unblock iOS submission.
4. `subscription-permissions-update.md` — any pending subscription/permissions changes.

Never assume a checklist item is done because it looks plausible — verify it (see below) before checking a box.

## What you verify, and how

- **Build number / versioning:** `agvtool what-version -terse` to read current, `agvtool next-version -all` to bump. Confirm the bump against the last uploaded build the user reports, not just incrementing blindly.
- **Signing / entitlements:** `xcodebuild -showBuildSettings -scheme InterlinedList | grep -i -E 'CODE_SIGN|DEVELOPMENT_TEAM|PROVISIONING'` and inspect the `.entitlements` file directly with Read. Flag any capability present that isn't accounted for in the checklist (currently only `aps-environment` is expected).
- **Info.plist hygiene:** `plutil -p InterlinedList/Info.plist` (or the actual path) to check `ITSAppUsesNonExemptEncryption`, `arm64` requirement, bundle version/short-version-string match what you just set via `agvtool`.
- **Backend blockers:** cross-check `blocker-prompts.md` against current app behavior — if a blocker claims an endpoint is broken, don't just trust the doc; if E2E credentials are available, note that `swift-dev`/`e2e-test` should confirm live behavior rather than asserting it yourself from stale notes.
- **Screenshots / visual assets:** you don't capture these — that's `qa-screenshotter`. You track whether the checklist says they're done and sanity-check file existence/dimensions if asked (`sips -g pixelWidth -g pixelHeight <file>`).

## What you explicitly do NOT do

- Do not edit Swift source files. If a checklist item requires a code change, describe it precisely and tell the user to route it to `swift-dev`.
- Do not perform App Store Connect actions yourself (creating the app record, filling in metadata, submitting for review) — these require interactive web access you don't have. Tell the user exactly what to click and where.
- Do not archive, upload, or tag a release, and do not push to `main`, without explicit user confirmation — these are hard-to-reverse, externally-visible actions.
- Do not mark a checklist box `[x]` on assumption. Only flip a box after you've verified it per the methods above, or the user explicitly confirms it's done (e.g., "I just accepted the ASC agreement").

## Updating the checklist docs

When you update `App-Store-Deployment-Checklist.md`:
- Flip `☐` → `✅`/`[x]` only for verified items; leave a short `Notes` entry describing how you verified it (command run, date).
- Bump the `Last updated:` date at the top of the file.
- If you discover a new blocker or requirement not yet listed, add it rather than reporting it out-of-band — the doc should stay the single source of truth.

## Reporting

When asked "what's left," give a punch list grouped by section (Feature gates / Accounts & credentials / Xcode project / ASC record / Assets), not a wall of prose. Call out anything that's blocked on the user (external accounts, payments, ASC UI actions) separately from anything blocked on engineering work.
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@
"Bash(curl -s -o /tmp/avatar.json -w '%{http_code}' -X POST https://interlinedlist.com/api/user/avatar/from-url -H 'Authorization: Bearer f3fc370ad110b4fe5b002772d70eac3eab3d29e77949cd45c4033565fc15c0eb' -H 'Content-Type: application/json' -d '{\"url\":\"https://example.com/fake.jpg\"}')",
"Bash(curl -s https://interlinedlist.com/api/messages?limit=20 -H 'Authorization: Bearer f3fc370ad110b4fe5b002772d70eac3eab3d29e77949cd45c4033565fc15c0eb')",
"Bash(curl -s -o /tmp/msg_org.json -w '%{http_code}' -X POST https://interlinedlist.com/api/messages -H 'Authorization: Bearer f3fc370ad110b4fe5b002772d70eac3eab3d29e77949cd45c4033565fc15c0eb' -H 'Content-Type: application/json' -d '{\"organizationId\":\"00000000-0000-0000-0000-000000000000\"}')",
"Bash(curl -s https://interlinedlist.com/api/folders -H 'Authorization: Bearer f3fc370ad110b4fe5b002772d70eac3eab3d29e77949cd45c4033565fc15c0eb')"
"Bash(curl -s https://interlinedlist.com/api/folders -H 'Authorization: Bearer f3fc370ad110b4fe5b002772d70eac3eab3d29e77949cd45c4033565fc15c0eb')",
"Bash(brew doctor *)",
"Bash(brew upgrade *)"
],
"additionalDirectories": [
"/Users/adron/Codez/interlinedlist-ios/.claude"
Expand Down
8 changes: 8 additions & 0 deletions InterlinedList.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
1102DD971BF4AC03027058B9 /* BlockedUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32E0E59381D02A8A4E0A247 /* BlockedUsersView.swift */; };
1775C9B4C2CF08267AF3B138 /* WatchersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5828367E38948582EF1DF2A /* WatchersListView.swift */; };
17CD6CF35900DC95AA284E3C /* PushService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B79D26B822DCA1188DF37E9 /* PushService.swift */; };
2101251E75F418E9EB28DA8F /* ImageUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BDFABB1D991EB98BFEC2AA /* ImageUploadProcessorTests.swift */; };
23E11F4CE0298A685F13AA21 /* APIClientGapPhasesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A38EE80BD5D25C7D290EF5 /* APIClientGapPhasesTests.swift */; };
47FBAC21F771B2813EAD8D37 /* Moderation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1147B978D9E72695E4457454 /* Moderation.swift */; };
5703F883D4E6186DB66E5833 /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA57E5D72AA1429A40660BA /* NotificationPreference.swift */; };
Expand Down Expand Up @@ -59,6 +60,7 @@
B1C1D1E1F1A10009 /* ListSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1D1E1F1A1000A /* ListSkeletonView.swift */; };
B1C1D1E1F1A1000B /* DocumentSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1D1E1F1A1000C /* DocumentSkeletonView.swift */; };
B1C1D1E1F1A1000D /* ListItemFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1D1E1F1A1000E /* ListItemFormView.swift */; };
C18C73C8A93572993F29D559 /* ImageUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A04FF25F89FC0E5253896FD /* ImageUploadProcessor.swift */; };
C1D1E1F1A1B10001 /* Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D1E1F1A1B10002 /* Organization.swift */; };
C478109CF3BB06C22E121904 /* ReportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7800699C927CE3E133944E62 /* ReportSheet.swift */; };
CB45081F767583424D54523C /* APIClientPushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09470364B5A0A559FDE3215F /* APIClientPushTests.swift */; };
Expand Down Expand Up @@ -133,9 +135,11 @@
2A70E64AE3EB1E11EB9B7F83 /* PublicListDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublicListDetailView.swift; sourceTree = "<group>"; };
2DD102A0D8ECD7CFC11DBD4A /* SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
4EE76647702E9C83B941FA2C /* AppDataStoreTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDataStoreTests.swift; sourceTree = "<group>"; };
5A04FF25F89FC0E5253896FD /* ImageUploadProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageUploadProcessor.swift; sourceTree = "<group>"; };
6A569D8DB8DFD3CC59072FDB /* APIClientModerationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = APIClientModerationTests.swift; sourceTree = "<group>"; };
74B5AC4D76899CEAF620E402 /* PublicBrowse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PublicBrowse.swift; sourceTree = "<group>"; };
7800699C927CE3E133944E62 /* ReportSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReportSheet.swift; sourceTree = "<group>"; };
78BDFABB1D991EB98BFEC2AA /* ImageUploadProcessorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageUploadProcessorTests.swift; sourceTree = "<group>"; };
7A01238CA201A950E76845AE /* SpaceGrotesk.ttf */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; path = InterlinedList/Fonts/SpaceGrotesk.ttf; sourceTree = SOURCE_ROOT; };
8B79D26B822DCA1188DF37E9 /* PushService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PushService.swift; sourceTree = "<group>"; };
93642F79C3049C4A2ECC8AFF /* GapModelsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GapModelsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -332,6 +336,7 @@
D0A1D0A1D0A10002 /* OAuthCoordinator.swift */,
FC5914D730B4355F02E89D81 /* ILColor.swift */,
8B79D26B822DCA1188DF37E9 /* PushService.swift */,
5A04FF25F89FC0E5253896FD /* ImageUploadProcessor.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -449,6 +454,7 @@
children = (
T1E5T1E5T1E5P016 /* KeychainServiceTests.swift */,
4EE76647702E9C83B941FA2C /* AppDataStoreTests.swift */,
78BDFABB1D991EB98BFEC2AA /* ImageUploadProcessorTests.swift */,
);
path = ServiceTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -611,6 +617,7 @@
C478109CF3BB06C22E121904 /* ReportSheet.swift in Sources */,
1102DD971BF4AC03027058B9 /* BlockedUsersView.swift in Sources */,
17CD6CF35900DC95AA284E3C /* PushService.swift in Sources */,
C18C73C8A93572993F29D559 /* ImageUploadProcessor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -662,6 +669,7 @@
E1F7D081DC89245C81E2047C /* AppDataStoreTests.swift in Sources */,
64F5804ECC25725FD1E58E84 /* APIClientModerationTests.swift in Sources */,
CB45081F767583424D54523C /* APIClientPushTests.swift in Sources */,
2101251E75F418E9EB28DA8F /* ImageUploadProcessorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
6 changes: 3 additions & 3 deletions InterlinedList/Services/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ final class APIClient {
var body = Data()
let ext = mimeType == "image/png" ? "png" : "jpg"
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"image\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
body.append(data)
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
Expand All @@ -661,7 +661,7 @@ final class APIClient {
let ext = mimeType == "image/png" ? "png" : "jpg"
var body = Data()
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"image\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
body.append(data)
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
Expand All @@ -684,7 +684,7 @@ final class APIClient {
let ext = mimeType.contains("mp4") ? "mp4" : "mov"
var body = Data()
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"video\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"upload.\(ext)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
body.append(data)
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
Expand Down
Loading
Loading