1. Pull latest main before starting work. Run git pull origin main to get the current docs/PLAN.md and architecture docs. Plans change frequently — never work from a stale copy.
2. Update docs/PLAN.md after completing any task — mark it ✅, 🚧, or ⬜.
3. Add new tasks to docs/PLAN.md when the user's prompt or the work itself requires steps not already tracked. Place them in the appropriate phase and section.
4. Add to docs/HUMAN_TASKS.md any step that requires a human: running commands on real hardware, creating accounts, provisioning API keys, configuring third-party services, app store setup, DNS, etc. Do not block on these — log them and move on.
5. All doc changes go directly to main — no feature branches for documentation.
| Layer | Technology | Notes |
|---|---|---|
| **Shared business logic** | Kotlin Multiplatform (KMP) | shared/ — targets iOS, macOS, Android |
| **iOS/macOS app** | SwiftUI + KMP framework | iosApp/ |
| **macOS CLI** | Kotlin/Native (Clikt) | cli/ |
| **Backend API** | Rust 2024, Axum 0.8 | server/ — deployed to AWS Lambda via cargo-lambda |
| **Database (server)** | Postgres on RDS t4g.nano | sqlx with compile-time checked queries |
| **File storage** | AWS S3 | Vault files stored at {user_id}/{type}/{file}.md |
| **Push notifications** | AWS SNS → APNs (iOS/macOS), FCM (Android future) | Silent push to trigger background sync |
| **AI (LLM + Vision)** | Claude on AWS Bedrock | aws-sdk-bedrockruntime — classify, structure, enrich, OCR. IAM auth, no API keys. |
| **Voice transcription** | Amazon Transcribe | aws-sdk-transcribe — async batch, reads audio from S3. Replaces Whisper. |
| **Infrastructure** | AWS (Lambda, API Gateway, RDS, S3, SNS, Bedrock, Transcribe) | Terraform in server/deploy/terraform/ |
| **Local DB (client)** | SQLite via SQLDelight | Index is disposable — rebuilt from .md files |
| **HTTP client (KMP)** | Ktor 3.0.3 | Used for both sync API calls and direct S3 transfers |
| **Auth** | Email/password + Sign in with Apple → JWT | Multi-provider, account linking. See docs/AUTH_ARCHITECTURE.md |
See docs/TECH_DECISIONS.md for the full rationale behind each decision (why S3 over iCloud, why Rust over Go/Kotlin, why AWS over Azure, etc.).
brew install openjdk@17export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home```bash
./gradlew :shared:build
./gradlew :shared:compileKotlinMacosArm64
./gradlew :cli:linkDebugExecutableMacosArm64
./cli/build/bin/macosArm64/debugExecutable/steel.kexe
```
```bash
export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home # macOS
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 # Linux/CI
./gradlew :shared:jvmTest
./gradlew :shared:jvmTest --tests "com.steelnotes.editor.FormattingEngineTest"
./gradlew :shared:macosArm64Test
./gradlew :shared:allTests
open shared/build/reports/tests/jvmTest/index.html
```
Test architecture: Tests live in shared/src/commonTest/ and run on all targets. A JVM target is configured specifically to enable running tests on Linux and CI without Xcode. JVM actual implementations for expect declarations live in shared/src/jvmMain/.
Current test suites:
| Suite | File | Tests |
|---|---|---|
| MarkdownDocumentParser | editor/MarkdownDocumentParserTest.kt | Block parsing, inline span parsing |
| MarkdownDocumentSerializer | editor/MarkdownDocumentSerializerTest.kt | Round-trip (parse → serialize) fidelity |
| FormattingEngine | editor/FormattingEngineTest.kt | Toggle bold/italic/code/highlight, headings, links, lists, blockquotes |
| MarkdownDocument | editor/MarkdownDocumentTest.kt | Model rawText serialization, document text property |
```bash
steel --vault /path/to/vault init
steel --vault /path/to/vault create source --title "Title" --author "Author" --tags "t1,t2" --body "..."
steel --vault /path/to/vault list [--type note|source|thought|question]
steel --vault /path/to/vault search "query" [--type source]
steel --vault /path/to/vault read <note-id>
steel --vault /path/to/vault delete <note-id>
steel --vault /path/to/vault index
steel --vault /path/to/vault --json list # JSON output for programmatic use
```
Default vault: ~/.steel-notes/vault
```
steel-notes/
├── shared/ # KMP shared Kotlin module (all business logic)
│ └── src/
│ ├── commonMain/ # Platform-agnostic code
│ ├── appleMain/ # Apple platform actuals (iOS + macOS)
│ ├── iosMain/ # iOS-specific code
│ └── macosMain/ # macOS-specific code
├── cli/ # Kotlin/Native CLI executable
│ └── src/
│ ├── commonMain/ # CLI commands (Clikt)
│ └── macosMain/ # macOS platform glue (getenv etc.)
├── iosApp/ # SwiftUI iOS/macOS app (not yet implemented)
├── gradle/ # Version catalog (libs.versions.toml)
└── AGENTS.md # This file
```
com.steelnotes.model — Data models: VaultItem (sealed interface), Note, Source, Thought, Question. See docs/NOTE_ARCHITECTURE.mdcom.steelnotes.parser — FrontmatterParser, WikilinkExtractor, TransclusionExtractor, TransclusionResolver, AnnotationParser, NoteSerializer, NoteFactorycom.steelnotes.db — SQLDelight generated code: SteelDatabase, SteelNotesQueriescom.steelnotes.search — SearchEngine (FTS5 + BM25), SearchResult, SearchFiltercom.steelnotes.vault — VaultManager (CRUD), VaultIndexer, PlatformFileSystem (expect/actual)com.steelnotes.platform — DatabaseDriverFactory (expect/actual)1. .md files with YAML frontmatter are the source of truth
2. FrontmatterParser + NoteFactory convert files → typed Note objects
3. VaultManager writes notes to disk AND inserts into SQLite index
4. SearchEngine queries FTS5 for full-text search
5. VaultIndexer rebuilds the entire SQLite index from vault files
Every .md file has YAML frontmatter with --- delimiters. The type and id fields are required.
See /Users/ethanpitts/Downloads/steel-notes-spec.md for full schema.
See docs/SWIFT_PITFALLS.md for the comprehensive list of Swift, SwiftUI, KMP interop, XcodeGen, and simulator pitfalls. Read it before touching iosApp/. Every entry caused a real build failure or runtime crash.
./gradlew (wrapper, pinned to 8.10), never the system gradle command../gradlew invocation. The system may default to a different JDK.applyDefaultHierarchyTemplate()** creates appleMain, iosMain, macosMain source sets automatically. Don't manually dependsOn() — it conflicts with the template.expect/actual classes** produce warnings about being in Beta. Suppressed via kotlin.suppressExpectActualClasses=true in gradle.properties.ExperimentalForeignApi** opt-in required for all Apple platform interop code (NSFileManager, NSString, posix). Add @file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) at the top of files.kotlin.native.cacheKind.macosArm64=none) due to linker issues with the current Kotlin/Native version.NULLS FIRST/NULLS LAST not supported. Use CASE WHEN x IS NULL THEN 0 ELSE 1 END instead.SearchNotes, SearchNotesFiltered) that are different from the Notes table type. You can't use one where the other is expected.-lsqlite3 linker flag** required for any executable or framework that uses SQLDelight's native driver. Add via linkerOpts("-lsqlite3") in build.gradle.kts.help parameter from constructors (CliktCommand, option(), argument()). Use the builder pattern instead if help text is needed.argument() names** are positional — don't pass help = as a named parameter.{vaultRoot}/{type.directory}/{slug}.md — e.g., sources/beyond-good-and-evil.md{prefix}-{YYYY-MM-DD}-{sequence} — e.g., src-2026-03-20-001steel indexCross-device sync is designed but not yet implemented. See docs/SYNC_ARCHITECTURE.md for the full design.
Key points for implementers:
POST /vault/push rejects if base_version doesn't match server. Client downloads the server version and runs ConflictResolver.steel/sync.db (separate from the disposable index.db)com.steelnotes.sync (SyncManager, SyncClient, ChangeDetector, ConflictResolver) and com.steelnotes.auth (AuthManager, TokenStore)AuthService.swift, SignInView.swift, SyncService.swift, ConflictResolutionView.swift, SyncSettingsView.swiftserver/ directory (monorepo)server/deploy/terraform/A static dashboard at plan/index.html is auto-generated from PLAN.md and docs/HUMAN_TASKS.md. It shows implementation progress by phase with expandable task details.
plan/build.py** — Python script that parses markdown files, extracts task statuses (✅/🚧/⬜ and [ ]/[x] checkboxes), and generates plan/index.html + plan/data.json.github/workflows/deploy-plan.yml** — GitHub Actions workflow that runs build.py and syncs plan/ to an S3 bucket on every push to maindev → S3 endpoint)```bash
python3 plan/build.py
open plan/index.html
```
The site rebuilds automatically on push to main. If you update PLAN.md or docs/HUMAN_TASKS.md, the next merge to main will update the site.
1. Cloud Vault Sync (Phase 2) — S3 + Rust/Axum backend + KMP sync client. See docs/SYNC_BUILD_GUIDE.md for the actionable build plan and docs/SYNC_ARCHITECTURE.md for the full design spec.
2. Capture Layer & AI Parsing (Phase 3) — Share Sheet, URL → structured Source, AI-powered classification and transposition. Spec TBD.
3. iOS UI Polish (Phase 4) — Reader, Editor, Search views. Intentionally delayed.
steel update CLI command