← Back to Plan

Agents

Steel Notes — Agent Guide

Important: Required Agent Behaviors

1. Call out 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: state them plainly in your final response so they can be tracked, and move on. This repo deliberately keeps no human-task file; tracking lives outside it.

2. Docs travel with the change they describe — update them on the same branch as the code, so a doc and the behavior it documents are never separately reviewable or separately mergeable.

Tech Stack

LayerTechnologyNotes
**Shared business logic**Kotlin Multiplatform (KMP)shared/ — targets iOS, macOS, Android
**iOS/macOS app**SwiftUI + KMP frameworkiosApp/
**macOS CLI**Kotlin/Native (Clikt)cli/
**Backend API**Rust 2024, Axum 0.8server/ — deployed to AWS Lambda via cargo-lambda
**Database (server)**Postgres on RDS t4g.nanosqlx with compile-time checked queries
**File storage**AWS S3Vault 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 Bedrockaws-sdk-bedrockruntime — classify, structure, enrich, OCR. IAM auth, no API keys.
**Voice transcription**Amazon Transcribeaws-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 SQLDelightIndex is disposable — rebuilt from .md files
**HTTP client (KMP)**Ktor 3.0.3Used for both sync API calls and direct S3 transfers
**Auth**Email/password + Sign in with Apple → JWTMulti-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.).


Quick Start

