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
2 changes: 2 additions & 0 deletions .claude/agents/qa-screenshotter.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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`.

**Verify what you claim.** A pass you didn't actually observe, or a screenshot at the wrong dimensions, defeats the point. Only mark a smoke-test step passed after you drove it and watched the result this session, and only accept a screenshot after confirming its pixel dimensions (below). When something fails, report the exact screen/action/expected-vs-actual so `swift-dev` can reproduce it without re-walking the flow.

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

- **6.9"**: iPhone 16 Pro Max — screenshots must be 1320 × 2868 px.
Expand Down
4 changes: 3 additions & 1 deletion .claude/agents/release-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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.

**Verification is the whole job.** A release tracker that reports unverified status is worse than none — it manufactures false confidence. Never flip a checklist box, call a blocker cleared, or report "ready" on plausibility alone: verify it with the commands below (or an explicit user confirmation) and record how you verified it.

## 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. `the-gaps.md` — the merged iOS↔web parity/gap doc: iOS-side defects and feature gaps **and** the backend/API work (Bearer-auth fixes, moderation docs, push contract, verb mismatches) needed to unblock submission, with ready-to-paste prompts for the `interlinedlist.com` team.
4. `subscription-permissions-update.md` — any pending subscription/permissions changes.
4. `the-gaps-access.md` — access/subscription gating notes (which features are subscriber-gated, and how).

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

Expand Down
17 changes: 14 additions & 3 deletions .claude/agents/swift-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ tools: Read, Edit, Write, Bash, Skill

You are an expert iOS/Swift engineer working on **InterlinedList**, a SwiftUI app that connects to the `interlinedlist.com` API.

## Verification is non-negotiable

Nothing is "done" until it is verified. Every change must clear this bar before you report back:

1. **Build succeeds** — see *Build verification* below. Never leave compile errors for the user.
2. **Tests written and green** — new `APIClient` methods, model decoding, and branching logic get unit tests (see *Unit tests*); run the suite and fix every failure.
3. **Review pass** — invoke `/ios-review` over the changed files before calling a feature complete; `/solid-check` if you changed structure across files.
4. **Report honestly** — state exactly what you built **and ran**, with the result. If a step was skipped or a test still fails, say so plainly; never imply verification you didn't perform.

If you genuinely cannot verify something (no simulator, a flow needing live GitHub/OAuth), say precisely what remains unverified and how the user can confirm it — do not report unverifiable work as complete.

## Mandatory principles

### SOLID
Expand All @@ -34,15 +45,15 @@ You are an expert iOS/Swift engineer working on **InterlinedList**, a SwiftUI ap
- **No `DispatchQueue.main.async`** — use `@MainActor` annotations or `MainActor.run {}`.
- **No comments** unless the reason is non-obvious (API quirk, hidden constraint, workaround). Do not describe what the code does; well-named identifiers already do that.
- **camelCase vs snake_case bodies** — `APIClient` has two encoder families and choosing wrong fails **silently** server-side. Use `postCamel`/`putCamel`/`patchCamel` (plain `camelCaseEncoder`) for the **many** camelCase endpoints (messages, lists, documents, organizations, watchers, identities, change-email, notification-preferences, message metadata, …); use `post`/`put`/`patch` (snake_case `encoder`) for the rest. **Check the existing method for that endpoint before adding a new one** — don't assume `/api/messages` is the only camelCase route.
- **Empty-string == nil** — **both** `ListFolder.parentId` **and** `UserList.folderId` may arrive as `""` instead of `null`. Treat empty-string the same as absent (this is what `ListTreeNode.buildTree` does).
- **Empty-string == nil** — lists nest via `UserList.parentId`, which may arrive as `""` instead of `null`. Treat empty-string the same as absent (this is what `ListTreeNode.buildTree` does). List folders no longer exist; only documents have folders (see below).
- **Token in Keychain only** — never `UserDefaults` or in-memory across app restarts without Keychain backing.
- Every new `View` file needs a `#Preview` macro block.
- Every interactive element without an obvious label needs `.accessibilityLabel`.

