Last Updated: January 2025 Target Audience: All AI agents (Claude, GPT-4, Gemini, Codex, etc.)
PROTOCOL.md is the source of truth for cross-platform compatibility.
However, if accessible, the original PROTOCOL.md in ~/WebstormProjects/redo-web-app/PROTOCOL.md supersedes any local copies. The web app is the leader platform and protocol authority.
- PROTOCOL.md (this directory or web app) - Cross-platform protocol specification
- CLAUDE.md (or your agent-specific file) - Platform-specific context and patterns
- PLANNING.md - Architecture decisions and rationale
- SESSION_X_SUMMARY.md - Recent development progress
Redo iOS is a native iOS/macOS task management app implementing the v1 event sourcing protocol shared with:
- Web App (leader):
~/WebstormProjects/redo-web-app - Android App:
~/StudioProjects/redo-android - Kotlin CLI:
~/IdeaProjects/redo
User Action → ChangeLogEntry → Local Storage → State Reconstruction → UI
↓
Firebase Sync (Background, Optional)
Think Git for Tasks:
getAllTasks()=git log(instant, reads local)createTask()=git commit(instant, writes local)syncChanges()=git fetch/push(async, background)
ZERO tolerance for invalid nodes. Cross-platform compatibility depends on this.
v1 Node Requirements:
versionMUST be exactly1(integer)idMUST be"sha256:"+ 64 lowercase hex charsparentsMUST be array of valid change IDstimestamp.lamportMUST be integer > 0timestamp.wallMUST be valid ISO 8601 stringauthor.userIdMUST be 32 lowercase hex chars (first 32 of public key)author.publicKey(optional) MUST be 64 lowercase hex charssignature(optional) MUST be 128 lowercase hex chars- NO Base58 encoding (web app's mistake, caused 125+ invalid nodes)
- NO uppercase hex (breaks cross-platform hashing)
- NO legacy v0 support (only v1 accepted)
Validation Location: Sources/RedoCore/Services/ChangeLogValidator.swift
If you consider relaxing these rules, STOP. Read PLANNING.md section 2.2 and the web app's 50+ hours of debugging pain.
All change IDs are SHA-256 hashes of canonical JSON per RFC 8785:
- Keys sorted alphabetically
- No whitespace
- Consistent encoding
- Deterministic across platforms
Test cross-platform compatibility:
let entry = /* same data as Kotlin/TypeScript */
let iosHash = try ContentAddressing.calculateChangeId(entry: entry)
let kotlinHash = "sha256:abc123..." // From Kotlin test
XCTAssertEqual(iosHash, kotlinHash) // MUST matchImplementation: Sources/RedoCrypto/ContentAddressing.swift
All operations MUST be instant (no network blocking):
// ✅ CORRECT
func loadTasks() async throws {
// Step 1: Load from local storage (instant)
let changes = try storage.getAllChanges(userId: userId)
tasks = try stateReconstructor.reconstructTasks(from: changes)
// Step 2: Sync in background (non-blocking)
Task { await syncChanges() }
}
// ❌ WRONG - Blocks UI on network
func loadTasks() async {
tasks = await firebaseSync.getTasks() // NO!
}Current state is always reconstructed from change log:
// ✅ CORRECT - Reconstruct every time
func getTasks() throws -> [RedoTask] {
let changes = try storage.getAllChanges(userId: userId)
return try stateReconstructor.reconstructTasks(from: changes)
}
// ❌ WRONG - Caching violates event sourcing
@AppStorage("cachedTasks") var tasks: Data // NO!Why? Change log is single source of truth. Caching introduces sync issues.
- Read PROTOCOL.md - Understand v1 node structure
- Review web/Android implementation - Avoid solved problems
- Check protocol impact - Will this break cross-platform compatibility?
- Write tests first - TDD approach prevents regressions
Before committing code, verify:
- Does this change canonical JSON serialization? → Test cross-platform hashing
- Does this modify validation rules? → Verify against PROTOCOL.md
- Does this block UI on network? → Refactor to background
- Does this cache state? → Use event sourcing instead
- Does this use uppercase hex? → Convert to lowercase
- Does this support v0 nodes? → Remove legacy support
// 1. Caching task state
@AppStorage("cachedTasks") var tasks: Data
// 2. Blocking UI on network
func loadTasks() async {
tasks = await firebase.getTasks() // Blocks!
}
// 3. Mixing token types
keychain.save(firebaseToken, forKey: "googleAccessToken") // Wrong!
// 4. Relaxing validation
if version == 1 || version == 2 { /* NO legacy! */ }
// 5. Uppercase hex
let hash = data.hexEncodedString(uppercase: true) // Breaks protocol!
// 6. Modifying existing changes
func updateChange(_ change: ChangeLogEntry) {
change.data["title"] = newTitle // Changes are immutable!
}// 1. Always reconstruct state
func loadTasks() async throws {
let changes = try storage.getAllChanges(userId: userId)
tasks = try stateReconstructor.reconstructTasks(from: changes)
Task { await syncChanges() } // Background
}
// 2. Validate at boundaries
func saveChange(_ change: ChangeLogEntry) throws {
guard validator.isValidV1Node(change) else {
throw ValidationError.invalidNode
}
try storage.saveChanges(userId: userId, newChanges: [change])
}
// 3. Separate credentials
keychain.save(googleOAuthToken, forKey: "googleOAuthToken")
keychain.save(firebaseCustomToken, forKey: "firebaseCustomToken")
// 4. Only v1 protocol
guard change.version == 1 else {
throw ValidationError.unsupportedVersion
}
// 5. Lowercase hex always
let hash = data.hexEncodedString().lowercased()
// 6. Create new change instead of modifying
let newChange = createChangeLogEntry(
action: .update,
taskId: task.guid.uuidString,
data: ["title": newTitle]
)
try storage.saveChanges(userId: userId, newChanges: [newChange])redo-ios/
├── Sources/
│ ├── RedoCore/ # Business logic (platform-agnostic)
│ │ ├── Models/ # RedoTask, TodoTask, ChangeLogEntry
│ │ ├── Services/ # StateReconstructor, ChangeLogValidator, TaskRanking
│ │ └── Storage/ # ChangeLogStorage, KeychainService
│ │
│ ├── RedoCrypto/ # Cryptography (Ed25519, SHA-256, Canonical JSON)
│ │ ├── Ed25519Manager.swift
│ │ ├── ContentAddressing.swift
│ │ └── CanonicalJSON.swift
│ │
│ ├── RedoUI/ # SwiftUI interface
│ │ ├── Views/ # TaskListView, CreateTaskView, AnalyticsView
│ │ ├── ViewModels/ # AppViewModel (MVVM pattern)
│ │ ├── Components/ # MatrixTaskCard, SearchBar, etc.
│ │ ├── Theme/ # MatrixTheme (colors, typography, modifiers)
│ │ └── Sync/ # FirebaseSyncService
│ │
│ ├── RedoWidgets/ # Home Screen widgets (iOS-exclusive)
│ │ └── Views/ # TaskListWidgetView, QuickActionsWidgetView
│ │
│ └── RedoIntents/ # Siri Shortcuts (iOS-exclusive)
│ ├── AppIntents.swift # iOS 16+ modern App Intents
│ └── IntentHandlers.swift # iOS 14-15 legacy intents
│
├── Tests/
│ ├── RedoCoreTests/ # Business logic tests
│ └── RedoCryptoTests/ # Cryptography tests
│
├── PROTOCOL.md # Cross-platform protocol (COPY from web app)
├── PLANNING.md # Architecture decisions and rationale
├── CLAUDE.md # Claude-specific instructions
├── AI.md # This file (all agents)
├── GEMINI.md # Gemini-specific instructions
├── CODEX.md # Codex-specific instructions
├── AGENTS.md # Generic AI agent instructions
└── SESSION_X_SUMMARY.md # Development session summaries
Location: Sources/RedoCore/Services/StateReconstructor.swift
func reconstructTasks(from changes: [ChangeLogEntry]) throws -> [RedoTask] {
// Step 1: Sort by Lamport clock (causal ordering)
let sorted = changes.sorted { $0.timestamp.lamport < $1.timestamp.lamport }
// Step 2: Validate each change
for change in sorted {
guard validator.isValidV1Node(change) else {
throw ValidationError.invalidNode(id: change.id)
}
}
// Step 3: Replay actions sequentially
var tasks: [UUID: RedoTask] = [:]
for change in sorted {
switch change.action {
case .create:
tasks[taskId] = createTask(from: change.data)
case .update:
tasks[taskId]?.update(from: change.data)
case .delete:
tasks[taskId] = nil
// ... etc
}
}
return Array(tasks.values)
}Location: Sources/RedoCore/Services/TaskRanking.swift
func calculateRank(task: RedoTask, currentTime: Date = Date()) -> Float {
var rank = Float(task.priority) // Base: 1-5
// Urgency multiplier (exponential based on days overdue)
if let dueDate = task.dueDate, dueDate < currentTime {
let daysOverdue = currentTime.timeIntervalSince(dueDate) / 86400
rank *= Float(pow(2.0, min(daysOverdue, 10))) // Cap at 2^10 = 1024x
}
// Circadian bonus (5am-12pm = +10%, 12pm-8pm = +20%, 8pm-5am = -10%)
let hour = Calendar.current.component(.hour, from: currentTime)
let circadianBonus: Float = {
switch hour {
case 5..<12: return 1.1 // Morning boost
case 12..<20: return 1.2 // Afternoon boost
default: return 0.9 // Evening penalty
}
}()
rank *= circadianBonus
// Recency bonus (newer tasks slightly higher)
let daysSinceCreated = currentTime.timeIntervalSince(task.created) / 86400
let recencyBonus = max(1.0, 1.2 - Float(daysSinceCreated * 0.01))
rank *= recencyBonus
return rank
}Location: Sources/RedoCrypto/ContentAddressing.swift
func calculateChangeId(entry: ChangeLogEntry) throws -> String {
// Step 1: Serialize to canonical JSON
let canonicalJSON = try CanonicalJSON.serialize(entry)
// Step 2: SHA-256 hash
let data = Data(canonicalJSON.utf8)
let hash = SHA256.hash(data: data)
// Step 3: Hex encode (lowercase!)
let hexHash = hash.compactMap { String(format: "%02x", $0) }.joined()
return "sha256:\(hexHash)"
}Hash Consistency Test:
func testHashMatchesKotlin() throws {
let entry = ChangeLogEntry(/* same data as Kotlin test */)
let iosHash = try ContentAddressing.calculateChangeId(entry: entry)
let kotlinHash = "sha256:abc123..." // From Kotlin test
XCTAssertEqual(iosHash, kotlinHash)
}Signature Verification Test:
func testEd25519CompatibilityWithWeb() throws {
let message = "test"
let privateKey = "abc123..." // Same key as web test
let signature = try Ed25519Manager.sign(string: message, privateKeyHex: privateKey)
// Paste signature into web app console:
// > Ed25519.verify(publicKey, signature, "test")
// Should return true
}MUST match web/Android exactly:
nodes/ # Global collection
{nodeId}/
id: "sha256:..."
version: 1
parents: ["sha256:..."]
timestamp: { lamport: 5, wall: "2025-01-09T..." }
author: {
userId: "abc123...", # Crypto userId (first 32 of public key)
deviceId: "...",
publicKey: "abc123..." # Full Ed25519 public key (64 hex)
}
action: "CREATE"
taskId: "uuid"
data: { /* task fields */ }
signature: "...",
accessList: ["105903..."], # Google OAuth IDs
createdAt: Timestamp
users/
{googleOAuthId}/ # Google OAuth subject ID (NOT crypto userId)
ownedNodes: ["sha256:...", ...]
Critical Distinctions:
author.userId= First 32 hex chars of Ed25519 public key (crypto identity)users/{googleOAuthId}= Google OAuth subject ID (Firebase auth identity)- These are different and must not be confused!
-
Update ChangeAction enum (
Sources/RedoCore/Models/ChangeLogEntry.swift):public enum ChangeAction: String, Codable { case create = "CREATE" case update = "UPDATE" case snooze = "SNOOZE" // NEW }
-
Add handler in StateReconstructor (
Sources/RedoCore/Services/StateReconstructor.swift):switch change.action { case .snooze: if let taskId = UUID(uuidString: change.taskId), let snoozeUntil = change.data["snoozeUntil"] as? String, let date = ISO8601DateFormatter().date(from: snoozeUntil) { tasks[taskId]?.snoozedUntil = date } }
-
Update web/Android implementations to match
-
Write tests for new action type
-
Follow Matrix theme patterns (
Sources/RedoUI/Theme/MatrixTheme.swift):struct MyNewView: View { var body: some View { ZStack { Color.matrixBackground.ignoresSafeArea() VStack(spacing: .matrixSpacingLarge) { Text("Title") .font(.matrixTitle1) .foregroundColor(.matrixNeon) .neonGlow() } } } }
-
Use Matrix color palette:
.matrixBackground- #020B09 (dark green-black).matrixNeon- #00FFB8 (neon cyan).matrixTextPrimary- #B8FFE6 (light cyan).matrixTextSecondary- #80BFA3 (muted green)
-
Add accessibility (VoiceOver support):
.accessibilityLabel("Task: \(task.title)") .accessibilityHint("Double tap to open task details") .accessibilityIdentifier("task_card_\(task.guid)")
-
Check Firebase path matching:
// iOS path: nodes/{nodeId} // Web path: nodes/{nodeId} // Must match exactly! -
Verify OAuth vs Crypto userId:
let oauthId = keychain.loadGoogleOAuthId() // For Firebase paths let cryptoUserId = keychain.loadUserId() // For node signing // These are DIFFERENT!
-
Enable Firebase debug logging:
FirebaseConfiguration.shared.setLoggerLevel(.debug)
-
Check change log validation:
let changes = try storage.getAllChanges(userId: userId) for change in changes { guard validator.isValidV1Node(change) else { print("Invalid node: \(change.id)") } }
| Metric | Target | Method |
|---|---|---|
| Task list render | < 16ms | 60 FPS smooth scrolling |
| State reconstruction (1000 tasks) | < 100ms | Efficient replay algorithm |
| Local operations (create/update) | < 50ms | Direct file I/O |
| Firebase sync (background) | < 500ms | Non-blocking async |
| Change log file size (1000 tasks) | ~1MB | ~1KB per task |
| Memory usage (typical) | < 10MB | Efficient data structures |
- Ed25519 for signatures (CryptoKit hardware-accelerated)
- SHA-256 for content addressing
- Canonical JSON for deterministic serialization
- Private keys in Keychain (
kSecAttrAccessibleWhenUnlockedThisDeviceOnly) - Public keys in Keychain (for backup)
- NO keys in UserDefaults/plist files
- Change log encrypted at rest (iOS default)
- Firebase data encrypted in transit (TLS)
- Firebase data encrypted at rest (Google Cloud default)
- Optional: End-to-end encryption (future feature)
Reference implementations:
- Web app (leader):
~/WebstormProjects/redo-web-app/src/models/RedoNode.ts(1,738 lines) - Android:
~/StudioProjects/redo-android/app/src/main/java/vision/salient/redo/ - Kotlin CLI:
~/IdeaProjects/redo/core/src/main/kotlin/vision/salient/redo/model/
Documentation:
- PROTOCOL.md (web app): Cross-platform v1 specification
- ARCHITECTURE.md (web app): Design rationale and decisions
- PLANNING.md (iOS): Architecture decisions specific to iOS
- SESSION_X_SUMMARY.md: Recent development progress
Debug checklist:
- Read PROTOCOL.md section on the relevant feature
- Compare iOS implementation to web/Android
- Check if change breaks cross-platform compatibility
- Verify tests pass (
swift test) - Test against web/Android if protocol-related
Foundation Phase ✅ Complete
- Core models (RedoTask, TodoTask, ChangeLogEntry)
- Cryptography (Ed25519, SHA-256, canonical JSON)
- State reconstruction engine
- Local storage (file-based change log)
- Firebase sync service
- SwiftUI UI (Matrix theme)
- Basic test coverage
iOS-Specific Features ✅ Complete
- Home Screen widgets (task list + stats)
- Siri Shortcuts integration (create/complete/view tasks)
- Saved filter presets (quick-apply combinations)
- Advanced analytics (trends, predictions, insights)
Next Focus Areas:
- Comprehensive test coverage
- Performance optimization
- UI/UX polish
- App Store preparation
- Cross-platform sync verification
See SESSION_4_SUMMARY.md for latest development progress.
- PROTOCOL.md is law - Cross-platform compatibility depends on strict adherence
- Web app PROTOCOL.md supersedes any local copies if accessible
- Local-first always - Never block UI on network operations
- Event sourcing always - Never cache state, always reconstruct
- Test cross-platform - Hash compatibility is non-negotiable
- v1 only - Zero tolerance for invalid/legacy nodes
When in doubt, ask: "Would this break sync with web/Android?" If yes, don't do it.
End of AI.md
Read CLAUDE.md / GEMINI.md / CODEX.md / AGENTS.md for agent-specific instructions.