Prerequisites

  • **JDK 17**: brew install openjdk@17
  • **Xcode 16+**: Required for iOS/macOS compilation
  • Set JAVA_HOME before running Gradle: export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
  • Build Commands

    ```bash

    Build shared KMP module (all Apple targets)

    ./gradlew :shared:build

    Compile just macOS arm64 (fastest for iteration)

    ./gradlew :shared:compileKotlinMacosArm64

    Build CLI executable

    ./gradlew :cli:linkDebugExecutableMacosArm64

    CLI binary location

    ./cli/build/bin/macosArm64/debugExecutable/steel.kexe

    ```

    Running Tests

    ```bash

    Set JAVA_HOME (use the JDK available on your system)

    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

    Run all shared module tests (JVM — works on any platform)

    ./gradlew :shared:jvmTest

    Run a specific test class

    ./gradlew :shared:jvmTest --tests "com.steelnotes.editor.FormattingEngineTest"

    Run native tests (requires macOS with Xcode)

    ./gradlew :shared:macosArm64Test

    Run all tests across all available targets

    ./gradlew :shared:allTests

    View HTML test report

    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/.

    CI/CD

    Pull requests into dev or main run .github/workflows/ci.yml: :shared:jvmTest and the Rust suite always, plus the arm64 Lambda build when server paths changed.

    Deploys are automatic and gated on those same suites, which live in reusable _test-*.yml workflows so CI and the deploy gates can never drift. Merging to dev deploys the dev server; merging to main deploys production and uploads a TestFlight build. Nothing to click, nothing to tag. See docs/CICD_PLAN.md for the environment layout and server/DEPLOY.md for the server specifics.

    Three things to know when touching this:

  • **The iOS simulator suite does not run in CI at all**, and does not gate the TestFlight release either. macOS runners bill at 10x and were the bulk of the Actions spend. Run it locally before you push — xcodebuild test per _test-ios.yml, which is kept dispatch-only for exactly that invocation. Nothing automated will catch an iOS regression for you.
  • Rust lint (cargo fmt --check, clippy -D warnings) is a blocking CI job, not advisory: it has no continue-on-error, so a fmt diff turns the whole run red. It doesn't gate deploys — those hang off _test-server.yml / _test-shared.yml — but a red CI on dev is a red CI. The job only runs when server/ changed, so a fmt diff can land on one push and surface on a later, unrelated one. Run cargo fmt in server/ before pushing Rust.
  • The Xcode pre-build phase resolves the JDK with java_home -v 21, so CI needs a JDK registered with the OS, not an actions/setup-java toolcache install. The remaining macOS job asserts this up front.
  • Current test suites:

    SuiteFileTests
    MarkdownDocumentParsereditor/MarkdownDocumentParserTest.ktBlock parsing, inline span parsing
    MarkdownDocumentSerializereditor/MarkdownDocumentSerializerTest.ktRound-trip (parse → serialize) fidelity
    FormattingEngineeditor/FormattingEngineTest.ktToggle bold/italic/code/highlight, headings, links, lists, blockquotes
    MarkdownDocumenteditor/MarkdownDocumentTest.ktModel rawText serialization, document text property
    ResourceViewPlannermodel/ResourceViewPlannerTest.ktWhich segments a resource offers and which one opens, per ResourceKind
    CompanionNotevault/CompanionNoteTest.kt (jvmTest)Resolving the Note that holds a resource's user notes (resources: [id])
    SearchEnginesearch/SearchEngineTest.kt (jvmTest)FTS + author/resource LIKE fallback, dedupe, limit, type filter, snippet windowing/stripping
    SearchResultResourceKindsearch/SearchResultResourceKindTest.kt (jvmTest)Search results carry the ResourceKind that drives each row's glyph
    EvalCaseMaterializationsync/EvalCaseMaterializationTest.ktEval cases never take the lazy binary-pull path, and a missing one is a gap to repair
    EvalCaseRepairPullsync/EvalCaseRepairPullTest.kt (jvmTest)A case recorded as synced but absent from disk is re-fetched, without a full state call on the clean path
    AttachmentCacheEvalSweepattachments/AttachmentCacheEvalSweepTest.kt (jvmTest)The time-GC sweep evicts idle photos but never eval cases

    CLI Usage

    ```bash

    steel --vault /path/to/vault init

    steel --vault /path/to/vault create resource --title "Title" --author "Author" --tags "t1,t2" --body "..."

    steel --vault /path/to/vault list [--type note|resource|thought|question]

    steel --vault /path/to/vault search "query" [--type resource]

    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


    Architecture

    Project Structure

    ```

    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

    ```

    Key Packages (shared module)

  • com.steelnotes.model — Data models: VaultItem (sealed interface), Note, Resource, Thought, Question. See docs/NOTE_ARCHITECTURE.md
  • com.steelnotes.parserFrontmatterParser, SteelUri, NoteLinkExtractor, TransclusionExtractor, TransclusionResolver, AnnotationParser, NoteSerializer, NoteFactory
  • com.steelnotes.db — SQLDelight generated code: SteelDatabase, SteelNotesQueries
  • com.steelnotes.searchSearchEngine (FTS5 + BM25), SearchResult, SearchFilter
  • com.steelnotes.vaultVaultManager (CRUD), VaultIndexer, PlatformFileSystem (expect/actual)
  • com.steelnotes.platformDatabaseDriverFactory (expect/actual)
  • Data Flow

    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

    File Format

    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.


    Pitfalls & Gotchas

    Swift / iOS

    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.

    Gradle / Build

  • **System Gradle is 9.4.1** — but SQLDelight 2.0.2 requires Gradle 8.x. Always use ./gradlew (wrapper, pinned to 8.10), never the system gradle command.
  • **JAVA_HOME must be set** to JDK 17 before every ./gradlew invocation. The system may default to a different JDK.
  • **First build downloads Kotlin/Native toolchain** (~500MB). Subsequent builds are fast.
  • **cargo lambda build can leave target/lambda/<bin>/bootstrap.zip stale** even when the Rust binary changed. Before deploying Lambda code, either delete the old zip and rebuild with cargo lambda build --output-format zip ... or otherwise verify the zip timestamp/hash changed. Do not assume the existing bootstrap.zip is fresh.
  • Kotlin Multiplatform

  • **applyDefaultHierarchyTemplate()** creates appleMain, iosMain, macosMain resource 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.
  • **Native cache disabled** for macOS targets (kotlin.native.cacheKind.macosArm64=none) due to linker issues with the current Kotlin/Native version.
  • SQLDelight

  • **SQLite 3.18 dialect** — NULLS FIRST/NULLS LAST not supported. Use CASE WHEN x IS NULL THEN 0 ELSE 1 END instead.
  • **FTS5 queries** return generated types (e.g., 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.
  • Clikt (CLI framework)

  • **Clikt 5.x** removed the 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.
  • Files & Vault

  • **Vault path convention**: {vaultRoot}/{type.directory}/{slug}.md — e.g., resources/beyond-good-and-evil.md
  • **ID pattern**: {prefix}-{YYYY-MM-DD}-{sequence} — e.g., src-2026-03-20-001
  • **SQLite index is disposable** — it can always be rebuilt from the .md files via steel index

  • Sync Architecture

    Cross-device sync is designed but not yet implemented. See docs/SYNC_ARCHITECTURE.md for the full design.

    Key points for implementers:

  • Sync uses **S3/R2 + custom REST API** — not iCloud Drive or CloudKit
  • **File bytes never touch the API** — clients upload/download directly to S3 via presigned URLs
  • The API is a thin coordination layer: auth, version tracking (Postgres), presigned URL generation, push notifications
  • **Auth: Sign in with Apple** (required for App Store) → JWT access tokens
  • **SQLite index is never synced** — rebuilt locally after pulling remote file changes
  • **Conflict detection is version-based** — POST /vault/push rejects if base_version doesn't match server. Client downloads the server version and runs ConflictResolver
  • Conflict resolution is **frontmatter-aware** — YAML fields merge independently from body text
  • **Push notifications (APNs)** notify other devices when a sync completes — near real-time sync without polling
  • Sync metadata lives in .steel/sync.db (separate from the disposable index.db)
  • New packages: com.steelnotes.sync (SyncManager, SyncClient, ChangeDetector, ConflictResolver) and com.steelnotes.auth (AuthManager, TokenStore)
  • Swift side: AuthService.swift, SignInView.swift, SyncService.swift, ConflictResolutionView.swift, SyncSettingsView.swift
  • **Backend:** Rust (Axum + sqlx + aws-sdk-s3), deployed to AWS Lambda. Code lives in server/ directory (monorepo)
  • **AWS infra:** Lambda + API Gateway (API), RDS Postgres (state), S3 (vault files), SNS (push). Terraform in server/deploy/terraform/

  • Plan Tracker Site (dev.pittsventures.com)

    A static dashboard at plan/index.html is auto-generated from PLAN.md. It shows implementation progress by phase with expandable task details.

    How it works

  • **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 main
  • **Hosting:** S3 static website + CloudFlare DNS (CNAME dev → S3 endpoint)
  • Local preview

    ```bash

    python3 plan/build.py

    open plan/index.html

    ```

    When to rebuild

    The site rebuilds automatically on push to main. If you update PLAN.md, the next merge to main will update the site.


    Current Priorities (in order)

    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 Resource, AI-powered classification and transposition. Spec TBD.

    3. iOS UI Polish (Phase 4) — Reader, Editor, Search views. Intentionally delayed.

    What's Not Yet Implemented

  • Cloud vault sync — Rust/Axum API + S3 + KMP client (Phase 2, **next up**)
  • User accounts / Sign in with Apple (part of Phase 2A)
  • Share Sheet extension (Phase 3A)
  • AI client + TranspositionEngine (Phase 3B)
  • iOS Reader/Editor/Search views (Phase 4)
  • SKIE bridging for Kotlin Flow → Swift async/await
  • Question lifecycle engine (resurfacing, transitions)
  • Synthesis generation
  • PDF reader
  • Idea graph visualization
  • macOS SwiftUI app
  • steel update CLI command
  • If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.

  • Screenshots & Visual Verification

    When asked for a screenshot or visual proof:

    1. Build and run on the iOS Simulator via xcodebuildmcp simulator build-and-run

    2. Navigate to the relevant screen using xcodebuildmcp ui-automation tap (use snapshot-ui for precise coordinates)

    3. Capture with xcrun simctl io <simulator-id> screenshot /tmp/<name>.png

    4. Upload to S3: aws s3 cp /tmp/<name>.png s3://dev.pittsventures.com/<name>.png --content-type image/png

    5. Share the URL: https://dev.pittsventures.com/<name>.png

    Use descriptive filenames (e.g., blockquote-style.png, transclusion-card.png). Images are publicly accessible once uploaded.