## File layout
```
InterlinedList/Models/ Codable structs, lightweight computed properties only
InterlinedList/Views/ SwiftUI views — one public struct per file (~41 files)
InterlinedList/Views/ SwiftUI views — one public struct per file (~52 files)
InterlinedList/Services/ APIClient, AuthState, AppDataStore, DataCache,
KeychainService, OAuthCoordinator, URLSessionProtocol,
PushService, ComposeImageUploader / ImageUploadProcessor
Expand Down Expand Up @@ -76,7 +87,7 @@ Services throw `APIError`. Views catch it and set a `String?` error state for di
Every non-trivial feature implementation must be accompanied by unit tests. Tests live in `InterlinedListTests/` (create the target if it does not exist). Follow these rules:

### What to test
- **Models:** `Codable` round-trips. Encode a struct to JSON and decode it back; assert all fields survive. Test edge cases: `null` vs empty-string for optional fields like `folderId`/`parentId`, unknown enum cases, missing keys that should produce `nil` not a crash.
- **Models:** `Codable` round-trips. Encode a struct to JSON and decode it back; assert all fields survive. Test edge cases: `null` vs empty-string for optional fields like `UserList.parentId`, unknown enum cases, missing keys that should produce `nil` not a crash.
- **APIClient methods:** Use a mock `URLSession` (inject via `APIClient(session:)`) that returns canned `Data` + `HTTPURLResponse`. Assert the correct URL path, HTTP method, and `Authorization` header are sent. Assert the decoded return value matches the canned fixture. Test 401, 403, and 5xx paths throw the expected `APIError` case.
- **Pure logic / computed properties:** `ListTreeNode.buildTree`, `JSONValue.displayString`, `User.displayNameOrUsername`, date-formatting helpers — test the logic in isolation, no network needed.

Expand Down
21 changes: 18 additions & 3 deletions .claude/commands/comment-and-commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,21 @@ Optional argument (`$ARGUMENTS`): extra context to fold into the message — a t
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
```
- **Never push** from this command.
- **Never commit on the default branch** (`main`): if HEAD is `main`, create and switch to a descriptive feature branch first.
- **Never commit on the default branch** (`main`): if HEAD is `main`, isolate the work first (see Worktrees).
- Every commit also ends with any session trailer the harness specifies (e.g. `Claude-Session: <url>`) in addition to the `Co-Authored-By` line above.

## Worktrees (default for feature work)

Isolated/feature work happens in a dedicated **git worktree**, not by switching branches in the primary checkout — this keeps the main checkout clean and lets parallel efforts coexist. Agent worktrees live under `.claude/worktrees/`.

- **Know where you are:** `git worktree list` (all trees + their branches) and `git rev-parse --show-toplevel` (current root). The first `worktree list` entry is the primary checkout.
- **Already in a worktree** (a `.claude/worktrees/…` or `../interlinedlist-ios-<topic>` dir): just commit here — the steps below are identical. Still never commit on the base branch.
- **Starting fresh isolated work** from the primary checkout: create a worktree with its own branch instead of editing in place, then work and commit inside it:
```bash
git worktree add ../interlinedlist-ios-<topic> -b <type>/<short-topic>
```
(If the harness exposes an `EnterWorktree` tool, prefer it — it does the same thing.)
- **Changes already dirty in the primary checkout** (you edited in place before branching): don't fight it — branch in place (`git switch -c <type>/<topic>`) and commit; use a worktree from the outset next time. `git worktree add` creates a *clean* tree and will not carry your uncommitted edits.

## Steps

Expand All @@ -24,11 +38,12 @@ Optional argument (`$ARGUMENTS`): extra context to fold into the message — a t
git diff HEAD # full diff: staged + unstaged
```

2. **Safety-check the branch**:
2. **Safety-check the branch / worktree** (see Worktrees above):
```bash
git rev-parse --abbrev-ref HEAD
git worktree list # confirm whether you're in the primary checkout or a worktree
```
If it prints `main`, branch first: `git switch -c <type>/<short-topic>`.
If HEAD is `main`: create a worktree for the work (`git worktree add ../interlinedlist-ios-<topic> -b <type>/<topic>`) and continue there, or — if changes are already dirty here — `git switch -c <type>/<short-topic>` in place.

3. **Review, then stage.** Read the diff and decide what belongs in this commit. By default stage everything that's part of the work:
```bash
Expand Down
11 changes: 11 additions & 0 deletions .claude/commands/commit-and-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Optional argument (`$ARGUMENTS`): a PR title hint, a base-branch override, or ex
```
- Opening a PR is outward-facing: verify the branch, base, and commit list look right before creating it.

## Worktrees

`/comment-and-commit` (step 1) creates or lands the commit in a dedicated git **worktree** by default (see its *Worktrees* section) so feature work stays isolated from the primary checkout. Everything here runs from **that** worktree — `git push -u origin HEAD` and `gh pr create` act on the current worktree's branch, so no special handling is needed; just stay in it. Confirm with `git worktree list`. After the PR merges, clean the worktree up (step 7). Never remove a worktree that has uncommitted changes or an un-pushed branch.

## Steps

1. **Commit waiting work.** Perform every step of `/comment-and-commit` so the tree is clean and all work is committed. If there's nothing to commit but the branch is already ahead of the base, continue.
Expand Down Expand Up @@ -55,3 +59,10 @@ Optional argument (`$ARGUMENTS`): a PR title hint, a base-branch override, or ex
```

