Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
50dd179
docs: align architecture with current pi rpc
ayagmar Jul 12, 2026
a36f448
fix(bridge): bound and verify network runtime
ayagmar Jul 12, 2026
25a8750
docs(rpc): record lifecycle compatibility blocker
ayagmar Jul 12, 2026
a81ee2e
fix(rpc): conform to pi 0.80.6 lifecycle
ayagmar Jul 12, 2026
4fdc5b7
fix(rpc): parse canonical compaction events
ayagmar Jul 12, 2026
08b209b
docs(sync): record incremental reconstruction blocker
ayagmar Jul 12, 2026
f9dac61
docs(sync): adopt published pi session entry contract
ayagmar Jul 12, 2026
e619832
perf(sync): use entry cursors during reconnect
ayagmar Jul 13, 2026
5cdc0ff
feat(sync): resync on bridge invalidation
ayagmar Jul 13, 2026
11413ff
feat(tree): use pi topology for active sessions
ayagmar Jul 13, 2026
bacd1c5
feat(sync): project incremental session entries
ayagmar Jul 13, 2026
218ccf3
perf(sync): complete incremental session recovery
ayagmar Jul 13, 2026
02e57ed
refactor(chat): split feature-owned responsibilities
ayagmar Jul 13, 2026
d6f19d0
feat(onboarding): guide first bridge connection
ayagmar Jul 13, 2026
3e0f872
chore(android): modernize release toolchain
ayagmar Jul 13, 2026
b679baa
docs: clean roadmap acceptance formatting
ayagmar Jul 13, 2026
a69511b
feat(onboarding): add QR bridge pairing
ayagmar Jul 13, 2026
1ddd95b
fix(chat): stabilize composer and latest scrolling
ayagmar Jul 13, 2026
084233f
fix(ci): isolate runtime-dependent tests
ayagmar Jul 13, 2026
46776be
fix(ci): pin target API lint policy
ayagmar Jul 13, 2026
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
177 changes: 67 additions & 110 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,117 +1,74 @@
name: ci

on:
pull_request:
push:
branches:
- master
pull_request:
push:
branches: [master]

permissions:
contents: read
pull-requests: write
contents: read

jobs:
android:
name: Android
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: gradle

- name: Lint & Test
run: ./gradlew :app:check

- name: Generate Kotlin coverage XML
run: ./gradlew :app:koverXmlReport

- name: Comment coverage on PR
if: github.event_name == 'pull_request'
uses: madrapps/jacoco-report@v1.7.1
with:
paths: ${{ github.workspace }}/app/build/reports/kover/report.xml
token: ${{ secrets.GITHUB_TOKEN }}
title: Kotlin Coverage
update-comment: true

bridge:
name: Bridge
runs-on: ubuntu-latest
defaults:
run:
working-directory: bridge
steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: bridge/pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint, Typecheck & Test
run: pnpm check

- name: Generate bridge coverage
run: pnpm run test:coverage

- name: Build bridge coverage PR comment
if: github.event_name == 'pull_request'
run: |
node <<'NODE'
const fs = require('node:fs');

const summaryPath = 'coverage/coverage-summary.json';
if (!fs.existsSync(summaryPath)) {
throw new Error(`Coverage summary not found at ${summaryPath}`);
}

const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
const total = summary.total ?? {};

const metricRow = (key, label) => {
const metric = total[key] ?? { covered: 0, total: 0, pct: 0 };
const covered = Number(metric.covered ?? 0);
const totalCount = Number(metric.total ?? 0);
const pct = Number(metric.pct ?? 0).toFixed(2);
return `| ${label} | ${covered} / ${totalCount} | ${pct}% |`;
};

const markdown = [
'## Bridge Coverage (Vitest)',
'',
'| Metric | Covered / Total | Coverage |',
'|---|---:|---:|',
metricRow('lines', 'Lines'),
metricRow('functions', 'Functions'),
metricRow('branches', 'Branches'),
metricRow('statements', 'Statements'),
'',
'_Source: bridge/coverage/coverage-summary.json_',
'',
].join('\n');

