diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 000000000..dde66fcf4
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,63 @@
+name: Documentation site
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "docs/**"
+ - "package.json"
+ - "package-lock.json"
+ - ".nvmrc"
+ - ".github/workflows/pages.yml"
+ pull_request:
+ branches: [main]
+ paths:
+ - "docs/**"
+ - "package.json"
+ - "package-lock.json"
+ - ".nvmrc"
+ - ".github/workflows/pages.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pages-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+ HUSKY: "0"
+
+jobs:
+ build:
+ name: Build documentation
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+ - run: npm ci
+ - run: npm run docs:build
+ - uses: actions/upload-pages-artifact@v4
+ if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
+ with:
+ path: docs/.vitepress/dist
+
+ deploy:
+ name: Publish documentation
+ if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/configure-pages@v5
+ - uses: actions/deploy-pages@v4
+ id: deployment
diff --git a/.github/workflows/real-javascript-recovery.yml b/.github/workflows/real-javascript-recovery.yml
index df3f7edec..7076a29cd 100644
--- a/.github/workflows/real-javascript-recovery.yml
+++ b/.github/workflows/real-javascript-recovery.yml
@@ -5,10 +5,10 @@ on:
paths:
- ".github/workflows/real-javascript-recovery.yml"
- "src/javascript/recovery/**"
- - "src/domain/javascript*.ts"
+ - "src/domain/javascript/**"
- "src/application/javascript/JavaScriptRecovery*.ts"
- "src/composition/javascriptRecovery.ts"
- - "src/contracts/javascriptRecoveryToolContracts.ts"
+ - "src/contracts/javascript/javascriptRecoveryToolContracts.ts"
- "src/cli/javascriptRecoveryCommands.ts"
- "src/cli/javascriptApplicationAnalysis.ts"
- "src/server/registerJavaScriptRecoveryTool.ts"
diff --git a/.github/workflows/website-pages.yml b/.github/workflows/website-pages.yml
new file mode 100644
index 000000000..3967a89e4
--- /dev/null
+++ b/.github/workflows/website-pages.yml
@@ -0,0 +1,44 @@
+name: Publish REA website
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: website-pages
+ cancel-in-progress: false
+
+jobs:
+ prepare:
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ pages: read
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@v4
+ - name: Configure Pages
+ uses: actions/configure-pages@v5
+ - name: Upload the website
+ uses: actions/upload-pages-artifact@v4
+ with:
+ path: website/public
+
+ deploy:
+ needs: prepare
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Publish to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 8396bc653..e699d15fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ coverage/
test-results.xml
.turbo/
.cache/
+docs/.vitepress/cache/
# OS
.DS_Store
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 731cd2cdc..07b182bd2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,6 +26,22 @@ worktrees. After a package, lockfile, or managed-skill version change, run
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
+## Documentation website
+
+The VitePress site uses the Markdown files in `docs/` and deploys to
+. Run `npm run docs:dev` for live editing,
+`npm run docs:build` to check the production build and links, and
+`npm run docs:preview` to preview that build at `/rea/`.
+
+Site navigation lives in `docs/.vitepress/config.ts`. Keep links to guides
+relative so they work on GitHub and the website; link to repository files
+outside `docs/` using their full GitHub URLs. Generated reference documents
+still use `npm run docs:generate`; `docs:build` only builds the website.
+
+The documentation workflow builds pull requests and deploys changes on `main`.
+The repository's **Settings → Pages → Build and deployment → Source** must be
+set to **GitHub Actions** before the first deployment.
+
## Development feedback and PR verification
For an ordinary edit, run the relevant regression and cached static checks:
diff --git a/README.md b/README.md
index 09d579a97..2bf765f54 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
-[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
+[Documentation](https://morluto.github.io/rea/) · [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
npx rea-agents setup
@@ -163,7 +163,7 @@ Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghid
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
-Static APK analysis uses a separately supplied headless JADX JAR and Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA inventory Evidence can be projected with [Apple application analysis](docs/apple-application-analysis.md).
+Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA inventory Evidence can be projected with [Apple application analysis](docs/apple-application-analysis.md).
Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
@@ -420,7 +420,7 @@ The public interface describes what the agent is trying to learn. Providers deci
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites. `rea capabilities` and `rea providers` describe the binary-session providers and auxiliary operations; they are not an inventory of every browser, Android, or application workflow. Use the connected MCP tool list and `binary_session` tool availability for the full MCP surface, and the relevant guide for each tool's prerequisites. Repository main can be ahead of [the npm release](docs/installation.md#released-package-and-main).
-Static Android APK inspection is verified on Linux with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
+Static Android APK inspection supports Linux and macOS; the current metadata bridge is verified on macOS arm64 with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through a selected deep provider. Hopper and Ghidra cover broad inventory and function analysis; the IDA adapter supplies its documented read-only function/string operations. Hopper also accepts `.hop` databases and supports annotations.
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
diff --git a/bridge/android/ReaJadxBridge.java b/bridge/android/ReaJadxBridge.java
new file mode 100644
index 000000000..51ac0f703
--- /dev/null
+++ b/bridge/android/ReaJadxBridge.java
@@ -0,0 +1,247 @@
+import com.atxx.jhmcp.*;
+import com.google.gson.*;
+import io.modelcontextprotocol.kotlin.sdk.server.*;
+import io.modelcontextprotocol.kotlin.sdk.types.*;
+import jadx.api.*;
+import jadx.core.dex.nodes.*;
+import java.io.PrintStream;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.stream.Collectors;
+import kotlin.Unit;
+import kotlin.coroutines.EmptyCoroutineContext;
+import kotlinx.coroutines.BuildersKt;
+import kotlinx.io.CoreKt;
+import kotlinx.io.JvmCoreKt;
+import kotlinx.serialization.json.JsonObject;
+
+/** REA-owned metadata boundary for the unmodified jadx-headless-mcp 0.7.1 JAR. */
+class ReaJadxBridge {
+ private static final Gson JSON = new Gson();
+ private final SessionHolder holder;
+ private JadxSession indexedSession;
+ private Map> classes = Map.of();
+
+ private record MethodSnapshot(String name, String shortId, Map summary) {}
+ private record ClassSnapshot(JavaClass node, List methods, Map summary) {}
+
+ private ReaJadxBridge(SessionHolder holder) { this.holder = holder; }
+
+ private static CallToolResult text(String value, boolean failed) {
+ return new CallToolResult(List.of(new TextContent(value, null, null)), failed, null, null);
+ }
+
+ private static CallToolResult json(Object value) { return text(JSON.toJson(value), false); }
+
+ private static String string(JsonObject args, String name) {
+ JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
+ if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
+ throw new IllegalArgumentException(name + " must be a string");
+ return value.getAsString();
+ }
+
+ private static int integer(JsonObject args, String name, int fallback) {
+ JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
+ if (value == null) return fallback;
+ if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber())
+ throw new IllegalArgumentException(name + " must be a nonnegative integer");
+ try {
+ int result = value.getAsBigDecimal().intValueExact();
+ if (result < 0) throw new ArithmeticException();
+ return result;
+ } catch (ArithmeticException error) {
+ throw new IllegalArgumentException(name + " must be a nonnegative integer", error);
+ }
+ }
+
+ private JadxSession session() {
+ JadxSession current = holder.current();
+ if (current == null) throw new IllegalArgumentException("No APK loaded");
+ return current;
+ }
+
+ private ClassSnapshot selected(JsonObject args) {
+ JadxSession current = session();
+ if (indexedSession != current) {
+ Map> index = new LinkedHashMap<>();
+ if (!current.getClasses().isEmpty()) {
+ JadxDecompiler decompiler = current.getClasses().get(0).getClassNode().root().getDecompiler();
+ // JavaClass.getMethods/getFields/getInnerClasses all load code. Raw nodes do not.
+ // Snapshot every identity before code generation can rewrite aliases or members.
+ for (JavaClass cls : decompiler.getClassesWithInners()) {
+ ClassNode node = cls.getClassNode();
+ List ordered = new ArrayList<>(node.getMethods());
+ ordered.sort(Comparator.comparing(MethodNode::getAlias));
+ List methods = ordered.stream().map(method -> {
+ String name = method.getAlias();
+ String signature = name + "(" + method.getArgTypes().stream()
+ .map(Object::toString).collect(Collectors.joining(", "))
+ + "): " + method.getReturnType().toString();
+ return new MethodSnapshot(name, method.getMethodInfo().getShortId(), Map.of(
+ "name", name, "signature", signature,
+ "is_constructor", method.isConstructor(), "def_pos", 0));
+ }).toList();
+ List