diff --git a/.claude/rules/48-parallel-execution.md b/.claude/rules/48-parallel-execution.md index 674be655..374bd22e 100644 --- a/.claude/rules/48-parallel-execution.md +++ b/.claude/rules/48-parallel-execution.md @@ -24,6 +24,29 @@ expected wall-clock saved > setup + review + conflict + resource-contention + If the answer isn't clearly positive, don't parallelize. +### The cost terms are WALL-CLOCK, and analytical fan-out prices differently + +Every term above is denominated in **wall-clock time** — and this decision test was written for the +`/dispatch` **worktree-lane** mechanism (multi-item, isolation-first). Applied to a read-only +*analytical* fan-out (a `Workflow`/`Explore` spawn that writes nothing), the terms price much +smaller: no worktree `setup`, no `conflict` (nothing written), no `resource` contention when it +overlaps the serial emulator/simulator run or precedes the exclusive resource being touched. The +residual real costs are the orchestrator's **review/reconcile time** on the findings and the +**failure** risk of the fan-out infrastructure itself — small but not zero, so the honest verdict +for overlapping/pre-write analytical fan-out is **net-positive in expectation** even at N=1, not +free. Under ultracode, the token-cost *objection* (a consideration external to this test) is +removed by the standing directive, which is what makes that expectation clearly positive; the +supporting evidence for the pre-write "how could this false-green?" pass is the #138 WI-6 incident, +where the post-write Codex block caught 3 false-green defects — a pass that *plausibly* +(hindsight-assessed; instrumented going forward, see rule 57) front-loads that class. **None of +this relaxes the hard rules below** (author/auditor separation, one-writer-per-file, sim isolation) +or licenses fan-out that contends the exclusive resource. + +The within-WI analytical fan-out this unlocks — even for a single work item — is specified in +**`.claude/rules/57-ultracode-within-wi-fanout.md`**. This rule (48) remains the *when parallelism +is legal*; rule 55 is the *`/dispatch` multi-item mechanism*; rule 57 is the *within-WI analytical +fan-out under ultracode*. + ## Hard rules (non-negotiable) 1. **Author/auditor separation**: the agent that writes a plan, code, or PR is never the agent that audits it. (cc-suite running Codex as a separate `codex exec` process satisfies this by accident; preserve the boundary explicitly.) diff --git a/.claude/rules/52-test-sim-isolation.md b/.claude/rules/52-test-sim-isolation.md index 9c65700f..ece26d95 100644 --- a/.claude/rules/52-test-sim-isolation.md +++ b/.claude/rules/52-test-sim-isolation.md @@ -74,7 +74,10 @@ The Android lane has the same ghost-class shapes as iOS, with Android tools: flags only a wedged `am instrument` or a detached `adb … logcat` capture. - **Emulator contention = simulator contention.** Driving the SAME emulator (`adb` / `am instrument` / screenshots) while an instrumentation run is in - flight wedges it. Serialize, or use a second emulator (AVD). + flight wedges it. Serialize, or use a second emulator (AVD): boot a 2nd AVD and + route each run to a distinct device via `ANDROID_SERIAL` (validated + exported + by `scripts/run-android-tests.sh`; leased per-serial by `sim-lease.sh acquire + android`) — never two runs on one emulator serial. - **Always run the Android gate through `scripts/run-android-tests.sh`** (the Android `run-tests.sh`): hard wall-clock timeout, exact-pid wait (rule 49), kills the process tree + Gradle daemon on timeout, prints one diff --git a/.claude/rules/55-lane-dispatch.md b/.claude/rules/55-lane-dispatch.md index 7aa68739..bfca2447 100644 --- a/.claude/rules/55-lane-dispatch.md +++ b/.claude/rules/55-lane-dispatch.md @@ -53,9 +53,10 @@ dispatch → sim leases → id-reserve → tracker-write integration tail (merge/tag/teardown), BEFORE Gate-5 — that is what lets the next batch's Gate-3 lanes overlap a Gate-5 pass (rule 48 matrix). - **Sim leases** (via `scripts/sim-lease.sh`): purpose-tagged (`test` ≤2, - `verify` 1); a UDID never serves both purposes (rule 52 mutual exclusion - mechanized). Every lane exit path releases its lease; a batch ends with - zero held. + `verify` 1, `android` ≤ online-emulator count — an Android lane leases an + `emulator-NNNN` serial); a device never serves two purposes (rule 52 + mutual exclusion mechanized). Every lane exit path releases its lease; a + batch ends with zero held. - **`id-reserve`**: all new row IDs are minted via `scripts/reserve-id.sh` BEFORE `tracker-write` is acquired; calling reserve-id while holding tracker-write is forbidden. @@ -237,13 +238,18 @@ the interest of brevity. ``` 2. The rule-48 six-field contract instantiated: objective (the one work unit), inputs (Spec block or bug micro-spec: repro + expected-vs-actual + - regression-test name; exact file list; leased `TEST_UDID`), allowed - writes (the `writes:` prefixes), forbidden (the orchestrator surfaces - above + "no Bash file edits" + no paths outside the write-set), output - format (the HANDOFF), stop condition (ready-for-integration or blocked). -3. The test-gate command shape: `TEST_UDID= scripts/run-tests.sh - ` (Android: `scripts/run-android-tests.sh`) — wrappers - only (rule 52), targeted suites only (never the >20-min full suite). + regression-test name; exact file list; the leased device — `TEST_UDID` + for an iOS/`shared` lane, `ANDROID_SERIAL=` for an + Android lane), allowed writes (the `writes:` prefixes), forbidden (the + orchestrator surfaces above + "no Bash file edits" + no paths outside + the write-set), output format (the HANDOFF), stop condition + (ready-for-integration or blocked). +3. The test-gate command shape: iOS/`shared` → `TEST_UDID= + scripts/run-tests.sh `; Android → + `ANDROID_SERIAL= ANDROID_CMD="…" + scripts/run-android-tests.sh` (the runner validates + re-exports the + serial) — wrappers only (rule 52), targeted suites only (never the + >20-min full suite). 4. The in-lane Gate-4 instruction: `scripts/run-codex.sh` (rule 53) is the PRIMARY rung — **probed 2026-07-09 (twice, incl. with `Skill` in the agent frontmatter): custom agents get NO Skill tool in this harness, so @@ -257,9 +263,15 @@ the interest of brevity. isn't worth it (rule 48 decision test). - **Memory pressure**: width 2 → 1 (`vm_stat` free-pages check before opening the second lane). -- **Android**: `android-app`/`android-spike` items dispatch at width 1 only - until `run-android-tests.sh` gains `ANDROID_SERIAL` routing (named - follow-up; the emulator is a single shared device today — rule 52 Cause D). +- **Android**: `run-android-tests.sh` now honors `ANDROID_SERIAL` (validate + + export + ambiguity guard) and `sim-lease.sh acquire android` leases an online + emulator serial, so an `android-app`/`android-spike` lane CAN be routed to a + specific emulator. Width is still **1 in practice** because only one AVD is + booted by default — the `android` lease capacity = the number of online + emulators. To run width 2, boot a **second AVD** (`avdmanager create avd …`; + `emulator -avd vreader-test-2` → `emulator-5556`), then each lane leases a + distinct serial and passes it as `ANDROID_SERIAL` (rule 52 Cause D — never two + runs on one emulator). - **Width gate weighs LANE TYPES, not lane count** (M-SHAKEDOWN finding): the 4GB `vm_stat` headroom check applies per SWIFT lane (xcodebuild + booted sim ≈ real load); a bash-only lane (hooks/scripts work) adds near-zero diff --git a/.claude/rules/57-ultracode-within-wi-fanout.md b/.claude/rules/57-ultracode-within-wi-fanout.md new file mode 100644 index 00000000..8d493f21 --- /dev/null +++ b/.claude/rules/57-ultracode-within-wi-fanout.md @@ -0,0 +1,111 @@ +# 57 — Ultracode Within-WI Fan-out + +Binding when **ultracode is ON** (a system-reminder confirms it). This rule closes the gap rule 48 +and rule 55 leave open: they govern the `/dispatch` **worktree-lane** mechanism (multi-item, +isolation-first, wall-clock-denominated), and they correctly degrade a single work item (N=1) to the +inline flow. Neither says anything about fanning out the **analytical** work *within* one WI — the +reads, the edge-case brainstorm, the audit. Ultracode's standing directive ("use the Workflow tool +on every substantive task; token cost is not a constraint") already makes that within-WI fan-out the +default; this rule resolves the rulebook ambiguity (rule 48's decision test read as if it priced +analytical spawns like worktree lanes) and specifies the mechanism. + +Origin: feature #138 WI-6 (2026-07-23), honestly decomposed (the first draft of this rule +over-claimed; a 5-angle adversarial grill of the rationale corrected it). A single acceptance-test +WI ran almost entirely inline. The post-write Gate-4 audits then surfaced three false-green defects, +and the three had **different** root causes: one had been *surfaced by the deep 5-lens pass and +mis-triaged in the solo reconcile step* (stated targets logged but only looser ceilings asserted); +one was a genuine lens-scope miss (a synthetic-fallback that hollowed the real-book acceptance); one +was implementation-emergent (a short-circuit only visible in the written code). The durable lessons: +(a) run the deep AND broad audits in parallel and let the block gate (the broad pass caught all +three); (b) the deep pass's triage/reconcile step needs hardening, not just more layers — see +"Follow-ups" below; (c) a **pre-write** "how could this false-green?" pass *plausibly* front-loads +the requirement-level defect classes (the first two), though this is a **hypothesis, hindsight- +assessed** — it is justified below on cost asymmetry, not on a claimed counterfactual save. + +## The principle + +**Fan out every analytical step that does NOT hold the exclusive resource.** The binding serial +resource (the single emulator/simulator — rule 52; one Codex/test session — rule 48) caps +*verification* parallelism, not *analysis* parallelism. Analytical fan-out either (a) runs +*concurrently* with the serial critical section (the emulator run) — genuinely ~zero added +wall-clock — or (b) *precedes* the exclusive resource being touched: **serial, but small relative to +the audit-block cycle it hedges** (a Gate-4 block costs a fix + connected re-run + another audit +round). With ultracode removing the token-cost objection, the residual costs are the ones to manage +honestly: the orchestrator's reconcile tax on fan-out findings, context dilution from each +synthesis, and occasional fan-out infrastructure failure (a workflow died on a schema retry-cap in +the origin session). So the default is **net-positive in expectation, not free**: on infrastructure +failure, retry once then degrade to inline analysis with a note; keep syntheses summary-sized; the +reconcile stays a deliberate solo step (see below). + +## The three sanctioned within-WI fan-outs + +Run these via the `Workflow` tool (or parallel read-only `Agent`/`Explore` spawns). They are the +default for any substantive inline WI under ultracode — a bug fix (`/fix`, `/fix-issue`) or a +feature WI (`/feature-workflow`). + +1. **Parallel context sweep — BEFORE the write.** Instead of sequential Reads to gather the files, + APIs, prior art, and constraints a WI needs, fan out read-only `Explore` agents to gather them at + once and return a structured map. Replaces serial reads; strictly faster. (Per rule 48's + context-absorption warning, treat the map as orientation — reconfirm load-bearing details + first-hand before editing.) + +2. **Pre-write adversarial brainstorm — BEFORE the write.** Before authoring the RED test / the fix, + fan out adversarial lenses over the *intended* change: "how could this test pass while proving + nothing?", "what edge cases (empty/nil/max/Unicode-CJK/RTL/concurrent/failure-injection) must it + cover?", "what invariant between subsystems could this break?" Fold the findings into the + test/impl you then write **once, solo**. Justification: cost asymmetry — the pass is minutes and + sits before any exclusive-resource work; the Gate-4 block cycle it hedges is tens of minutes. + Whether it actually front-loads block-class defects is **instrumented, not assumed**: when you run + it, record in the WI's notes whether its findings intersect the subsequent Gate-4 findings — + sustained zero intersection over ~5 WIs retires this fan-out to optional. + +3. **Deep + broad audit — AFTER the write, before merge, in PARALLEL.** Run the per-claim + adversarial fan-out AND the broad Codex audit (`scripts/run-codex.sh`) concurrently. The evidence + (n=2: #131 plan gate, #138 WI-6) is that the broad pass catches classes the deep pass misses — + whether that is true complementarity or a deep-triage defect, the remedy is the same: run both, + and when they disagree, **trust the block on cost-asymmetry grounds** (a false block costs one + bounded re-check; a false ship ships a hollow gate). Reconcile deliberately: a deep-pass finding + you decide to accept/deflect rather than fix is exactly the shape that produced a Codex block in + the origin session — record the reason in the audit artifact so the choice is reviewable. + +## What to keep SOLO (do NOT fan out) + +- **The write itself.** One coherent file/change has ONE author (rule 48 hard rule 3 — + one-writer-per-file). Fanning out the authoring produces merge conflict, not speed. +- **The reconcile judgment.** Deciding which audit findings to fix vs accept is one judgment call, + made by the main agent that owns the change — but it is the step that failed in the origin + session, so it carries its own discipline: every accepted/deflected Medium-or-above finding gets a + one-line written rationale in the audit artifact (reviewable, not silent). +- **Anything that would CONTEND the exclusive resource.** Never fan out "run the connected test" onto + the same emulator/simulator (rule 52 — contention wedges the run into a ghost). Verification stays + serial on one device; only a second leased device unlocks parallel runs (rule 55 Android tier + + `scripts/sim-lease.sh`). + +## Scope boundaries + +- **Binds the INLINE flows and the ORCHESTRATOR, not restricted-tool lanes.** A `/dispatch` + `implementer` lane is a subagent with no `Agent`/Workflow tool and cannot fan out (Workflow nesting + is one level). So this rule binds the inline authoring flows (`/feature-workflow`, `/fix`, + `/fix-issue`) and the dispatch *orchestrator's* own analytical steps (e.g., the deep+broad plan + audit before spawning lanes) — never the lane interior. The lane's analytical work is front-loaded + by the orchestrator before dispatch. +- **Ultracode-gated.** When ultracode is OFF, the rule-48 decision test governs (the token-cost term + is back in play, so a marginal fan-out may not clear the bar). When ON, treat these three fan-outs + as the default and note any WI where you *skipped* one and why. + +## Follow-ups (named, so they don't silently drop) + +- **Mechanize the logged-vs-asserted check**: a test that logs `target_met()` while asserting a + looser bound is detectable mechanically — worth a lint/hook so the origin session's High-2 class + can't recur on discipline alone. +- **Deep-pass falsification discipline**: each adversarial lens states what evidence WOULD falsify + the change, so a "sound" verdict is a checked claim, not a skim. +- **Instrumentation review**: after ~5 ultracode WIs, review the fan-out-#2 hit-rate notes and keep, + tune, or retire it. + +## What this rule does NOT change + +TDD order (rule 10), the six gates (rule 47), sim/emulator isolation (rule 52), codex isolation +(rule 53), background-shell discipline (rule 49), the `/dispatch` multi-item mechanism (rule 55). +Rule 48 remains the *when parallelism is legal*; rule 55 is the *`/dispatch` multi-item how*; this +rule is the *within-WI analytical how under ultracode*. diff --git a/.claude/skills/dispatch/SKILL.md b/.claude/skills/dispatch/SKILL.md index 7a846263..a36a39b8 100644 --- a/.claude/skills/dispatch/SKILL.md +++ b/.claude/skills/dispatch/SKILL.md @@ -27,8 +27,11 @@ docs sync) and all policy decisions. Lanes never decide anything. `dispatch` lock**, released when the inline flow finishes. 4. **Width**: default 1 lane; open a second ONLY with memory headroom — check `vm_stat` free+inactive pages ≳ 4GB equivalent. Hard cap 2. - `android-app`/`android-spike` items always width 1 (no ANDROID_SERIAL - routing yet). + `android-app`/`android-spike` items are width 1 **in practice** because only + one AVD is booted by default (the `android` lease capacity = online-emulator + count). Routing exists — `run-android-tests.sh` honors `ANDROID_SERIAL` and + `sim-lease.sh acquire android` leases a distinct serial — so width 2 is + possible once a **second AVD** is booted (rule 55 Android tier / rule 52). ## Step 1 — intake + the ephemeral ledger @@ -37,7 +40,7 @@ tracker rows, GH gate-timeline comments, `git worktree list`, `scripts/sim-lease.sh status`, `scripts/agent-lock.sh status`): ``` -| item | branch | write-set | UDID | status | verdict/version | +| item | branch | write-set | device (UDID or emulator serial) | status | verdict/version | ``` Statuses: dispatched → returned → integrating → merged | requeued | escalated. @@ -68,8 +71,11 @@ For each candidate item: Per item, in order: main tree clean (pbxproj signing carve-out excepted) → `scripts/worktree-setup.sh ` (records the ABSOLUTE path) → -GH issue exists with `GH: #N` stamped → `scripts/sim-lease.sh acquire test` -(record UDID in the ledger) → brief generated from the template below. +GH issue exists with `GH: #N` stamped → **lease the device by platform** +(`code_paths_platform`): an iOS/`shared` item → `scripts/sim-lease.sh acquire +test` (record the UDID); an `android-app`/`android-spike` item → +`scripts/sim-lease.sh acquire android` (record the `emulator-NNNN` SERIAL) → +brief generated from the template below. ## Step 3 — the lane brief (generated, NEVER hand-written) @@ -99,15 +105,19 @@ the interest of brevity. ``` 2. The six-field contract instantiated (objective = the one item; inputs = - Spec block or micro-spec + exact file list + leased `TEST_UDID`; allowed - writes = the `writes:` prefixes; forbidden = rule 55's shared surfaces + - "no Bash file edits" + nothing outside the write-set; output = the - rule-55 HANDOFF JSON; stop = ready-for-integration or blocked). + Spec block or micro-spec + exact file list + the leased device: `TEST_UDID` + for iOS/`shared`, or `ANDROID_SERIAL=` for an Android item; + allowed writes = the `writes:` prefixes; forbidden = rule 55's shared + surfaces + "no Bash file edits" + nothing outside the write-set; output = + the rule-55 HANDOFF JSON; stop = ready-for-integration or blocked). 3. **Skill-override clause**: the lane contract OVERRIDES any standing skill phases — no PR creation, no tracker edits, no close-gate, no version bump, no `git tag`; STOP at ready-for-integration + HANDOFF. -4. Test gate shape: `TEST_UDID= scripts/run-tests.sh ` - (Android: `scripts/run-android-tests.sh`) — wrappers only, targeted only. +4. Test gate shape: iOS/`shared` → `TEST_UDID= scripts/run-tests.sh + `; Android → `ANDROID_SERIAL= ANDROID_CMD="…" + scripts/run-android-tests.sh` (the runner validates + re-exports the serial, + so the connected task targets the leased emulator). Wrappers only, targeted + only — never a bare `xcodebuild`/`gradle`. 5. Gate-4 ladder (probed 2026-07-09): `scripts/run-codex.sh` (rule 53) is the lanes' PRIMARY audit rung — custom agents have no Skill tool (probe: "Skill exists but is not enabled in this context") — artifact @@ -133,7 +143,8 @@ contamination check (both probes — see "Contamination checks" below). **One cleanup routine for EVERY non-ready outcome** (invalid HANDOFF, missing HANDOFF, `outcome: failed`, `outcome: blocked`): release the lane's -lease (`scripts/sim-lease.sh release `), tear down the worktree +lease (`scripts/sim-lease.sh release ` — the ledger's device +column, whichever kind the lane leased), tear down the worktree (`scripts/worktree-teardown.sh ` — or preserve it with an explicit "preserved for investigation" ledger note when the failure needs forensics), update the ledger row, THEN requeue-once (fresh lane) or escalate per cause. @@ -149,9 +160,10 @@ a. `scripts/check-write-set.sh ` (+ b. Rebase the branch on `origin/main` IN the worktree. Conflict ⇒ `git rebase --abort`, requeue for serialized redo (fresh lane brief: "rebase onto current main and resolve"), no version burned, no PR. -c. Independent re-run of the lane's declared targeted suite - (`TEST_UDID= scripts/run-tests.sh `) — never trust the - HANDOFF's RESULT line. +c. Independent re-run of the lane's declared targeted suite — never trust the + HANDOFF's RESULT line. iOS/`shared`: `TEST_UDID= + scripts/run-tests.sh `; Android: `ANDROID_SERIAL= + ANDROID_CMD="…" scripts/run-android-tests.sh`. d. Apply `tracker_edit` + `docs_sync` yourself via Edit **on files under `/`** (e.g. `/docs/bugs.md`), committed on the lane branch with `git -C commit` — the tracker/docs deltas ride @@ -183,7 +195,9 @@ g. **Tag per PR, immediately**: on the MAIN checkout `git pull --rebase` single batch-end tag pass (rule 40: every PR's bump gets its tag on its own merge commit; a batch-end pass can miss earlier PRs and corrupts version-at-slot's latest-tag input for the NEXT slot). -h. Lane cleanup: `scripts/sim-lease.sh release ` + +h. Lane cleanup: `scripts/sim-lease.sh release ` (the + ledger's device column — an Android lane's `emulator-NNNN` serial lease + releases through the same command) + `scripts/worktree-teardown.sh --delete-branch` (post-merge). Batch ends with `sim-lease.sh status` clean. @@ -220,6 +234,15 @@ rows (grep, never a full Read of the 499KB tracker), one-line gh results, `RUN-* RESULT:` lines, `--name-only` lists, `/.reports/` paths (pass them on, don't open them). +## Within-WI fan-out is the ORCHESTRATOR's, never the lane's (rule 57) + +A lane is a restricted-tool `implementer` subagent — no `Agent`/Workflow tool, and Workflow nesting +is one level — so a lane CANNOT fan out. Under ultracode, the analytical fan-out that rule 57 +mandates (parallel context sweep, pre-write adversarial brainstorm, deep+broad audit) is front-loaded +by the **orchestrator** *before* dispatch (it already owns Gate-2's deep+broad plan audit) and folded +into the generated lane brief's Spec block. The lane then authors solo against a pre-hardened spec. +Never expect a lane to run a Workflow. + ## Escalation Audit round-3 failure, needs-design (rule 51 — file the `needs-design` diff --git a/.claude/skills/feature-workflow/SKILL.md b/.claude/skills/feature-workflow/SKILL.md index 07027542..b312e64d 100644 --- a/.claude/skills/feature-workflow/SKILL.md +++ b/.claude/skills/feature-workflow/SKILL.md @@ -53,7 +53,8 @@ feature's changed files with `code_paths_platform` and **substitute the lane in every gate** (the gates are written iOS-first): - **Gate 3 test gate**: iOS → `scripts/run-tests.sh`; Android → - `scripts/run-android-tests.sh` (never bare `./gradlew` — rule 52 Cause D). + `scripts/run-android-tests.sh` (never bare `./gradlew` — rule 52 Cause D; + in a lane, with the leased `ANDROID_SERIAL=` — see Gate 3c). - **Gate 5 verify**: iOS → iPhone 17 Pro Sim + `vreader-debug://`; Android → `scripts/run-android-verify.sh` (emulator; rule 47 Android tier). Evidence `device_or_simulator` = the AVD. @@ -294,6 +295,16 @@ For each Work Item, run the per-WI inner loop: ## 3b. RED → GREEN → REFACTOR +**Ultracode within-WI fan-out (rule 57 — when ultracode is ON, do this first).** Before writing the +RED test, run the two pre-write fan-outs that cost ~zero wall-clock and front-load defect discovery: +(1) a **parallel context sweep** (`Explore` agents) to gather the files/APIs/prior-art the WI +touches; (2) a **pre-write adversarial brainstorm** (a `Workflow` fan-out) over the *intended* test — +"how could this pass while proving nothing?" + the edge-case matrix (empty/nil/max/Unicode-CJK/RTL/ +concurrent/failure-injection). Fold the findings into the RED test you then write **once, solo**. +This is the same defect class a Gate-4 audit block would otherwise catch *after* the write (rule 57 +origin: feature #138 WI-6). Not applicable inside a `/dispatch` lane (restricted tools — the +orchestrator front-loads it before dispatch). + Per `.claude/rules/10-tdd.md`: 1. **RED** — write a failing test that captures the WI's behavior. - SwiftData boundary → persistence test with in-memory container @@ -313,9 +324,11 @@ dependency counts only when the dependency WI's branch has MERGED**) may run as dispatch lanes via the `dispatch` skill. Inside a lane, this skill's standing steps are OVERRIDDEN: the lane runs ONE WI's 3a–3d (RED → GREEN → REFACTOR → in-lane Gate-4 via `scripts/run-codex.sh`) in its worktree on its -leased `TEST_UDID`, then STOPS with the rule-55 HANDOFF. No PR, no merge, no -tracker edits, no docs sync, no version bump, no tags — the orchestrator -owns the integration tail and Gate 5 (verify-sim lease). +leased device — `TEST_UDID=` for an iOS/`shared` WI, or +`ANDROID_SERIAL=` for an Android WI (the orchestrator leases it +via `sim-lease.sh acquire android` and passes it in the brief) — then STOPS +with the rule-55 HANDOFF. No PR, no merge, no tracker edits, no docs sync, no +version bump, no tags — the orchestrator owns the integration tail and Gate 5. ## 3c. Test gate @@ -327,15 +340,18 @@ ALWAYS through the watchdog wrapper — never a raw Xcode test invocation # one wrapper call per suite: scripts/run-tests.sh vreaderTests/ -# Android WIs route through the Android wrapper instead (rule 52 Cause D): -# scripts/run-android-tests.sh +# Android WIs route through the Android wrapper instead (rule 52 Cause D), +# passing the leased emulator serial so the connected task targets it: +# ANDROID_SERIAL= ANDROID_CMD="cd android && ./gradlew :app:connectedDebugAndroidTest -P…=" \ +# scripts/run-android-tests.sh ``` Pass → continue. Fail → fix and retry. 3 failures → stop, report. A `RUN-TESTS RESULT: TIMEOUT` is sim contention, not flakiness (rule 52 hard rule 3). Full-suite sweeps (`TIMEOUT_SECS=2400 scripts/run-tests.sh vreaderTests`) are periodic/CI, never a per-WI gate. In a parallel lane, pass -the leased sim via `TEST_UDID=`. +the leased device via `TEST_UDID=` (iOS) or `ANDROID_SERIAL=` +(Android — `run-android-tests.sh` validates + re-exports it). > **Note**: `xcodebuild` CLI builds to a different DerivedData than > Xcode's Run button. **Never use `simctl uninstall`** — wipes user diff --git a/project.yml b/project.yml index bcb778a1..d0b14cce 100644 --- a/project.yml +++ b/project.yml @@ -212,8 +212,8 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.vreader.app - CURRENT_PROJECT_VERSION: 1049 - MARKETING_VERSION: 3.67.7 + CURRENT_PROJECT_VERSION: 1050 + MARKETING_VERSION: 3.67.8 # Feature #42 Phase-2 WI-1b: compile + link the vendored libmobi C. # USE_LIBXML2 turns on libmobi's OPF/XML writer (KF8→EPUB needs it); # the iOS SDK ships libxml2 headers at $(SDKROOT)/usr/include/libxml2 diff --git a/scripts/__tests__/run-android-tests.test.sh b/scripts/__tests__/run-android-tests.test.sh index 8877955a..e3950cd4 100755 --- a/scripts/__tests__/run-android-tests.test.sh +++ b/scripts/__tests__/run-android-tests.test.sh @@ -47,5 +47,45 @@ else echo "skip — NO_EMULATOR case (an emulator is online)" fi +echo "== ANDROID_SERIAL routing (mocked device source; feature #138 follow-up) ==" +ONE='printf "List of devices attached\nemulator-5554\tdevice\n"' +TWO='printf "List of devices attached\nemulator-5554\tdevice\nemulator-5556\tdevice\n"' +NONE='printf "List of devices attached\n"' + +# A requested serial that is NOT online → NO_EMULATOR (validation), even for a JVM ANDROID_CMD. +assert_result "RUN-ANDROID-TESTS RESULT: NO_EMULATOR" 2 "ANDROID_SERIAL not online → NO_EMULATOR" \ + -- ANDROID_CMD="true" ANDROID_SERIAL="emulator-9999" ANDROID_DEVICES_CMD="$NONE" + +# A requested serial that IS online → routes + runs (the ANDROID_CMD asserts the serial is exported). +assert_result "RUN-ANDROID-TESTS RESULT: SUCCEEDED" 0 "ANDROID_SERIAL online → exported + run" \ + -- ANDROID_CMD='[ "$ANDROID_SERIAL" = emulator-5554 ]' ANDROID_SERIAL="emulator-5554" ANDROID_DEVICES_CMD="$ONE" + +# >1 emulator online + no serial + emulator-driving DEFAULT cmd → AMBIGUOUS_SERIAL (hard fail). +assert_result "RUN-ANDROID-TESTS RESULT: AMBIGUOUS_SERIAL" 2 "2 emulators + no serial + default → AMBIGUOUS" \ + -- ANDROID_DEVICES_CMD="$TWO" + +# >1 emulator online + no serial + caller-owned ANDROID_CMD (may be JVM) → WARN only, still runs. +assert_result "RUN-ANDROID-TESTS RESULT: SUCCEEDED" 0 "2 emulators + no serial + ANDROID_CMD → warn+run" \ + -- ANDROID_CMD="true" ANDROID_DEVICES_CMD="$TWO" + +# 1 emulator + 1 PHYSICAL device + no serial + default cmd → AMBIGUOUS (bare adb is ambiguous with +# ANY 2 devices, not just 2 emulators — Gate-4 High-2). +EMU_PHYS='printf "List of devices attached\nemulator-5554\tdevice\nR58M12345678\tdevice\n"' +assert_result "RUN-ANDROID-TESTS RESULT: AMBIGUOUS_SERIAL" 2 "emulator+physical + no serial + default → AMBIGUOUS" \ + -- ANDROID_DEVICES_CMD="$EMU_PHYS" + +# A PHYSICAL device serial is a valid ANDROID_SERIAL target (general adb routing, not emulator-only). +assert_result "RUN-ANDROID-TESTS RESULT: SUCCEEDED" 0 "physical ANDROID_SERIAL online → routes + runs" \ + -- ANDROID_CMD='[ "$ANDROID_SERIAL" = R58M12345678 ]' ANDROID_SERIAL="R58M12345678" ANDROID_DEVICES_CMD="$EMU_PHYS" + +# Single emulator online + no serial + caller ANDROID_CMD → NOT ambiguous, no warning, runs (backward +# compat: the common one-AVD case is unchanged). Uses ANDROID_CMD=true so nothing drives the emulator. +out="$(env ANDROID_DEVICES_CMD="$ONE" ANDROID_CMD="true" bash "$RUN" 2>&1)"; rc=$? +if grep -q "RUN-ANDROID-TESTS RESULT: SUCCEEDED" <<<"$out" && ! grep -q "WARNING:" <<<"$out" && [ "$rc" -eq 0 ]; then + echo "ok — 1 emulator + no serial → not ambiguous, no warning (backward compat)" +else + echo "FAIL — 1 emulator + no serial should run cleanly (rc=$rc): $(grep -E 'RESULT|WARNING' <<<"$out")"; fails=$((fails+1)) +fi + echo if [ "$fails" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "$fails FAILURE(S)"; exit 1; fi diff --git a/scripts/__tests__/sim-lease.test.sh b/scripts/__tests__/sim-lease.test.sh index ddc7df94..e58525ca 100644 --- a/scripts/__tests__/sim-lease.test.sh +++ b/scripts/__tests__/sim-lease.test.sh @@ -101,5 +101,45 @@ EMPTY="$TMP/empty.sh"; printf '#!/usr/bin/env bash\necho "{\\"devices\\":{}}"\n' OUT="$(env LOCK_OWNER_PID=$$ SIM_LEASE_LOCK_ROOT="$TMP/locks2" SIM_LEASE_STATE_DIR="$TMP/state2" SIM_LEASE_DISCOVER_CMD="$EMPTY" SIM_LEASE_BOOT_CMD="$BOOT" bash "$CLI" acquire test 2>&1)"; RC=$? if [ "$RC" -eq 1 ]; then ok "no available sims → exit 1"; else fail "empty pool (rc=$RC): $OUT"; fi +echo "== sim-lease.sh android emulator leases (feature #138 follow-up) ==" +# Android emulator discovery injected via ANDROID_DEVICES_CMD (no real adb). Two online emulators. +ADEV_TWO="$TMP/adev2.sh"; printf '#!/usr/bin/env bash\nprintf "List of devices attached\\nemulator-5554\\tdevice\\nemulator-5556\\tdevice\\n"\n' > "$ADEV_TWO"; chmod +x "$ADEV_TWO" +ADEV_NONE="$TMP/adev0.sh"; printf '#!/usr/bin/env bash\nprintf "List of devices attached\\n"\n' > "$ADEV_NONE"; chmod +x "$ADEV_NONE" +arun() { env LOCK_OWNER_PID=$$ SIM_LEASE_LOCK_ROOT="$TMP/alocks" SIM_LEASE_STATE_DIR="$TMP/astate" ANDROID_DEVICES_CMD="$1" bash "$CLI" "${@:2}" 2>&1; } + +# 8. two android leases resolve to DISTINCT online serials +A1="$(arun "$ADEV_TWO" acquire android | grep -oE 'emulator-[0-9]+' | head -1)" +A2="$(arun "$ADEV_TWO" acquire android | grep -oE 'emulator-[0-9]+' | head -1)" +if [ -n "$A1" ] && [ -n "$A2" ] && [ "$A1" != "$A2" ]; then ok "two android leases → distinct serials ($A1,$A2)"; else fail "android leases A1=$A1 A2=$A2"; fi + +# 9. capacity = online emulator count: a third acquire with both leased → BUSY exit 2 +OUT="$(arun "$ADEV_TWO" acquire android)"; RC=$? +if [ "$RC" -eq 2 ]; then ok "android capacity = online count (2) → third BUSY"; else fail "android cap (rc=$RC): $OUT"; fi + +# 10. no online emulator → ERROR exit 1 (no boot-on-demand) +OUT="$(arun "$ADEV_NONE" acquire android)"; RC=$? +if [ "$RC" -eq 1 ]; then ok "no online emulator → ERROR exit 1"; else fail "android none (rc=$RC): $OUT"; fi + +# 11. release both serials → zero held +arun "$ADEV_TWO" release "$A1" >/dev/null; arun "$ADEV_TWO" release "$A2" >/dev/null +OUT="$(arun "$ADEV_TWO" status)" +if ! grep -q "held" <<<"$OUT"; then ok "android leases released → zero held"; else fail "android leases left: $OUT"; fi + +# 12. a FAILING device command (adb missing / errors) → graceful ERROR + RESULT line, NOT a silent +# set -e exit (Gate-4 Medium: `serials="$(list_emulators)"` must not crash the script). +OUT="$(arun "false" acquire android)"; RC=$? +if [ "$RC" -eq 1 ] && grep -q "SIM-LEASE RESULT: ERROR" <<<"$OUT"; then ok "failing device cmd → ERROR + RESULT line (no silent set -e exit)"; else fail "failing device cmd (rc=$RC): $OUT"; fi + +# 13. CONCURRENCY: 4 concurrent `acquire android` on 2 online emulators → exactly 2 winners (the same +# SELECT_LOCK serializes discovery+capacity+lease for android as for test — mirrors race test 6b). +AWINS="$TMP/arace-wins"; : > "$AWINS" +for i in 1 2 3 4; do + ( env LOCK_OWNER_PID=$$ SIM_LEASE_LOCK_ROOT="$TMP/alocks-race" SIM_LEASE_STATE_DIR="$TMP/astate-race" \ + ANDROID_DEVICES_CMD="$ADEV_TWO" bash "$CLI" acquire android >> "$AWINS" 2>/dev/null ) & +done +wait +ARACE_OK=$(grep -c "ACQUIRED android" "$AWINS" || true) +if [ "$ARACE_OK" -eq 2 ]; then ok "4-way android race → exactly 2 leases (capacity = online count)"; else fail "android race: $ARACE_OK winners"; fi + echo if [ "$fails" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "$fails FAILURE(S)"; exit 1; fi diff --git a/scripts/run-android-tests.sh b/scripts/run-android-tests.sh index 4bd23e07..eeb253c8 100755 --- a/scripts/run-android-tests.sh +++ b/scripts/run-android-tests.sh @@ -10,7 +10,13 @@ # owner, one completion channel; the watchdog is cancelled the instant the # run finishes, so it never outlives this invocation. # - Emits ONE unambiguous final line: -# "RUN-ANDROID-TESTS RESULT: SUCCEEDED|FAILED|TIMEOUT|NO_EMULATOR". +# "RUN-ANDROID-TESTS RESULT: SUCCEEDED|FAILED|TIMEOUT|NO_EMULATOR|AMBIGUOUS_SERIAL". +# - Targets a SPECIFIC emulator via ANDROID_SERIAL (validated + exported), so +# two AVDs never race on an ambiguous `adb` (feature #138 follow-up — rule 52 +# Cause D / rule 55 Android tier). Create a 2nd AVD for parallel runs with: +# avdmanager create avd -n vreader-test-2 -k "system-images;android-35;google_apis;arm64-v8a" +# emulator -avd vreader-test-2 -no-snapshot-save & # boots as emulator-5556 +# then pass ANDROID_SERIAL=emulator-5556 to route a run to it. # # Target: until feature #106's `android/` app shell exists there is NO root # `./gradlew` — the only real Android target is the Spike-B harness, so this @@ -22,6 +28,8 @@ # ANDROID_CMD="./gradlew :app:testDebugUnitTest" scripts/run-android-tests.sh # post-#106 # ANDROID_CMD="true" scripts/run-android-tests.sh # contract self-test # TIMEOUT_SECS=600 scripts/run-android-tests.sh +# ANDROID_SERIAL=emulator-5556 ANDROID_CMD="cd android && ./gradlew :app:connectedDebugAndroidTest" \ +# scripts/run-android-tests.sh # route to a SPECIFIC emulator # # IMPORTANT (rule 52): do NOT drive the SAME emulator (adb/am instrument/ # screenshots) while this runs — contention is what wedges Gradle/instrumentation. @@ -42,14 +50,57 @@ else REQUIRE_EMULATOR=1 fi +# The `adb devices` source, overridable for tests (ANDROID_DEVICES_CMD — the +# emulator analog of sim-lease's SIM_LEASE_DISCOVER_CMD). ALWAYS exits 0 (prints +# nothing when adb is absent) so a missing-adb pipe never trips a caller. +android_devices() { + if [ -n "${ANDROID_DEVICES_CMD:-}" ]; then eval "$ANDROID_DEVICES_CMD" || true + elif command -v adb >/dev/null 2>&1; then adb devices 2>/dev/null || true + fi + return 0 +} # Real "booted emulator" detection (Codex Gate-4): `adb get-state` is not it — -# it passes for a physical device and errors with multiple devices. Require an -# `emulator-NNNN` serial in `device` state. +# it passes for a physical device and errors with multiple devices. With $1 = a +# specific serial, check ONLY that serial is online in `device` state (a physical +# device serial is a valid ANDROID_SERIAL target too); without $1, check ANY +# `emulator-NNNN` is online (the spike genuinely needs an emulator). emulator_online() { - command -v adb >/dev/null 2>&1 || return 1 - adb devices 2>/dev/null | awk '/^emulator-[0-9]+[[:space:]]+device$/ {f=1} END {exit !f}' + if [ -n "${1:-}" ]; then + android_devices | awk -v s="$1" '$1==s && $2=="device" {f=1} END {exit !f}' + else + android_devices | awk '/^emulator-[0-9]+[[:space:]]+device$/ {f=1} END {exit !f}' + fi } -if [ "$REQUIRE_EMULATOR" -eq 1 ] && ! emulator_online; then +# ALL online devices in `device` state (emulator + physical) — bare `adb`/Gradle +# is ambiguous ("more than one device") with >1 of EITHER kind, so the ambiguity +# guard counts both, not just emulators (Gate-4: a physical phone counts too). +online_device_count() { android_devices | awk '$2=="device" {n++} END {print n+0}'; } + +# ANDROID_SERIAL routing (feature #138 follow-up — parallel-emulator support; +# rule 52 Cause D / rule 55 Android tier). One AVD is the default today, but a +# run MUST be able to TARGET a specific device so two AVDs (once created) don't +# race on an ambiguous `adb`. adb + Gradle's connected task both honor the +# exported ANDROID_SERIAL env var natively, so routing = validate + export. +ANDROID_SERIAL="${ANDROID_SERIAL:-}" +if [ -n "$ANDROID_SERIAL" ]; then + if ! emulator_online "$ANDROID_SERIAL"; then + echo "RUN-ANDROID-TESTS RESULT: NO_EMULATOR (ANDROID_SERIAL=$ANDROID_SERIAL not online in 'device' state)" + exit 2 + fi + export ANDROID_SERIAL # every adb/Gradle connected command now targets THIS device +elif [ "$(online_device_count)" -gt 1 ]; then + # >1 device online + no serial → bare adb is ambiguous ("more than one device"). + # Hard-fail the device-driving default/spike path; only WARN for a caller-owned + # ANDROID_CMD (which may be a device-less JVM task like testDebugUnitTest — + # forcing a serial there would be wrong). + if [ "$REQUIRE_EMULATOR" -eq 1 ]; then + echo "RUN-ANDROID-TESTS RESULT: AMBIGUOUS_SERIAL ($(online_device_count) devices online — set ANDROID_SERIAL= to pick one)" + exit 2 + fi + echo "[run-android-tests] WARNING: $(online_device_count) devices online + no ANDROID_SERIAL — an adb/connected task may fail ambiguously; set ANDROID_SERIAL= to target one." +fi + +if [ "$REQUIRE_EMULATOR" -eq 1 ] && ! emulator_online "$ANDROID_SERIAL"; then echo "RUN-ANDROID-TESTS RESULT: NO_EMULATOR (no emulator-NNNN device online — boot an AVD or pass ANDROID_CMD)" exit 2 fi diff --git a/scripts/sim-lease.sh b/scripts/sim-lease.sh index 74f59d68..46822b5b 100755 --- a/scripts/sim-lease.sh +++ b/scripts/sim-lease.sh @@ -9,16 +9,25 @@ # exit path releases its test lease; the verify flow releases before ENDED; # `status` must show zero held at batch end. # +# A third purpose `android` leases an ONLINE Android emulator SERIAL (feature +# #138 follow-up — rule 55 Android tier): capacity = number of online emulators +# (1 today, N once more AVDs boot), exclusive per-serial, no boot-on-demand +# (require the emulator already online). The leased serial is what a lane passes +# as ANDROID_SERIAL to scripts/run-android-tests.sh. +# # Usage: # sim-lease.sh acquire {test|verify} -> 0 "SIM-LEASE RESULT: ACQUIRED " # 2 BUSY (capacity) | 1 ERROR (no sims) -# sim-lease.sh release -> 0 RELEASED | 3 NOT-OWNER +# sim-lease.sh acquire android -> 0 "SIM-LEASE RESULT: ACQUIRED android emulator-NNNN" +# 2 BUSY (all online emulators leased) | 1 ERROR (none online) +# sim-lease.sh release -> 0 RELEASED | 3 NOT-OWNER # sim-lease.sh status -> one "held:" line per lease # Env: SIM_LEASE_LOCK_ROOT (default /.claude/locks), # SIM_LEASE_STATE_DIR (default /.claude/state), # SIM_LEASE_DISCOVER_CMD (default `xcrun simctl list -j devices available`), -# SIM_LEASE_BOOT_CMD (default `xcrun simctl boot`), VERIFY_UDID, -# LOCK_OWNER_PID. +# SIM_LEASE_BOOT_CMD (default `xcrun simctl boot`), +# ANDROID_DEVICES_CMD (default `adb devices`; purpose=android discovery), +# VERIFY_UDID, LOCK_OWNER_PID. set -euo pipefail @@ -64,6 +73,20 @@ boot_sim() { # $1=udid fi } +# --- Android emulator discovery (purpose=android; feature #138 follow-up) ------ +# The `adb devices` source, overridable for tests (ANDROID_DEVICES_CMD — same +# hook name as scripts/run-android-tests.sh so a serial routed to a run matches +# the one leased here). ALWAYS exits 0 (prints nothing when adb is absent) so a +# missing-adb pipe never trips `set -e` in the command-substitution below — +# which would otherwise exit the whole script WITHOUT the "no online emulator" +# RESULT line (Gate-4 Medium). +android_devices() { + if [ -n "${ANDROID_DEVICES_CMD:-}" ]; then eval "$ANDROID_DEVICES_CMD" || true + elif command -v adb >/dev/null 2>&1; then adb devices 2>/dev/null || true; fi + return 0 +} +list_emulators() { android_devices | awk '/^emulator-[0-9]+[[:space:]]+device$/ {print $1}'; } + lease_dir() { echo "$LOCK_ROOT/sim-$1.lock.d"; } lease_purpose() { # $1=udid → recorded purpose ('' if not leased) @@ -116,13 +139,43 @@ cmd="${1:-}" case "$cmd" in acquire) purpose="${2:-}" - case "$purpose" in test|verify) : ;; *) - echo "usage: sim-lease.sh acquire {test|verify}" >&2; exit 64 ;; + case "$purpose" in test|verify|android) : ;; *) + echo "usage: sim-lease.sh acquire {test|verify|android}" >&2; exit 64 ;; esac mkdir -p "$LOCK_ROOT" "$STATE_DIR" select_lock_acquire || exit 1 trap 'select_lock_release' EXIT + if [ "$purpose" = "android" ]; then + # Lease an ONLINE emulator SERIAL, exclusive per-serial (rule 55 Android + # tier / rule 52 Cause D). Capacity = number of online emulators (1 today; + # N once more AVDs are booted) — enforced by leasing the first UNLEASED + # online serial and BUSY-ing when all are held. NO boot-on-demand: an AVD + # boot needs its name + is heavy/async, so require the emulator already + # online (unlike an iOS sim, which boots on lease). Emulators live in a + # separate namespace (emulator-NNNN) from iOS UDIDs, so no verify exclusion. + serials="$(list_emulators || true)" + if [ -z "$serials" ]; then + echo "SIM-LEASE RESULT: ERROR no online emulator to lease (boot an AVD: emulator -avd )" >&2 + exit 1 + fi + acquired="" + while IFS= read -r serial; do + [ -n "$serial" ] || continue + if lock_acquire "$(lease_dir "$serial")" 2>/dev/null; then + echo "android" > "$(lease_dir "$serial")/purpose" + acquired="$serial" + break + fi + done <<< "$serials" + if [ -z "$acquired" ]; then + echo "SIM-LEASE RESULT: BUSY android (all $(printf '%s\n' "$serials" | grep -c . ) online emulator(s) leased)" + exit 2 + fi + echo "SIM-LEASE RESULT: ACQUIRED android $acquired" + exit 0 + fi + if [ "$purpose" = "verify" ]; then if [ "$(held_count verify)" -ge 1 ]; then echo "SIM-LEASE RESULT: BUSY verify (capacity 1)" @@ -198,7 +251,7 @@ case "$cmd" in exit 0 ;; *) - echo "usage: sim-lease.sh {acquire {test|verify} | release | status}" >&2 + echo "usage: sim-lease.sh {acquire {test|verify|android} | release | status}" >&2 exit 64 ;; esac diff --git a/vreader.xcodeproj/project.pbxproj b/vreader.xcodeproj/project.pbxproj index c1f153ad..4450d5f7 100644 --- a/vreader.xcodeproj/project.pbxproj +++ b/vreader.xcodeproj/project.pbxproj @@ -8113,7 +8113,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1049; + CURRENT_PROJECT_VERSION = 1050; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited) USE_LIBXML2=1"; HEADER_SEARCH_PATHS = "$(inherited) $(SDKROOT)/usr/include/libxml2 $(SRCROOT)/vreader/Services/Libmobi/src"; INFOPLIST_FILE = vreader/SupportingFiles/Info.plist; @@ -8121,7 +8121,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.67.7; + MARKETING_VERSION = 3.67.8; OTHER_LDFLAGS = "$(inherited) -lxml2"; PRODUCT_BUNDLE_IDENTIFIER = com.vreader.app; SDKROOT = iphoneos; @@ -8267,7 +8267,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1049; + CURRENT_PROJECT_VERSION = 1050; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited) USE_LIBXML2=1"; HEADER_SEARCH_PATHS = "$(inherited) $(SDKROOT)/usr/include/libxml2 $(SRCROOT)/vreader/Services/Libmobi/src"; INFOPLIST_FILE = vreader/SupportingFiles/Info.plist; @@ -8275,7 +8275,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.67.7; + MARKETING_VERSION = 3.67.8; OTHER_LDFLAGS = "$(inherited) -lxml2"; PRODUCT_BUNDLE_IDENTIFIER = com.vreader.app; SDKROOT = iphoneos;