diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 23593e4..584d85a 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,52 +1,44 @@ # BUILD_REPORT ## sprint objective -Implement Bridge Sprint 3 (`B3`) review queue + explainability scope: -- ship `alice_review_queue` -- ship `alice_review_apply` -- support review actions (`approve`, `reject`, `edit-and-approve`, `supersede-existing`) -- expose explanation/provenance rationale in review surfaces -- verify deterministic recall/resume effects after approved review actions +Deliver Bridge Sprint 4 (`B4`) closeout scope only: package and document the shipped bridge phase for external operators, publish recommended/fallback Hermes config guidance, strengthen bridge smoke validation evidence, and provide a one-command local demo path. ## completed work -- Added MCP tool surface `alice_review_queue` with deterministic queue/detail behavior. -- Added MCP tool surface `alice_review_apply` with B3 action vocabulary mapped to continuity correction semantics: - - `approve` -> `confirm` - - `edit-and-approve` -> `edit` - - `reject` -> `delete` - - `supersede-existing` -> `supersede` -- Kept `alice_memory_review` and `alice_memory_correct` as compatibility aliases. -- Extended continuity review serialization to include shared explanation records on review objects. -- Added deterministic `proposal_rationale` to continuity explanation output. -- Ensured explanation chain remains shared across review, recall, and resume paths. -- Updated B3-scoped integration docs for MCP and Hermes memory-provider guidance. -- Updated architecture status markers so B3 review surfaces are marked implemented and only B4 follow-up remains planned. -- Updated control-doc truth checker markers to B3 active-sprint truth. -- Updated B3 review evidence report (`REVIEW_REPORT.md`). -- Added/updated sprint-owned tests for: - - MCP tool surface and B3 names - - action alias mapping and deterministic correction semantics - - review queue explainability presence - - recall exclusion after reject and recall/resume updates after supersede +- Published canonical B4 operator guide: `docs/integrations/hermes-bridge-operator-guide.md`. +- Published operator decision note: `docs/integrations/hermes-provider-plus-mcp-why.md`. +- Published concrete Hermes `config.yaml` examples: + - recommended path: `docs/integrations/examples/hermes-config.provider-plus-mcp.yaml` + - fallback path: `docs/integrations/examples/hermes-config.mcp-only.yaml` +- Updated in-scope integration docs (`README.md`, `hermes.md`, `hermes-memory-provider.md`, `mcp.md`, `hermes-skill-pack.md`) to align on: + - recommended path: provider plus MCP + - fallback path: MCP-only + - migration path from MCP-only to provider plus MCP + - one-command demo command: `./.venv/bin/python scripts/run_hermes_bridge_demo.py` +- Strengthened `scripts/run_hermes_mcp_smoke.py` to validate bridge flow beyond recall/resume/open-loops by also validating B2/B3 capture and review operations (`alice_capture_candidates`, `alice_commit_captures`, `alice_review_queue`, `alice_review_apply`). +- Added one-command demo helper: `scripts/run_hermes_bridge_demo.py`. +- Added sprint-owned validation coverage for the demo helper: `tests/unit/test_hermes_bridge_demo.py`. +- Updated `scripts/check_control_doc_truth.py` required markers to B4-active truth so the required verifier aligns with the active sprint packet. +- Updated `REVIEW_REPORT.md` to grade against B4-specific acceptance criteria and evidence. ## incomplete work -- None in B3 sprint scope. +- None within B4 sprint scope. ## files changed -- `ARCHITECTURE.md` - `PRODUCT_BRIEF.md` - `README.md` - `ROADMAP.md` -- `apps/api/src/alicebot_api/continuity_explainability.py` -- `apps/api/src/alicebot_api/continuity_review.py` -- `apps/api/src/alicebot_api/contracts.py` -- `apps/api/src/alicebot_api/mcp_tools.py` -- `docs/integrations/mcp.md` - `docs/integrations/hermes-memory-provider.md` +- `docs/integrations/hermes-skill-pack.md` +- `docs/integrations/hermes.md` +- `docs/integrations/mcp.md` +- `docs/integrations/hermes-bridge-operator-guide.md` +- `docs/integrations/hermes-provider-plus-mcp-why.md` +- `docs/integrations/examples/hermes-config.provider-plus-mcp.yaml` +- `docs/integrations/examples/hermes-config.mcp-only.yaml` +- `scripts/run_hermes_mcp_smoke.py` +- `scripts/run_hermes_bridge_demo.py` - `scripts/check_control_doc_truth.py` -- `tests/unit/test_continuity_review.py` -- `tests/unit/test_mcp.py` -- `tests/integration/test_mcp_server.py` +- `tests/unit/test_hermes_bridge_demo.py` - `REVIEW_REPORT.md` - `BUILD_REPORT.md` @@ -54,15 +46,21 @@ Implement Bridge Sprint 3 (`B3`) review queue + explainability scope: - `python3 scripts/check_control_doc_truth.py` - Result: PASS - `./.venv/bin/python -m pytest tests/unit tests/integration -q` - - Result: `1189 passed in 196.98s (0:03:16)` (latest re-run) + - Result: `1191 passed in 187.48s (0:03:07)` - `./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py` - Result: PASS - - Evidence summary: single-external-provider enforcement message emitted; structural payload reports `single_external_enforced=true` and `bridge_status.ready=true`. - - Local filesystem-specific path fields from script output were intentionally omitted for identifier hygiene. + - Evidence summary: `bridge_status.ready=true`, `single_external_enforced=true`, provider registered. +- `./.venv/bin/python scripts/run_hermes_mcp_smoke.py` + - Result: PASS + - Evidence summary: required Hermes MCP tools registered, `recall_items=2`, `open_loop_count=1`, `capture_candidate_count=2`, `capture_auto_saved_count=1`, `capture_review_queued_count=1`, `review_apply_resolved_action=confirm`. +- `./.venv/bin/python scripts/run_hermes_bridge_demo.py` + - Result: PASS + - Evidence summary: `status=pass`, `recommended_path=provider_plus_mcp`, `fallback_path=mcp_only`. ## blockers/issues -- No functional blockers. -- No outstanding evidence or documentation blockers after alignment updates. +- Initial run of `scripts/run_hermes_mcp_smoke.py` failed due local database schema lag and sandbox DB access restriction. +- Resolved by applying local migrations (`./scripts/migrate.sh`) and rerunning smoke commands with local DB access available. +- No remaining blockers. ## recommended next step -Proceed to Bridge Sprint 4 (`B4`) packaging/docs/smoke closeout using the now-shipped B3 review queue/apply surfaces as baseline. +Request B4 review against this evidence and, if approved, proceed with the single sprint PR for squash merge closeout. diff --git a/PRODUCT_BRIEF.md b/PRODUCT_BRIEF.md index 5db4bfe..39fc2ea 100644 --- a/PRODUCT_BRIEF.md +++ b/PRODUCT_BRIEF.md @@ -70,7 +70,7 @@ Review-required: - low-confidence extractions ## Active Sprint Status -Bridge Sprint 3 (`B3`) is now the active execution sprint. It is limited to review queue and explainability work on top of the shipped Hermes provider surface plus the `B1` and `B2` bridge foundations. +Bridge Sprint 4 (`B4`) is now the active execution sprint. It is limited to packaging, docs, config examples, smoke validation, and local-demo closeout on top of the shipped `B1` through `B3` bridge foundations. ## Known Gaps To Resolve Before Build - Candidate scoring rubric and confidence calibration method are not specified. diff --git a/README.md b/README.md index 744fcb6..2f4497a 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Phase 11 is complete and shipped: - A bridge phase is now active: Hermes Auto-Capture - `B1` Hermes Provider Contract Foundation is shipped - `B2` Auto-Capture Pipeline is shipped -- `B3` Review Queue + Explainability is the active sprint +- `B3` Review Queue + Explainability is shipped +- `B4` Packaging, Docs, and Smoke Validation is the active sprint - Historical planning and control docs: [docs/archive/planning/2026-04-08-context-compaction/README.md](docs/archive/planning/2026-04-08-context-compaction/README.md) ## Why Alice exists @@ -217,6 +218,8 @@ This makes it straightforward to plug Alice into MCP-capable assistants and deve See: +- [docs/integrations/hermes-bridge-operator-guide.md](docs/integrations/hermes-bridge-operator-guide.md) +- [docs/integrations/hermes-provider-plus-mcp-why.md](docs/integrations/hermes-provider-plus-mcp-why.md) - [docs/integrations/mcp.md](docs/integrations/mcp.md) - [docs/integrations/hermes.md](docs/integrations/hermes.md) - [docs/integrations/hermes-memory-provider.md](docs/integrations/hermes-memory-provider.md) @@ -224,13 +227,22 @@ See: - [docs/integrations/phase11-local-provider-adapters.md](docs/integrations/phase11-local-provider-adapters.md) - [docs/integrations/phase11-azure-autogen.md](docs/integrations/phase11-azure-autogen.md) -Hermes runtime smoke test: +Recommended Hermes architecture is provider plus MCP, with MCP-only as a fallback. + +One-command Hermes bridge demo: + +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py +``` + +Hermes runtime smoke tests: ```bash -./scripts/run_hermes_mcp_smoke.py +./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py +./.venv/bin/python scripts/run_hermes_mcp_smoke.py ``` -If you use Hermes, Alice supports three integration modes: MCP, skill pack, and a first-class external memory provider for turn prefetch plus recall, resumption, and open-loop tools. +If you use Hermes, run provider plus MCP as the recommended mode, add the skill pack for policy guidance, and keep MCP-only available as fallback. ## OpenClaw and imported workflows diff --git a/REVIEW_REPORT.md b/REVIEW_REPORT.md index 9c642b0..3ea9af9 100644 --- a/REVIEW_REPORT.md +++ b/REVIEW_REPORT.md @@ -4,43 +4,54 @@ PASS ## criteria met -- `alice_review_queue` is implemented and exposed on MCP. -- `alice_review_apply` is implemented and exposed on MCP. -- Required B3 review actions are supported through shipped surface semantics: - - `approve` -> `confirm` - - `edit-and-approve` -> `edit` - - `reject` -> `delete` - - `supersede-existing` -> `supersede` -- Review payloads now include explainability/provenance chain data (`source_facts`, `evidence_segments`, `trust`, `supersession_notes`, `proposal_rationale`). -- Approved review actions deterministically affect later recall/resume behavior (validated in integration flow using supersede). -- Rejected review items are not treated as accepted continuity state (validated by recall exclusion after reject). -- No local identifiers (local usernames/absolute machine paths) were found in changed code/docs/reports. +- External-operator Hermes bridge documentation is complete and B4-specific via: + - `docs/integrations/hermes-bridge-operator-guide.md` + - `docs/integrations/hermes-provider-plus-mcp-why.md` +- Provider-plus-MCP recommended architecture is documented clearly. +- MCP-only fallback path is documented clearly. +- MCP-only to provider-plus-MCP migration guidance is documented. +- Example Hermes configs are present for both paths: + - `docs/integrations/examples/hermes-config.provider-plus-mcp.yaml` + - `docs/integrations/examples/hermes-config.mcp-only.yaml` +- One-command local demo path is present and documented: + - `./.venv/bin/python scripts/run_hermes_bridge_demo.py` +- Smoke validation for the shipped bridge path passes: + - provider smoke PASS + - MCP smoke PASS (including B2/B3 capture/review flow checks) + - bridge demo PASS +- `BUILD_REPORT.md` now lists the exact sprint-owned changed files, including the previously omitted `PRODUCT_BRIEF.md` and `ROADMAP.md`. +- No local identifiers (local machine paths/usernames) were found in changed docs/scripts/reports. +- No B4 changes reopen B1/B2/B3 implementation scope or imply post-bridge scope. ## criteria missed -- None functionally against B3 acceptance criteria. +- None. ## quality issues -- No blocking quality issues found in B3 scope. +- No blocking quality issues found in B4 scope. ## regression risks -- Low: MCP surface additions are additive, and targeted + full unit/integration suites pass. -- Moderate-low: review queue objects now include full explanation payloads, increasing response size; monitor MCP client assumptions on payload size/shape. +- Low: changes are primarily docs/config examples plus additive smoke/demo orchestration. +- Moderate-low: expanded MCP smoke depends on local schema being migrated before execution. ## docs issues -- None blocking. Architecture and build evidence alignment issues are fixed. +- None blocking. ## should anything be added to RULES.md? - No required change. ## should anything update ARCHITECTURE.md? -- No additional architecture changes required for B3. +- No required architecture update for B4 closeout. ## recommended next action -1. Approve B3 for merge. -2. Start B4 packaging/docs/demo closeout. +1. Proceed with sprint PR finalization and squash-merge flow. +2. Keep the bridge demo command in release notes/operator handoff for external adopters. ## verification evidence checked - `python3 scripts/check_control_doc_truth.py` -> PASS -- `./.venv/bin/python -m pytest tests/unit/test_continuity_review.py tests/unit/test_mcp.py tests/integration/test_mcp_server.py -q` -> `13 passed` -- `./.venv/bin/python -m pytest tests/unit tests/integration -q` -> `1189 passed in 196.98s (0:03:16)` +- `./.venv/bin/python -m pytest tests/unit tests/integration -q` -> `1191 passed in 187.48s (0:03:07)` - `./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py` -> PASS +- `./.venv/bin/python scripts/run_hermes_mcp_smoke.py` -> PASS +- `./.venv/bin/python scripts/run_hermes_bridge_demo.py` -> PASS +- Recommended path documented: `provider_plus_mcp` +- Fallback path documented: `mcp_only` +- Demo command documented: `./.venv/bin/python scripts/run_hermes_bridge_demo.py` diff --git a/ROADMAP.md b/ROADMAP.md index 4ec5487..8331f1f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,8 +8,8 @@ Phase 11 remains baseline truth and is not future scope. ## Active Planning Status -- Bridge Sprint 3 (`B3`) is the active execution sprint. -- The remaining bridge-phase milestones are planned but not yet promoted. +- Bridge Sprint 4 (`B4`) is the active execution sprint. +- `B4` is the final planned bridge-phase sprint. ## Bridge Phase: Hermes Auto-Capture (Planned) diff --git a/docs/integrations/examples/hermes-config.mcp-only.yaml b/docs/integrations/examples/hermes-config.mcp-only.yaml new file mode 100644 index 0000000..4197a3d --- /dev/null +++ b/docs/integrations/examples/hermes-config.mcp-only.yaml @@ -0,0 +1,22 @@ +# Fallback deployment shape when provider install is not available. +# This keeps explicit MCP workflows, but no provider prefetch/capture hooks. +memory: + provider: builtin + +mcp_servers: + alice_core: + command: "/path/to/alicebot/.venv/bin/python" + args: ["-m", "alicebot_api.mcp_server"] + env: + DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot" + ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001" + PYTHONPATH: "/path/to/alicebot/apps/api/src:/path/to/alicebot/workers" + tools: + include: + - alice_recall + - alice_resume + - alice_open_loops + - alice_review_queue + - alice_review_apply + resources: false + prompts: false diff --git a/docs/integrations/examples/hermes-config.provider-plus-mcp.yaml b/docs/integrations/examples/hermes-config.provider-plus-mcp.yaml new file mode 100644 index 0000000..08c7295 --- /dev/null +++ b/docs/integrations/examples/hermes-config.provider-plus-mcp.yaml @@ -0,0 +1,24 @@ +# Recommended bridge deployment shape. +# Keep the Alice memory provider active for always-on prefetch/capture, +# and keep Alice MCP attached for explicit deep workflows. +memory: + provider: alice + +mcp_servers: + alice_core: + command: "/path/to/alicebot/.venv/bin/python" + args: ["-m", "alicebot_api.mcp_server"] + env: + DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot" + ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001" + PYTHONPATH: "/path/to/alicebot/apps/api/src:/path/to/alicebot/workers" + tools: + include: + - alice_recall + - alice_resume + - alice_open_loops + - alice_review_queue + - alice_review_apply + - alice_explain + resources: false + prompts: false diff --git a/docs/integrations/hermes-bridge-operator-guide.md b/docs/integrations/hermes-bridge-operator-guide.md new file mode 100644 index 0000000..ba07c88 --- /dev/null +++ b/docs/integrations/hermes-bridge-operator-guide.md @@ -0,0 +1,101 @@ +# Hermes Bridge Operator Guide (B4) + +This is the canonical operator guide for the shipped bridge phase. + +Recommended deployment shape: **provider plus MCP**. + +- Provider handles always-on prefetch and post-turn lifecycle hooks. +- MCP handles explicit deep workflows (review, correction, explainability, targeted recall). + +Use MCP-only as a fallback when provider install is temporarily blocked. + +## Integration Modes + +| Mode | Status | When to use | Tradeoff | +|---|---|---|---| +| Provider + MCP | Recommended | Default production/dev setup | Full bridge behavior with explicit deep actions | +| MCP-only | Fallback | Provider plugin cannot be installed yet | No provider lifecycle hooks or automatic prefetch/capture | + +## Config Examples (`~/.hermes/config.yaml`) + +- Recommended mode: `docs/integrations/examples/hermes-config.provider-plus-mcp.yaml` +- Fallback mode: `docs/integrations/examples/hermes-config.mcp-only.yaml` + +### Recommended snippet (provider + MCP) + +```yaml +memory: + provider: alice + +mcp_servers: + alice_core: + command: "/path/to/alicebot/.venv/bin/python" + args: ["-m", "alicebot_api.mcp_server"] +``` + +### Fallback snippet (MCP-only) + +```yaml +memory: + provider: builtin + +mcp_servers: + alice_core: + command: "/path/to/alicebot/.venv/bin/python" + args: ["-m", "alicebot_api.mcp_server"] +``` + +## One-Command Local Demo + +Run the bridge demo command from this repository: + +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py +``` + +Expected result: + +- `status` is `pass` +- `recommended_path` is `provider_plus_mcp` +- `fallback_path` is `mcp_only` +- provider smoke and MCP smoke steps both return `0` + +## Validation Commands + +Run these directly when you need independent evidence: + +```bash +./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py +./.venv/bin/python scripts/run_hermes_mcp_smoke.py +``` + +## MCP-Only to Provider+MCP Migration + +1. Keep your existing MCP block unchanged. +2. Install the Alice provider plugin: + +```bash +./scripts/install_hermes_alice_memory_provider.py +``` + +3. Run Hermes memory setup and select `alice`: + +```bash +hermes memory setup +``` + +4. Set `memory.provider` to `alice` in `config.yaml`. +5. Re-run the one-command demo: + +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py +``` + +6. Keep MCP enabled for explicit review/correction/explain workflows. + +## Related Docs + +- `docs/integrations/hermes-provider-plus-mcp-why.md` +- `docs/integrations/hermes-memory-provider.md` +- `docs/integrations/hermes.md` +- `docs/integrations/hermes-skill-pack.md` diff --git a/docs/integrations/hermes-memory-provider.md b/docs/integrations/hermes-memory-provider.md index e2d6392..f99d81d 100644 --- a/docs/integrations/hermes-memory-provider.md +++ b/docs/integrations/hermes-memory-provider.md @@ -1,6 +1,10 @@ # Hermes External Memory Provider: Alice This guide installs Alice as a Hermes **external memory provider**. +For the canonical bridge operator path and config examples, see: + +- `docs/integrations/hermes-bridge-operator-guide.md` +- `docs/integrations/hermes-provider-plus-mcp-why.md` Hermes behavior with this provider: @@ -50,6 +54,11 @@ Optional flags: ## Configure +Recommended Hermes `config.yaml` examples are published here: + +- `docs/integrations/examples/hermes-config.provider-plus-mcp.yaml` (recommended) +- `docs/integrations/examples/hermes-config.mcp-only.yaml` (fallback) + Use the Hermes setup flow: ```bash @@ -91,6 +100,12 @@ Run provider smoke validation from this repository: ./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py ``` +Run the one-command bridge demo (provider smoke + MCP smoke): + +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py +``` + Smoke output includes `structural.bridge_status` with: - `ready`: bridge-phase config readiness @@ -130,8 +145,8 @@ Use this split to avoid overlapping integrations: Practical default: -- choose provider when you want continuity context injected every turn -- choose MCP when you need wider Alice operations beyond memory-provider scope +- choose provider plus MCP as the default deployment shape +- choose MCP-only only when provider install is temporarily blocked - add skill pack when you want stricter workflow prompting and response policy ## Provider Config Keys diff --git a/docs/integrations/hermes-provider-plus-mcp-why.md b/docs/integrations/hermes-provider-plus-mcp-why.md new file mode 100644 index 0000000..abea8bf --- /dev/null +++ b/docs/integrations/hermes-provider-plus-mcp-why.md @@ -0,0 +1,25 @@ +# Why Provider + MCP Is Recommended + +## Decision + +Use **provider plus MCP** as the default Hermes architecture for Alice continuity. + +## Why + +- Provider gives always-on turn-start continuity prefetch without requiring tool calls. +- Provider runs bridge lifecycle hooks (`prefetch`, `queue_prefetch`, `sync_turn`, `on_session_end`) so capture behavior is consistent. +- MCP preserves explicit deep workflows (`alice_review_queue`, `alice_review_apply`, `alice_explain`) for operator control. +- Keeping both paths avoids workflow regressions while preserving deterministic Alice semantics. + +## Fallback + +Use MCP-only when provider installation is blocked by environment policy. + +- Keep `memory.provider: builtin`. +- Keep Alice MCP server configured. +- Migrate to provider+MCP once provider install is available. + +## Operator Rule + +- Prefer provider for automatic continuity lifecycle behavior. +- Prefer MCP for explicit deep actions and audit-friendly corrections. diff --git a/docs/integrations/hermes-skill-pack.md b/docs/integrations/hermes-skill-pack.md index 8a0665f..5ee098a 100644 --- a/docs/integrations/hermes-skill-pack.md +++ b/docs/integrations/hermes-skill-pack.md @@ -58,9 +58,13 @@ HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" \ - Skills: decision policy and workflow instructions (when to call tools, how to format output, what evidence to include). - External memory provider: always-on prefetch and memory-provider-native recall tools. - MCP tools: runtime execution and deterministic continuity data retrieval/update. -- Practical rule: use skills to decide behavior; use provider or MCP to execute continuity reads/writes. +- Practical rule: use skills to decide behavior; use provider plus MCP as the recommended execution shape. -See `docs/integrations/hermes-memory-provider.md` for the external provider setup path. +See: + +- `docs/integrations/hermes-bridge-operator-guide.md` +- `docs/integrations/hermes-memory-provider.md` +- `docs/integrations/hermes.md` (MCP-only fallback) ## When Hermes Should Prefer Alice Tools diff --git a/docs/integrations/hermes.md b/docs/integrations/hermes.md index d5b5fbd..ebdce23 100644 --- a/docs/integrations/hermes.md +++ b/docs/integrations/hermes.md @@ -1,22 +1,18 @@ -# Hermes MCP Integration +# Hermes MCP Integration (Fallback Path) -This guide connects Hermes Agent to Alice MCP and verifies the exact tool path -for: +This document covers **MCP-only fallback** setup for Hermes. -- `alice_recall` -- `alice_resume` -- `alice_open_loops` +For the recommended deployment shape (provider plus MCP), use: -## Choose The Integration Path +- `docs/integrations/hermes-bridge-operator-guide.md` -- External memory provider: `docs/integrations/hermes-memory-provider.md` -- MCP tools: this document (`docs/integrations/hermes.md`) -- Hermes skill pack: `docs/integrations/hermes-skill-pack.md` +## When To Use This Path -Use MCP when you want broad Alice tool access through Hermes `mcp_servers`. -Use the external memory provider when you want always-on prefetch and memory -tools inside Hermes memory-provider flow. Use the skill pack for tool-routing -and response policy on top of either path. +Use MCP-only when the Alice provider plugin cannot be installed yet. + +- Keep `memory.provider: builtin`. +- Attach Alice through `mcp_servers`. +- Migrate to provider plus MCP when possible. ## Prerequisites @@ -26,21 +22,29 @@ and response policy on top of either path. ## Config (`~/.hermes/config.yaml`) -Use `mcp_servers` in Hermes config. - ### Option A: local command (direct Python) ```yaml +memory: + provider: builtin + mcp_servers: alice_core: - command: "/ABS/PATH/TO/AliceBot/.venv/bin/python" + command: "/path/to/alicebot/.venv/bin/python" args: ["-m", "alicebot_api.mcp_server"] env: DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot" ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001" - PYTHONPATH: "/ABS/PATH/TO/AliceBot/apps/api/src:/ABS/PATH/TO/AliceBot/workers" + PYTHONPATH: "/path/to/alicebot/apps/api/src:/path/to/alicebot/workers" tools: - include: [alice_recall, alice_resume, alice_open_loops] + include: + - alice_recall + - alice_resume + - alice_open_loops + - alice_capture_candidates + - alice_commit_captures + - alice_review_queue + - alice_review_apply resources: false prompts: false ``` @@ -48,26 +52,33 @@ mcp_servers: ### Option B: `npx` command (via `alice-cli` package) ```yaml +memory: + provider: builtin + mcp_servers: alice_core: command: "npx" - args: ["-y", "--package", "/ABS/PATH/TO/AliceBot/packages/alice-cli", "alice", "mcp"] + args: ["-y", "--package", "/path/to/alicebot/packages/alice-cli", "alice", "mcp"] env: NPM_CONFIG_CACHE: "/tmp/alice-npm-cache" - ALICEBOT_PYTHON: "/ABS/PATH/TO/AliceBot/.venv/bin/python" + ALICEBOT_PYTHON: "/path/to/alicebot/.venv/bin/python" DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot" ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001" - PYTHONPATH: "/ABS/PATH/TO/AliceBot/apps/api/src:/ABS/PATH/TO/AliceBot/workers" + PYTHONPATH: "/path/to/alicebot/apps/api/src:/path/to/alicebot/workers" tools: - include: [alice_recall, alice_resume, alice_open_loops] + include: + - alice_recall + - alice_resume + - alice_open_loops + - alice_capture_candidates + - alice_commit_captures + - alice_review_queue + - alice_review_apply resources: false prompts: false ``` -`alice mcp` shells out to `${ALICEBOT_PYTHON} -m alicebot_api.mcp_server`. - -If you have a published CLI version with `mcp` support, you can replace args -with: +If you use a published CLI package with `mcp` support, replace args with: ```yaml args: ["-y", "@aliceos/alice-cli", "mcp"] @@ -85,107 +96,40 @@ Expected: - `Tools discovered` - includes `alice_recall`, `alice_resume`, `alice_open_loops` -## Verify Tool Calls (Hermes Runtime Path) - -Run the smoke script: +## Verify Runtime Tool Calls ```bash -./scripts/run_hermes_mcp_smoke.py +./.venv/bin/python scripts/run_hermes_mcp_smoke.py ``` Expected JSON output includes: -- `registered_tools` containing: - - `mcp_alice_core_alice_recall` - - `mcp_alice_core_alice_resume` - - `mcp_alice_core_alice_open_loops` +- registered MCP tool names for recall/resume/open-loops and B2/B3 capture/review tools - non-zero `recall_items` -- `open_loop_count` >= `1` - -## Sample Hermes Prompts - -Hermes prefixes MCP tools as `mcp__`. With server name -`alice_core`, the names are: +- non-zero `capture_candidate_count` +- non-zero `capture_review_queued_count` +- `review_apply_resolved_action` = `confirm` -- `mcp_alice_core_alice_recall` -- `mcp_alice_core_alice_resume` -- `mcp_alice_core_alice_open_loops` +## One-Command Demo -Prompts: +For the full bridge demo command (provider smoke + MCP smoke): -```text -Use mcp_alice_core_alice_recall with {"query":"Hermes docs","limit":5} and summarize the top 3 memories. +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py ``` -```text -Use mcp_alice_core_alice_resume with {"thread_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","max_recent_changes":5,"max_open_loops":5}. Return only decisions, next action, and blockers. -``` +## Migrate To Recommended Path -```text -Use mcp_alice_core_alice_open_loops with {"thread_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","limit":10}. Group results by waiting_for, blocker, stale, next_action. -``` +When provider install is available, move from MCP-only to provider plus MCP: -## Alice Workflow Skill Pack +1. Install provider plugin: `./scripts/install_hermes_alice_memory_provider.py` +2. Run `hermes memory setup` and select `alice` +3. Set `memory.provider: alice` +4. Keep MCP server configured for deep workflows +5. Re-run `./.venv/bin/python scripts/run_hermes_bridge_demo.py` -To make Alice tool usage more consistent in Hermes sessions, install the -Hermes-native Alice skill pack: +## Related Docs +- `docs/integrations/hermes-bridge-operator-guide.md` +- `docs/integrations/hermes-memory-provider.md` - `docs/integrations/hermes-skill-pack.md` - -The pack includes: - -- `alice-continuity-recall` -- `alice-resumption` -- `alice-open-loop-review` -- `alice-explain-provenance` -- `alice-correction-loop` - -Skills decide when and how to call tools. MCP tools perform deterministic -continuity reads and writes. - -## Troubleshooting - -### `Connection failed` in `hermes mcp test` - -- Confirm `command` points to an existing executable. -- Use absolute paths for `command` and `PYTHONPATH`. -- Run the server command directly: - - `"/ABS/PATH/TO/AliceBot/.venv/bin/python" -m alicebot_api.mcp_server --help` - -### Tool list is missing `alice_recall`/`alice_resume`/`alice_open_loops` - -- Check `tools.include` values are unprefixed tool names: - - `alice_recall`, `alice_resume`, `alice_open_loops` -- Run `/reload-mcp` in Hermes after config changes. -- Re-run `hermes mcp test alice_core`. - -### Tools register but calls fail at runtime - -- Validate `DATABASE_URL` is reachable and points to a migrated DB. -- Validate `ALICEBOT_AUTH_USER_ID` is a UUID string. -- Run `./scripts/run_hermes_mcp_smoke.py` to isolate server/runtime issues. - -### `npx` path fails - -- Check `npx --version`. -- Ensure `args` contains a valid local package path or a published package. -- If npm cache permissions are locked down, set `NPM_CONFIG_CACHE` to a writable path. -- If `npx` is blocked in your environment, use Option A (local command). - -## Demo Screenshots - -`hermes mcp test` against Alice: - -![Hermes MCP test with Alice](assets/hermes/hermes-mcp-test.png) - -Hermes runtime tool-call smoke result: - -![Hermes runtime smoke result](assets/hermes/hermes-runtime-smoke.png) - -## Test Record - -Validated on `2026-04-09`: - -- `HERMES_HOME=/tmp/alice-hermes-home ./.venv/bin/hermes mcp test alice_core` (local command config) -- `HERMES_HOME=/tmp/alice-hermes-home ./.venv/bin/hermes mcp test alice_core` (`npx --package ... alice mcp` config) -- `./scripts/run_hermes_mcp_smoke.py` diff --git a/docs/integrations/mcp.md b/docs/integrations/mcp.md index 3ff01e7..ebb273b 100644 --- a/docs/integrations/mcp.md +++ b/docs/integrations/mcp.md @@ -67,10 +67,22 @@ MCP uses the same local runtime scope as CLI: For Hermes Agent-specific setup, prompts, and troubleshooting: +- `docs/integrations/hermes-bridge-operator-guide.md` (recommended provider+MCP path) - `docs/integrations/hermes.md` - `docs/integrations/hermes-memory-provider.md` - `docs/integrations/hermes-skill-pack.md` +Recommended bridge deployment shape: + +- provider plus MCP is the default operator path +- MCP-only remains available as fallback when provider install is blocked + +One-command bridge demo: + +```bash +./.venv/bin/python scripts/run_hermes_bridge_demo.py +``` + ## Contract Guardrails - tool set is intentionally narrow and stable diff --git a/scripts/check_control_doc_truth.py b/scripts/check_control_doc_truth.py index c5b007c..32e78f0 100644 --- a/scripts/check_control_doc_truth.py +++ b/scripts/check_control_doc_truth.py @@ -21,7 +21,7 @@ class ControlDocTruthRule: "Phase 10 is complete and shipped.", "Phase 11 is complete and shipped:", "`B2` Auto-Capture Pipeline is shipped", - "`B3` Review Queue + Explainability is the active sprint", + "`B4` Packaging, Docs, and Smoke Validation is the active sprint", "Historical planning and control docs: [docs/archive/planning/2026-04-08-context-compaction/README.md]", ), ), @@ -29,13 +29,13 @@ class ControlDocTruthRule: relative_path="ROADMAP.md", required_markers=( "Phase 11 remains baseline truth and is not future scope.", - "Bridge Sprint 3 (`B3`) is the active execution sprint.", + "Bridge Sprint 4 (`B4`) is the active execution sprint.", ), ), ControlDocTruthRule( relative_path=".ai/active/SPRINT_PACKET.md", required_markers=( - "Bridge Sprint 3 (B3): Review Queue + Explainability", + "Bridge Sprint 4 (B4): Packaging, Docs, and Smoke Validation", "Phase 10 is complete and shipped.", "Phase 11 is complete and shipped.", ), @@ -53,7 +53,7 @@ class ControlDocTruthRule: "Phase 9 is shipped.", "Phase 10 is shipped.", "Phase 11 is shipped and remains baseline truth.", - "Bridge Sprint 3 (`B3`) is the active execution sprint.", + "Bridge Sprint 4 (`B4`) is the active execution sprint.", ), ), ControlDocTruthRule( diff --git a/scripts/run_hermes_bridge_demo.py b/scripts/run_hermes_bridge_demo.py new file mode 100755 index 0000000..5c1f428 --- /dev/null +++ b/scripts/run_hermes_bridge_demo.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import subprocess +import sys +from typing import Any + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_PROVIDER_SMOKE_SCRIPT = REPO_ROOT / "scripts" / "run_hermes_memory_provider_smoke.py" +DEFAULT_MCP_SMOKE_SCRIPT = REPO_ROOT / "scripts" / "run_hermes_mcp_smoke.py" +DEFAULT_DEMO_COMMAND = "./.venv/bin/python scripts/run_hermes_bridge_demo.py" + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="run_hermes_bridge_demo.py", + description=( + "Run the bridge-phase Hermes demo in one command by executing " + "provider and MCP smoke validations and printing a compact summary." + ), + ) + parser.add_argument( + "--python-command", + default=sys.executable, + help="Python executable used to run smoke scripts.", + ) + parser.add_argument( + "--provider-smoke-script", + type=Path, + default=DEFAULT_PROVIDER_SMOKE_SCRIPT, + help="Path to run_hermes_memory_provider_smoke.py.", + ) + parser.add_argument( + "--mcp-smoke-script", + type=Path, + default=DEFAULT_MCP_SMOKE_SCRIPT, + help="Path to run_hermes_mcp_smoke.py.", + ) + parser.add_argument( + "--database-url", + default=os.getenv("DATABASE_URL", ""), + help="Optional DATABASE_URL override passed to the MCP smoke script.", + ) + return parser + + +def _parse_json_output(stdout: str) -> dict[str, Any] | None: + payload = stdout.strip() + if payload == "": + return None + try: + parsed = json.loads(payload) + except json.JSONDecodeError: + return None + return parsed if isinstance(parsed, dict) else None + + +def _short_text(value: str, *, limit: int = 240) -> str: + normalized = " ".join(value.split()) + if len(normalized) <= limit: + return normalized + return normalized[: limit - 3] + "..." + + +def _run_step(*, name: str, command: list[str]) -> dict[str, Any]: + completed = subprocess.run( + command, + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + parsed = _parse_json_output(completed.stdout) + return { + "name": name, + "command": command, + "returncode": completed.returncode, + "json": parsed, + "stdout_excerpt": _short_text(completed.stdout), + "stderr_excerpt": _short_text(completed.stderr), + } + + +def _provider_checks(step: dict[str, Any]) -> dict[str, Any]: + payload = step.get("json") + if not isinstance(payload, dict): + return {"json_available": False, "bridge_ready": False} + + structural = payload.get("structural") + if not isinstance(structural, dict): + return {"json_available": True, "bridge_ready": False} + + bridge_status = structural.get("bridge_status") + if not isinstance(bridge_status, dict): + return {"json_available": True, "bridge_ready": False} + + return { + "json_available": True, + "bridge_ready": bool(bridge_status.get("ready", False)), + "provider_registered": bool(structural.get("alice_registered", False)), + "single_external_enforced": bool(structural.get("single_external_enforced", False)), + } + + +def _mcp_checks(step: dict[str, Any]) -> dict[str, Any]: + payload = step.get("json") + if not isinstance(payload, dict): + return {"json_available": False, "bridge_flow_validated": False} + + required_fields = ( + "recall_items", + "open_loop_count", + "capture_candidate_count", + "capture_auto_saved_count", + "capture_review_queued_count", + "review_apply_resolved_action", + ) + if not all(field in payload for field in required_fields): + return {"json_available": True, "bridge_flow_validated": False} + + return { + "json_available": True, + "bridge_flow_validated": ( + payload.get("recall_items", 0) > 0 + and payload.get("open_loop_count", 0) > 0 + and payload.get("capture_candidate_count", 0) > 0 + and payload.get("capture_auto_saved_count", 0) > 0 + and payload.get("capture_review_queued_count", 0) > 0 + and payload.get("review_apply_resolved_action") == "confirm" + ), + "recall_items": payload.get("recall_items"), + "open_loop_count": payload.get("open_loop_count"), + "capture_candidate_count": payload.get("capture_candidate_count"), + "capture_auto_saved_count": payload.get("capture_auto_saved_count"), + "capture_review_queued_count": payload.get("capture_review_queued_count"), + } + + +def main(argv: list[str] | None = None) -> int: + args = _build_parser().parse_args(argv) + + provider_script = args.provider_smoke_script.resolve() + mcp_script = args.mcp_smoke_script.resolve() + if not provider_script.exists(): + raise RuntimeError(f"provider smoke script not found: {provider_script}") + if not mcp_script.exists(): + raise RuntimeError(f"mcp smoke script not found: {mcp_script}") + + provider_step = _run_step( + name="provider_smoke", + command=[args.python_command, str(provider_script)], + ) + + mcp_command = [ + args.python_command, + str(mcp_script), + "--python-command", + args.python_command, + "--repo-root", + str(REPO_ROOT), + ] + if args.database_url: + mcp_command.extend(["--database-url", args.database_url]) + + mcp_step = _run_step( + name="mcp_smoke", + command=mcp_command, + ) + + provider_summary = _provider_checks(provider_step) + mcp_summary = _mcp_checks(mcp_step) + + ok = ( + provider_step["returncode"] == 0 + and mcp_step["returncode"] == 0 + and provider_summary.get("bridge_ready") is True + and mcp_summary.get("bridge_flow_validated") is True + ) + + payload = { + "status": "pass" if ok else "fail", + "recommended_path": "provider_plus_mcp", + "fallback_path": "mcp_only", + "demo_command": DEFAULT_DEMO_COMMAND, + "steps": [ + { + "name": provider_step["name"], + "returncode": provider_step["returncode"], + "summary": provider_summary, + "stderr_excerpt": provider_step["stderr_excerpt"], + }, + { + "name": mcp_step["name"], + "returncode": mcp_step["returncode"], + "summary": mcp_summary, + "stderr_excerpt": mcp_step["stderr_excerpt"], + }, + ], + } + + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_hermes_mcp_smoke.py b/scripts/run_hermes_mcp_smoke.py index 6715b36..169c3c5 100755 --- a/scripts/run_hermes_mcp_smoke.py +++ b/scripts/run_hermes_mcp_smoke.py @@ -18,6 +18,10 @@ "mcp_alice_core_alice_recall", "mcp_alice_core_alice_resume", "mcp_alice_core_alice_open_loops", + "mcp_alice_core_alice_capture_candidates", + "mcp_alice_core_alice_commit_captures", + "mcp_alice_core_alice_review_queue", + "mcp_alice_core_alice_review_apply", ) @@ -26,7 +30,7 @@ def _build_parser() -> argparse.ArgumentParser: prog="run_hermes_mcp_smoke.py", description=( "Verify Hermes MCP runtime can discover and call Alice MCP tools " - "(alice_recall, alice_resume, alice_open_loops)." + "(recall/resume/open-loops plus B2 capture and B3 review flows)." ), ) parser.add_argument( @@ -124,7 +128,15 @@ def main(argv: list[str] | None = None) -> int: "PYTHONPATH": pythonpath, }, "tools": { - "include": ["alice_recall", "alice_resume", "alice_open_loops"], + "include": [ + "alice_recall", + "alice_resume", + "alice_open_loops", + "alice_capture_candidates", + "alice_commit_captures", + "alice_review_queue", + "alice_review_apply", + ], "resources": False, "prompts": False, }, @@ -153,6 +165,77 @@ def main(argv: list[str] | None = None) -> int: tool_name="mcp_alice_core_alice_open_loops", arguments={"thread_id": str(THREAD_ID), "limit": 5}, ) + capture_candidates = _dispatch_mcp_tool( + registry, + tool_name="mcp_alice_core_alice_capture_candidates", + arguments={ + "session_id": "hermes-smoke-session", + "source_kind": "sync_turn", + "user_content": "Decision: Keep provider plus MCP as the default Hermes deployment shape.", + "assistant_content": "Note: Track a short migration runbook for MCP-only users.", + }, + ) + + candidate_list = capture_candidates.get("candidates") + if not isinstance(candidate_list, list): + raise RuntimeError("Capture candidates payload missing candidates list.") + + commit_captures = _dispatch_mcp_tool( + registry, + tool_name="mcp_alice_core_alice_commit_captures", + arguments={ + "mode": "assist", + "source_kind": "sync_turn", + "sync_fingerprint": "hermes-smoke-sync-001", + "candidates": candidate_list, + }, + ) + commit_summary = commit_captures.get("summary") + if not isinstance(commit_summary, dict): + raise RuntimeError("Commit captures payload missing summary.") + + review_queue_before = _dispatch_mcp_tool( + registry, + tool_name="mcp_alice_core_alice_review_queue", + arguments={"status": "pending_review", "limit": 10}, + ) + review_items = review_queue_before.get("items") + if not isinstance(review_items, list): + raise RuntimeError("Review queue payload missing items list.") + + review_item_id: str | None = None + for item in review_items: + if not isinstance(item, dict): + continue + if item.get("object_type") == "Note": + item_id = item.get("id") + if isinstance(item_id, str): + review_item_id = item_id + break + if review_item_id is None: + raise RuntimeError("Review queue did not contain a queued Note item from capture commit.") + + review_apply = _dispatch_mcp_tool( + registry, + tool_name="mcp_alice_core_alice_review_apply", + arguments={ + "review_item_id": review_item_id, + "action": "approve", + "reason": "Hermes smoke validation approved queued note.", + }, + ) + review_queue_after = _dispatch_mcp_tool( + registry, + tool_name="mcp_alice_core_alice_review_queue", + arguments={"status": "pending_review", "limit": 10}, + ) + review_summary_before = review_queue_before.get("summary") + review_summary_after = review_queue_after.get("summary") + if not isinstance(review_summary_before, dict) or not isinstance(review_summary_after, dict): + raise RuntimeError("Review queue payload missing summary.") + review_action = review_apply.get("review_action") + if not isinstance(review_action, dict): + raise RuntimeError("Review apply payload missing review_action.") if recall["summary"]["returned_count"] < 1: raise RuntimeError("Recall returned no continuity items.") @@ -160,12 +243,28 @@ def main(argv: list[str] | None = None) -> int: raise RuntimeError("Resume did not surface the seeded decision.") if open_loops["dashboard"]["waiting_for"]["items"][0]["id"] != str(waiting_for["id"]): raise RuntimeError("Open loops did not surface the seeded waiting-for item.") + if commit_summary.get("auto_saved_count", 0) < 1: + raise RuntimeError("Commit captures did not auto-save any candidate.") + if commit_summary.get("review_queued_count", 0) < 1: + raise RuntimeError("Commit captures did not queue any candidate for review.") + if review_summary_before.get("total_count", 0) < 1: + raise RuntimeError("Review queue did not contain pending_review items after commit.") + if review_action.get("resolved_action") != "confirm": + raise RuntimeError("Review apply did not resolve action to confirm.") + if review_summary_after.get("total_count", 0) >= review_summary_before.get("total_count", 0): + raise RuntimeError("Review queue count did not drop after approval.") summary = { "registered_tools": sorted(required_tools), "recall_items": recall["summary"]["returned_count"], "resume_last_decision_title": resume["brief"]["last_decision"]["item"]["title"], "open_loop_count": open_loops["dashboard"]["summary"]["total_count"], + "capture_candidate_count": capture_candidates["summary"]["candidate_count"], + "capture_auto_saved_count": commit_summary["auto_saved_count"], + "capture_review_queued_count": commit_summary["review_queued_count"], + "review_queue_pending_before_apply": review_summary_before["total_count"], + "review_apply_resolved_action": review_action["resolved_action"], + "review_queue_pending_after_apply": review_summary_after["total_count"], } print(json.dumps(summary, separators=(",", ":"), sort_keys=True)) finally: diff --git a/tests/unit/test_hermes_bridge_demo.py b/tests/unit/test_hermes_bridge_demo.py new file mode 100644 index 0000000..05bedbb --- /dev/null +++ b/tests/unit/test_hermes_bridge_demo.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import subprocess + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_PATH = REPO_ROOT / "scripts" / "run_hermes_bridge_demo.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("run_hermes_bridge_demo_test_module", SCRIPT_PATH) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_bridge_demo_runs_provider_and_mcp_smokes_in_order(monkeypatch, capsys) -> None: + module = _load_module() + commands: list[list[str]] = [] + + def _fake_run(command, cwd, capture_output, text, check): # type: ignore[no-untyped-def] + del cwd, capture_output, text, check + commands.append(list(command)) + command_text = " ".join(command) + if "run_hermes_memory_provider_smoke.py" in command_text: + return subprocess.CompletedProcess( + command, + 0, + stdout=json.dumps( + { + "structural": { + "alice_registered": True, + "single_external_enforced": True, + "bridge_status": {"ready": True}, + } + } + ), + stderr="", + ) + if "run_hermes_mcp_smoke.py" in command_text: + return subprocess.CompletedProcess( + command, + 0, + stdout=json.dumps( + { + "recall_items": 1, + "open_loop_count": 1, + "capture_candidate_count": 2, + "capture_auto_saved_count": 1, + "capture_review_queued_count": 1, + "review_apply_resolved_action": "confirm", + } + ), + stderr="", + ) + raise AssertionError(f"unexpected command: {command}") + + monkeypatch.setattr(module.subprocess, "run", _fake_run) + + exit_code = module.main(["--python-command", "python3", "--database-url", "postgresql://demo"]) + assert exit_code == 0 + + captured = capsys.readouterr() + payload = json.loads(captured.out) + assert payload["status"] == "pass" + assert payload["recommended_path"] == "provider_plus_mcp" + assert payload["fallback_path"] == "mcp_only" + + assert len(commands) == 2 + assert any("run_hermes_memory_provider_smoke.py" in part for part in commands[0]) + assert any("run_hermes_mcp_smoke.py" in part for part in commands[1]) + assert "--database-url" in commands[1] + + +def test_bridge_demo_fails_when_mcp_smoke_output_is_incomplete(monkeypatch, capsys) -> None: + module = _load_module() + + def _fake_run(command, cwd, capture_output, text, check): # type: ignore[no-untyped-def] + del cwd, capture_output, text, check + command_text = " ".join(command) + if "run_hermes_memory_provider_smoke.py" in command_text: + return subprocess.CompletedProcess( + command, + 0, + stdout=json.dumps({"structural": {"bridge_status": {"ready": True}}}), + stderr="", + ) + if "run_hermes_mcp_smoke.py" in command_text: + return subprocess.CompletedProcess( + command, + 0, + stdout=json.dumps({"recall_items": 1}), + stderr="", + ) + raise AssertionError(f"unexpected command: {command}") + + monkeypatch.setattr(module.subprocess, "run", _fake_run) + + exit_code = module.main(["--python-command", "python3"]) + assert exit_code == 1 + + captured = capsys.readouterr() + payload = json.loads(captured.out) + assert payload["status"] == "fail"