# AGENTS.md

Operational instructions for coding agents working in this repository.
Humans: see `README.md` for product overview and run configs.
Agents: read this file (and the nearest nested `AGENTS.md`) before editing.

Nested guides take precedence when editing under those trees:

- [`crates/vnidrop/AGENTS.md`](crates/vnidrop/AGENTS.md) — Rust core
- [`shared/AGENTS.md`](shared/AGENTS.md) — Compose Multiplatform UI / KMP

---

## Project overview

VniDrop is a cross-platform **local P2P file transfer** app (Android, iOS, Desktop).

| Layer | Path | Responsibility |
|-------|------|----------------|
| Rust core | `crates/vnidrop/` | Iroh endpoint, blobs, SQLite, tickets, approval, streaming |
| Shared KMP | `shared/` | Compose UI, ViewModels, expect/actual platform bridges |
| Hosts | `androidApp/`, `iosApp/`, `desktopApp/` | Thin app shells |

**Invariant:** UI/platform opens files and handles pickers; **Rust streams bytes**.
Do not design features that move transfer payloads through Kotlin heap by default.

Domain docs (reference, do not paste into PRs):

- [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md)
- [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md)

---

## Absolute rules

1. Prefer PRs into `master`. Do not merge to `master` locally unless the user asks.
2. Do not `git push`, force-push, or open a PR unless the user asks.
3. If `commit.gpgsign` is enabled, create **signed** commits only. If signing fails
   (empty `ssh-add -l`), stop and tell the user to unlock the key. Never switch to
   unsigned commits to “unblock” yourself.
4. Change only files required for the task. No drive-by refactors, dependency bumps,
   or repo-wide formatting.
5. Do not force architecture migrations (MVI, Hilt, Nav3, etc.) unless requested.
6. Never commit secrets, key material, or passphrases.
7. Destructive git (`reset --hard`, `push --force`, dropping DBs) only with explicit
   user approval.