6. **Report the PR URL** that `gh` prints. If `gh` isn't authenticated, surface the error and tell the user to run `! gh auth login` in the prompt (so the interactive login lands in this session), then re-run.

7. **Clean up the worktree — only after the PR is merged.** If the commit landed in a dedicated worktree, remove it once merged so it doesn't linger:
```bash
git worktree remove ../interlinedlist-ios-<topic> # run from the primary checkout, not inside the worktree
git worktree prune
```
Skip if the work was committed in place (no dedicated worktree) or the PR isn't merged yet — in that case leave the worktree and say so, so the user can remove it after merge.
2 changes: 1 addition & 1 deletion .claude/commands/ios-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Perform a focused code review of the Swift/SwiftUI changes on the current branch

### Project-specific
- [ ] Correct encoder per endpoint: camelCase bodies use the `postCamel`/`putCamel`/`patchCamel` helpers, snake_case bodies use `post`/`put`/`patch` — mismatches fail silently server-side (check the existing method, don't assume)
- [ ] Empty-string `folderId` / `parentId` treated same as `nil` (both `UserList.folderId` and `ListFolder.parentId`)
- [ ] Empty-string `UserList.parentId` treated same as `nil` (list nesting via `parentId`; list folders no longer exist)
- [ ] Document-folder path-scoping honored: folder reads/creates use `/api/documents/folders/{id}/documents`; root routes are root-only
- [ ] Tokens stored in Keychain only — no `UserDefaults`
- [ ] New `.swift` files registered in `project.pbxproj` (no synced groups)
Expand Down
19 changes: 9 additions & 10 deletions .claude/commands/unit-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,9 @@ final class UserListCodableTests: XCTestCase {
func test_decode_emptyParentIdTreatedAsNil() throws {
let json = #"{"id":"1","title":"L","parentId":"","createdAt":"2024-01-01T00:00:00Z"}"#
let list = try JSONDecoder().decode(UserList.self, from: Data(json.utf8))
// folderId maps parentId; empty string is NOT nil at decode time — test the usage guard
XCTAssertEqual(list.folderId, "")
// The tree-builder treats "" as absent — verify that invariant here
XCTAssertTrue((list.folderId ?? "").isEmpty)
// "" is NOT nil at decode time; the tree-builder is what treats it as absent
XCTAssertEqual(list.parentId, "")
XCTAssertTrue((list.parentId ?? "").isEmpty)
}
}
```
Expand All @@ -135,20 +134,20 @@ final class UserListCodableTests: XCTestCase {

```swift
final class ListTreeNodeTests: XCTestCase {
func test_buildTree_rootListWithNoFolder_appearsAtRoot() {
let list = UserList(id: "1", name: "Root", description: nil, folderId: nil,
func test_buildTree_rootListWithNoParent_appearsAtRoot() {
let list = UserList(id: "1", name: "Root", description: nil, parentId: nil,
isPublic: nil, createdAt: "2024-01-01T00:00:00Z",
updatedAt: nil, itemCount: nil)
let nodes = ListTreeNode.buildTree(folders: [], lists: [list])
let nodes = ListTreeNode.buildTree(lists: [list])
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(nodes.first?.name, "Root")
}

func test_buildTree_listWithEmptyFolderIdTreatedAsRoot() {
let list = UserList(id: "1", name: "L", description: nil, folderId: "",
func test_buildTree_listWithEmptyParentIdTreatedAsRoot() {
let list = UserList(id: "1", name: "L", description: nil, parentId: "",
isPublic: nil, createdAt: "2024-01-01T00:00:00Z",
updatedAt: nil, itemCount: nil)
let nodes = ListTreeNode.buildTree(folders: [], lists: [list])
let nodes = ListTreeNode.buildTree(lists: [list])
XCTAssertEqual(nodes.count, 1)
}
}
Expand Down
Loading
Loading