fs.mkdirSync('coverage', { recursive: true });
fs.writeFileSync('coverage/pr-comment.md', markdown);
NODE

- name: Comment bridge coverage on PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: bridge-coverage
path: bridge/coverage/pr-comment.md
android:
name: Android static, unit, lint, and assembly
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: gradle
- uses: android-actions/setup-android@v3
- name: Install Android SDK 36
run: sdkmanager "platforms;android-36" "build-tools;36.0.0"
- name: Verify Android
run: ./gradlew clean ktlintCheck detekt test :app:lintDebug :app:assembleDebug :app:assembleRelease
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: pi-mobile-debug
path: app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error

android-connected:
name: Android connected tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: gradle
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
profile: pixel_7
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
disable-animations: true
script: ./gradlew :app:connectedDebugAndroidTest

bridge:
name: Bridge checks and production audit
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: bridge
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: bridge/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm run check
- run: pnpm audit --prod --audit-level high
52 changes: 52 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Pi Mobile contributor and agent guide

## Architecture invariants

- Preserve Android → authenticated WebSocket bridge → one isolated `pi --mode rpc` process per cwd.
- Do not migrate the bridge to the Pi SDK or remove cwd/session control locks.
- Pi 0.80.6 is the minimum tested runtime.
- Use documented RPC commands only. Active reads use `get_entries` and `get_tree`; the internal extension exists only for tree navigation because Pi 0.80.6 has no navigation RPC command.
- Unknown session entries trigger one explicit full rebuild. Never guess private session-file behavior.

## Modules

- `app`: Compose UI, screen state, host/token storage, and session controller.
- `core-rpc`: typed Pi commands/events and sanitized compatibility fixtures.
- `core-net`: WebSocket transport, authenticated bridge control, request correlation, reconnect, and entry cursors.
- `core-sessions`: cached session-index models and repository.
- `bridge`: authentication, WebSocket envelopes, locks, process lifecycle, inactive-session indexing, and internal extensions.
- `benchmark`: device-owned macrobenchmark/profile scaffolding.

Keep regression tests beside the owning module. Protocol fixtures belong in `core-rpc/src/test/resources/rpc`; they must be sanitized.

## Required commands

Use JDK 21, Node 22+, pnpm 10, Android SDK 36, and Pi 0.80.6+.

```bash
./gradlew clean ktlintCheck detekt test :app:lintDebug :app:assembleDebug :app:assembleRelease
(cd bridge && pnpm install --frozen-lockfile && pnpm run check && pnpm audit --prod)
```

Compile device tests without launching a device:

```bash
./gradlew :app:compileDebugAndroidTestKotlin
```

Do not run connected/device acceptance unless the operator explicitly owns and requests it.

## Secrets

- Never print, render, log, commit, or copy tokens, authorization headers, `.env` contents, credentials, or private sessions.
- Tokens use Android Keystore AES-GCM and must not enter ordinary preferences or backup.
- Do not add signing credentials. Release assembly uses repository-safe unsigned/default configuration.

## Plan protocol

1. Read `plans/README.md`, the complete active plan, referenced docs, and current Pi docs.
2. Run the plan drift check and mark the row `IN PROGRESS` before editing.
3. Add characterization/regression tests with behavior changes.
4. Run focused checks, full plan gates, and `git diff --check`.
5. Use small Conventional Commits commits. Never push, merge, or rewrite history.
6. Mark a plan done only when non-device gates pass. Device-only validation is recorded as `PENDING — operator-owned` with executable steps and evidence fields.
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The bridge is a small Node.js service that translates WebSocket to pi's stdin/st
- [Custom extensions](docs/extensions.md)
- [Bridge protocol reference](docs/bridge-protocol.md)
- [Testing guide](docs/testing.md)
- [Onboarding and recovery](docs/onboarding.md)

> Note: `docs/ai/` contains planning/progress artifacts used during development. User-facing and maintenance docs live in the top-level `docs/` files above.

Expand All @@ -67,16 +68,19 @@ The bridge is a small Node.js service that translates WebSocket to pi's stdin/st