8. **Every bug fix includes a regression test** at the lowest layer that catches it.
9. After code changes, run the **relevant** checks in [Build and test](#build-and-test)
   and fix failures before finishing.

---

## Build and test

Install prerequisites when missing: Rust stable + rustfmt + clippy, JDK 17,
Android NDK/SDK only if building Android, Xcode only for iOS.

### Rust core (`crates/vnidrop` or workspace root)

Run from the **repo root** (Cargo workspace):

```bash
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --all-targets
```

Focused:

```bash
cargo test -p vnidrop
cargo test -p vnidrop --test output_sink
cargo test -p vnidrop --test transfer
cargo test -p vnidrop --test approval
cargo test -p vnidrop --test lifecycle
```

After finishing Rust edits, format:

```bash
cargo fmt --all
```

CI also runs `cargo doc --workspace --no-deps` with `RUSTDOCFLAGS=-D warnings`
(see `.github/workflows/rust-core.yml`). Run it before large Rust public-API changes.

### Shared KMP / Compose (`shared/`)

```bash
./gradlew :shared:jvmTest
./gradlew :shared:compileKotlinJvm
```

Other targets (slower / machine-dependent):

```bash
./gradlew :shared:testAndroidHostTest
./gradlew :shared:iosSimulatorArm64Test   # macOS + Xcode
./gradlew :androidApp:assembleDebug
./gradlew :desktopApp:run
```

**Note:** `jvmTest` CI currently runs on **macOS**. Gobley host cargo is enabled
for the current host and architecture, so local Linux and Windows builds embed
their matching desktop Rust library. Prefer macOS only when exact CI parity is
required.

### What to run before finishing

| You changed… | Minimum verification |
|--------------|----------------------|
| `crates/vnidrop/**` only | `cargo fmt`, `cargo clippy … -D warnings`, `cargo test -p vnidrop` |
| Cancel / export / sinks | Above + `cargo test -p vnidrop --test output_sink` |
| `shared/**` only | `./gradlew :shared:jvmTest` |
| Both | Rust suite + `:shared:jvmTest` |
| Docs only | No suite required; verify links/paths |

Do not kill long `cargo` / Gradle runs mid-flight unless they hang past several
minutes with no output; first builds are slow.

---

## Repository map (edit here)

### Rust runtime (keep split; do not re-merge into one file)

```
crates/vnidrop/src/runtime/
  mod.rs        # CoreInner, startup recovery, emit helpers
  facade.rs     # UniFFI VnidropCore + block_on
  share.rs      # import / share
  receive.rs    # receive, download, export, output sinks
  lifecycle.rs  # cancel, delete, status, access mode, shutdown
  provider.rs   # provider events, per-connection send progress
```

Other core modules: `filesystem.rs`, `repository.rs`, `approval.rs`,
`handshake.rs`, `ticket.rs`, `access_policy.rs`, `event_hub.rs`, `api.rs`.

### Shared app

```
shared/src/commonMain/kotlin/com/vnidrop/app/
  core/           # CoreGateway, models, pickers interfaces
  feature/send|receive|approvals|settings|app/
  ui/theme|components|navigation|feedback|state/
androidMain|iosMain|jvmMain/   # expect/actual implementations
```

### Platform file rules (do not violate)

- Desktop / path-based iOS: paths; directory walk in Rust when `is_directory`.
- Android **share**: ParcelFileDescriptor **file** FDs only — never a directory FD.
  Folder share expands SAF trees in Kotlin to per-file FDs + relative names.
- Android **receive** default: MediaStore Downloads sink; custom trees via SAF write.
- Receive publish: no-overwrite temp + hard link / exclusive rename
  (see `CORE_FLOW.md`).

---

## Code style

### General

- Match surrounding code (naming, imports, error handling).
- Prefer small, reviewable diffs. Avoid files growing past ~800 LoC without
  splitting when adding substantial logic.
- Do not add one-off helpers used only once if an inline block is clearer.
- Prefer exhaustive `when` / `match`; avoid wildcards that hide new cases.

### Comments (strict)

Comment **why**, invariants, and platform/concurrency traps only.

- Do comment: cancel-before-await ordering, SAF/FD limits, security-scoped
  leases, “exactly one finish/abort after start_file”, durability rules.
- Do **not** comment: restating the next line, tutorial narration, section
  banners that repeat the function name, pasted docs from this file.

### Rust

- Follow Clippy with `-D warnings` (CI fails otherwise).
- Do not hold `std::sync::MutexGuard` or other guards across `.await`.
- Prefer `Handle::block_on` via existing `VnidropCore::block_on` for concurrent
  API entry; cancel signals active transfers **synchronously** before async work.
- Prefer private modules; export only what UniFFI / other crates need.
- New public traits/types: short docs when the role is non-obvious.

### Kotlin / Compose

For UI and presentation work, **load and follow** the in-repo skill:

```text
.codex/skills/compose-skill/SKILL.md
```

- Open at most one `references/*.md` file when the skill’s Quick Routing requires it.
- Do not invent a second Compose style guide.
- VniDrop uses **MVVM-style** ViewModels (`*State` + `StateFlow` + named methods),
  not a forced MVI `onEvent` base — adapt, do not rewrite.
- Theme via `LocalVniDropColors` / `VniDropThemeTokens` only.
  Brand primary (light): HSL `271, 91%, 65%` ≈ `#A855F7`.
- Strings: CMP `Res.string.*` / composeResources — not Android `R` in `commonMain`.
- Verify multiplatform target support before adding AndroidX/Jetpack deps to
  `commonMain`.

Details: [`shared/AGENTS.md`](shared/AGENTS.md).

---

## Testing instructions

- Prefer deterministic tests (gates, fixed sizes, public API fixtures).
- Avoid long sleeps; if polling is required: short interval + hard timeout +
  clear assertion message.
- Rust integration tests use **public** UniFFI API + `tests/support/` only.
- Failure paths: assert durable status and/or events when applicable, not only
  the error string.
- Do not add tests for pure static constants.
- Do not add negative tests for code you deleted.
- Prefer comparing whole objects when equality is meaningful.

Layout:

| Layer | Location |
|-------|----------|
| Rust unit / private | `crates/vnidrop/src/tests/` |
| Rust integration | `crates/vnidrop/tests/` |
| Shared logic | `shared/src/commonTest/` |
| Shared Compose/JVM | `shared/src/jvmTest/` |

---

## Git and PR instructions

### Branches

Name the change, not a roadmap step:

- Good: `feat/folder-share`, `fix/cancel-export-hang`, `docs/agents-md`
- Bad: `feat/step3-remaining`, `wip`, `temp`

After a PR merges: delete the feature branch **locally and on `origin`**, then
branch from updated `master`.

### Commits

- Style in history: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `docs:`, `ci:`.
- Subject = outcome; body only when needed.
- Signed when repo requires it.

### Pull requests

- Title matches the main change.
- Summary: short bullets of what/why.
- **Test plan must be executable for this PR**:
  - exact commands, and/or
  - 1–2 concrete scenarios that would catch a regression.
- No filler plans (“everything works”, “CI green”) without commands or scenarios.

---

## Security considerations

- Treat tickets and endpoint IDs as sensitive enough not to log full blobs in
  production paths.
- Do not weaken approval/access checks for convenience.
- Do not store secrets in the repo; app data dirs and key files stay out of git.
- Be careful with file publish races (no-clobber rename/link policy exists for a reason).

---

## Common tasks → start files

| Task | Start here |
|------|------------|
| Share / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` |
| Receive / export / sinks | `runtime/receive.rs` |
| Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` |
| Per-receiver send progress | `runtime/provider.rs`, `ui/state/AppUiModels.kt` |
| Approvals | `feature/approvals/`, `approval.rs` |
| QR / NFC invitations | `TransferShareActions.*`, `ReceiveInvitationActions.*` |
| Theme / brand | `ui/theme/VniDropTheme.kt` |
| Compose skill | `.codex/skills/compose-skill/SKILL.md` |

---

## Anti-patterns (never)

- Streaming multi-MB transfer data through Kotlin as the primary design
- Passing Android **directory** FDs into Rust
- Nested exclusive `Runtime::block_on` that deadlocks cancel during receive
- Holding locks across `.await`
- Rebuilding a monolithic `runtime.rs`
- Flaky multi-minute sleeps in tests
- Unsigned commits when signing is required
- Force-push or secret commits without explicit user direction

---

## Implementation checklist

1. Read this file + nearest nested `AGENTS.md`.
2. For Compose/UI: load `compose-skill`.
3. Smallest correct change; tests for bugs/behavior changes.
4. Run relevant build/test commands; fix failures.
5. Sparse comments only where non-obvious.
6. Commit (signed) / push / PR only as the user requests.
7. Summarize what changed and what you ran.
