diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b53258601..b4abd478d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -28,7 +28,7 @@ intentionally not included. --> - Provider, bridge, target-format, or platform compatibility: - Evidence, artifact, provenance, or reconstruction contract: - Process execution, authorization, cleanup, or containment impact: -- Generated metadata (`docs/product-catalog.json`), package, or installation impact (the regen-generated bot pushes tracked generated files; never hand-edit them): +- Generated metadata (`docs/public/product-catalog.json`), package, or installation impact (CI generates and validates ignored catalogs and `skills/`; edit authored instructions in `skill-src/` and never commit build outputs): ## Evidence and regression coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7ed573ba..b9fe2b375 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -488,3 +488,13 @@ jobs: cache: npm - run: npm ci - run: npm run docs:check + - name: Generation leaves authored files unchanged + run: git diff --exit-code + - uses: actions/upload-artifact@v4 + with: + name: generated-docs + path: | + docs/public/product-catalog.json + docs/verification/managed-conformance-*.json + skills/reverse-engineer-anything + if-no-files-found: error diff --git a/.github/workflows/regen-generated.yml b/.github/workflows/regen-generated.yml deleted file mode 100644 index 0ce5e9c5f..000000000 --- a/.github/workflows/regen-generated.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Regenerate generated metadata - -on: - pull_request: - branches: [main] - paths: - - "src/**" - - "skills/**/SKILL.md" - - "scripts/generate-*.mjs" - - "scripts/check-doc-facts.mjs" - - "scripts/lib/*.mjs" - - "scripts/verify/managed/**" - - "scripts/fixtures/managed/**" - - "package.json" - - "package-lock.json" - -concurrency: - group: regen-${{ github.event.pull_request.number }} - cancel-in-progress: true - -env: - HUSKY: "0" - -permissions: - contents: write - -jobs: - regen: - name: Regenerate and push tracked generated files - # Same-repo PRs only: the default token cannot push to forks. - # The bot-actor guard stops looped runs triggered by our own push. - if: >- - github.actor != 'github-actions[bot]' && - github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: npm - - run: npm ci - - run: npm run docs:generate - - name: Commit regenerated files - shell: bash - env: - HEAD_REF: ${{ github.event.pull_request.head.ref }} - run: | - generated_paths=( - docs/product-catalog.json - docs/error-contract.schema.json - docs/verification/managed-conformance-manifest.json - docs/verification/managed-conformance-ledger.json - skills/reverse-engineer-anything/SKILL.md - src/generatedPackageMetadata.ts - ) - if git diff --quiet -- "${generated_paths[@]}"; then - echo "Generated metadata is already canonical." - exit 0 - fi - git diff --check - # Refuse to sweep unrelated working-tree changes into the bot commit. - unexpected="$(git diff --name-only | grep -vxF -f <(printf '%s\n' "${generated_paths[@]}") || true)" - if [[ -n "${unexpected}" ]]; then - echo "docs:generate left unexpected tracked changes:" >&2 - printf '%s\n' "${unexpected}" >&2 - exit 1 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -- "${generated_paths[@]}" - git commit -m "chore: regenerate generated metadata" - git push origin "HEAD:${HEAD_REF}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04e385404..e10fc6206 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,43 +170,9 @@ jobs: if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' run: npm run docs:generate - - name: Commit canonical release catalog + - name: Validate generated release documentation if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' - shell: bash - env: - # Use the same identity for the follow-up commit so it also triggers CI. - GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} - RELEASE_PR_JSON: ${{ steps.release.outputs.pr }} - run: | - release_pr_branch="$(node --input-type=module -e ' - const value = JSON.parse(process.env.RELEASE_PR_JSON ?? ""); - if ( - typeof value !== "object" || - value === null || - typeof value.headBranchName !== "string" || - value.headBranchName.length === 0 - ) { - process.exit(1); - } - process.stdout.write(value.headBranchName); - ')" - changed_paths="$(git diff --name-only)" - if [[ -z "${changed_paths}" ]]; then - echo "Release documentation is already canonical." - exit 0 - fi - if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then - echo "docs:generate changed unexpected tracked paths:" >&2 - printf '%s\n' "${changed_paths}" >&2 - exit 1 - fi - git diff --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -- docs/product-catalog.json - git commit -m "chore: normalize release product catalog" - gh auth setup-git - git push origin "HEAD:${release_pr_branch}" + run: npm run docs:check && git diff --exit-code publish: name: Publish to npm diff --git a/.gitignore b/.gitignore index aa778dec1..a362e63e8 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ test-results.xml .turbo/ .cache/ docs/.vitepress/cache/ +docs/.vitepress/dist/ # OS .DS_Store @@ -51,3 +52,10 @@ Thumbs.db # Build-generated MCP tool catalog (regenerated by `npm run build`; never commit) src/generatedMcpToolCatalog.ts + +# Derived catalogs, portable conformance projections, and packaged skill bundles +# (regenerated from this checkout by `npm run build:cached`; never commit) +/skills/ +/docs/public/product-catalog.json +/docs/verification/managed-conformance-manifest.json +/docs/verification/managed-conformance-ledger.json diff --git a/AGENTS.md b/AGENTS.md index c12152690..f7f8d9ea1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure - `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points. - `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters. - `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks. -- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly. +- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections. - `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`. ## Build, Test, and Development Commands @@ -36,7 +36,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure - `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`). - `npm run check:fast`: run cached typecheck and lint checks. - `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time. -- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them. +- `npm run docs:check`: build and validate generated documents for the current checkout; `npm run docs:generate` regenerates them. CI retains ignored outputs as artifacts and does not push snapshot commits to feature branches. - For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification. - Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d489205b..ca5fff69d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ 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. +use `npm run docs:generate`; `docs:build` generates them before building the website. Pull requests run `npm run docs:check`. `.github/workflows/pages.yml` is a manual VitePress build and does not publish. The public site at @@ -74,17 +74,24 @@ require the matching real-provider `verify:*` lane. Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use the same configuration. Pre-commit formats and lints staged files; pre-push runs `check:fast`. -`docs:check` checks committed generated metadata. `docs:generate` regenerates -those files, and the docs CI lane checks them. -The committed product catalog contains documented facts and their provider +`docs:check` builds and validates generated metadata for the current checkout. +The product catalog (`docs/public/product-catalog.json`), portable managed +conformance projections (`docs/verification/managed-conformance-*.json`), and +packaged skill (`skills/`) are ignored build outputs. Edit skill instructions +and references in `skill-src/`; the build adds catalog-dependent metadata to +the packaged copy without rewriting authored files. The generated manifest +commits to that exact packaged skill bundle. It is a portable projection of +the deterministic managed verifier, not a record of optional real-provider runs. +CI validates these outputs and retains them as artifacts instead of pushing +generated-only commits onto feature branches. Reviewed source metadata such as +`src/generatedPackageMetadata.ts` and `docs/error-contract.schema.json` remains +tracked and checked for freshness. Do not commit ignored generated outputs. +The build-generated product catalog contains documented facts and their provider identity, rather than full runtime schema hashes. The managed skill contains instructions and inventory metadata; doctor compares its installed files with -the canonical bundle. Schema-only fixes should not change these files or the +the canonical bundle. Schema-only fixes should not change these outputs or the skill commitment in the conformance manifest. Runtime schema identity remains -available through doctor and `binary_session`. If a branch conflicts only in -these generated files, merge the source changes first, then run -`npm run docs:generate` to regenerate from the merged sources; keep hand-written -skill instructions from both branches. +available through doctor and `binary_session`. Real-provider execution remains uncached; deterministic builds use Turbo. Local `npm test` runs every deterministic Vitest project without coverage or diff --git a/README.md b/README.md index 225f13db8..9bbba0359 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents) [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml) -[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/product-catalog.json) +[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog) [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/) [![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything) [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE) @@ -296,7 +296,7 @@ For exact options, prerequisites and result contracts: - [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine. - [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses. - [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations. -- [Tool catalog](docs/product-catalog.json): generated inventory of tools, providers and CLI commands. +- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands. - [Roadmap](docs/roadmap.md): planned work and capability trackers. Report vulnerabilities through [SECURITY.md](SECURITY.md). diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index a833edc36..e337e0fa1 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -27,7 +27,7 @@ REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \ REA never installs Python, packages or GDB, changes a user init file, launches the selected object as a host process, or requests runtime library resolution. The Python process uses isolated mode and an owned cache. Exact upstream profiles are recorded in -[upstream provenance](../third_party/pwntools/README.md). +[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/pwntools/README.md). ## Interpreting results diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 5c0c67ae6..8db25973e 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -24,7 +24,7 @@ their distinct unavailable-provider or capability classification. The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`; `REA_EVM_PRLIMIT_COMMAND` can select another absolute executable. REA installs no system tools and changes no user configuration. [Upstream provenance and -license](../third_party/evmole/README.md) are packaged. +license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged. ## Evidence semantics diff --git a/docs/installation.md b/docs/installation.md index 67f7bcd87..bc21b585b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -95,7 +95,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything This installs agent instructions and bundled references, not REA MCP registration or analysis engines. Follow the skill's -[conditional connection guide](https://github.com/morluto/rea/blob/main/skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed). +[conditional connection guide](https://github.com/morluto/rea/blob/main/skill-src/reverse-engineer-anything/SKILL.md#connect-only-when-needed). Working tools can be used immediately. If tools are missing, inspect the current client's registration with `doctor --client codex --json` (substitute its client ID), then plan repairs with `setup --client codex --dry-run --json`. Show and diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index a405b1f4b..b0a0df4c0 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -25,7 +25,7 @@ part of the current tool set. This guide describes the implementation and verification of [ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical -tool inventory is [`product-catalog.json`](https://github.com/morluto/rea/blob/main/docs/product-catalog.json). +tool inventory is the [build-generated catalog](mcp-contracts.md#generated-catalog). ## Shipped scope diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 70bf2259c..2ce8f5c3d 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -1,5 +1,15 @@ # MCP runtime contracts +## Generated catalog + +Run `npm run build:cached` in a source checkout to generate the machine-readable +catalog at `docs/public/product-catalog.json`. The [catalog](product-catalog.json) +is also included in documentation builds. PR CI retains it with the packaged +skill and portable conformance projections in the `generated-docs` artifact. +These outputs describe the exact source revision being built; they are not +checked-in snapshots. For a running server, `binary_session` remains the +authoritative source of catalog identity and tool availability. + ## Identity and discovery `binary_session` reports the active package, server, SDK, and negotiated diff --git a/docs/product-catalog.json b/docs/product-catalog.json deleted file mode 100644 index 3f05b7e2b..000000000 --- a/docs/product-catalog.json +++ /dev/null @@ -1,719 +0,0 @@ -{ - "package": { - "name": "rea-agents", - "version": "6.0.0", - "sdk": { - "server": "2.3.1", - "client": "2.3.1", - "core": "2.3.1" - }, - "skill_version": "33" - }, - "tools": { - "total": 139, - "families": [ - { - "id": "direct", - "surface": "official-proxy", - "count": 41, - "tools": [ - "address_name", - "address_to_file_offset", - "annotate_native_function", - "comment", - "current_address", - "current_document", - "current_procedure", - "goto_address", - "inline_comment", - "inspect_native_data_type", - "inspect_native_instruction", - "inspect_native_load_image", - "list_bookmarks", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "next_address", - "prev_address", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "resolve_native_call_targets", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", - "set_bookmark", - "set_comment", - "set_current_document", - "set_inline_comment", - "unset_bookmark", - "xrefs" - ] - }, - { - "id": "enhanced", - "surface": "enhanced", - "count": 14, - "tools": [ - "analyze_function", - "analyze_swift_types", - "batch_decompile", - "binary_overview", - "find_xrefs_to_name", - "get_call_graph", - "get_objc_classes", - "get_objc_protocols", - "inspect_native_api", - "inspect_native_dispatch_metadata", - "trace_call_path", - "trace_feature", - "trace_native_ui_action", - "trace_native_values" - ] - }, - { - "id": "native", - "surface": "native-provider", - "count": 10, - "tools": [ - "capture_native_ui_scenario", - "demangle_swift", - "inspect_binary_layout", - "inspect_macho", - "inspect_plist", - "inspect_recorded_crash", - "inspect_signature", - "list_architectures", - "observe_native_calls", - "observe_native_ui" - ] - }, - { - "id": "artifact", - "surface": "artifact-provider", - "count": 7, - "tools": [ - "decode_interface_builder", - "extract_artifact", - "inspect_artifact", - "inspect_asset_catalog", - "inspect_evm_interface", - "inspect_keyed_archive", - "trace_dylib_resolution" - ] - }, - { - "id": "managed", - "surface": "managed", - "count": 7, - "tools": [ - "compare_managed_members", - "import_managed_reconstruction", - "inspect_managed_artifact", - "inspect_managed_members", - "inspect_managed_native_boundaries", - "project_managed_application_graph", - "verify_managed_native_boundaries" - ] - }, - { - "id": "firmware", - "surface": "firmware-provider", - "count": 2, - "tools": ["extract_firmware", "inspect_firmware_regions"] - }, - { - "id": "android", - "surface": "android-provider", - "count": 5, - "tools": [ - "inspect_android_class", - "inspect_android_method", - "inspect_android_package", - "search_android_classes", - "trace_android_references" - ] - }, - { - "id": "browser", - "surface": "browser-provider", - "count": 12, - "tools": [ - "analyze_web_bundle", - "capture_browser_scenario", - "capture_web_screenshot", - "compare_web_captures", - "compare_web_screenshots", - "discover_webmcp_tools", - "inspect_web_event_listeners", - "inspect_web_network_capture", - "inspect_web_page", - "list_browser_targets", - "observe_web_execution", - "observe_web_session" - ] - }, - { - "id": "electron", - "surface": "electron-provider", - "count": 5, - "tools": [ - "analyze_javascript_application", - "capture_electron_scenario", - "inspect_electron_page", - "list_electron_targets", - "reconcile_javascript_runtime" - ] - }, - { - "id": "javascript-runtime", - "surface": "runtime-provider", - "count": 2, - "tools": [ - "list_javascript_runtime_targets", - "observe_javascript_runtime" - ] - }, - { - "id": "application", - "surface": "application-workflow", - "count": 13, - "tools": [ - "build_reconstruction_obligation_ledger", - "compare_application_versions", - "compare_javascript_export_shapes", - "compare_source_to_bundle", - "evaluate_reconstruction_coverage", - "export_web_scripts", - "project_android_application_graph", - "project_apple_application_graph", - "recover_javascript_sources", - "trace_application_feature", - "trace_javascript_semantics", - "trace_web_module_imports", - "trace_web_source_location" - ] - }, - { - "id": "session", - "surface": "session", - "count": 21, - "tools": [ - "binary_session", - "build_call_path", - "capture_process_scenario", - "close_binary", - "compare_artifacts", - "compare_bundles", - "compare_functions", - "compare_process_captures", - "correlate_static_and_runtime", - "export_evidence_bundle", - "find_changed_behavior", - "get_evidence_bundle", - "get_navigation_context", - "import_evidence_bundle", - "inspect_address_context", - "list_unknowns", - "open_binary", - "record_unknown", - "update_unknown", - "verify_reconstruction", - "verify_unknown_resolution" - ] - } - ] - }, - "providers": [ - { - "id": "binwalk", - "name": "Binwalk", - "version": "3.1.0", - "capabilities": ["inspect_firmware_regions"] - }, - { - "id": "evmole-interface", - "name": "REA EVM interface adapter", - "version": "evmole@0.9.3", - "capabilities": ["inspect_evm_interface"] - }, - { - "id": "ghidra", - "name": "Ghidra", - "version": null, - "capabilities": [ - "address_name", - "address_to_file_offset", - "analyze_function", - "annotate_native_function", - "inspect_native_data_type", - "inspect_native_instruction", - "inspect_native_load_image", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "resolve_native_call_targets", - "search_procedures", - "search_strings", - "xrefs" - ] - }, - { - "id": "har-schema/lossless-json", - "name": "REA HAR capture adapter", - "version": "har-schema@2.0.0;jsonc-parser@3.3.1;lossless-json@4.3.1", - "capabilities": ["inspect_web_network_capture"] - }, - { - "id": "hopper", - "name": "Hopper Disassembler", - "version": null, - "capabilities": [ - "address_name", - "address_to_file_offset", - "analyze_function", - "comment", - "current_address", - "current_document", - "current_procedure", - "goto_address", - "inline_comment", - "list_bookmarks", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "next_address", - "prev_address", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "procedure_references", - "read_bytes", - "read_function_instructions", - "resolve_containing_procedure", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", - "set_bookmark", - "set_comment", - "set_current_document", - "set_inline_comment", - "unset_bookmark", - "xrefs" - ] - }, - { - "id": "ida", - "name": "IDA Pro MCP adapter", - "version": "1", - "capabilities": [ - "analyze_function", - "list_procedures", - "list_strings", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_pseudo_code", - "read_function_instructions", - "search_procedures", - "search_strings", - "xrefs" - ] - }, - { - "id": "jadx", - "name": "JADX headless", - "version": "0.7.1", - "capabilities": [ - "inspect_android_class", - "inspect_android_method", - "inspect_android_package", - "search_android_classes", - "trace_android_references" - ] - }, - { - "id": "mitmproxy-native-tnetstring", - "name": "REA offline native mitmproxy adapter", - "version": "12.2.3", - "capabilities": ["inspect_web_network_capture"] - }, - { - "id": "native-macos", - "name": "macOS native inspection utilities", - "version": null, - "capabilities": [ - "capture_native_ui_scenario", - "demangle_swift", - "inspect_macho", - "inspect_plist", - "inspect_signature", - "list_architectures", - "observe_native_calls", - "observe_native_ui" - ] - }, - { - "id": "pwntools-elf", - "name": "REA pwntools ELF adapter", - "version": "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", - "capabilities": ["inspect_binary_layout", "inspect_recorded_crash"] - }, - { - "id": "rea-android-application", - "name": "REA Android application workflows", - "version": "1", - "capabilities": ["project_android_application_graph"] - }, - { - "id": "rea-apple-application", - "name": "REA Apple application workflows", - "version": "1", - "capabilities": ["project_apple_application_graph"] - }, - { - "id": "rea-artifact-graph", - "name": "REA safe artifact graph provider", - "version": "1", - "capabilities": [ - "decode_interface_builder", - "extract_artifact", - "inspect_artifact", - "inspect_asset_catalog", - "inspect_keyed_archive", - "trace_dylib_resolution" - ] - }, - { - "id": "rea-cdp-browser", - "name": "REA Chrome DevTools Protocol observation provider", - "version": "2", - "capabilities": [ - "analyze_web_bundle", - "capture_web_screenshot", - "compare_web_captures", - "compare_web_screenshots", - "discover_webmcp_tools", - "inspect_web_event_listeners", - "inspect_web_page", - "list_browser_targets", - "observe_web_execution", - "observe_web_session" - ] - }, - { - "id": "rea-cdp-electron", - "name": "REA Electron file-page CDP observation provider", - "version": "1", - "capabilities": ["inspect_electron_page", "list_electron_targets"] - }, - { - "id": "rea-dotnet-static", - "name": "REA managed static analysis provider", - "version": "1", - "capabilities": [ - "inspect_managed_artifact", - "inspect_managed_members", - "inspect_managed_native_boundaries" - ] - }, - { - "id": "rea-dotnet-workflows", - "name": "REA managed-code workflows", - "version": "1", - "capabilities": [ - "compare_managed_members", - "import_managed_reconstruction", - "project_managed_application_graph", - "verify_managed_native_boundaries" - ] - }, - { - "id": "rea-javascript-application", - "name": "REA JavaScript application analyzer", - "version": "1", - "capabilities": ["analyze_javascript_application"] - }, - { - "id": "rea-javascript-application-workflows", - "name": "REA JavaScript application workflows", - "version": "1", - "capabilities": [ - "build_reconstruction_obligation_ledger", - "compare_application_versions", - "compare_javascript_export_shapes", - "compare_source_to_bundle", - "evaluate_reconstruction_coverage", - "trace_application_feature", - "trace_javascript_semantics" - ] - }, - { - "id": "rea-javascript-runtime-reconciliation", - "name": "REA JavaScript runtime reconciliation", - "version": "1", - "capabilities": ["reconcile_javascript_runtime"] - }, - { - "id": "rea-playwright-browser-scenario", - "name": "REA Playwright browser scenario capture provider", - "version": "1", - "capabilities": ["capture_browser_scenario"] - }, - { - "id": "rea-playwright-electron-active", - "name": "REA Playwright active Electron observation provider", - "version": "1", - "capabilities": ["capture_electron_scenario"] - }, - { - "id": "rea-v8-inspector", - "name": "REA passive Node/Electron V8 Inspector provider", - "version": "1", - "capabilities": [ - "list_javascript_runtime_targets", - "observe_javascript_runtime" - ] - }, - { - "id": "rea-web-script-export", - "name": "REA captured script export", - "version": "1", - "capabilities": [ - "export_web_scripts", - "trace_web_module_imports", - "trace_web_source_location" - ] - }, - { - "id": "unblob", - "name": "Unblob", - "version": "26.6.4", - "capabilities": ["extract_firmware"] - }, - { - "id": "wakaru", - "name": "Wakaru", - "version": "1.13.0", - "capabilities": ["recover_javascript_sources"] - } - ], - "setup_clients": [ - { - "id": "claude_code", - "display_name": "Claude Code", - "format": "json", - "configuration": "managed" - }, - { - "id": "claude_desktop", - "display_name": "Claude Desktop", - "format": "json", - "configuration": "managed" - }, - { - "id": "codex", - "display_name": "Codex", - "format": "toml", - "configuration": "managed" - }, - { - "id": "cursor", - "display_name": "Cursor", - "format": "json", - "configuration": "managed" - }, - { - "id": "gemini_cli", - "display_name": "Gemini CLI", - "format": "json", - "configuration": "managed" - }, - { - "id": "windsurf", - "display_name": "Windsurf", - "format": "json", - "configuration": "managed" - }, - { - "id": "devin", - "display_name": "Devin", - "format": "json", - "configuration": "managed" - }, - { - "id": "opencode", - "display_name": "OpenCode", - "format": "opencode", - "configuration": "managed" - }, - { - "id": "antigravity", - "display_name": "Antigravity", - "format": "json", - "configuration": "managed" - }, - { - "id": "copilot_cli", - "display_name": "GitHub Copilot CLI", - "format": "copilot_cli", - "configuration": "managed" - }, - { - "id": "commandcode", - "display_name": "Command Code", - "format": "commandcode", - "configuration": "managed" - }, - { - "id": "vscode", - "display_name": "VS Code", - "format": "vscode", - "configuration": "managed" - } - ], - "cli": { - "primary_count": 94, - "commands": [ - "address-to-file-offset", - "analyze", - "analyze-javascript-application", - "analyze-web-bundle", - "annotate-native-function", - "build-reconstruction-obligation-ledger", - "capabilities", - "capture-browser-scenario", - "capture-electron-scenario", - "capture-native-ui-scenario", - "capture-process", - "capture-web-screenshot", - "compare", - "compare-application-versions", - "compare-javascript-export-shapes", - "compare-managed-members", - "compare-process-captures", - "compare-source-to-bundle", - "compare-web-captures", - "compare-web-screenshots", - "decode-interface-builder", - "decompile", - "demangle-swift", - "discover-webmcp-tools", - "doctor", - "evaluate-reconstruction-coverage", - "evidence-export", - "evidence-import", - "export-web-scripts", - "extract-artifact", - "extract-firmware", - "function", - "import-managed-reconstruction", - "import-reference-source", - "inspect", - "inspect-android-class", - "inspect-android-method", - "inspect-android-package", - "inspect-artifact", - "inspect-asset-catalog", - "inspect-binary-layout", - "inspect-electron-page", - "inspect-evm-interface", - "inspect-firmware-regions", - "inspect-keyed-archive", - "inspect-macho", - "inspect-managed-artifact", - "inspect-managed-members", - "inspect-managed-native-boundaries", - "inspect-native-api", - "inspect-native-data-type", - "inspect-native-dispatch-metadata", - "inspect-native-instruction", - "inspect-native-load-image", - "inspect-plist", - "inspect-recorded-crash", - "inspect-signature", - "inspect-web-event-listeners", - "inspect-web-network-capture", - "inspect-web-page", - "instructions", - "list-architectures", - "list-browser-targets", - "list-electron-targets", - "list-javascript-runtime-targets", - "observe-javascript-runtime", - "observe-native-calls", - "observe-native-ui", - "observe-web-execution", - "observe-web-session", - "project-android-application-graph", - "project-apple-application-graph", - "project-managed-application-graph", - "providers", - "read-bytes", - "reconcile-javascript-runtime", - "recover-javascript-sources", - "resolve-native-call-targets", - "search", - "search-android-classes", - "setup", - "trace", - "trace-android-references", - "trace-application-feature", - "trace-dylib-resolution", - "trace-javascript-semantics", - "trace-native-ui-action", - "trace-native-values", - "trace-web-module-imports", - "trace-web-source-location", - "uninstall", - "update", - "verify-managed-native-boundaries", - "xrefs" - ], - "aliases": [ - { - "name": "compare-bundles", - "target": "compare" - } - ] - }, - "runtime_catalog": { - "counts": { - "cli_commands": 94, - "mcp_tools": 139, - "mcp_prompts": 6 - }, - "digests": { - "providers_sha256": "954b499c76514890062d2b6c936d73cb3a9c45412d522a081125a325e81cdec5" - } - } -} diff --git a/docs/releasing.md b/docs/releasing.md index 98aa1174d..56fc66264 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -144,8 +144,8 @@ before cutting the next checkpoint; otherwise main retains the previous release baseline and can propose an already-published version again. After publication, open a PR from the release branch back to main. Preserve -main's later implementation changes and resolve generated-file conflicts by -regenerating from the combined contracts with the released package version. +main's later implementation changes and regenerate ignored build outputs from +the combined contracts with the released package version. Review and test this synchronization PR, then use a merge commit so the release tag remains in main's ancestry. Keep the released tag unchanged. @@ -163,9 +163,12 @@ git merge-base --is-ancestor rea-agents-5.0.0 origin/main Close any superseded rolling release PR. Future releases repeat the checkpoint procedure from main; never resume automatic release-PR refreshes on main pushes. -The generated-metadata workflow stays limited to pull requests into main so it -does not push commits onto a frozen candidate. Preparation normalizes -`docs/product-catalog.json` on the bot pull request. +Preparation generates and validates the catalog, portable conformance +projections, and packaged skill from the candidate checkout without committing +them. The checkpoint validator binds tracked version authority to the source +SHA; generated-catalog and package checks validate the derived representation. +No generated-metadata workflow pushes follow-up commits onto feature or release +branches. ## Partial publication and retries diff --git a/docs/verification/managed-conformance-ledger.json b/docs/verification/managed-conformance-ledger.json deleted file mode 100644 index 0476f20a4..000000000 --- a/docs/verification/managed-conformance-ledger.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472", - "records": [ - { - "claim_id": "managed.application-graph-projection", - "status": "pass", - "evidence_ids": [ - "ev_b061a5a559acb3748cb9784c637f6693f74faeb564ba2b11d6fc32619e757459" - ] - }, - { - "claim_id": "managed.decompiler-reconstruction-import", - "status": "pass", - "evidence_ids": [ - "ev_13f3401a3aaddc3f6d087ad160fa928374e142cdb9861254359b46865b8c992e" - ] - }, - { - "claim_id": "managed.dotnet-framework-x86-pinvoke", - "status": "pass", - "evidence_ids": [ - "ev_6f026216826ca06e080b77ff608f02634fa945fbd2e151b4cd261c1444f8bbb1" - ] - }, - { - "claim_id": "managed.ilspy-reconstruction-oracle", - "status": "unsupported", - "evidence_ids": [ - "ev_65a94c4de03f9c6ebdaff3f381cca377821f349d3ca6dfad47a31cc586fae939" - ] - }, - { - "claim_id": "managed.malformed-metadata", - "status": "pass", - "evidence_ids": [ - "ev_deae4259ea4395623176de40d06c2abcbc2b3a77f0bfb068d76e568d801f45a8" - ] - }, - { - "claim_id": "managed.managed-native-verification", - "status": "pass", - "evidence_ids": [ - "ev_0edcbbcaa1191a4e5324a437f739957b77f9ac45e7897724337f2d6d06b3f9d1" - ] - }, - { - "claim_id": "managed.modern-dotnet-anycpu", - "status": "pass", - "evidence_ids": [ - "ev_d1af50e8dc04cd589385769efd8758e82095425f6e9c1cc4bf2f530c6b434c93" - ] - }, - { - "claim_id": "managed.mvid-and-token-drift", - "status": "pass", - "evidence_ids": [ - "ev_5493f40bc875535488d789a490361abcc1f76bfbdf52536d09ff127442a8e854" - ] - }, - { - "claim_id": "managed.not-managed", - "status": "pass", - "evidence_ids": [ - "ev_03ce0cc0183ee0710f8120e837565129559ab3bc41927df326066370c2341699" - ] - }, - { - "claim_id": "managed.operator-local-manifest", - "status": "unsupported", - "evidence_ids": [ - "ev_9d3e50da1d372c55f46649e69eae702cbe8f0ba6a81d1e46afc3e1a284c81757" - ] - }, - { - "claim_id": "managed.operator-manifest-self-test", - "status": "pass", - "evidence_ids": [ - "ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a" - ] - }, - { - "claim_id": "managed.unicode-obfuscated-identifiers", - "status": "pass", - "evidence_ids": [ - "ev_c4f38ce76d8359d92ee8e9442503e47b7913550442349a0df688bd274a3def67" - ] - }, - { - "claim_id": "managed.x64-ready-to-run-native-body", - "status": "pass", - "evidence_ids": [ - "ev_3e05ef1d6a796d3d4cb3372fe8e549efec759c59bf09d8ff660059a2e448cbc0" - ] - } - ], - "summary": { - "total": 13, - "pass": 11, - "fail": 0, - "unsupported": 2, - "truncated": 0, - "unknown": 0, - "complete": false - } -} diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json deleted file mode 100644 index f83f01995..000000000 --- a/docs/verification/managed-conformance-manifest.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "manifest_id": "ecm_0abe486e63d1ebdeec6487dfd19a5bd92d47a90d84e60df96926fa66602552ea", - "verifier": { - "id": "rea-managed-conformance", - "version": "1" - }, - "environment": { - "platform": "portable", - "architecture": "portable", - "runtime": "node", - "runtime_version": "portable" - }, - "skill_digests": [ - { - "skill_id": "reverse-engineer-anything", - "sha256": "0e6130b292e3cd3fe1da214b2b3ca5eba8661c81a5bf64679b7ef29828393f29" - } - ], - "claims": [ - { - "claim_id": "managed.application-graph-projection", - "scenario": { - "id": "managed-application-graph-projection", - "version": 1 - }, - "artifact_sha256s": [ - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.decompiler-reconstruction-import", - "scenario": { - "id": "decompiler-reconstruction-import", - "version": 1 - }, - "artifact_sha256s": [ - "cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.dotnet-framework-x86-pinvoke", - "scenario": { - "id": "dotnet-framework-x86-pinvoke", - "version": 1 - }, - "artifact_sha256s": [ - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.ilspy-reconstruction-oracle", - "scenario": { - "id": "ilspy-reconstruction-oracle", - "version": 1 - }, - "artifact_sha256s": [], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.malformed-metadata", - "scenario": { - "id": "malformed-metadata", - "version": 1 - }, - "artifact_sha256s": [ - "827e10b6391d29edca48c191e2ddd79f3bc1165e46e4ba2f4edf41ab60f45e78" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.managed-native-verification", - "scenario": { - "id": "managed-native-verification", - "version": 1 - }, - "artifact_sha256s": [ - "9999999999999999999999999999999999999999999999999999999999999999", - "f17fe6286cb07c6385e786e0aa877564d44123197f02617dfec70c537a33e4ea" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.modern-dotnet-anycpu", - "scenario": { - "id": "modern-dotnet-anycpu", - "version": 1 - }, - "artifact_sha256s": [ - "92b588ae43d418117fa108c0004b5d0451f6112238d21bad8d75c0eb1da860f0" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.mvid-and-token-drift", - "scenario": { - "id": "mvid-and-token-drift", - "version": 1 - }, - "artifact_sha256s": [ - "276395c671a656ab57d583158443c10de552dccfb37dc97cd013f673c0f86b43", - "ef009dab7424fdeba996f2524dbe86fd899c96394498cca0463fcfdb228c5572" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.not-managed", - "scenario": { - "id": "not-managed", - "version": 1 - }, - "artifact_sha256s": [ - "de74dab8ab034054a811d4248efcda0bed505187a061a806e274ae87bc453754" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.operator-local-manifest", - "scenario": { - "id": "operator-local-manifest", - "version": 1 - }, - "artifact_sha256s": [], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.operator-manifest-self-test", - "scenario": { - "id": "operator-manifest-self-test", - "version": 1 - }, - "artifact_sha256s": [ - "cdc11ec7fd0f392e43b9b23971cb41ec49fd0a997fc33c24fcc553cd4386ae37" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.unicode-obfuscated-identifiers", - "scenario": { - "id": "unicode-obfuscated-identifiers", - "version": 1 - }, - "artifact_sha256s": [ - "cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - }, - { - "claim_id": "managed.x64-ready-to-run-native-body", - "scenario": { - "id": "x64-ready-to-run-native-body", - "version": 1 - }, - "artifact_sha256s": [ - "e47402428322cdcb06f561036d7d108528cbc6e262d92a21f869218a20c80bfb" - ], - "provider": { - "id": "rea-managed-conformance", - "version": "1" - } - } - ] -} diff --git a/package.json b/package.json index d4858abe0..26fffefde 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check", "prebuild": "npm run deps:check && npm run metadata:check", "build": "node scripts/run-exclusive.mjs build npm run build:unlocked", - "build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs", + "build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs", "precheck": "npm run deps:check", "check": "turbo run typecheck lint format:check knip metadata:check", "precheck:fast": "npm run deps:check", @@ -179,7 +179,9 @@ "config:print": "node scripts/print-mcp-config.mjs", "evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs", "evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check", + "predocs:dev": "npm run docs:generate", "docs:dev": "vitepress dev docs", + "predocs:build": "npm run docs:generate", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked", diff --git a/release-please-config.json b/release-please-config.json index 85a7eee3b..4560b53fa 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -34,11 +34,6 @@ "type": "json", "path": "server.json", "jsonpath": "$.packages[0].version" - }, - { - "type": "json", - "path": "docs/product-catalog.json", - "jsonpath": "$.package.version" } ] } diff --git a/scripts/generate-completion-ledger.mjs b/scripts/generate-completion-ledger.mjs index fa3bf6b4a..ce63ffd40 100644 --- a/scripts/generate-completion-ledger.mjs +++ b/scripts/generate-completion-ledger.mjs @@ -30,7 +30,7 @@ const { stdout } = await exec( }, ); const verifierOutput = JSON.parse(stdout); -// Managed conformance claims are host-independent. Keep the checked-in +// Managed conformance claims are host-independent. Keep the generated // commitment reproducible across macOS, Linux, and Windows rather than // embedding the machine that happened to generate it. const portableCompletionReport = { diff --git a/scripts/generate-package-metadata.mjs b/scripts/generate-package-metadata.mjs index 2a2336839..8247dac1c 100644 --- a/scripts/generate-package-metadata.mjs +++ b/scripts/generate-package-metadata.mjs @@ -17,7 +17,7 @@ const lock = JSON.parse( await readFile(join(root, "package-lock.json"), "utf8"), ); const skill = await readFile( - join(root, "skills/reverse-engineer-anything/SKILL.md"), + join(root, "skill-src/reverse-engineer-anything/SKILL.md"), "utf8", ); const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1]; diff --git a/scripts/generate-product-catalog.mjs b/scripts/generate-product-catalog.mjs index c13e80c3e..fa6aa4ad8 100644 --- a/scripts/generate-product-catalog.mjs +++ b/scripts/generate-product-catalog.mjs @@ -1,3 +1,4 @@ +import { mkdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -14,8 +15,10 @@ for (const argument of arguments_) const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const catalog = await createProductCatalog(root); +const path = join(root, "docs/public/product-catalog.json"); +if (!arguments_.has("--check")) await mkdir(dirname(path), { recursive: true }); await ensureGeneratedFile({ - path: join(root, "docs/product-catalog.json"), + path, source: await serializeProductCatalog(catalog), check: arguments_.has("--check"), generateCommand: "npm run docs:generate", diff --git a/scripts/generate-skill-metadata.mjs b/scripts/generate-skill-metadata.mjs index f1bdbd769..490976fb5 100644 --- a/scripts/generate-skill-metadata.mjs +++ b/scripts/generate-skill-metadata.mjs @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { mkdir, readFile, readdir, rm } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -10,25 +10,69 @@ for (const argument of arguments_) throw new Error(`Unknown skill metadata option: ${argument}`); const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const path = join(root, "skills/reverse-engineer-anything/SKILL.md"); -const current = await readFile(path, "utf8"); +const sourceRoot = join(root, "skill-src/reverse-engineer-anything"); +const outputRoot = join(root, "skills/reverse-engineer-anything"); +const check = arguments_.has("--check"); +const current = ( + await readFile(join(sourceRoot, "SKILL.md"), "utf8") +).replaceAll("\r\n", "\n"); const { CATALOG_IDENTITY } = await import( `${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}` ); -const withCount = current.replace( - /^\s{2}tool_count:\s*\d+\s*$/mu, - ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, -); -// Skill identity follows the installed instruction bundle. Runtime schema identity -// is still reported independently by doctor and binary_session. -const source = withCount.replace( - /^ {2}catalog_digest:[^\r\n]*(?:\r?\n|$)/mu, - "", +if (/^\s{2}(?:tool_count|catalog_digest):/mu.test(current)) + throw new Error( + "Catalog metadata belongs in the generated skill, not skill-src", + ); +const versionLine = /^ {2}version: "[^"\r\n]+"$/mu; +if (!versionLine.test(current)) + throw new Error("Missing authored skill version"); +const source = current.replace( + versionLine, + `$&\n tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, ); +const paths = await filePaths(sourceRoot); +// Rebuild this owned output directory so removed references cannot survive a build. +if (!check) await rm(outputRoot, { recursive: true, force: true }); +for (const relativePath of paths) { + const path = join(outputRoot, relativePath); + if (!check) await mkdir(dirname(path), { recursive: true }); + await ensureGeneratedFile({ + path, + source: + relativePath === "SKILL.md" + ? source + : (await readFile(join(sourceRoot, relativePath), "utf8")).replaceAll( + "\r\n", + "\n", + ), + check, + generateCommand: "npm run docs:generate", + }); +} +if ( + check && + JSON.stringify(await filePaths(outputRoot)) !== JSON.stringify(paths) +) + throw new Error( + "Generated skill file inventory drifted; run npm run docs:generate", + ); -await ensureGeneratedFile({ - path, - source, - check: arguments_.has("--check"), - generateCommand: "npm run docs:generate", -}); +async function filePaths(directory, prefix = "") { + const paths = []; + for (const entry of (await readdir(directory, { withFileTypes: true })).sort( + (left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + )) { + const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`; + if (entry.isDirectory()) + paths.push( + ...(await filePaths(join(directory, entry.name), relativePath)), + ); + else if (entry.isFile()) paths.push(relativePath); + else + throw new Error( + `Skill bundle requires regular files or directories: ${relativePath}`, + ); + } + return paths; +} diff --git a/scripts/lib/docs-facts.mjs b/scripts/lib/docs-facts.mjs index ad50dbc8e..d45d545aa 100644 --- a/scripts/lib/docs-facts.mjs +++ b/scripts/lib/docs-facts.mjs @@ -77,7 +77,12 @@ export const documentationFactIssues = async (root, catalog) => { requireText(issues, path, content, "MCP-tool_catalog"); if (path === "README.md") { requireText(issues, path, content, "(docs/installation.md"); - requireText(issues, path, content, "(docs/product-catalog.json)"); + requireText( + issues, + path, + content, + "(docs/mcp-contracts.md#generated-catalog)", + ); continue; } for (const client of catalog.setup_clients) @@ -99,11 +104,16 @@ export const documentationFactIssues = async (root, catalog) => { requireText(issues, installationPath, installation, client.display_name); const agents = await readFile(join(root, "AGENTS.md"), "utf8"); - requireText(issues, "AGENTS.md", agents, "docs/product-catalog.json"); + requireText(issues, "AGENTS.md", agents, "docs/public/product-catalog.json"); const templatePath = ".github/pull_request_template.md"; const template = await readFile(join(root, templatePath), "utf8"); - requireText(issues, templatePath, template, "docs/product-catalog.json"); + requireText( + issues, + templatePath, + template, + "docs/public/product-catalog.json", + ); return issues; }; diff --git a/scripts/lib/product-catalog.mjs b/scripts/lib/product-catalog.mjs index 21a421b25..266e37291 100644 --- a/scripts/lib/product-catalog.mjs +++ b/scripts/lib/product-catalog.mjs @@ -65,9 +65,9 @@ export const createProductCatalog = async (root) => { }; }; -/** Stable checked-in representation of the product catalog. */ +/** Stable build-generated representation of the product catalog. */ export const serializeProductCatalog = (catalog) => formatGeneratedFile( - "docs/product-catalog.json", + "docs/public/product-catalog.json", JSON.stringify(catalog, null, 2), ); diff --git a/scripts/lib/release-checkpoint.mjs b/scripts/lib/release-checkpoint.mjs index 201cbb179..1d1950d5f 100644 --- a/scripts/lib/release-checkpoint.mjs +++ b/scripts/lib/release-checkpoint.mjs @@ -180,13 +180,8 @@ async function validateCandidate(git, sha, version, commits) { ["server.json", server.version], ["server.json rea-agents package", npmPackage.version], ); - const catalog = await readJson( - git, - sha, - "docs/product-catalog.json", - z.object({ package: versionSchema }), - ); - versions.push(["docs/product-catalog.json", catalog.package.version]); + // Catalogs are generated and checked from this candidate by the release job; + // they are not version authority stored in the immutable source tree. const generated = await git([ "show", `${sha}:src/generatedPackageMetadata.ts`, diff --git a/scripts/verify-package-pack.mjs b/scripts/verify-package-pack.mjs index 8cae7bf4a..600ecf7a0 100644 --- a/scripts/verify-package-pack.mjs +++ b/scripts/verify-package-pack.mjs @@ -1,4 +1,5 @@ import { join } from "node:path"; +import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js"; import { exec } from "./lib/verify-package-core.mjs"; import { verifyPackedBridge } from "./verify-packed-bridge.mjs"; @@ -28,6 +29,26 @@ export async function verifyPackagePack({ root, workspace }) { throw new Error("package did not expose both rea command entry points"); if (packedManifest.mcpName !== "io.github.morluto/rea") throw new Error("package did not retain its MCP Registry ownership marker"); + if (packedFiles.some((path) => path.startsWith("package/skill-src/"))) + throw new Error( + "package included authored skill sources instead of only the generated bundle", + ); + const skill = ( + await exec("tar", [ + "-xOf", + join(root, tarball), + "package/skills/reverse-engineer-anything/SKILL.md", + ]) + ).stdout; + if ( + !skill.includes( + ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}\n`, + ) || + skill.includes(" catalog_digest:") + ) + throw new Error( + "packaged skill metadata did not match the catalog shipped in the package", + ); if ( packedFiles.some( (path) => path.includes("__pycache__") || path.endsWith(".pyc"), diff --git a/skills/reverse-engineer-anything/SKILL.md b/skill-src/reverse-engineer-anything/SKILL.md similarity index 99% rename from skills/reverse-engineer-anything/SKILL.md rename to skill-src/reverse-engineer-anything/SKILL.md index 79923da1f..629a72731 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skill-src/reverse-engineer-anything/SKILL.md @@ -3,7 +3,6 @@ name: reverse-engineer-anything description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis. metadata: version: "33" - tool_count: 139 --- # REA diff --git a/skills/reverse-engineer-anything/references/android-applications.md b/skill-src/reverse-engineer-anything/references/android-applications.md similarity index 100% rename from skills/reverse-engineer-anything/references/android-applications.md rename to skill-src/reverse-engineer-anything/references/android-applications.md diff --git a/skills/reverse-engineer-anything/references/evidence-workflows.md b/skill-src/reverse-engineer-anything/references/evidence-workflows.md similarity index 100% rename from skills/reverse-engineer-anything/references/evidence-workflows.md rename to skill-src/reverse-engineer-anything/references/evidence-workflows.md diff --git a/skills/reverse-engineer-anything/references/javascript-applications.md b/skill-src/reverse-engineer-anything/references/javascript-applications.md similarity index 100% rename from skills/reverse-engineer-anything/references/javascript-applications.md rename to skill-src/reverse-engineer-anything/references/javascript-applications.md diff --git a/skills/reverse-engineer-anything/references/native-and-artifacts.md b/skill-src/reverse-engineer-anything/references/native-and-artifacts.md similarity index 100% rename from skills/reverse-engineer-anything/references/native-and-artifacts.md rename to skill-src/reverse-engineer-anything/references/native-and-artifacts.md diff --git a/skills/reverse-engineer-anything/references/runtime-observation.md b/skill-src/reverse-engineer-anything/references/runtime-observation.md similarity index 100% rename from skills/reverse-engineer-anything/references/runtime-observation.md rename to skill-src/reverse-engineer-anything/references/runtime-observation.md diff --git a/tests/boundary/cli/productCatalog.test.ts b/tests/boundary/cli/productCatalog.test.ts index 77fdec7df..6dcc93b66 100644 --- a/tests/boundary/cli/productCatalog.test.ts +++ b/tests/boundary/cli/productCatalog.test.ts @@ -95,7 +95,7 @@ describe("canonical product catalog", () => { ); }); - it("matches every source-derived checked-in product fact", async () => { + it("matches every source-derived build-generated product fact", async () => { const catalog = await createProductCatalog(root); expect(catalog.tools.total).toBe(TOOL_CONTRACTS.length); expect( @@ -204,10 +204,10 @@ describe("canonical product catalog", () => { providerCatalogDigest(catalog.providers), ); expect( - JSON.parse(await readFile("docs/product-catalog.json", "utf8")), + JSON.parse(await readFile("docs/public/product-catalog.json", "utf8")), ).toEqual(catalog); expect(await serializeProductCatalog(catalog)).toBe( - await readFile("docs/product-catalog.json", "utf8"), + await readFile("docs/public/product-catalog.json", "utf8"), ); await expect( assertDocumentationFacts(root, catalog), diff --git a/tests/boundary/filesystem/dependencyInstall.test.ts b/tests/boundary/filesystem/dependencyInstall.test.ts index fac9035c9..39c771791 100644 --- a/tests/boundary/filesystem/dependencyInstall.test.ts +++ b/tests/boundary/filesystem/dependencyInstall.test.ts @@ -164,7 +164,7 @@ const createBuildCacheFixture = async (): Promise => { "scripts/clean-build-output.mjs", "scripts/generate-package-metadata.mjs", "scripts/lib/generated-file.mjs", - "skills/reverse-engineer-anything/SKILL.md", + "skill-src/reverse-engineer-anything/SKILL.md", "src/generatedPackageMetadata.ts", ]; await Promise.all( diff --git a/tests/boundary/filesystem/generatedSkill.test.ts b/tests/boundary/filesystem/generatedSkill.test.ts new file mode 100644 index 000000000..c5db95c1a --- /dev/null +++ b/tests/boundary/filesystem/generatedSkill.test.ts @@ -0,0 +1,125 @@ +import { execFile } from "node:child_process"; +import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const exec = promisify(execFile); +const repositoryRoot = fileURLToPath(new URL("../../..", import.meta.url)); +const authored = + '---\nname: reverse-engineer-anything\nmetadata:\n version: "33"\n---\n\n# Instructions\n\nInspect the caller-selected artifact.\n'; + +async function fixture(eol = "\n") { + const root = await createTestTempDirectory("rea-generated-skill-"); + for (const path of [ + "scripts/generate-skill-metadata.mjs", + "scripts/lib/generated-file.mjs", + ]) { + await mkdir(dirname(join(root, path)), { recursive: true }); + await copyFile(join(repositoryRoot, path), join(root, path)); + } + const sourceRoot = join(root, "skill-src/reverse-engineer-anything"); + const outputRoot = join(root, "skills/reverse-engineer-anything"); + await mkdir(join(sourceRoot, "references"), { recursive: true }); + await mkdir(join(root, "dist")); + await writeFile(join(root, "package.json"), '{"type":"module"}\n'); + await writeFile(join(sourceRoot, "SKILL.md"), authored.replaceAll("\n", eol)); + await writeFile( + join(sourceRoot, "references/guide.md"), + `# Guide${eol}Preserve evidence.${eol}`, + ); + const identity = async (count: number, digest: string) => + writeFile( + join(root, "dist/catalogIdentity.js"), + `export const CATALOG_IDENTITY = ${JSON.stringify({ counts: { mcp_tools: count }, digests: { combined_sha256: digest } })};\n`, + ); + await identity(139, "a".repeat(64)); + const generate = (...args: string[]) => + exec( + process.execPath, + [join(root, "scripts/generate-skill-metadata.mjs"), ...args], + { cwd: root }, + ); + return { sourceRoot, outputRoot, identity, generate }; +} + +it.each(["\n", "\r\n"])( + "generates revision-specific metadata without rewriting authored instructions (%j)", + async (eol) => { + const f = await fixture(eol); + await f.generate(); + const first = await readFile(join(f.outputRoot, "SKILL.md"), "utf8"); + expect(first).not.toContain("catalog_digest:"); + expect(first).toContain("tool_count: 139"); + expect( + first.replace(/^ {2}(?:tool_count|catalog_digest):[^\n]*\n/gmu, ""), + ).toBe(authored); + expect( + await readFile(join(f.outputRoot, "references/guide.md"), "utf8"), + ).toBe("# Guide\nPreserve evidence.\n"); + await f.identity(139, "b".repeat(64)); + await f.generate(); + expect(await readFile(join(f.outputRoot, "SKILL.md"), "utf8")).toBe(first); + await f.identity(140, "b".repeat(64)); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("missing or stale"), + }); + await f.generate(); + expect(await readFile(join(f.outputRoot, "SKILL.md"), "utf8")).toContain( + "tool_count: 140", + ); + expect(await readFile(join(f.sourceRoot, "SKILL.md"), "utf8")).toBe( + authored.replaceAll("\n", eol), + ); + expect( + await readFile(join(f.sourceRoot, "references/guide.md"), "utf8"), + ).toBe(`# Guide${eol}Preserve evidence.${eol}`); + await expect(f.generate("--check")).resolves.toMatchObject({ stderr: "" }); + }, +); + +it("rejects missing, stale, and extra packaged references and removes deleted source files on rebuild", async () => { + const f = await fixture(); + await f.generate(); + const guide = join(f.outputRoot, "references/guide.md"); + await rm(guide); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("guide.md is missing or stale"), + }); + await f.generate(); + await writeFile(guide, "modified reference\n"); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("guide.md is missing or stale"), + }); + await f.generate(); + await writeFile( + join(f.outputRoot, "references/removed.md"), + "obsolete instructions\n", + ); + await expect(f.generate("--check")).rejects.toMatchObject({ + stderr: expect.stringContaining("file inventory drifted"), + }); + await rm(join(f.sourceRoot, "references/guide.md")); + await f.generate(); + await expect(readFile(guide)).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(join(f.outputRoot, "references/removed.md")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await f.generate("--check"); +}); + +it("rejects catalog-dependent metadata in the authored source", async () => { + const f = await fixture(); + await writeFile( + join(f.sourceRoot, "SKILL.md"), + authored.replace(' version: "33"', ' version: "33"\n tool_count: 139'), + ); + await expect(f.generate()).rejects.toMatchObject({ + stderr: expect.stringContaining( + "Catalog metadata belongs in the generated skill", + ), + }); +}); diff --git a/tests/boundary/filesystem/releaseCheckpoint.test.ts b/tests/boundary/filesystem/releaseCheckpoint.test.ts index 6ab3e2496..00fff5087 100644 --- a/tests/boundary/filesystem/releaseCheckpoint.test.ts +++ b/tests/boundary/filesystem/releaseCheckpoint.test.ts @@ -50,7 +50,6 @@ async function metadata(directory: string, version: string, notes: string) { version, packages: [{ identifier: "rea-agents", version }], }, - "docs/product-catalog.json": { package: { version } }, "release-please-config.json": { "changelog-sections": [ { type: "feat", hidden: false }, @@ -371,7 +370,6 @@ it("validates candidate options before inspecting the working directory", async it.each([ "package-lock.json", "server.json", - "docs/product-catalog.json", "src/generatedPackageMetadata.ts", ])("rejects inconsistent version metadata in %s", async (path) => { const f = await fixture(); diff --git a/tests/boundary/filesystem/releaseWorkflow.test.ts b/tests/boundary/filesystem/releaseWorkflow.test.ts index 7385c3e4f..c8d404ef1 100644 --- a/tests/boundary/filesystem/releaseWorkflow.test.ts +++ b/tests/boundary/filesystem/releaseWorkflow.test.ts @@ -103,11 +103,11 @@ it("requires explicit release preparation or publication instead of main pushes" "skip-github-release": "${{ inputs.phase == 'prepare' }}", "skip-github-pull-request": "${{ inputs.phase == 'publish' }}", }); - const catalogCommit = workflow.jobs["release-please"].steps.find( - (step) => step.name === "Commit canonical release catalog", + const catalogValidation = workflow.jobs["release-please"].steps.find( + (step) => step.name === "Validate generated release documentation", ); - expect(catalogCommit?.env?.GH_TOKEN).toBe( - "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", + expect(catalogValidation?.run).toBe( + "npm run docs:check && git diff --exit-code", ); }); @@ -203,7 +203,7 @@ it("binds npm and MCP publication to the same immutable release SHA", async () = "Set up Node.js for generated documentation", "Install dependencies", "Regenerate release documentation", - "Commit canonical release catalog", + "Validate generated release documentation", ]; for (const name of preparation) { expect( diff --git a/tests/boundary/filesystem/setupSkill.test.ts b/tests/boundary/filesystem/setupSkill.test.ts index aa457b057..e90adeb1b 100644 --- a/tests/boundary/filesystem/setupSkill.test.ts +++ b/tests/boundary/filesystem/setupSkill.test.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; @@ -14,6 +15,7 @@ import { import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; +import { z } from "zod"; import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs"; describe("canonical skill transaction", () => { @@ -88,6 +90,7 @@ describe("canonical skill transaction", () => { expect(installedSkill).toContain( `tool_count: ${String(TOOL_CONTRACTS.length)}`, ); + expect(installedSkill).not.toContain("catalog_digest:"); expect(await readFile(sibling, "utf8")).toBe("unrelated skill\n"); expect( await readFile( @@ -102,6 +105,45 @@ describe("canonical skill transaction", () => { expect(await installCanonicalSkill(home)).toBe("unchanged"); expect(await skillReferenceIssues(dirname(destination))).toEqual([]); }); + + it("binds portable conformance to the generated skill bytes that setup installs", async () => { + const home = await createTestTempDirectory("rea-skill-commitment-"); + expect(await installCanonicalSkill(home)).toBe("installed"); + const paths = [ + "SKILL.md", + "references/android-applications.md", + "references/evidence-workflows.md", + "references/javascript-applications.md", + "references/native-and-artifacts.md", + "references/runtime-observation.md", + ]; + const records = await Promise.all( + paths.map(async (path) => { + const bytes = await readFile( + join(home, ".agents/skills/reverse-engineer-anything", path), + ); + return `${path}\0${createHash("sha256").update(bytes).digest("hex")}\n`; + }), + ); + const manifest = z + .object({ + skill_digests: z.array( + z.object({ skill_id: z.string(), sha256: z.string() }), + ), + }) + .parse( + JSON.parse( + await readFile( + "docs/verification/managed-conformance-manifest.json", + "utf8", + ), + ), + ); + expect(manifest.skill_digests).toContainEqual({ + skill_id: "reverse-engineer-anything", + sha256: createHash("sha256").update(records.join("")).digest("hex"), + }); + }); }); it("doctor verifies installed instructions and references even when metadata is current", async () => { diff --git a/tests/conformance/releaseConfiguration.test.ts b/tests/conformance/releaseConfiguration.test.ts index abe6bb478..ddcadac61 100644 --- a/tests/conformance/releaseConfiguration.test.ts +++ b/tests/conformance/releaseConfiguration.test.ts @@ -47,11 +47,6 @@ describe("release configuration", () => { path: "server.json", jsonpath: "$.packages[0].version", }, - { - type: "json", - path: "docs/product-catalog.json", - jsonpath: "$.package.version", - }, ], }, }, diff --git a/turbo.json b/turbo.json index 5ab13eb44..6fcbf9fb1 100644 --- a/turbo.json +++ b/turbo.json @@ -10,8 +10,14 @@ "scripts/clean-build-output.mjs", "scripts/generate-mcp-tool-catalog.mjs", "scripts/generate-package-metadata.mjs", - "scripts/lib/generated-file.mjs", - "skills/reverse-engineer-anything/SKILL.md", + "scripts/generate-skill-metadata.mjs", + "scripts/generate-product-catalog.mjs", + "scripts/generate-completion-ledger.mjs", + "scripts/lib/**/*.mjs", + "scripts/verify/managed/**", + "scripts/fixtures/managed/**", + ".oxfmtrc.json", + "skill-src/**", "src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.fixture.ts", @@ -22,7 +28,11 @@ "outputs": [ "dist/**", ".cache/tsconfig.build.tsbuildinfo", - "src/generatedMcpToolCatalog.ts" + "src/generatedMcpToolCatalog.ts", + "skills/**", + "docs/public/product-catalog.json", + "docs/verification/managed-conformance-manifest.json", + "docs/verification/managed-conformance-ledger.json" ] }, "typecheck": { @@ -46,7 +56,7 @@ "package-lock.json", "scripts/generate-package-metadata.mjs", "scripts/lib/generated-file.mjs", - "skills/reverse-engineer-anything/SKILL.md", + "skill-src/reverse-engineer-anything/SKILL.md", "src/generatedPackageMetadata.ts" ] }, @@ -124,6 +134,7 @@ "src/generated*.ts", "src/contracts/**/*.ts", "docs/**", + "skill-src/**", "skills/**/SKILL.md", "README.md" ]