Install pi if you haven't:
```bash
npm install -g @mariozechner/pi-coding-agent
npm install -g @earendil-works/pi-coding-agent@^0.80.6
pi --version # minimum and tested version: 0.80.6
```

Clone and start the bridge:
```bash
git clone https://github.com/yourusername/pi-mobile.git
git clone https://github.com/ayagmar/pi-mobile.git
cd pi-mobile/bridge
pnpm install
# create .env and set BRIDGE_AUTH_TOKEN (see Configuration section below)
pnpm start
# In another shell, when BRIDGE_ENABLE_HEALTH_ENDPOINT=true:
curl --fail http://127.0.0.1:8787/health
```

The bridge binds to `127.0.0.1:8787` by default. Set `BRIDGE_HOST` to your laptop Tailscale IP to allow phone access (avoid `0.0.0.0` unless you enforce firewall restrictions). It spawns pi processes on demand per working directory.
Expand All @@ -91,15 +95,24 @@ adb install app/build/outputs/apk/debug/app-debug.apk

### 3. Connect

1. Add a host in the app:
1. With the bridge configured, print a pairing code:

```bash
cd bridge
pnpm pair
```

2. In Pi Mobile, open **Hosts**, tap **Scan QR**, scan the terminal code, review the populated connection, and save it. The QR uses the existing `BRIDGE_AUTH_TOKEN`; keep it private. If automatic Tailscale hostname discovery is unavailable, run `pnpm pair -- --host <reachable-hostname>`.

3. Manual entry remains available:
- Host: your laptop's Tailscale MagicDNS hostname (`<device>.<tailnet>.ts.net`)
- Port: `8787` (or whatever the bridge uses)
- Use TLS: off for local/Tailscale bridge unless you've put TLS in front
- Token: set this in `bridge/.env` as `BRIDGE_AUTH_TOKEN`
- Token: set this in `bridge/.env` as `BRIDGE_AUTH_TOKEN`; stored tokens are never displayed

2. The app will fetch your sessions from `~/.pi/agent/sessions/` (or `BRIDGE_SESSION_DIR` if overridden)
4. Use the connection test to distinguish network, authentication, and Pi readiness failures. The app fetches sessions from `~/.pi/agent/sessions/` (or `BRIDGE_SESSION_DIR` if overridden).

3. Tap a session to resume it
5. Tap a session to resume it.

## How It Works

Expand Down Expand Up @@ -191,7 +204,7 @@ benchmark/ - Macrobenchmark / baseline profile scaffolding

### Running Tests

Use JDK 21 for Android and Gradle work in this repo.
Use JDK 21, Android SDK 36, Node 22+, and pnpm 10 for this repo. See [the dependency matrix](docs/dependency-matrix.md).

```bash
# Android tests
Expand All @@ -203,8 +216,8 @@ cd bridge && pnpm test
# Bridge full checks (lint + typecheck + tests)
cd bridge && pnpm run check

# All Android quality checks
./gradlew ktlintCheck detekt test
# Complete non-device Android gate
./gradlew clean ktlintCheck detekt test :app:lintDebug :app:assembleDebug :app:assembleRelease
```

### Logs to Watch
Expand Down Expand Up @@ -238,6 +251,9 @@ BRIDGE_RECONNECT_GRACE_MS=30000 # Keep control locks after disconnect (ms)
BRIDGE_SESSION_DIR=/absolute/path/to/.pi/agent/sessions # Override the session dir used for indexing and spawned pi runtimes
BRIDGE_LOG_LEVEL=info # fatal,error,warn,info,debug,trace,silent
BRIDGE_ENABLE_HEALTH_ENDPOINT=true # set false to disable /health endpoint
BRIDGE_WEBSOCKET_MAX_PAYLOAD_BYTES=16777216 # maximum WebSocket message size (16 MiB)
BRIDGE_IMPORT_MAX_BYTES=10485760 # maximum UTF-8 JSONL import size (10 MiB)
BRIDGE_PI_COMMAND=pi # Pi executable path/name; probed with --version at startup
```

### App Build Variants
Expand Down
Loading
Loading