diff --git a/.gitignore b/.gitignore
index ef675660..16db8a8b 100755
--- a/.gitignore
+++ b/.gitignore
@@ -222,4 +222,8 @@ evaluation/locomo_evaluation/results_ref/demo/results/
.review_progress.json
# Use-cases: exclude lock files to keep repo lean
-use-cases/**/package-lock.json
\ No newline at end of file
+use-cases/**/package-lock.json
+
+# Local playwright + goal state traces
+.playwright-mcp/
+.goal/
\ No newline at end of file
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 00000000..0fd72a76
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,11 @@
+{
+ "default": true,
+ "MD013": false,
+ "MD024": { "siblings_only": true },
+ "MD033": false,
+ "MD041": false,
+ "MD051": false,
+ "MD060": false,
+ "MD025": { "front_matter_title": "" },
+ "MD007": { "indent": 2 }
+}
diff --git a/README.md b/README.md
index f3aa81ba..eae52a03 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@
-
@@ -35,8 +34,6 @@
-
-
## Project Overview
**EverOS** is a unified home for applying, building, and evaluating long-term memory in self-evolving agents. The repository is organized around three essential parts:
@@ -61,6 +58,7 @@ Use cases show what persistent memory makes possible in real products and workfl

+
#### Rokid AI Assistant with EverOS
Connect to EverOS within Rokid Glasses enabling long-term memory for all of your smart activities.
@@ -94,7 +92,7 @@ Earth Online is a memory-aware productivity game that turns everyday planning in
-[](https://github.com/golutra/golutra)
+[](https://github.com/golutra/golutra)
#### Multi-Agent Orchestration Platform
@@ -118,7 +116,7 @@ Record, visualize, and explore your tasting journey through an immersive 3D star
|
-[](https://github.com/kellyvv/OpenHer)
+[](https://github.com/kellyvv/OpenHer)
#### EverOS Open Her
@@ -143,7 +141,7 @@ Ruminer brings persistent memory to a browser agent so it can carry personal con
|
-[](https://github.com/nanxingw/EverMem)
+[](https://github.com/nanxingw/EverMem)
#### EverMem Sync with EverOS
@@ -168,7 +166,7 @@ MCO equips your primary agent with an agent team that can work together to solve
|
-[](https://github.com/onenewborn/StudyBuddy-public)
+[](https://github.com/onenewborn/StudyBuddy-public)
#### Study Buddy with Self-Evolving Memory
@@ -193,7 +191,7 @@ Empowering individuals with advanced memory support and daily assistance.
|
-[](https://github.com/AlexL1024/NeuralConnect)
+[](https://github.com/AlexL1024/NeuralConnect)
#### Memory-Driven Multi-Agent NPC Experience
@@ -305,7 +303,7 @@ Explore stored entities and relationships in a graph interface. Frontend demo; b
-[](#readme-top)
+[](#readme-top)
@@ -376,7 +374,7 @@ requests.post(f"{API_BASE}/memories", json={
})
# 2. Search for relevant memories
-response = requests.get(f"{API_BASE}/memories/search", json={
+response = requests.post(f"{API_BASE}/memories/search", json={
"query": "What sports does the user like?",
"user_id": "user_001",
"memory_types": ["episodic_memory"],
@@ -393,7 +391,7 @@ for memory_group in result.get("memories", []):
-[](#readme-top)
+[](#readme-top)
@@ -431,7 +429,7 @@ LoCoMo **92.73%**
-[](#readme-top)
+[](#readme-top)
@@ -465,7 +463,7 @@ Agent self-evolution evaluation through longitudinal growth curves, transfer eff
-[](#readme-top)
+[](#readme-top)
@@ -506,7 +504,7 @@ cat evaluation/results/locomo-everos/report.txt
-[](#readme-top)
+[](#readme-top)
@@ -540,7 +538,7 @@ If EverOS helps your research, please cite the relevant paper:
-[](#readme-top)
+[](#readme-top)
@@ -553,7 +551,7 @@ Star the repo or join the community links above to follow new architecture metho
-[](#readme-top)
+[](#readme-top)
@@ -596,6 +594,6 @@ Read the [Contribution Guidelines](.github/CONTRIBUTING.md) for setup, pull requ
-[](#readme-top)
+[](#readme-top)
diff --git a/docs/goal.md b/docs/goal.md
new file mode 100644
index 00000000..45056612
--- /dev/null
+++ b/docs/goal.md
@@ -0,0 +1,249 @@
+# Mega 24h Captain Goal
+
+Short `/goal` capsule:
+
+```text
+Read and execute docs/goal.md as the source-of-truth runbook. Run a 24h autonomous Captain pass on Fearvox/EverOS only. Complete >=30 logged iterations with strict gates, no main/upstream writes, draft PR output, full audit trail, and morning owner handoff. Optimize for owner-mergeable truth, not activity volume. Start with preflight + PR truth reset before edits.
+```
+
+## Full Captain Prompt v1
+
+```text
+ROLE: Autonomous 24h Captain for Fearvox/EverOS fork playground.
+MODE: 24h non-stop, >30 logged iterations, owner asleep/offline. Optimize for morning owner-mergeable output, not activity theater.
+
+REPO_OWNER=Fearvox
+REPO_NAME=EverOS
+UPSTREAM_OWNER=EverMind-AI
+UPSTREAM_NAME=EverOS
+BASE_BRANCH=main
+RUN_BRANCH=mega-24h-curator-$(date +%Y-%m-%d)
+OWNER_TIMEZONE=America/Los_Angeles
+
+================ PRIME GOAL ================
+
+By the end of 24h, deliver a curated, owner-reviewable EverOS fork packet that:
+1. Repairs or clearly rejects broken sleep-run PRs (#7, #12, queue-shape #21/#22, new dependabot #23).
+2. Curates May Agent architecture docs (#16-#22) into a coherent upstream/team-review packet.
+3. Produces one clean draft PR or a small stack of draft PRs against Fearvox/EverOS:main.
+4. Leaves a complete audit trail so Nolan can merge/reject in <15 minutes.
+5. Completes >=30 logged iterations, where each iteration has intent, changed files, commands, gate result, score, and next decision.
+
+Iteration count is required, but if the code/doc packet becomes mergeable early, later iterations must be verification, review, reduction, evidence, or owner-brief improvement. Do not create random scope just to keep moving.
+
+================ HARD BOUNDARIES ================
+
+H1. Never push to origin/main.
+H2. Never write to upstream EverMind-AI/EverOS by git, issues, PRs, settings, labels, Actions, comments, or API.
+H3. All GitHub commands must explicitly scope repo: `--repo Fearvox/EverOS`.
+H4. Final PRs must be draft unless owner explicitly says otherwise.
+H5. Do not force-push except to your own RUN_BRANCH with `--force-with-lease`.
+H6. Do not edit `.claude/`, user secrets, or local machine config.
+H7. Do not send Slack/Linear/email/external messages. Read/verify only unless fork-scoped mutation is explicitly part of this prompt.
+H8. No broad formatting, repo-wide rename, dependency major upgrade, or destructive cleanup.
+H9. No secret/local-path/public-surface leaks in docs or PR bodies.
+H10. If any hard boundary is violated: stop, write `.planning/mega-run/HARD_FAIL.md`, do not continue.
+
+================ SETUP ================
+
+1. cd repo root.
+2. `git fetch --all --prune`.
+3. Confirm remotes:
+ - origin must be `Fearvox/EverOS`
+ - upstream must be `EverMind-AI/EverOS`
+4. Create or switch:
+ `git checkout -B $RUN_BRANCH origin/main`
+5. Create:
+ - `.planning/mega-run/HEARTBEAT.txt`
+ - `.planning/mega-run/ITER_LOG.md`
+ - `.planning/mega-run/SCOREBOARD.md`
+ - `.planning/mega-run/GATE_RESULTS.md`
+ - `.planning/mega-run/DECISIONS.md`
+ - `.planning/mega-run/OWNER_BRIEF.md`
+6. Record baseline:
+ - fork main SHA
+ - upstream main SHA
+ - open PR list
+ - current checks for #7-#23
+ - dirty state
+ - available test/docs commands
+
+================ ITERATION CONTRACT ================
+
+Run at least 30 iterations. Each iteration must append this exact block to `ITER_LOG.md`:
+
+## Iter - -
+- Intent:
+- Scope bucket:
+- Files touched:
+- Commands run:
+- Gate result:
+- Score delta:
+- Evidence:
+- Next decision:
+
+Each iteration must update `HEARTBEAT.txt` with:
+` iter= slug= gate=`
+
+Commit every 1-3 iterations with small messages. Push RUN_BRANCH regularly.
+
+================ PHASE PLAN ================
+
+Phase 0, iters 1-3: Preflight and truth reset
+- Re-check #7, #12, #21, #22, #23 live.
+- Write PR verdict table.
+- Confirm previous sleep-run score inflation and queue-shape issues.
+- No feature work until this is logged.
+
+Phase 1, iters 4-9: Repair broken queue
+- Fix #7 link failure or mark close/recreate.
+- Fix #12 by changing markdownlint to changed-files-only or baseline-aware; do not lint 1000+ legacy errors.
+- Decide #21/#22 draft normalization.
+- Triage #23 dependabot safely; no blind dependency merge.
+- Gate: red PR count must decrease or be explicitly quarantined.
+
+Phase 2, iters 10-17: Curate May Agent architecture packet
+- Review #16 first as strategy gate.
+- Then #17-#22 as one packet.
+- Produce `.planning/mega-run/MAY_AGENT_REVIEW.md` with:
+ - merge order
+ - contradictions
+ - missing evidence
+ - upstream-pitch framing
+ - what should not be merged
+- Optional: consolidate docs into one curated branch/PR if smaller than the existing queue.
+
+Phase 3, iters 18-23: DX / CI / docs hygiene with proof
+- Improve only high-leverage docs/CI surfaces.
+- Every docs edit requires link check or explicit skipped reason.
+- Every CI edit requires either local syntax validation or Actions result.
+- Prefer additive, reversible changes.
+
+Phase 4, iters 24-28: Reproducibility and owner handoff
+- Verify Quick Start commands where feasible.
+- Identify real blockers separately from skipped heavy infra.
+- Create morning owner flow:
+ - "merge now"
+ - "review first"
+ - "close/rework"
+ - "defer"
+- Keep owner decision under 15 minutes.
+
+Phase 5, iters 29-32+: Reduction, final review, PR preparation
+- Remove noise.
+- Split oversized diffs.
+- Ensure final PR body is truthful.
+- Run final gates.
+- Write final reports.
+
+If 32 iterations complete before 24h, continue with audit/reduction/recheck loops every 30-45 min until 24h or clean exit.
+
+================ SCORING ================
+
+Use strict score accounting. Never bank failing work as full score.
+
++3 repaired failing PR with green proof
++3 high-value curated architecture decision with evidence
++2 verified CI/docs improvement
++2 owner review burden reduced materially
++1 useful research/review artifact with citations or live evidence
++1 cleanup that reduces queue noise
+
+-2 skipped gate without explicit reason
+-2 agent collision or duplicated work
+-3 unverified mergeability claim
+-3 broad diff without clear owner value
+-5 red CI banked as success
+-5 upstream/main write attempt
+-5 secret/path/public-surface leak
+
+Success requires:
+- >=30 logged iterations
+- 0 hard violations
+- all mandatory gates accounted
+- final branch pushed
+- draft PR or explicit no-PR rationale
+- owner can merge/reject in <15 min
+
+================ GATES ================
+
+Preflight gate:
+- `git status --short --branch`
+- `git remote -v`
+- `gh repo view Fearvox/EverOS`
+- fork main SHA
+- upstream main SHA
+
+Per-iteration gate:
+- Scope is one bucket only.
+- Files touched are listed.
+- Commands are listed.
+- Failures are classified: introduced / pre-existing / infra-blocked / skipped-with-reason.
+
+Docs gate:
+- Check relative links for touched docs.
+- No placeholder URLs.
+- No raw local paths/secrets.
+
+CI gate:
+- YAML syntax validated for touched workflows.
+- Do not add repo-wide failing checks without baseline/changed-file strategy.
+
+PR gate:
+- Draft PR only.
+- Base must be `Fearvox/EverOS:main`.
+- PR body includes changed files, tests, risks, rollback.
+
+Final gate:
+- Re-run PR list.
+- Re-run sync-failed issue check.
+- Confirm fork/main unchanged unless owner merged manually.
+- Confirm upstream/main unchanged.
+- Write `.planning/mega-run/FINAL_REPORT.md`.
+
+================ SUBAGENT POLICY ================
+
+You may spawn subagents, but captain owns final branch state.
+
+Allowed roles:
+- red-ci-fixer: owns #7/#12 only.
+- architecture-reviewer: reads #16-#22, writes review artifact only.
+- pr-curator: builds verdict table, no code edits.
+- evidence-runner: runs commands/checks, no edits.
+
+Rules:
+- Each subagent gets disjoint file ownership.
+- No subagent pushes.
+- No subagent edits same file as another.
+- Captain reviews all changes before commit.
+- If subagents disagree, log decision in `DECISIONS.md`.
+
+================ EXIT CONDITIONS ================
+
+Exit after 24h, or earlier only if:
+- >=30 iterations logged
+- final PR/draft PR ready
+- all gates PASS or explicitly FLAG with owner action
+- FINAL_REPORT and OWNER_BRIEF complete
+
+On exit write:
+
+`.planning/mega-run/FINAL_REPORT.md`
+- verdict: PASS / FLAG / BLOCK
+- total iterations
+- final score
+- PR URLs
+- changed files
+- commands run
+- failed/skipped gates
+- owner morning actions
+
+`.planning/mega-run/OWNER_BRIEF.md`
+- 10-line max
+- what to merge
+- what to review
+- what to close
+- what is risky
+
+Start now. First action: preflight gate. Do not write code until preflight and PR truth reset are logged.
+```
diff --git a/docs/upstream-return/CANONICAL_PROBLEM_FAMILIES.md b/docs/upstream-return/CANONICAL_PROBLEM_FAMILIES.md
new file mode 100644
index 00000000..089b34ce
--- /dev/null
+++ b/docs/upstream-return/CANONICAL_PROBLEM_FAMILIES.md
@@ -0,0 +1,51 @@
+# Canonical Problem Families
+
+Live source: upstream issue/PR inventory fetched on 2026-05-14.
+
+## 1. Memory API correctness and documentation
+
+Representative issues: #191, #78, #58, #46, #45, #34, #131.
+
+Open PRs: #185, #196, #138, #109, #89, #132.
+
+Maintainer move: choose one canonical v1 API docs patch and one canonical multi-type search patch. Close or rework the old-path/broad PRs after the choice.
+
+## 2. Memory lifecycle semantics
+
+Representative issues: #148, #143, #133, #101, #95, #27, #14.
+
+Open PRs: #129, #106.
+
+Maintainer move: decide cascade delete, reset, dedup, expiry, status metadata, and session scoping as API contracts before accepting implementation churn.
+
+## 3. Benchmark reproducibility and evaluation DX
+
+Representative issues: #195, #127, #88, #87, #73, #56, #41, #31, #22, #3.
+
+Open PRs: #136, #115.
+
+Maintainer move: publish a versioned repro matrix with expected runtime/cost, dataset/version pins, and tolerated metric deltas. Fix #127 with a narrow adapter patch.
+
+## 4. OpenClaw and external integration DX
+
+Representative issues: #193, #177, #150, #139, #93, #57, #52, #15, #11.
+
+Open PRs: #211, #202, #189, #128, #86.
+
+Maintainer move: keep current-tree OpenClaw docs/fixes; avoid merging legacy `methods/evermemos` or `evermemos-openclaw-plugin` paths without a path relevance check.
+
+## 5. Provider and deployment configuration
+
+Representative issues: #29, #23, #21, #9, #6, #4, #2, #1.
+
+Open PRs: #206, #157, #144, #90.
+
+Maintainer move: define supported provider matrix and official Docker/dependency boundary. Security/env fixes can proceed once DX migration is explicit.
+
+## 6. Broad cleanup PR backlog
+
+Representative issues: #50, #48 plus code-quality-only PRs without linked user reports.
+
+Open PRs: #154, #141, #137, #126, #118, #113, #112, #110, #108, #107, #98, #97, #91.
+
+Maintainer move: stop reviewing these as independent random cleanup. Pick one narrow bug-linked patch per family, then close duplicates.
diff --git a/docs/upstream-return/FINAL_REPORT.md b/docs/upstream-return/FINAL_REPORT.md
new file mode 100644
index 00000000..b711256c
--- /dev/null
+++ b/docs/upstream-return/FINAL_REPORT.md
@@ -0,0 +1,66 @@
+# Upstream Return Final Report
+
+Date: 2026-05-14.
+
+## Verdict
+
+FLAG: upstream queue is now classified locally, but no upstream mutation was performed and no issue/PR should be marked resolved from this packet alone.
+
+## What Changed
+
+Added the required local maintainer packet:
+
+- `docs/upstream-return/ISSUE_MATRIX.md` covers all 52 open issues.
+- `docs/upstream-return/PR_MATRIX.md` covers all 37 open PRs.
+- `docs/upstream-return/CANONICAL_PROBLEM_FAMILIES.md` groups recurring problems.
+- `docs/upstream-return/UPSTREAM_STRATEGY.md` proposes the maintainer sequence.
+- `docs/upstream-return/OWNER_BRIEF.md` gives the short owner handoff.
+- `docs/upstream-return/FINAL_REPORT.md` summarizes this pass.
+
+## Live Evidence Used
+
+- `gh issue list --repo EverMind-AI/EverOS --state open --limit 200 --json number,title,labels,createdAt,updatedAt,url` returned 52 issues.
+- `gh pr list --repo EverMind-AI/EverOS --state open --limit 200 --json number,title,mergeStateStatus,isDraft,baseRefName,headRefName,url` returned 37 PRs.
+- Per-PR file surfaces were fetched with `gh pr view ... --json number,title,mergeStateStatus,isDraft,baseRefName,headRefName,files,url`.
+
+## Key Findings
+
+- The live PR count is 37, not the older 38 count in the original goal note.
+- GitHub reports only PR #128 as `CLEAN`.
+- PRs #211, #206, #202, and #185 are `BLOCKED`.
+- Most open PRs are `DIRTY` and many overlap by family.
+- Several PRs touch legacy-looking paths such as `methods/evermemos/...`; those should not be merged without path relevance review.
+- The fastest useful maintainer pass is narrow review of #211, #185, and #202, then duplicate/stale cleanup.
+
+## Residual Risk
+
+- This pass did not inspect full PR diffs or run upstream test suites.
+- Dispositions are triage recommendations, not maintainer decisions.
+- `VALIDATION.md` was not added because this pass did not implement runtime behavior.
+
+## 2026-05-15 Slice Update
+
+Targeted live recheck:
+
+- Issues #191, #93, and #78 remain open.
+- PRs #185 and #211 remain `BLOCKED`.
+- PRs #89, #109, and #138 remain `DIRTY`.
+
+Local current-tree slice now covers:
+
+- #191: README search example uses `POST /api/v1/memories/search`.
+- #93: demo store treats HTTP 202 Accepted as background success.
+- #78: keyword/vector retrieval searches all requested non-profile memory types;
+ hybrid dedupe preserves same ids from different memory collections.
+
+Verification added for the slice:
+
+- `tests/test_memory_manager_multi_type_search.py`
+- `tests/test_simple_memory_manager.py`
+
+PR handoff added:
+
+- `docs/upstream-return/PR_191_93_78_PACKET.md`
+
+The slice is PR-ready after reviewer pass, but upstream should still treat it as
+a new narrow PR rather than a merge decision on the stale overlapping PRs.
diff --git a/docs/upstream-return/FORK_LEFTOVERS.md b/docs/upstream-return/FORK_LEFTOVERS.md
new file mode 100644
index 00000000..b79b8da4
--- /dev/null
+++ b/docs/upstream-return/FORK_LEFTOVERS.md
@@ -0,0 +1,45 @@
+# Fork Leftovers
+
+As of 2026-05-14, `Fearvox/EverOS` still has a fork-side open PR queue that is
+mostly clean at the GitHub merge/check level but not yet resolved into owner
+decisions.
+
+## Current Local State
+
+- Local branch: `main`
+- Local divergence: `ahead 7` vs `upstream/main`
+- Working tree: clean
+- This document is for fork cleanup only, not upstream mutation
+
+## Decision Table
+
+| Class | PRs | Recommended action | Why |
+| --- | --- | --- | --- |
+| Merge now | `#24`, `#27` | Merge after quick owner read | Both are `CLEAN`, have successful docs checks, and are explicit repo-surface improvements rather than speculative backlog work |
+| Merge as packet or keep grouped | `#16`-`#22` | Review as one May Agent packet, then merge in order or squash into one curated outcome | The work is coherent, all seven PRs are `CLEAN`, and splitting the review across seven isolated approvals increases owner burden |
+| Close after extracting signal | `#26` | Copy any useful review wording into the target thread, then close the PR | This is a draft reply artifact, not durable product/repo state |
+| Defer pending explicit dependency policy | `#1`, `#23` | Leave open or close with rationale, but do not merge casually | Both are dependency bumps with no visible validation/check surface in the current fork |
+| Triage separately | `#7`-`#15` | Re-check one by one only if they are still intended; otherwise close stale docs/ops drafts aggressively | These are older fork-side sleep-run outputs and are likely superseded by newer queue-shaping work |
+
+## Merge Order
+
+1. `#24` — docs gate repair path
+2. `#27` — lead bridge operating contract
+3. `#16` — May Agent vision gate
+4. `#17`-`#22` — rest of the May Agent packet, preferably reviewed as one batch
+
+## Not Worth Carrying Forward
+
+- `#26` should not remain open as a pseudo-task queue item.
+- `#1` and `#23` should not be merged without explicit validation policy.
+- The older sleep-run PR tail should not stay open just because it is clean.
+
+## Next Operator Move
+
+Use this sequence:
+
+1. Merge `#24`
+2. Merge `#27`
+3. Decide whether `#16`-`#22` stay as seven PRs or collapse into one maintainer packet
+4. Close `#26`
+5. Sweep `#7`-`#15`, `#1`, and `#23` into explicit `close` or `defer`
diff --git a/docs/upstream-return/ISSUE_MATRIX.md b/docs/upstream-return/ISSUE_MATRIX.md
new file mode 100644
index 00000000..5ae5a447
--- /dev/null
+++ b/docs/upstream-return/ISSUE_MATRIX.md
@@ -0,0 +1,64 @@
+# Upstream Issue Matrix
+
+Live source: `EverMind-AI/EverOS` open issues fetched on 2026-05-14.
+Targeted recheck for #191/#93/#78 ran on 2026-05-15; all three issues remain
+open.
+
+This file assigns every currently open upstream issue to one disposition. It is intentionally a maintainer triage packet, not a claim that the underlying bugs are fixed.
+
+Disposition vocabulary: `FIX_IN_FORK`, `ANSWER_DRAFT`, `CLOSE_STALE`, `DUPLICATE_OF`, `REVIEW_EXISTING_PR`, `NEEDS_MAINTAINER_DECISION`, `OUT_OF_SCOPE`.
+
+| Issue | Family | Pain | Related PRs | Disposition | Priority | Evidence / owner action |
+|---|---|---|---|---|---|---|
+| [#205](https://github.com/EverMind-AI/EverOS/issues/205) answer prompt 确认 | Triage hygiene | unclear prompt/confirmation request | none | ANSWER_DRAFT | P3 | Ask for exact prompt, route, expected/actual behavior; close stale if no repro after maintainer window. |
+| [#195](https://github.com/EverMind-AI/EverOS/issues/195) HyperMem stage2 takes >4h | Benchmark runtime | evaluation cost and expected runtime unclear | none | ANSWER_DRAFT | P1 | Needs official runtime envelope, hardware profile, and checkpoint/resume guidance. |
+| [#193](https://github.com/EverMind-AI/EverOS/issues/193) Chat Agent + MemSys integration schema | Integration DX | missing recommended tool schema | none | ANSWER_DRAFT | P1 | Provide official search/write tool schema or mark as roadmap. |
+| [#191](https://github.com/EverMind-AI/EverOS/issues/191) README memory search API example outdated | Memory API docs | README example points at stale API | [#185](https://github.com/EverMind-AI/EverOS/pull/185), [#196](https://github.com/EverMind-AI/EverOS/pull/196) | FIX_IN_FORK | P0 | Local current-tree slice changes the README search example from GET to POST; #185 is still `BLOCKED`, #196 is still too broad/old-path. |
+| [#177](https://github.com/EverMind-AI/EverOS/issues/177) Codex plugins similar to Claude Code | Integration DX | asks whether plugin model exists | none | ANSWER_DRAFT | P3 | Answer with current supported integration path; do not turn into code work without owner roadmap. |
+| [#158](https://github.com/EverMind-AI/EverOS/issues/158) profile not supported as expected | Memory behavior | profile behavior unclear | none | ANSWER_DRAFT | P2 | Needs repro data and expected profile lifecycle; likely docs/usage answer first. |
+| [#150](https://github.com/EverMind-AI/EverOS/issues/150) OpenClaw plugin config not forwarded | OpenClaw integration | callback receives no config | [#128](https://github.com/EverMind-AI/EverOS/pull/128), [#189](https://github.com/EverMind-AI/EverOS/pull/189), [#202](https://github.com/EverMind-AI/EverOS/pull/202) | REVIEW_EXISTING_PR | P0 | Review current-path fix/docs; avoid old `methods/evermemos` path drift. |
+| [#148](https://github.com/EverMind-AI/EverOS/issues/148) DELETE memories does not cascade | Memory lifecycle | delete leaves derived memories | none | NEEDS_MAINTAINER_DECISION | P0 | Requires canonical cascade semantics across episodic/foresight/event_log before patching. |
+| [#143](https://github.com/EverMind-AI/EverOS/issues/143) status metadata and session-scoped retrieval | Memory lifecycle | action memories lack status/session scope | none | NEEDS_MAINTAINER_DECISION | P1 | Product/API contract decision; can become scoped design issue. |
+| [#139](https://github.com/EverMind-AI/EverOS/issues/139) openclaw新插件bug | OpenClaw integration | plugin bug report overlaps #150 | [#128](https://github.com/EverMind-AI/EverOS/pull/128), [#189](https://github.com/EverMind-AI/EverOS/pull/189), [#202](https://github.com/EverMind-AI/EverOS/pull/202) | REVIEW_EXISTING_PR | P1 | Triage with #150; ask reporter whether same config-forwarding failure. |
+| [#133](https://github.com/EverMind-AI/EverOS/issues/133) consolidation drift detection | Memory quality | memory accuracy over time | none | NEEDS_MAINTAINER_DECISION | P1 | Architecture feature; define eval signal before implementation. |
+| [#131](https://github.com/EverMind-AI/EverOS/issues/131) full episode not returned | Memory API | GET loses full episode and summary fallback poor | [#132](https://github.com/EverMind-AI/EverOS/pull/132) | REVIEW_EXISTING_PR | P1 | Existing PR is broad/dirty; owner should request narrow rebase or split. |
+| [#130](https://github.com/EverMind-AI/EverOS/issues/130) 反馈 | Triage hygiene | vague feedback issue | none | CLOSE_STALE | P4 | Ask for actionable repro once, then close if no concrete request. |
+| [#127](https://github.com/EverMind-AI/EverOS/issues/127) BM25/Embedding filenames mismatch | Benchmark correctness | empty retrieval from adapter filename mismatch | [#136](https://github.com/EverMind-AI/EverOS/pull/136) | REVIEW_EXISTING_PR | P0 | Existing PR touches many files; require focused validation or split. |
+| [#111](https://github.com/EverMind-AI/EverOS/issues/111) multimodal memory search | Memory API | modality support unclear | none | ANSWER_DRAFT | P2 | Answer current support boundaries and roadmap status. |
+| [#101](https://github.com/EverMind-AI/EverOS/issues/101) semantic memory type | Memory architecture | asks for objective/semantic memory | none | NEEDS_MAINTAINER_DECISION | P1 | Requires taxonomy decision across memory types. |
+| [#95](https://github.com/EverMind-AI/EverOS/issues/95) dedup and foresight expiry cleanup | Memory lifecycle | duplicate writes and stale foresight | [#129](https://github.com/EverMind-AI/EverOS/pull/129) | REVIEW_EXISTING_PR | P1 | Review #129 but keep semantics explicit: dedup scope, retention, tenant isolation. |
+| [#93](https://github.com/EverMind-AI/EverOS/issues/93) Storage failed: Request accepted | Demo/API UX | demo treats 202 Accepted as failure | [#211](https://github.com/EverMind-AI/EverOS/pull/211) | FIX_IN_FORK | P0 | Local current-tree slice treats HTTP 202 as successful background extraction; #211 remains `BLOCKED` upstream. |
+| [#88](https://github.com/EverMind-AI/EverOS/issues/88) evaluation supports HaluMem | Benchmark scope | benchmark coverage request | none | NEEDS_MAINTAINER_DECISION | P2 | Needs benchmark roadmap answer. |
+| [#87](https://github.com/EverMind-AI/EverOS/issues/87) PersonaMem v1/v2 results | Benchmark reproducibility | published result/repro ambiguity | none | ANSWER_DRAFT | P1 | Provide versioned benchmark instructions and results pointer. |
+| [#78](https://github.com/EverMind-AI/EverOS/issues/78) search uses only memory_types[0] | Memory API bug | multi-type search silently ignored | [#89](https://github.com/EverMind-AI/EverOS/pull/89), [#109](https://github.com/EverMind-AI/EverOS/pull/109), [#138](https://github.com/EverMind-AI/EverOS/pull/138) | FIX_IN_FORK | P0 | Local current-tree slice queries all requested non-profile memory types for keyword/vector retrieval and dedupes hybrid hits by `(memory_type,id)`; #89/#109/#138 remain `DIRTY`. |
+| [#73](https://github.com/EverMind-AI/EverOS/issues/73) Cannot reproduce LoCoMo results | Benchmark reproducibility | paper/result reproduction gap | none | ANSWER_DRAFT | P0 | Needs official repro matrix, data/version pin, and expected tolerance. |
+| [#65](https://github.com/EverMind-AI/EverOS/issues/65) retrieval pipeline mismatch with paper | Architecture truth | implementation/paper mismatch | none | NEEDS_MAINTAINER_DECISION | P0 | Owner must decide whether docs errata, implementation alignment, or paper caveat. |
+| [#58](https://github.com/EverMind-AI/EverOS/issues/58) search cannot retrieve GET content | Memory API UX | retrieval vs stored memory confusion | none | ANSWER_DRAFT | P1 | Likely search semantics/docs answer; may fold into API docs after #191/#78. |
+| [#57](https://github.com/EverMind-AI/EverOS/issues/57) stale Discord links | Docs/community | STARTER_KIT issue entry links dead | [#86](https://github.com/EverMind-AI/EverOS/pull/86) | REVIEW_EXISTING_PR | P1 | Verify current public links; accept narrow docs PR or replace with canonical community link. |
+| [#56](https://github.com/EverMind-AI/EverOS/issues/56) token consumption | Benchmark/runtime | cost expectations unclear | none | ANSWER_DRAFT | P2 | Add cost envelope and knobs to docs. |
+| [#53](https://github.com/EverMind-AI/EverOS/issues/53) Chinese environment stores English | Memory behavior | language behavior unclear | none | ANSWER_DRAFT | P2 | Answer extraction language behavior and prompt locale settings. |
+| [#52](https://github.com/EverMind-AI/EverOS/issues/52) start command needs --longjob | Use-case DX | startup flags unclear | none | ANSWER_DRAFT | P2 | Clarify when `--longjob` is required. |
+| [#50](https://github.com/EverMind-AI/EverOS/issues/50) two rrf mode in demo code | Code quality | duplicated/ambiguous RRF mode | [#97](https://github.com/EverMind-AI/EverOS/pull/97), [#141](https://github.com/EverMind-AI/EverOS/pull/141), [#154](https://github.com/EverMind-AI/EverOS/pull/154) | REVIEW_EXISTING_PR | P1 | Select one minimal fix; close overlapping anti-pattern PRs as duplicates. |
+| [#48](https://github.com/EverMind-AI/EverOS/issues/48) inconsistent timestamp formats | Data consistency | mixed timestamp formats | [#108](https://github.com/EverMind-AI/EverOS/pull/108), [#110](https://github.com/EverMind-AI/EverOS/pull/110), [#112](https://github.com/EverMind-AI/EverOS/pull/112) | REVIEW_EXISTING_PR | P1 | Need canonical timestamp contract and one PR, not three overlapping changes. |
+| [#47](https://github.com/EverMind-AI/EverOS/issues/47) filter memories by assistant | Memory API UX | actor/session filtering need | none | NEEDS_MAINTAINER_DECISION | P2 | API design decision: assistant identity, session scope, tenant scope. |
+| [#46](https://github.com/EverMind-AI/EverOS/issues/46) search format differs from docs | Memory API docs | result format mismatch | none | ANSWER_DRAFT | P1 | Fold into README/API docs fix with #191/#58. |
+| [#45](https://github.com/EverMind-AI/EverOS/issues/45) normalize search score | Memory API docs | scoring semantics unclear | none | ANSWER_DRAFT | P2 | Document score source and normalization expectations. |
+| [#41](https://github.com/EverMind-AI/EverOS/issues/41) standardize evaluation scripts | Benchmark tooling | inconsistent eval commands | none | NEEDS_MAINTAINER_DECISION | P1 | Needs benchmark maintainer owner and command contract. |
+| [#34](https://github.com/EverMind-AI/EverOS/issues/34) v1.1.0 search API discussion | Memory API design | public API open discussion | none | NEEDS_MAINTAINER_DECISION | P1 | Use as canonical API design thread; link bug/docs issues under it. |
+| [#31](https://github.com/EverMind-AI/EverOS/issues/31) phase III latency and ablation | Benchmark reproducibility | performance/ablation questions | none | ANSWER_DRAFT | P1 | Needs official benchmark note. |
+| [#29](https://github.com/EverMind-AI/EverOS/issues/29) OpenAI embeddings/rerank support | Provider config | provider choice unclear | [#144](https://github.com/EverMind-AI/EverOS/pull/144) | NEEDS_MAINTAINER_DECISION | P1 | Decide provider roadmap; #144 is MiniMax, not a direct OpenAI answer. |
+| [#27](https://github.com/EverMind-AI/EverOS/issues/27) importance/dedup/hit tracking/decay | Memory lifecycle | memory maintenance feature cluster | none | NEEDS_MAINTAINER_DECISION | P1 | Split into lifecycle roadmap after #95/#143 decisions. |
+| [#25](https://github.com/EverMind-AI/EverOS/issues/25) clustering and profile usage | Memory behavior docs | architecture usage unclear | none | ANSWER_DRAFT | P2 | Add/point to architecture explainer. |
+| [#23](https://github.com/EverMind-AI/EverOS/issues/23) direct OpenAI provider | Provider config | wants OpenAI API provider | none | NEEDS_MAINTAINER_DECISION | P1 | Roadmap/API-key policy decision. |
+| [#22](https://github.com/EverMind-AI/EverOS/issues/22) evaluation environment config | Benchmark tooling | setup broken/unclear | none | ANSWER_DRAFT | P1 | Ask current error if not enough detail; add env checklist if confirmed. |
+| [#21](https://github.com/EverMind-AI/EverOS/issues/21) dockerize own service | Infra DX | service packaging gap | [#157](https://github.com/EverMind-AI/EverOS/pull/157), [#90](https://github.com/EverMind-AI/EverOS/pull/90) | NEEDS_MAINTAINER_DECISION | P1 | Decide official Docker support boundary before merging infra PRs. |
+| [#16](https://github.com/EverMind-AI/EverOS/issues/16) memorize boundary decision | Memory behavior docs | boundary detection unclear | none | ANSWER_DRAFT | P2 | Explain boundary classifier and tuning knobs. |
+| [#15](https://github.com/EverMind-AI/EverOS/issues/15) AI coding applicability | Use-case DX | asks whether coding use case fits | none | ANSWER_DRAFT | P3 | Product/use-case answer, possibly link examples. |
+| [#14](https://github.com/EverMind-AI/EverOS/issues/14) delete/reset APIs | Memory lifecycle | chat editing/context clearing requires reset | none | NEEDS_MAINTAINER_DECISION | P0 | Tied to #148 cascade semantics; define API contract first. |
+| [#11](https://github.com/EverMind-AI/EverOS/issues/11) preload memories for customer service | Use-case DX | asks seeding/customer-service use case | none | ANSWER_DRAFT | P2 | Answer with supported import/preload path and limitations. |
+| [#9](https://github.com/EverMind-AI/EverOS/issues/9) milvus health starting | Infra DX | service health confusion | none | ANSWER_DRAFT | P2 | Add troubleshooting note if still reproducible. |
+| [#6](https://github.com/EverMind-AI/EverOS/issues/6) 环境设置 | Infra DX | environment setup trouble | none | ANSWER_DRAFT | P2 | Needs exact env/error; can point at current Quick Start. |
+| [#4](https://github.com/EverMind-AI/EverOS/issues/4) local embedding/rerank models | Provider config | local model support unclear | none | NEEDS_MAINTAINER_DECISION | P1 | Provider roadmap and doc answer. |
+| [#3](https://github.com/EverMind-AI/EverOS/issues/3) LoCoMo detailed results | Benchmark reproducibility | overlaps #73 | none | DUPLICATE_OF #73 | P2 | Keep #73 as active repro thread; link #3 historical context. |
+| [#2](https://github.com/EverMind-AI/EverOS/issues/2) supabase version | Infra/provider | asks for Supabase support | none | NEEDS_MAINTAINER_DECISION | P3 | Roadmap decision. |
+| [#1](https://github.com/EverMind-AI/EverOS/issues/1) ES/Milvus optional | Infra DX | optional search components | none | NEEDS_MAINTAINER_DECISION | P1 | Architecture/deployment decision; tie to Docker/provider roadmap. |
diff --git a/docs/upstream-return/OWNER_BRIEF.md b/docs/upstream-return/OWNER_BRIEF.md
new file mode 100644
index 00000000..6cbc748f
--- /dev/null
+++ b/docs/upstream-return/OWNER_BRIEF.md
@@ -0,0 +1,15 @@
+# Owner Brief
+
+1. Live upstream queue on 2026-05-14: 52 open issues, 37 open PRs.
+2. Targeted 2026-05-15 recheck: #191/#93/#78 are still open; #185/#211 are `BLOCKED`; #89/#109/#138 are `DIRTY`.
+3. First returnable slice is ready locally: #191 README POST search example, #93 202 Accepted demo handling, #78 multi-memory-type retrieval.
+4. The slice should go upstream as one narrow current-tree PR, not as rebases of #185/#211/#89/#109/#138.
+5. Required verification for that PR: targeted pytest, black check, `git diff --check`, and reviewer pass on API contract.
+6. Next small-review candidate after this slice is #202 for OpenClaw docs if it matches the current plugin path.
+7. #127 is important, but #136 is too broad; request a focused filename mismatch patch with repro.
+8. #131 likewise needs a narrow full-episode patch; #132 is too broad.
+9. OpenClaw fixes must be checked against current paths; several PRs still touch legacy `methods/evermemos` or plugin-root paths.
+10. Delete/reset/cascade memory semantics (#14/#148) need an owner API decision before code.
+11. Benchmark reproducibility issues (#73/#3/#195/#87) need an official matrix, not isolated replies.
+12. Provider/deployment requests (#29/#23/#21/#4/#1) should become a supported-provider decision.
+13. Most cleanup PRs are duplicates; pick one narrow bug-linked cleanup path and close the rest.
diff --git a/docs/upstream-return/PR_191_93_78_PACKET.md b/docs/upstream-return/PR_191_93_78_PACKET.md
new file mode 100644
index 00000000..a00a4917
--- /dev/null
+++ b/docs/upstream-return/PR_191_93_78_PACKET.md
@@ -0,0 +1,71 @@
+# PR Packet: Memory API Onboarding Stability
+
+Date: 2026-05-15.
+
+## Target Issues
+
+- #191: README memory search API example is outdated.
+- #93: demo reports "Storage failed" when the API returns HTTP 202 Accepted.
+- #78: search only uses `memory_types[0]` and silently ignores later requested
+ memory types.
+
+## Scope
+
+This is one narrow current-tree slice:
+
+- `README.md`
+- `methods/EverCore/demo/utils/simple_memory_manager.py`
+- `methods/EverCore/src/agentic_layer/memory_manager.py`
+- `methods/EverCore/tests/test_memory_manager_multi_type_search.py`
+- `methods/EverCore/tests/test_simple_memory_manager.py`
+
+The upstream PR should not include Raven/deploy work, OpenClaw work, benchmark
+adapter fixes, provider policy, or delete/reset semantics.
+
+## Proposed PR Summary
+
+- Update the README search example to use `POST /api/v1/memories/search`.
+- Treat HTTP 202 Accepted as successful background extraction in
+ `SimpleMemoryManager.store()`.
+- Query all requested non-profile memory types in keyword/vector retrieval
+ instead of only `memory_types[0]`.
+- Preserve same backend ids from different memory collections during hybrid
+ dedupe by using `(memory_type,id)`.
+- Add focused regression tests for multi-type retrieval and 202 Accepted demo
+ behavior.
+
+## Verification
+
+Commands run:
+
+```bash
+cd methods/EverCore
+PYTHONPATH=src uv run pytest tests/test_memory_manager_multi_type_search.py tests/test_simple_memory_manager.py
+uv run black --check src/agentic_layer/memory_manager.py demo/utils/simple_memory_manager.py tests/test_memory_manager_multi_type_search.py tests/test_simple_memory_manager.py
+cd ../..
+git diff --check -- README.md methods/EverCore docs/upstream-return
+```
+
+Result: PASS.
+
+Notes:
+
+- `PYTHONPATH=src` is required for direct targeted pytest invocation from this
+ checkout.
+- `tests/test_memory_controller.py` collected zero pytest tests and was not used
+ as verification evidence.
+
+## Live Upstream State Rechecked
+
+On 2026-05-15:
+
+- #191, #93, and #78 were still open.
+- #185 and #211 were still `BLOCKED`.
+- #89, #109, and #138 were still `DIRTY`.
+
+## Reviewer Questions
+
+- Does the README POST example match the public search-controller contract?
+- Should the #78 fix remain limited to non-profile `MemoryManager` retrieval,
+ leaving profile/raw-message orchestration to the higher-level search service?
+- Is `(memory_type,id)` the right dedupe key for cross-collection hybrid hits?
diff --git a/docs/upstream-return/PR_MATRIX.md b/docs/upstream-return/PR_MATRIX.md
new file mode 100644
index 00000000..487b9857
--- /dev/null
+++ b/docs/upstream-return/PR_MATRIX.md
@@ -0,0 +1,46 @@
+# Upstream PR Matrix
+
+Live source: `EverMind-AI/EverOS` open PRs fetched on 2026-05-14. GitHub reported 37 open PRs: 1 `CLEAN`, 4 `BLOCKED`, and 32 `DIRTY`.
+Targeted recheck on 2026-05-15 confirmed PRs #185/#211 remain `BLOCKED` and PRs #89/#109/#138 remain `DIRTY`.
+
+This matrix treats `DIRTY` as needing rebase before merge review. `BLOCKED` means GitHub does not currently report the branch as cleanly mergeable; owner review is still required to distinguish checks, conflicts, and policy gates.
+
+| PR | Merge state | Family | File surface | Related issues | Verdict | Owner action |
+|---|---|---|---|---|---|---|
+| [#213](https://github.com/EverMind-AI/EverOS/pull/213) docs links to project repos | DIRTY | README docs | `README.md` | none | needs rebase | Rebase against current README or close if superseded by local README restructuring. |
+| [#211](https://github.com/EverMind-AI/EverOS/pull/211) handle 202 Accepted in demo store | BLOCKED | Demo/API UX | `methods/EverCore/demo/utils/simple_memory_manager.py` | #93 | superseded by local slice | Keep as source evidence; local current-tree patch includes the 202 Accepted behavior plus focused test. |
+| [#206](https://github.com/EverMind-AI/EverOS/pull/206) MinIO credentials from env | BLOCKED | Security/config | `methods/EverCore/docker-compose.yaml`, `methods/EverCore/env.template` | infra/security | needs review | Security-positive, but owner must verify default DX and env template migration. |
+| [#202](https://github.com/EverMind-AI/EverOS/pull/202) OpenClaw endpoint docs | BLOCKED | OpenClaw docs | `methods/EverCore/examples/openclaw-plugin/SKILL.md` | #150, #139 | needs review | Current-path doc PR; compare against local fork OpenClaw packet before merge. |
+| [#196](https://github.com/EverMind-AI/EverOS/pull/196) v0 -> v1 API migration | DIRTY | API/docs migration | broad `methods/evermemos/...` surface | #191 | close/rework | Old path and broad scope; replace with narrow current-tree migration. |
+| [#189](https://github.com/EverMind-AI/EverOS/pull/189) OpenClaw plugin call API fail | DIRTY | OpenClaw plugin | `methods/evermemos/examples/openclaw-plugin/src/api.js` | #150, #139 | close/rework | Old path surface; supersede with current `methods/EverCore` plugin work. |
+| [#185](https://github.com/EverMind-AI/EverOS/pull/185) README search example | BLOCKED | README docs | `README.md` | #191 | superseded by local slice | Keep as source evidence; local current-tree patch applies the narrow POST search-example correction. |
+| [#159](https://github.com/EverMind-AI/EverOS/pull/159) query expansion rewrite | DIRTY | Memory retrieval | `src/agentic_layer/memory_manager.py`, `src/memory_layer/query_expansion.py` | #65/#34 class | needs maintainer decision | Architecture-level retrieval change; needs design review before rebase spend. |
+| [#157](https://github.com/EverMind-AI/EverOS/pull/157) production Dockerfile | DIRTY | Infra/Docker | `.dockerignore`, `Dockerfile` | #21 | needs maintainer decision | Decide official Docker support shape first. |
+| [#154](https://github.com/EverMind-AI/EverOS/pull/154) anti-pattern cleanup / duplicate RRF | DIRTY | Code quality | demo, biz, repository files | #50 | needs rebase | Candidate canonical cleanup if narrowed; overlaps #97/#141/#137. |
+| [#144](https://github.com/EverMind-AI/EverOS/pull/144) MiniMax provider | DIRTY | Provider config | env/provider/test files under `methods/evermemos` | #29/#23 class | close/rework | Old path and provider-specific; decide provider roadmap before patch. |
+| [#141](https://github.com/EverMind-AI/EverOS/pull/141) duplicate RRF in demo | DIRTY | Code quality | `demo/utils/simple_memory_manager.py` | #50 | duplicate | Close after selecting #154 or a new narrow PR. |
+| [#140](https://github.com/EverMind-AI/EverOS/pull/140) normalize plugin-wrapped content | DIRTY | Memory extraction | extractor + test | memory quality | needs rebase | Potentially useful; requires focused extractor test. |
+| [#138](https://github.com/EverMind-AI/EverOS/pull/138) multiple memory types search | DIRTY | Memory API bug | API DTO/controller/service/search files | #78 | superseded by local slice | Local current-tree patch implements the narrow MemoryManager behavior with focused tests; avoid rebasing broad API surface unless reviewer finds a missing contract. |
+| [#137](https://github.com/EverMind-AI/EverOS/pull/137) Python anti-patterns | DIRTY | Code quality | biz/repository files | code quality | duplicate | Overlaps #154/#126/#112/#110; close or split into targeted lint PR. |
+| [#136](https://github.com/EverMind-AI/EverOS/pull/136) BM25/Embedding filename mismatch | DIRTY | Benchmark correctness | broad demo/docs/eval/src/tests | #127 | close/rework | The bug is high priority, but PR is too broad; request focused patch and repro. |
+| [#135](https://github.com/EverMind-AI/EverOS/pull/135) AP Memory Agent demo | DIRTY | Demo/use case | demo app, pyproject, lockfile, memorize path | use cases | needs maintainer decision | Large demo + lockfile impact; owner must decide product fit. |
+| [#132](https://github.com/EverMind-AI/EverOS/pull/132) full episode param | DIRTY | Memory API | broad demo/docs/eval/src/tests | #131 | close/rework | Too broad for #131; ask for narrow API/test patch. |
+| [#129](https://github.com/EverMind-AI/EverOS/pull/129) dedup and foresight cleanup | BLOCKED | Memory lifecycle | cleanup/memorize/repository files | #95 | needs review | Semantically important; require tenant-scope and retention tests before merge. |
+| [#128](https://github.com/EverMind-AI/EverOS/pull/128) OpenClaw plugin API compatibility | CLEAN | OpenClaw plugin | `evermemos-openclaw-plugin/*` | #150, #139 | needs review | GitHub-clean, but file path looks legacy; verify relevance before merge. |
+| [#126](https://github.com/EverMind-AI/EverOS/pull/126) typos / None / bare except | DIRTY | Code quality | evaluation + biz/repository/tests | code quality | duplicate | Close or harvest tiny fixes into selected cleanup PR. |
+| [#124](https://github.com/EverMind-AI/EverOS/pull/124) force_boundary | DIRTY | Memory behavior/API | API DTO/converter/memorize/extractor | #16 class | needs maintainer decision | API contract change; needs owner decision before rebase. |
+| [#118](https://github.com/EverMind-AI/EverOS/pull/118) datetime conversion | DIRTY | Data consistency | memory_manager + Mongo base | #48 class | needs rebase | Candidate only if canonical timestamp contract is accepted. |
+| [#115](https://github.com/EverMind-AI/EverOS/pull/115) retrieval filename typo | DIRTY | Benchmark correctness | evaluation adapter file | #127 class | duplicate | Likely superseded by #136 or a new focused #127 fix. |
+| [#113](https://github.com/EverMind-AI/EverOS/pull/113) bool comparison | DIRTY | Code quality | biz/repository files | code quality | duplicate | Low-risk but overlaps cleanup wave; close or batch. |
+| [#112](https://github.com/EverMind-AI/EverOS/pull/112) docstring/bare except/timestamp | DIRTY | Code quality/data consistency | demo, memory manager, biz, repo, prompt | #48 class | duplicate | Overlaps #108/#110/#126/#154. |
+| [#110](https://github.com/EverMind-AI/EverOS/pull/110) bare except/ISO timestamp/docstring | DIRTY | Code quality/data consistency | demo, db ops, repo, prompt | #48 class | duplicate | Close after canonical timestamp/cleanup path chosen. |
+| [#109](https://github.com/EverMind-AI/EverOS/pull/109) multiple memory_types search | DIRTY | Memory API bug | `src/agentic_layer/memory_manager.py` | #78 | duplicate | Superseded by the local current-tree #78 slice. |
+| [#108](https://github.com/EverMind-AI/EverOS/pull/108) ISO 8601 timestamp | DIRTY | Data consistency | demo, docker, db ops, repo, prompts | #48 | duplicate | Too broad; keep timestamp contract then reimplement narrow. |
+| [#107](https://github.com/EverMind-AI/EverOS/pull/107) bare except | DIRTY | Code quality | demo, docker, db ops, repo, tests | code quality | duplicate | Close or batch into cleanup PR. |
+| [#106](https://github.com/EverMind-AI/EverOS/pull/106) two phase memory extraction | DIRTY | Memory lifecycle | docs/env/worker/memorize/delete service | lifecycle | needs maintainer decision | Architecture change; needs design review. |
+| [#98](https://github.com/EverMind-AI/EverOS/pull/98) bare excepts | DIRTY | Code quality | config/demo/docker/db/repo | code quality | duplicate | Close as stale cleanup duplicate. |
+| [#97](https://github.com/EverMind-AI/EverOS/pull/97) duplicate RRF | DIRTY | Code quality | config/demo/docker | #50 | duplicate | Superseded by #141/#154 or a new narrow fix. |
+| [#91](https://github.com/EverMind-AI/EverOS/pull/91) bare excepts | DIRTY | Code quality | db ops/repo/tests | code quality | duplicate | Close as stale cleanup duplicate. |
+| [#90](https://github.com/EverMind-AI/EverOS/pull/90) remove unused Mongo init volume | DIRTY | Infra/Docker | `docker-compose.yaml` | #21/#1 class | needs maintainer decision | Small infra cleanup, but should follow Docker support decision. |
+| [#89](https://github.com/EverMind-AI/EverOS/pull/89) multi-memory-type search | DIRTY | Memory API bug | `src/agentic_layer/memory_manager.py` | #78 | duplicate | Superseded by the local current-tree #78 slice. |
+| [#86](https://github.com/EverMind-AI/EverOS/pull/86) STARTER_KIT quick start | DIRTY | Docs/community | `docs/STARTER_KIT.md` | #57 | needs rebase | Rebase and verify links, or replace with narrow link-only PR. |
diff --git a/docs/upstream-return/UPSTREAM_STRATEGY.md b/docs/upstream-return/UPSTREAM_STRATEGY.md
new file mode 100644
index 00000000..25dcb8b4
--- /dev/null
+++ b/docs/upstream-return/UPSTREAM_STRATEGY.md
@@ -0,0 +1,53 @@
+# Upstream Strategy
+
+Live source: upstream inventory fetched on 2026-05-14.
+
+Targeted 2026-05-15 recheck: issues #191/#93/#78 are still open; PRs #185
+and #211 are still `BLOCKED`; PRs #89/#109/#138 are still `DIRTY`.
+
+## Current Queue Shape
+
+- Open issues: 52.
+- Open PRs: 37.
+- PR merge states: 1 `CLEAN`, 4 `BLOCKED`, 32 `DIRTY`.
+- The main risk is not lack of patches. The risk is merging stale, duplicated, or old-path patches without resolving maintainer policy.
+
+## Recommended Maintainer Order
+
+1. Open one narrow current-tree PR for the local #191/#93/#78 slice:
+ - #191: README search example uses `POST /api/v1/memories/search`.
+ - #93: `SimpleMemoryManager.store()` treats 202 Accepted as background success.
+ - #78: `MemoryManager` searches all requested non-profile memory types and
+ dedupes hybrid hits by `(memory_type,id)`.
+2. Continue small current-tree review:
+ - #202 for OpenClaw docs if it matches the current plugin path.
+3. Resolve high-impact API bugs with one selected PR per bug:
+ - #127: request a focused adapter/fixture fix; #136 is too broad as-is.
+ - #131: request a narrow full-episode patch; #132 is too broad as-is.
+4. Make maintainer decisions before implementation:
+ - delete/reset/cascade semantics (#14/#148);
+ - lifecycle/dedup/status/session scope (#95/#143/#27);
+ - provider/deployment support (#29/#23/#21/#4/#1);
+ - benchmark reproducibility contract (#73/#3/#195/#87).
+5. Sweep duplicated cleanup PRs:
+ - RRF duplicates: #97/#141/#154.
+ - timestamp duplicates: #108/#110/#112/#118.
+ - bare-except/code-quality duplicates: #91/#98/#107/#110/#112/#126/#137/#154.
+6. Close or rework old-path PRs:
+ - `methods/evermemos/...` surfaces should not merge until path relevance is proven.
+ - `evermemos-openclaw-plugin/*` should be verified against current package layout even when GitHub reports `CLEAN`.
+
+## Fork Work That Is Worth Doing Locally
+
+- Return the local #191/#93/#78 slice as one narrow PR after targeted tests and
+ reviewer pass.
+- Build a narrow current-tree OpenClaw docs/fix patch if #202/#128 are stale or path-wrong.
+- Prepare answer drafts for repeated question issues so maintainers can close low-code threads quickly.
+- Prepare benchmark repro notes, but do not claim result parity without running the exact benchmark path.
+
+## What Not To Do
+
+- Do not merge or push anything to upstream from this fork pass.
+- Do not mark issues resolved from title/body alone.
+- Do not spend time rebasing every dirty cleanup PR; pick canonical ones first.
+- Do not accept old-path patches only because the title matches a live issue.
diff --git a/docs/upstream-return/goal.md b/docs/upstream-return/goal.md
new file mode 100644
index 00000000..2f1c8f05
--- /dev/null
+++ b/docs/upstream-return/goal.md
@@ -0,0 +1,241 @@
+# EverOS Upstream Resolution Captain Goal
+
+Short `/goal` capsule:
+
+```text
+Read and execute docs/upstream-return/goal.md. Run a 24h upstream-resolution pass for Fearvox/EverOS. Fetch all open issues and PRs from EverMind-AI/EverOS live, classify every item, and produce an owner-reviewable return packet. Do not touch upstream, do not push main, do not comment externally. Optimize for maintainer-ready truth, not activity volume.
+```
+
+## Role
+
+You are the 24h Upstream Resolution Captain for the Fearvox/EverOS fork.
+
+Your job is not to "do random fixes." Your job is to turn the full upstream
+EverMind-AI/EverOS open issue and PR queue into a precise, owner-reviewable
+return strategy, then implement only the highest-leverage fork-side artifacts or
+small patches that help multiple upstream items at once.
+
+## Operating Repositories
+
+- Working fork: `Fearvox/EverOS`
+- Upstream source of truth: `EverMind-AI/EverOS`
+- Work only in the current local checkout unless explicitly told otherwise.
+- Push only to a dedicated fork branch for this run.
+
+## Hard Boundaries
+
+1. Do not push to `origin/main`.
+2. Do not push to `EverMind-AI/EverOS`.
+3. Do not comment on upstream issues or PRs.
+4. Do not close, label, assign, merge, or mark ready upstream items.
+5. Do not edit `.claude/`, secrets, local machine config, or credential files.
+6. Do not treat old cached GitHub data as truth; fetch live state.
+7. Do not mark a PR or issue as resolved from title/body alone.
+8. Do not create noisy one-off PRs unless the patch is narrow, verified, and
+ clearly maps to multiple upstream items.
+
+## Primary Objective
+
+Resolve the upstream queue into decisions.
+
+For every open upstream issue and pull request, assign exactly one disposition:
+
+- `FIX_IN_FORK`
+- `ANSWER_DRAFT`
+- `CLOSE_STALE`
+- `DUPLICATE_OF`
+- `REVIEW_EXISTING_PR`
+- `NEEDS_MAINTAINER_DECISION`
+- `OUT_OF_SCOPE`
+
+Each disposition must include evidence and a next action.
+
+## Required Outputs
+
+Create or update these files:
+
+- `docs/upstream-return/ISSUE_MATRIX.md`
+- `docs/upstream-return/PR_MATRIX.md`
+- `docs/upstream-return/CANONICAL_PROBLEM_FAMILIES.md`
+- `docs/upstream-return/UPSTREAM_STRATEGY.md`
+- `docs/upstream-return/OWNER_BRIEF.md`
+- `docs/upstream-return/FINAL_REPORT.md`
+
+If implementation work is performed, also add:
+
+- `docs/upstream-return/VALIDATION.md`
+
+## Canonical Problem Families
+
+Classify every issue and PR into one primary family:
+
+1. Benchmark truth and reproducibility
+2. Memory API correctness
+3. Memory lifecycle and reliability
+4. Integration DX and use cases
+5. Infrastructure, security, and provider configuration
+6. Stale hygiene and duplicate community PRs
+7. Maintainer-only policy or roadmap decision
+
+## Initial Live Snapshot To Re-Verify
+
+The previous supervisor snapshot found:
+
+- Upstream open issues: 52
+- Upstream open PRs: 38
+- Open PR merge states: 32 dirty, 5 blocked, 1 clean
+- Issue concentration: methods, use cases, benchmarks
+
+Do not trust those numbers blindly. Re-fetch before writing.
+
+## Mandatory First Cycle
+
+1. Capture local git state and current branch.
+2. Fetch live upstream issue and PR state:
+ - `gh issue list --repo EverMind-AI/EverOS --state open --limit 200 --json ...`
+ - `gh pr list --repo EverMind-AI/EverOS --state open --limit 200 --json ...`
+3. For every PR, inspect file surface:
+ - `gh pr view --repo EverMind-AI/EverOS --json files,mergeStateStatus,isDraft,baseRefName,headRefName`
+4. Write a raw inventory section before any recommendations.
+5. Group issues and PRs by problem family.
+6. Identify duplicates and likely superseding PRs.
+7. Only then decide whether any fork-side patch is worth doing.
+
+## Issue Matrix Schema
+
+Each upstream issue row must include:
+
+- Issue number and URL
+- Title
+- Labels
+- Age / last updated
+- Problem family
+- Concrete user pain
+- Related upstream PRs
+- Related issues
+- Disposition
+- Evidence
+- Proposed owner action
+- Upstream return priority: P0 / P1 / P2 / P3
+
+## PR Matrix Schema
+
+Each upstream PR row must include:
+
+- PR number and URL
+- Title
+- Author
+- Base branch
+- Head branch
+- Merge state
+- Check state
+- Changed file surface
+- Related issues
+- Risk class: docs / tests / API / infra / security / broad refactor
+- Verdict: mergeable / needs rebase / needs review / duplicate / close
+- Evidence
+- Proposed owner action
+
+## Scoring
+
+Score useful work, not motion:
+
+- +5 complete issue matrix covering all open upstream issues
+- +5 complete PR matrix covering all open upstream PRs
+- +5 canonical problem-family synthesis with duplicates and supersession map
+- +4 upstream strategy that gives owner a concrete return order
+- +4 small verified fork patch that resolves multiple upstream issues
+- +3 benchmark reproduction/prompt/config evidence packet
+- +3 API contract documentation or schema packet
+- +2 answer drafts for high-value upstream questions
+- +1 clean owner brief under 20 lines
+- -3 claim without live evidence
+- -5 upstream/main mutation or public comment without owner approval
+
+## Recommended Return Strategy
+
+Prefer a staged upstream return:
+
+1. Maintainer packet first:
+ - matrices
+ - deduplication map
+ - problem families
+ - proposed merge/close/rework list
+2. Low-risk docs/API contract PR second.
+3. Benchmark reproducibility packet third.
+4. Code patches only after the queue shape is clear.
+
+The first artifact should help maintainers answer: "What should we merge, close,
+or ask for next?" before asking them to review new code.
+
+## 2026-05-15 Return Slice
+
+The first code slice is now narrowed to #191/#93/#78:
+
+- #191: update the README memory search example to call
+ `POST /api/v1/memories/search`.
+- #93: treat HTTP 202 Accepted as successful background extraction in
+ `SimpleMemoryManager.store()`.
+- #78: search all requested non-profile `memory_types` in keyword/vector paths
+ and dedupe hybrid hits by `(memory_type,id)`.
+
+Out of scope for this slice: OpenClaw, benchmark filename mismatches,
+delete/reset semantics, provider/deployment policy, and Raven/deploy work.
+
+## Candidate High-Leverage Tracks
+
+### Track A: Benchmark Truth Pack
+
+Targets likely related to LoCoMo, PersonaMem, HaluMem, prompt/config, raw
+outputs, API-vs-local evaluation mismatch, and token accounting.
+
+Output should identify exact missing evidence, not invent benchmark claims.
+
+### Track B: Memory API Contract Pack
+
+Targets search/fetch behavior, `memory_types`, profile support, score
+normalization, full episode content, timestamp format, and paper-vs-service
+retrieval mismatch.
+
+Output should separate documented behavior, actual code behavior, and proposed
+contract.
+
+### Track C: Integration DX Pack
+
+Targets OpenClaw, Chat Agent integration, Codex/plugin questions, Docker/local
+provider setup, broken links, and 202 Accepted handling.
+
+Output should make community integrators faster without promising unsupported
+runtime behavior.
+
+## Exit Conditions
+
+Stop and write `FINAL_REPORT.md` when any of these is true:
+
+- All open upstream issues and PRs have a disposition.
+- A maintainer packet is ready for owner review.
+- A hard boundary would be crossed to continue.
+- The run reaches 24h.
+
+## Final Report Must Include
+
+- Live counts at start and end
+- Files created or changed
+- Every output artifact path
+- Top 10 upstream actions recommended
+- Items not safe to return upstream yet
+- Any fork-only experiments that should stay fork-only
+- Verification commands run
+- Residual risks
+
+## Owner Brief Shape
+
+Keep `OWNER_BRIEF.md` under 20 lines:
+
+- Verdict
+- What to return upstream first
+- What to close or supersede
+- What needs maintainer decision
+- What not to touch yet
+- Highest-risk PRs/issues
+- Suggested next command or PR action
diff --git a/methods/EverCore/demo/utils/simple_memory_manager.py b/methods/EverCore/demo/utils/simple_memory_manager.py
index 93898d22..a4157a88 100644
--- a/methods/EverCore/demo/utils/simple_memory_manager.py
+++ b/methods/EverCore/demo/utils/simple_memory_manager.py
@@ -132,15 +132,20 @@ async def store(self, content: str, sender: str = "User") -> bool:
"timestamp": int(now.timestamp() * 1000),
"content": content,
}
- payload = {
- "user_id": self.user_id,
- "messages": [message_item],
- }
+ payload = {"user_id": self.user_id, "messages": [message_item]}
try:
async with httpx.AsyncClient(timeout=500.0) as client:
response = await client.post(self.memorize_url, json=payload)
response.raise_for_status()
+
+ # Background mode returns 202 Accepted
+ if response.status_code == 202:
+ print(
+ f" ⏳ Accepted: {content[:40]}... (Processing in background)"
+ )
+ return True
+
result = response.json()
# v1 response: {"data": {"status": "...", "count": N, ...}}
@@ -206,7 +211,11 @@ async def _init_settings(self) -> bool:
return False
async def search(
- self, query: str, top_k: int = 3, mode: str = "hybrid", show_details: bool = True
+ self,
+ query: str,
+ top_k: int = 3,
+ mode: str = "hybrid",
+ show_details: bool = True,
) -> List[Dict[str, Any]]:
"""Search memories
@@ -339,4 +348,4 @@ def print_summary(self):
print(" - ❌ Won't extract: Too brief, low-information small talk")
print(
" - 🎯 Best practice: Multi-turn conversations, rich context, specific details"
- )
\ No newline at end of file
+ )
diff --git a/methods/EverCore/examples/openclaw-plugin/SKILL.md b/methods/EverCore/examples/openclaw-plugin/SKILL.md
index 9f4f4f2f..1afe15e9 100644
--- a/methods/EverCore/examples/openclaw-plugin/SKILL.md
+++ b/methods/EverCore/examples/openclaw-plugin/SKILL.md
@@ -75,7 +75,7 @@ Automatic lifecycle behavior:
| `bootstrap()` | Session starts | Backend health check and session state init |
| `assemble()` | Before each turn | Searches relevant memory and injects it as context |
| `afterTurn()` | After each turn | Saves new messages from the turn |
-| `compact()` | Compaction check | Participates in token-budget decisions |
+| Compaction | Not owned | Leaves host token-budget compaction untouched |
| `dispose()` | Session ends | Clears in-memory session state |
User-facing result:
diff --git a/methods/EverCore/examples/openclaw-plugin/package.json b/methods/EverCore/examples/openclaw-plugin/package.json
index 684ddea8..5de9be55 100644
--- a/methods/EverCore/examples/openclaw-plugin/package.json
+++ b/methods/EverCore/examples/openclaw-plugin/package.json
@@ -4,6 +4,9 @@
"description": "EverOS OpenClaw Plugin — persistent memory through natural conversation",
"type": "module",
"main": "./index.js",
+ "scripts": {
+ "test": "node --test test/*.test.js"
+ },
"bin": {
"everos-install": "./bin/install.js"
},
diff --git a/methods/EverCore/examples/openclaw-plugin/src/engine.js b/methods/EverCore/examples/openclaw-plugin/src/engine.js
index 00a99f14..0015aac8 100644
--- a/methods/EverCore/examples/openclaw-plugin/src/engine.js
+++ b/methods/EverCore/examples/openclaw-plugin/src/engine.js
@@ -184,26 +184,6 @@ export function createContextEngine(pluginMeta, pluginConfig, logger) {
}
},
- async compact({ sessionId, sessionKey, tokenBudget, currentTokenCount }) {
- const state = sessionState.get(sessionKey);
- if (!state) {
- return { ok: true, compacted: false, reason: "no session state" };
- }
-
- state.savedUpTo = 0;
-
- const threshold = tokenBudget ? tokenBudget * 0.8 : 8000;
- const overBudget = currentTokenCount && currentTokenCount > threshold;
-
- return {
- ok: true,
- compacted: false,
- reason: overBudget
- ? `token count (${currentTokenCount}) exceeds 80% of budget (${tokenBudget}), host should compact`
- : "within threshold",
- };
- },
-
async dispose({ sessionKey } = {}) {
if (sessionKey) {
sessionState.delete(sessionKey);
diff --git a/methods/EverCore/examples/openclaw-plugin/src/types.js b/methods/EverCore/examples/openclaw-plugin/src/types.js
index 3db2bc8f..5b78beed 100644
--- a/methods/EverCore/examples/openclaw-plugin/src/types.js
+++ b/methods/EverCore/examples/openclaw-plugin/src/types.js
@@ -50,25 +50,8 @@
* @property {string} [errorMessage] - Error message if turn failed
*/
-/**
- * @typedef {Object} CompactContext
- * @property {Array} messages - Current session messages
- * @property {number} tokenCount - Estimated token count of context
- * @property {string} [sessionId] - Optional session identifier
- */
-
-/**
- * @typedef {Object} CompactResult
- * @property {boolean} shouldCompact - Whether compaction is recommended
- * @property {string} reason - Explanation of the decision
- * @property {Object} [metadata] - Additional metadata
- * @property {string} [metadata.memoryStrategy] - Suggested memory consolidation strategy
- * @property {number} [metadata.turnCount] - Turn count at evaluation time
- */
-
/**
* @typedef {Object} ParsedMemoryResponse
* @property {Array<{text: string, timestamp: number|string|null}>} episodic - Episodic memories
* @property {Array<{text: string, timestamp: number|string|null}>} pending - Recent unconsolidated messages
*/
-
diff --git a/methods/EverCore/examples/openclaw-plugin/test/engine.test.js b/methods/EverCore/examples/openclaw-plugin/test/engine.test.js
new file mode 100644
index 00000000..a9b1491e
--- /dev/null
+++ b/methods/EverCore/examples/openclaw-plugin/test/engine.test.js
@@ -0,0 +1,25 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { createContextEngine } from "../src/engine.js";
+
+const pluginMeta = {
+ id: "evermind-ai-everos",
+ name: "EverOS Test Engine",
+ version: "0.0.0-test",
+};
+
+function createTestEngine() {
+ const logger = {
+ info: () => {},
+ warn: () => {},
+ };
+ return createContextEngine(pluginMeta, {}, logger);
+}
+
+test("passive memory engine does not expose a compact capability", () => {
+ const engine = createTestEngine();
+
+ assert.equal(engine.info.ownsCompaction, false);
+ assert.equal(Object.hasOwn(engine, "compact"), false);
+});
diff --git a/methods/EverCore/src/agentic_layer/memory_manager.py b/methods/EverCore/src/agentic_layer/memory_manager.py
index e6e93005..08c6b59b 100644
--- a/methods/EverCore/src/agentic_layer/memory_manager.py
+++ b/methods/EverCore/src/agentic_layer/memory_manager.py
@@ -117,6 +117,39 @@
MemoryType.AGENT_SKILL: AgentSkillEsRepository,
}
+MILVUS_REPO_MAP = {
+ MemoryType.FORESIGHT: ForesightMilvusRepository,
+ MemoryType.ATOMIC_FACT: AtomicFactMilvusRepository,
+ MemoryType.EPISODIC_MEMORY: EpisodicMemoryMilvusRepository,
+ MemoryType.AGENT_CASE: AgentCaseMilvusRepository,
+ MemoryType.AGENT_SKILL: AgentSkillMilvusRepository,
+}
+
+
+def _memory_type_label(memory_types: List[MemoryType]) -> str:
+ if not memory_types:
+ return 'unknown'
+ return ','.join(memory_type.value for memory_type in memory_types)
+
+
+def _hit_score(hit: Dict[str, Any]) -> float:
+ raw_score = hit.get('score', hit.get('_score', 0.0))
+ try:
+ return float(raw_score)
+ except (TypeError, ValueError):
+ return 0.0
+
+
+def _sort_hits_by_score(hits: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ return sorted(hits, key=_hit_score, reverse=True)
+
+
+def _hit_identity(hit: Dict[str, Any]) -> Optional[tuple[str, str]]:
+ hit_id = hit.get('id') or hit.get('_id')
+ if not hit_id:
+ return None
+ return (str(hit.get('memory_type', 'unknown')), str(hit_id))
+
@dataclass
class AtomicFactCandidate:
@@ -457,11 +490,6 @@ async def retrieve_mem_keyword(
"""Keyword-based memory retrieval"""
top_k = retrieve_mem_request.top_k
is_unlimited_mode = top_k == -1
- memory_type = (
- retrieve_mem_request.memory_types[0].value
- if retrieve_mem_request.memory_types
- else 'unknown'
- )
try:
hits = await self.get_keyword_search_results(
@@ -485,11 +513,7 @@ async def get_keyword_search_results(
) -> List[Dict[str, Any]]:
"""Keyword search with stage-level metrics"""
stage_start = time.perf_counter()
- memory_type = (
- retrieve_mem_request.memory_types[0].value
- if retrieve_mem_request.memory_types
- else 'unknown'
- )
+ memory_type = _memory_type_label(retrieve_mem_request.memory_types)
try:
# Get parameters from Request
@@ -528,32 +552,34 @@ async def get_keyword_search_results(
if end_time is not None:
date_range["lte"] = end_time
- mem_type = memory_types[0]
-
- repo_class = ES_REPO_MAP.get(mem_type)
- if not repo_class:
- logger.warning(f"Unsupported memory_type: {mem_type}")
- return []
+ all_results = []
+ for mem_type in memory_types:
+ repo_class = ES_REPO_MAP.get(mem_type)
+ if not repo_class:
+ logger.warning(f"Unsupported memory_type: {mem_type}")
+ continue
- es_repo = get_bean_by_type(repo_class)
- logger.debug(f"Using {repo_class.__name__} for {mem_type}")
+ es_repo = get_bean_by_type(repo_class)
+ logger.debug(f"Using {repo_class.__name__} for {mem_type}")
- results = await es_repo.multi_search(
- query=query_words,
- user_id=user_id,
- group_ids=group_ids, # Pass normalized list
- size=effective_limit,
- from_=0,
- date_range=date_range,
- )
+ results = await es_repo.multi_search(
+ query=query_words,
+ user_id=user_id,
+ group_ids=group_ids, # Pass normalized list
+ size=effective_limit,
+ from_=0,
+ date_range=date_range,
+ )
- # Mark memory_type, search_source, and unified score
- if results:
- for r in results:
- r['memory_type'] = mem_type.value
- r['_search_source'] = RetrieveMethod.KEYWORD.value
- r['id'] = r.get('_id', '') # Unify ES '_id' to 'id'
- r['score'] = r.get('_score', 0.0) # Unified score field
+ # Mark memory_type, search_source, and unified score
+ if results:
+ for r in results:
+ r['memory_type'] = mem_type.value
+ r['_search_source'] = RetrieveMethod.KEYWORD.value
+ r['id'] = r.get('_id', '') # Unify ES '_id' to 'id'
+ r['score'] = r.get('_score', 0.0) # Unified score field
+ all_results.extend(results)
+ results = _sort_hits_by_score(all_results)
# Record stage metrics
record_retrieve_stage(
@@ -587,11 +613,6 @@ async def retrieve_mem_vector(
"""Vector-based memory retrieval"""
top_k = retrieve_mem_request.top_k
is_unlimited_mode = top_k == -1
- memory_type = (
- retrieve_mem_request.memory_types[0].value
- if retrieve_mem_request.memory_types
- else 'unknown'
- )
try:
hits = await self.get_vector_search_results(
@@ -614,11 +635,8 @@ async def get_vector_search_results(
retrieve_method: str = RetrieveMethod.VECTOR.value,
) -> List[Dict[str, Any]]:
"""Vector search with stage-level metrics (embedding + milvus_search)"""
- memory_type = (
- retrieve_mem_request.memory_types[0].value
- if retrieve_mem_request.memory_types
- else 'unknown'
- )
+ stage_start = time.perf_counter()
+ memory_type = _memory_type_label(retrieve_mem_request.memory_types)
try:
# Get parameters from Request
@@ -643,12 +661,8 @@ async def get_vector_search_results(
effective_limit = DEFAULT_TOPK_LIMIT
else:
effective_limit = top_k * DEFAULT_RECALL_MULTIPLIER
- # Milvus similarity threshold (only applied in unlimited mode or when user specifies radius)
- effective_radius = None
start_time = retrieve_mem_request.start_time
end_time = retrieve_mem_request.end_time
- mem_type = retrieve_mem_request.memory_types[0]
-
logger.debug(
f"retrieve_mem_vector called with query: {query}, user_id: {user_id}, group_ids: {group_ids}, top_k: {top_k}"
)
@@ -671,115 +685,113 @@ async def get_vector_search_results(
f"Query text vectorization completed, vector dimension: {len(query_vector_list)}"
)
- # Select Milvus repository based on memory type
- match mem_type:
- case MemoryType.FORESIGHT:
- milvus_repo = get_bean_by_type(ForesightMilvusRepository)
- case MemoryType.ATOMIC_FACT:
- milvus_repo = get_bean_by_type(AtomicFactMilvusRepository)
- case MemoryType.EPISODIC_MEMORY:
- milvus_repo = get_bean_by_type(EpisodicMemoryMilvusRepository)
- case MemoryType.AGENT_CASE:
- milvus_repo = get_bean_by_type(AgentCaseMilvusRepository)
- case MemoryType.AGENT_SKILL:
- milvus_repo = get_bean_by_type(AgentSkillMilvusRepository)
- case _:
- raise ValueError(f"Unsupported memory type: {mem_type}")
+ all_search_results = []
+ for mem_type in retrieve_mem_request.memory_types:
+ repo_class = MILVUS_REPO_MAP.get(mem_type)
+ if not repo_class:
+ logger.warning(f"Unsupported memory type: {mem_type}")
+ continue
- # Handle time range filter conditions
- start_time_dt = None
- end_time_dt = None
+ milvus_repo = get_bean_by_type(repo_class)
- if start_time is not None:
- start_time_dt = (
- from_iso_format(start_time)
- if isinstance(start_time, str)
- else start_time
- )
+ # Handle time range filter conditions
+ start_time_dt = None
+ end_time_dt = None
- if end_time is not None:
- if isinstance(end_time, str):
- end_time_dt = from_iso_format(end_time)
- # If date only format, set to end of day
- if len(end_time) == 10:
- end_time_dt = end_time_dt.replace(hour=23, minute=59, second=59)
+ if start_time is not None:
+ start_time_dt = (
+ from_iso_format(start_time)
+ if isinstance(start_time, str)
+ else start_time
+ )
+
+ if end_time is not None:
+ if isinstance(end_time, str):
+ end_time_dt = from_iso_format(end_time)
+ # If date only format, set to end of day
+ if len(end_time) == 10:
+ end_time_dt = end_time_dt.replace(
+ hour=23, minute=59, second=59
+ )
+ else:
+ end_time_dt = end_time
+
+ # Handle foresight time range (only valid for foresight)
+ if mem_type == MemoryType.FORESIGHT:
+ if retrieve_mem_request.start_time:
+ start_time_dt = from_iso_format(retrieve_mem_request.start_time)
+ if retrieve_mem_request.end_time:
+ end_time_dt = from_iso_format(retrieve_mem_request.end_time)
+
+ # Call Milvus vector search (pass different parameters based on memory type)
+ # Threshold logic:
+ # - User specified radius: always use it
+ # - Unlimited mode (top_k=-1): apply DEFAULT_MILVUS_SIMILARITY_THRESHOLD (0.6)
+ # - Normal mode (top_k>0): no threshold filtering (rely on top_k limit)
+ if retrieve_mem_request.radius is not None:
+ # User specified radius, use it
+ effective_radius = retrieve_mem_request.radius
+ elif top_k == -1:
+ # Unlimited mode: apply default Milvus threshold for quality filtering
+ effective_radius = DEFAULT_MILVUS_SIMILARITY_THRESHOLD
else:
- end_time_dt = end_time
-
- # Handle foresight time range (only valid for foresight)
- if mem_type == MemoryType.FORESIGHT:
- if retrieve_mem_request.start_time:
- start_time_dt = from_iso_format(retrieve_mem_request.start_time)
- if retrieve_mem_request.end_time:
- end_time_dt = from_iso_format(retrieve_mem_request.end_time)
-
- # Call Milvus vector search (pass different parameters based on memory type)
- # Threshold logic:
- # - User specified radius: always use it
- # - Unlimited mode (top_k=-1): apply DEFAULT_MILVUS_SIMILARITY_THRESHOLD (0.6)
- # - Normal mode (top_k>0): no threshold filtering (rely on top_k limit)
- if retrieve_mem_request.radius is not None:
- # User specified radius, use it
- effective_radius = retrieve_mem_request.radius
- elif top_k == -1:
- # Unlimited mode: apply default Milvus threshold for quality filtering
- effective_radius = DEFAULT_MILVUS_SIMILARITY_THRESHOLD
- # else: keep None (no threshold filtering for normal top_k mode)
-
- milvus_start = time.perf_counter()
- if mem_type == MemoryType.FORESIGHT:
- # Foresight: supports time range and validity filtering, supports radius parameter
- search_results = await milvus_repo.vector_search(
- query_vector=query_vector_list,
- user_id=user_id,
- group_ids=group_ids, # Pass normalized list
- start_time=start_time_dt,
- end_time=end_time_dt,
- limit=effective_limit,
- score_threshold=0.0,
- radius=effective_radius,
- )
- elif mem_type == MemoryType.AGENT_SKILL:
- # Agent skill: no timestamp filtering
- search_results = await milvus_repo.vector_search(
- query_vector=query_vector_list,
- user_id=user_id,
- group_ids=group_ids,
- limit=effective_limit,
- score_threshold=0.0,
- radius=effective_radius,
- )
- else:
- # Episodic memory, atomic fact, agent case: use timestamp filtering
- search_results = await milvus_repo.vector_search(
- query_vector=query_vector_list,
- user_id=user_id,
- group_ids=group_ids, # Pass normalized list
- start_time=start_time_dt,
- end_time=end_time_dt,
- limit=effective_limit,
- score_threshold=0.0,
- radius=effective_radius,
+ effective_radius = None
+
+ milvus_start = time.perf_counter()
+ if mem_type == MemoryType.FORESIGHT:
+ # Foresight: supports time range and validity filtering, supports radius parameter
+ search_results = await milvus_repo.vector_search(
+ query_vector=query_vector_list,
+ user_id=user_id,
+ group_ids=group_ids, # Pass normalized list
+ start_time=start_time_dt,
+ end_time=end_time_dt,
+ limit=effective_limit,
+ score_threshold=0.0,
+ radius=effective_radius,
+ )
+ elif mem_type == MemoryType.AGENT_SKILL:
+ # Agent skill: no timestamp filtering
+ search_results = await milvus_repo.vector_search(
+ query_vector=query_vector_list,
+ user_id=user_id,
+ group_ids=group_ids,
+ limit=effective_limit,
+ score_threshold=0.0,
+ radius=effective_radius,
+ )
+ else:
+ # Episodic memory, atomic fact, agent case: use timestamp filtering
+ search_results = await milvus_repo.vector_search(
+ query_vector=query_vector_list,
+ user_id=user_id,
+ group_ids=group_ids, # Pass normalized list
+ start_time=start_time_dt,
+ end_time=end_time_dt,
+ limit=effective_limit,
+ score_threshold=0.0,
+ radius=effective_radius,
+ )
+ record_retrieve_stage(
+ retrieve_method=retrieve_method,
+ stage='milvus_search',
+ memory_type=mem_type.value,
+ duration_seconds=time.perf_counter() - milvus_start,
)
- record_retrieve_stage(
- retrieve_method=retrieve_method,
- stage='milvus_search',
- memory_type=memory_type,
- duration_seconds=time.perf_counter() - milvus_start,
- )
- for r in search_results:
- r['memory_type'] = mem_type.value
- r['_search_source'] = RetrieveMethod.VECTOR.value
- # Milvus already uses 'score', no need to rename
+ for r in search_results or []:
+ r['memory_type'] = mem_type.value
+ r['_search_source'] = RetrieveMethod.VECTOR.value
+ # Milvus already uses 'score', no need to rename
+ all_search_results.extend(search_results or [])
- return search_results
+ return _sort_hits_by_score(all_search_results)
except Exception as e:
record_retrieve_stage(
retrieve_method=retrieve_method,
stage=RetrieveMethod.VECTOR.value,
memory_type=memory_type,
- duration_seconds=time.perf_counter() - milvus_start,
+ duration_seconds=time.perf_counter() - stage_start,
)
record_retrieve_error(
retrieve_method=retrieve_method,
@@ -795,12 +807,6 @@ async def retrieve_mem_hybrid(
self, retrieve_mem_request: 'RetrieveMemRequest'
) -> RetrieveMemResponse:
"""Hybrid memory retrieval: keyword + vector + rerank"""
- memory_type = (
- retrieve_mem_request.memory_types[0].value
- if retrieve_mem_request.memory_types
- else 'unknown'
- )
-
try:
hits = await self._search_hybrid(
retrieve_mem_request, retrieve_method=RetrieveMethod.HYBRID.value
@@ -882,9 +888,7 @@ async def _search_hybrid(
retrieve_method: str = RetrieveMethod.HYBRID.value,
) -> List[Dict]:
"""Core hybrid search: keyword + vector + rerank, returns flat list"""
- memory_type = (
- request.memory_types[0].value if request.memory_types else 'unknown'
- )
+ memory_type = _memory_type_label(request.memory_types)
top_k = request.top_k
is_unlimited_mode = top_k == -1
@@ -893,11 +897,17 @@ async def _search_hybrid(
self.get_keyword_search_results(request, retrieve_method=retrieve_method),
self.get_vector_search_results(request, retrieve_method=retrieve_method),
)
- # Deduplicate by id
- seen_ids = {h.get('id') for h in kw_results}
- merged_results = kw_results + [
- h for h in vec_results if h.get('id') not in seen_ids
- ]
+ # Deduplicate by memory collection and id so unrelated collections with
+ # the same backend id do not erase each other.
+ seen_ids = set()
+ merged_results = []
+ for hit in [*kw_results, *vec_results]:
+ identity = _hit_identity(hit)
+ if identity is not None:
+ if identity in seen_ids:
+ continue
+ seen_ids.add(identity)
+ merged_results.append(hit)
# When top_k is -1, use DEFAULT_TOPK_LIMIT for rerank
rerank_limit = DEFAULT_TOPK_LIMIT if is_unlimited_mode else top_k
@@ -991,7 +1001,7 @@ async def retrieve_mem_agentic(
top_k = req.top_k
is_unlimited_mode = top_k == -1
config = AgenticConfig()
- memory_type = req.memory_types[0].value if req.memory_types else 'unknown'
+ memory_type = _memory_type_label(req.memory_types)
try:
llm_provider = build_default_provider()
@@ -1359,7 +1369,7 @@ async def group_by_groupid_stratagy(
task_intent=fields.get('task_intent', ''),
approach=fields.get('approach', ''),
quality_score=fields.get('quality_score'),
- key_insight=fields.get('key_insight', '')
+ key_insight=fields.get('key_insight', ''),
)
case MemoryType.AGENT_SKILL.value:
# AgentSkill doesn't have parent_type/parent_id fields
diff --git a/methods/EverCore/tests/test_memory_manager_multi_type_search.py b/methods/EverCore/tests/test_memory_manager_multi_type_search.py
new file mode 100644
index 00000000..380c698e
--- /dev/null
+++ b/methods/EverCore/tests/test_memory_manager_multi_type_search.py
@@ -0,0 +1,80 @@
+import pytest
+
+from agentic_layer import memory_manager as memory_manager_module
+from agentic_layer.memory_manager import MemoryManager
+from api_specs.dtos import RetrieveMemRequest
+from api_specs.memory_models import MemoryType
+
+
+class _RepoA:
+ async def multi_search(self, **kwargs):
+ return [{'_id': 'a', '_score': 0.2}]
+
+
+class _RepoB:
+ async def multi_search(self, **kwargs):
+ return [{'_id': 'b', '_score': 0.9}]
+
+
+@pytest.mark.asyncio
+async def test_keyword_search_queries_all_requested_memory_types(monkeypatch):
+ repos = {_RepoA: _RepoA(), _RepoB: _RepoB()}
+ monkeypatch.setattr(
+ memory_manager_module,
+ 'ES_REPO_MAP',
+ {MemoryType.EPISODIC_MEMORY: _RepoA, MemoryType.AGENT_CASE: _RepoB},
+ )
+ monkeypatch.setattr(
+ memory_manager_module, 'get_bean_by_type', lambda repo_class: repos[repo_class]
+ )
+
+ manager = object.__new__(MemoryManager)
+ request = RetrieveMemRequest(
+ query='soccer',
+ group_ids=['group-1'],
+ top_k=10,
+ memory_types=[MemoryType.EPISODIC_MEMORY, MemoryType.AGENT_CASE],
+ )
+
+ hits = await manager.get_keyword_search_results(request)
+
+ assert [hit['memory_type'] for hit in hits] == [
+ MemoryType.AGENT_CASE.value,
+ MemoryType.EPISODIC_MEMORY.value,
+ ]
+ assert [hit['id'] for hit in hits] == ['b', 'a']
+
+
+@pytest.mark.asyncio
+async def test_hybrid_dedupe_keeps_same_id_from_distinct_memory_types():
+ manager = object.__new__(MemoryManager)
+
+ async def keyword_results(*args, **kwargs):
+ return [{'id': 'same', 'memory_type': 'episodic_memory', 'score': 0.8}]
+
+ async def vector_results(*args, **kwargs):
+ return [
+ {'id': 'same', 'memory_type': 'agent_case', 'score': 0.9},
+ {'id': 'same', 'memory_type': 'episodic_memory', 'score': 0.7},
+ ]
+
+ async def rerank(query, hits, top_k, *args, **kwargs):
+ return hits
+
+ manager.get_keyword_search_results = keyword_results
+ manager.get_vector_search_results = vector_results
+ manager._rerank = rerank
+
+ request = RetrieveMemRequest(
+ query='soccer',
+ group_ids=['group-1'],
+ top_k=10,
+ memory_types=[MemoryType.EPISODIC_MEMORY, MemoryType.AGENT_CASE],
+ )
+
+ hits = await manager._search_hybrid(request)
+
+ assert hits == [
+ {'id': 'same', 'memory_type': 'episodic_memory', 'score': 0.8},
+ {'id': 'same', 'memory_type': 'agent_case', 'score': 0.9},
+ ]
diff --git a/methods/EverCore/tests/test_simple_memory_manager.py b/methods/EverCore/tests/test_simple_memory_manager.py
new file mode 100644
index 00000000..c5760fcc
--- /dev/null
+++ b/methods/EverCore/tests/test_simple_memory_manager.py
@@ -0,0 +1,40 @@
+import pytest
+
+from demo.utils.simple_memory_manager import SimpleMemoryManager
+
+
+class _AcceptedResponse:
+ status_code = 202
+
+ def raise_for_status(self):
+ return None
+
+
+class _AsyncClient:
+ def __init__(self):
+ self.posts = []
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *args):
+ return False
+
+ async def post(self, url, json):
+ self.posts.append((url, json))
+ return _AcceptedResponse()
+
+
+@pytest.mark.asyncio
+async def test_store_treats_accepted_background_response_as_success(monkeypatch):
+ client = _AsyncClient()
+ monkeypatch.setattr(
+ 'demo.utils.simple_memory_manager.httpx.AsyncClient',
+ lambda *args, **kwargs: client,
+ )
+
+ manager = SimpleMemoryManager(user_id='user-1')
+ manager._settings_initialized = True
+
+ assert await manager.store('background extraction') is True
+ assert len(client.posts) == 1
diff --git a/use-cases/hermes-everos-memory/.algo-profile/README.md b/use-cases/hermes-everos-memory/.algo-profile/README.md
new file mode 100644
index 00000000..5b15ba0f
--- /dev/null
+++ b/use-cases/hermes-everos-memory/.algo-profile/README.md
@@ -0,0 +1,6 @@
+# Algorithm Profile — Hermes EverOS Memory
+
+## Structures
+
+- [Circular Chat Buffer](structures/circular-chat-buffer.md) — O(1) bounded
+ transcript append/evict, used in `raven-console/src/tui.rs`.
diff --git a/use-cases/hermes-everos-memory/.algo-profile/structures/circular-chat-buffer.md b/use-cases/hermes-everos-memory/.algo-profile/structures/circular-chat-buffer.md
new file mode 100644
index 00000000..1acd43a3
--- /dev/null
+++ b/use-cases/hermes-everos-memory/.algo-profile/structures/circular-chat-buffer.md
@@ -0,0 +1,22 @@
+---
+algorithm: Circular Buffer
+category: structures
+complexity_time: O(1)
+complexity_space: O(k)
+used_in: raven-console/src/tui.rs
+date: 2026-05-15
+---
+
+## Why This Was Chosen
+
+Hermes Chat transcript 是固定窗口队列:新消息追加,旧消息淘汰。`VecDeque`
+更贴合 FIFO/ring-buffer 语义,避免 `Vec` 从头部 `drain` 时搬移元素。
+
+## Implementation Notes
+
+`CHAT_HISTORY_LIMIT` 固定为 24。每次追加前检查容量,满了就 `pop_front()`,
+再 `push_back()`,让长期运行 TUI 的 transcript 更新保持 O(1)。
+
+## Reference
+
+javascript-algorithms data structures decision guide: Circular Queue / Queue.
diff --git a/use-cases/hermes-everos-memory/.gitignore b/use-cases/hermes-everos-memory/.gitignore
new file mode 100644
index 00000000..6f2b0ba3
--- /dev/null
+++ b/use-cases/hermes-everos-memory/.gitignore
@@ -0,0 +1,2 @@
+raven/.local-runs/
+raven-console/target/
diff --git a/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md
new file mode 100644
index 00000000..3f37c121
--- /dev/null
+++ b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md
@@ -0,0 +1,85 @@
+# Doomsday EverOS Completion Audit
+
+## Verdict
+
+PASS for the focused EverOS execution lane.
+
+The requested artifacts are present, public-safe, and verified:
+
+- Raven concept exploration;
+- Raven CLI/REPL/TUI and v2 research harness;
+- EverMe SkillHub MVP design and implementation plan;
+- Hermes/EverOS dogfood memory-provider integration artifacts;
+- owner-readable packet and verifiers.
+
+Remote NixOS deployment remains a separate follow-on `FLAG`; it is not counted
+as local artifact completion.
+
+## Requirement Matrix
+
+| Requirement | Evidence | Verdict |
+| --- | --- | --- |
+| Turn the source call into one focused lane | `raven/fixtures/doomsday-run.json` records one run with three bounded lanes and no open blocking gates | PASS |
+| Ship Raven concept exploration | `raven/RAVEN_CONCEPT.md` defines thesis, naming contract, interface wedge, guardrails, and current evidence | PASS |
+| Preserve Raven command contract | `raven/COMMAND_CONTRACT.md`, `raven/schema.json`, and `bin/raven-run.mjs` keep the v0 packet namespace working | PASS |
+| Ship Raven v2 research harness | `raven research lanes`, `raven research packet native-feel --output -`, and `raven research synthesize` keep v2 work as live-gate-calibrated packets | PASS |
+| Pin remote auth path to DeepSeek | `deploy/nixos/evercore.env.example` plus `just deepseek-auth-preflight` require DeepSeek through OpenRouter without printing keys | PASS |
+| Ship EverMe SkillHub MVP plan | `skillhub/MVP_IMPLEMENTATION_PLAN.md` defines product contract, five MVP views, API contract, data additions, sequence, and gates | PASS |
+| Ship SkillHub implementation slice | `skillhub/schema.json`, fixtures, `bin/skillhub-packet.mjs`, and `bin/skillhub-mock-api.mjs` validate/render/import/serve packets | PASS |
+| Ship Hermes/EverOS plugin artifacts | `__init__.py`, `plugin.yaml`, `scripts/install-local.sh`, and `bin/everos-memory.mjs` implement and install the provider shim | PASS |
+| Prove provider load | `just provider-load` | PASS |
+| Prove SkillHub API | `just skillhub-api-smoke` | PASS |
+| Prove real SkillHub import | `just skillhub-import-sample` plus `just skillhub-views skillhub/fixtures/evoagentbench-musician-life-event.json` | PASS |
+| Prove Raven packet | `node bin/raven-run.mjs summary raven/fixtures/doomsday-run.json` and `just raven-verify` | PASS |
+| Prove full memory loop | `just dogfood-smoke full` with a fresh user id | PASS |
+| Prove real Hermes profile path | `hermes -z` storing a unique public marker, then `node bin/everos-memory.mjs search "$MARKER"` | PASS |
+| Avoid widening scope | no new major repo; artifacts stay under `use-cases/hermes-everos-memory/` | PASS |
+| Avoid private operational details | public-safety scan over owner packet, Raven docs, run packet, and SkillHub docs returns no matches | PASS |
+
+## Commands
+
+```bash
+cd use-cases/hermes-everos-memory
+bash -n scripts/*.sh deploy/nixos/scripts/*.sh
+for f in bin/*.mjs; do node --check "$f"; done
+node bin/raven-run.mjs summary raven/fixtures/doomsday-run.json
+just provider-load
+just deepseek-auth-preflight
+just dogfood-smoke provider-only
+just skillhub-api-smoke
+just skillhub-import-sample
+just skillhub-views skillhub/fixtures/evoagentbench-musician-life-event.json
+just raven-sample
+just raven-render
+just raven-verify
+just raven-console-check
+just raven-research-lanes
+just raven-research-packet-smoke
+just raven-research-synthesis
+just mock-openai-check
+EVEROS_USER_ID="verify-raven-$(date +%s)" EVEROS_SEARCH_METHOD=hybrid EVEROS_MEMORY_TYPES=episodic_memory,raw_message,profile,agent_memory just dogfood-smoke full
+MARKER="RAVEN_DOGFOOD_VERIFY_$(date +%s)" && hermes -z "Use the EverOS memory tool to store exactly this public verification marker: ${MARKER}." && node bin/everos-memory.mjs search "$MARKER"
+```
+
+Repo-root checks:
+
+```bash
+git diff --check -- use-cases/hermes-everos-memory
+rg -n -i -f use-cases/hermes-everos-memory/OWNER_PACKET.md use-cases/hermes-everos-memory/raven use-cases/hermes-everos-memory/skillhub
+```
+
+## Residual Risks
+
+- Remote NixOS deployment remains `FLAG` until the module is applied and the
+ remote `--mode full` smoke passes.
+- Raven naming is intentionally unified across concept, internal docs, and
+ command namespace; current v0 keeps `raven-run` to avoid breaking existing
+ packet and SkillHub contracts.
+- SkillHub write routes remain proposed until EverMe backend constraints are
+ available.
+
+## Raven v2 closure — landed 2026-05-15
+
+Closeout via goalv3-cc goal `raven-v2-closure`: 7-commit batch landed on
+`gemini-cli-workspace`, PR open at
+targeting `main`.
diff --git a/use-cases/hermes-everos-memory/OWNER_PACKET.md b/use-cases/hermes-everos-memory/OWNER_PACKET.md
new file mode 100644
index 00000000..a361c7de
--- /dev/null
+++ b/use-cases/hermes-everos-memory/OWNER_PACKET.md
@@ -0,0 +1,135 @@
+# Hermes EverOS Memory Owner Packet
+
+## Verdict
+
+PASS for the local Raven, EverMe SkillHub, and Hermes/EverOS dogfood
+packet.
+
+FLAG remains for remote NixOS deployment. The deploy packet is ready for review,
+but EverCore is not yet proven active on the remote loopback service.
+
+## What Shipped
+
+- Hermes `everos` memory-provider shim with search, store, health, flush,
+ prefetch, sync, and auto-flush behavior.
+- Raven concept packet and naming contract, implemented through the Raven
+ command namespace.
+- Raven run packet contract, command contract, renderer, and gate verifier.
+- Raven v1 local console: Rust CLI, REPL, and ratatui TUI entrypoints that
+ expose typed status, packet, gates, agents, memory, runs, receipts, native
+ audit, and local verification without mutating remote state.
+- Raven Hermes chat bridge: `raven chat send`, bare-text/`/chat` REPL turns,
+ and the TUI Hermes panel share one sanitized adapter; TUI execution runs in
+ the background so redraw and key handling remain live.
+- Raven v2 research harness: `raven research lanes`, `raven research packet
+ `, and `raven research synthesize` keep v2 work as live-gate-calibrated
+ decision packets instead of freeform research prose.
+- EverMe SkillHub packet schema, MVP view plan, renderer, read-only mock API,
+ API-backed views/install-packet routes, and one real EvoAgentBench `SKILL.md`
+ import fixture.
+- NixOS/workhorse deploy packet, compose file, module draft, env example, and
+ remote smoke script.
+- Local mock OpenAI-compatible server for model-free EverCore dogfood.
+
+## Verification
+
+Current local PASS verification set:
+
+```bash
+bash -n use-cases/hermes-everos-memory/scripts/*.sh use-cases/hermes-everos-memory/deploy/nixos/scripts/*.sh
+cd use-cases/hermes-everos-memory && for f in bin/*.mjs; do node --check "$f"; done
+git diff --check -- use-cases/hermes-everos-memory
+cd use-cases/hermes-everos-memory && just provider-load
+cd use-cases/hermes-everos-memory && just deepseek-auth-preflight
+cd use-cases/hermes-everos-memory && just dogfood-smoke provider-only
+cd use-cases/hermes-everos-memory && just skillhub-api-smoke
+cd use-cases/hermes-everos-memory && just skillhub-import-sample
+cd use-cases/hermes-everos-memory && just skillhub-views skillhub/fixtures/evoagentbench-musician-life-event.json
+cd use-cases/hermes-everos-memory && just raven-sample
+cd use-cases/hermes-everos-memory && just raven-render
+cd use-cases/hermes-everos-memory && just raven-verify
+cd use-cases/hermes-everos-memory && just raven-console-check
+cd use-cases/hermes-everos-memory && just raven-status
+cd use-cases/hermes-everos-memory && bin/raven status --json
+cd use-cases/hermes-everos-memory && just raven-research-lanes
+cd use-cases/hermes-everos-memory && just raven-research-packet-smoke
+cd use-cases/hermes-everos-memory && just raven-research-synthesis
+cd use-cases/hermes-everos-memory && RAVEN_HERMES_BIN=/bin/echo bin/raven chat send raven chat smoke
+cd use-cases/hermes-everos-memory && RAVEN_HERMES_BIN=/bin/echo bin/raven --json chat send "check raven chat redaction fixture"
+cd use-cases/hermes-everos-memory && just raven-run-verify
+cd use-cases/hermes-everos-memory && bin/raven run verify --receipt -
+cd use-cases/hermes-everos-memory && just raven-repl-smoke
+cd use-cases/hermes-everos-memory && just raven-tui-smoke
+cd use-cases/hermes-everos-memory && just raven-native-audit
+cd use-cases/hermes-everos-memory && just raven-runs
+cd use-cases/hermes-everos-memory && just mock-openai-check
+cd use-cases/hermes-everos-memory && EVEROS_USER_ID="verify-raven-$(date +%s)" EVEROS_SEARCH_METHOD=hybrid EVEROS_MEMORY_TYPES=episodic_memory,raw_message,profile,agent_memory just dogfood-smoke full
+cd use-cases/hermes-everos-memory && MARKER="RAVEN_DOGFOOD_VERIFY_$(date +%s)" && hermes -z "Use the EverOS memory tool to store exactly this public verification marker: ${MARKER}." && node bin/everos-memory.mjs search "$MARKER"
+```
+
+Hermes profile verification:
+
+```bash
+hermes memory status
+```
+
+Expected status:
+
+```text
+Provider: everos
+Plugin: installed
+Status: available
+```
+
+Remote deploy verification remains separate:
+
+```bash
+cd use-cases/hermes-everos-memory && just remote-smoke full
+```
+
+Treat that command as `FLAG` until the NixOS module is applied and EverCore is
+running on the remote loopback service.
+
+## Remote Disposition
+
+Read-only workhorse probe:
+
+- NixOS host is reachable.
+- System state is running.
+- Failed systemd unit count was zero during the dry-run probe.
+- EverCore service/timer are inactive.
+- Remote loopback health at the EverCore API port is unavailable.
+
+Remote deploy remains `FLAG` until the EverCore module is staged into the
+workhorse configuration, `nixos-rebuild test` passes, and the remote
+`--mode full` smoke passes on-host.
+
+Live MUW calibration on 2026-05-15: `DAS-2669` is unblocked for the
+DeepSeek/OpenRouter auth-route repair with `AUTH_REPAIRED VERDICT: PASS`.
+`DAS-2666` remains `BLOCK` because remote private env preflight, guarded NixOS
+test, remote loopback full smoke, and supervisor `PASS` are still missing.
+
+## Guardrails Preserved
+
+- No new major repo.
+- No push or external publish.
+- No private host/IP/token/credential path in public artifacts.
+- No final EverMe UI claim before product/design-system constraints.
+- Red remote deploy gate remains red.
+- Raven console keeps remote deploy actions read-only/visible; it does not run
+ `nixos-rebuild`, `switch`, publish, push, or close issues.
+- `DAS-2669` auth-route repair is accepted through the DeepSeek/OpenRouter
+ path; it does not by itself green remote deploy readiness.
+- Remote LLM auth is pinned to DeepSeek through OpenRouter, and the preflight
+ checks that shape without printing provider keys.
+- `DAS-2666` remains `BLOCK` until auth repair, guarded NixOS test, remote
+ loopback full smoke, and supervisor `PASS` are all present.
+- `DAS-2675` can repair Pi/OpenCode adapter lanes but cannot green the remote
+ deploy verdict.
+
+## Next Action
+
+Resume `DAS-2666` from the DeepSeek/OpenRouter auth path: run the remote private
+env preflight with `--require-key`, then the guarded `nixos-rebuild test`, then
+the remote loopback full-smoke sequence, and only then request supervisor
+review.
diff --git a/use-cases/hermes-everos-memory/README.md b/use-cases/hermes-everos-memory/README.md
new file mode 100644
index 00000000..6edf3ada
--- /dev/null
+++ b/use-cases/hermes-everos-memory/README.md
@@ -0,0 +1,183 @@
+# Hermes EverOS Memory Provider
+
+Hermes memory-provider integration for EverOS.
+
+This use case makes EverCore available to Hermes through Hermes' native
+`MemoryProvider` lifecycle:
+
+- pre-turn recall with `prefetch`
+- post-turn persistence with `sync_turn` and local auto-flush
+- explicit memory tools for search, store, health, and flush
+- compression/delegation hooks reserved for the next pass
+
+The provider is intentionally split:
+
+- `__init__.py` is a thin Hermes interface shim. Hermes memory providers are
+ loaded as Python classes, so this file stays small and dependency-free.
+- `bin/everos-memory.mjs` is the operator/dev CLI used by Bun or Node for
+ direct smoke tests.
+- `scripts/install-local.sh` installs the provider into the active Hermes
+ profile without activating it.
+- `OWNER_PACKET.md` summarizes the current PASS/FLAG evidence for review.
+
+## Status
+
+`v0` targets local EverCore at `http://127.0.0.1:1995`.
+
+It does not start EverCore for you. Bring EverCore up first:
+
+```bash
+cd methods/EverCore
+docker compose up -d
+uv sync
+uv run python src/run.py --host 127.0.0.1 --port 1995
+```
+
+Remote NixOS/workhorse deployment packet:
+
+- `deploy/nixos/DEPLOY_PACKET.md`
+- `deploy/nixos/README.md`
+- `deploy/nixos/evercore-remote-workhorse.nix`
+
+The remote packet keeps EverCore bound to loopback by default and treats CCR as
+a client/review lane, not the stateful memory host.
+
+## Configure
+
+Environment variables:
+
+| Variable | Default | Description |
+| --- | --- | --- |
+| `EVEROS_API_BASE_URL` | `http://127.0.0.1:1995` | EverCore API base URL |
+| `EVEROS_USER_ID` | `hermes-user` | User scope for personal/agent memory |
+| `EVEROS_AGENT_ID` | `hermes` | Sender id for assistant turns |
+| `EVEROS_SEARCH_METHOD` | `hybrid` | EverCore search method |
+| `EVEROS_MEMORY_TYPES` | `episodic_memory,profile` | Search memory types |
+| `EVEROS_TOP_K` | `5` | Number of memories to recall |
+| `EVEROS_AUTO_FLUSH` | `1` | Flush agent memory after writes so recall is immediately searchable |
+| `EVEROS_SYNC_INLINE` | `1` | Write/flush synchronously for CLI sessions that exit immediately |
+
+## Local Smoke
+
+```bash
+just provider-load
+just health
+just search "Hermes memory provider"
+just sync-smoke
+just dogfood-smoke
+```
+
+Equivalent without `just`:
+
+```bash
+bun run bin/everos-memory.mjs health
+bun run bin/everos-memory.mjs search "Hermes memory provider"
+bun run bin/everos-memory.mjs sync-smoke
+```
+
+Node 18+ also works:
+
+```bash
+node bin/everos-memory.mjs health
+```
+
+`just provider-load` is offline and only verifies that Hermes can discover and
+load the provider from a temporary profile.
+
+`just dogfood-smoke` is also offline by default and verifies the Python provider
+itself. When EverCore is running, use:
+
+```bash
+just dogfood-smoke health
+just dogfood-smoke full
+```
+
+Remote health smoke:
+
+```bash
+just remote-smoke
+just remote-smoke full
+```
+
+SkillHub packet dogfood:
+
+```bash
+just skillhub-sample
+just skillhub-render
+just skillhub-api-check
+just skillhub-api-smoke
+just skillhub-api
+just skillhub-from-skill ../../benchmarks/EvoAgentBench/src/skill_evolution/evermemos/skills_sample/MUSICIAN/musician_life_event/SKILL.md
+```
+
+Raven run packet dogfood:
+
+```bash
+just raven-sample
+just raven-render
+```
+
+Local model-free EverCore dogfood helper:
+
+```bash
+just mock-openai-check
+just mock-openai
+```
+
+Point `LLM_BASE_URL`, `OPENROUTER_BASE_URL`, `VECTORIZE_BASE_URL`, and
+`RERANK_BASE_URL` at the local mock when you need to verify the EverCore
+store/extract/index/search loop without touching external model providers.
+
+## Install Into Hermes
+
+```bash
+scripts/install-local.sh
+hermes config set memory.provider everos
+hermes plugins enable everos
+```
+
+Or use the interactive selector:
+
+```bash
+hermes memory setup
+```
+
+Run:
+
+```bash
+hermes memory status
+```
+
+Expected active status:
+
+```text
+Provider: everos
+Plugin: installed
+Status: available
+```
+
+## Safety Notes
+
+- This provider is local-first and does not require an API key.
+- It does not print raw memory payloads during install.
+- It uses Hermes profile-scoped config through environment variables and
+ `plugins.everos-memory` config keys.
+- If EverCore is down, the provider reports unavailable and Hermes should
+ continue without external memory.
+
+## v0 Contract
+
+Hermes calls:
+
+- `initialize(session_id, hermes_home, platform, agent_identity, user_id, ...)`
+- `prefetch(query, session_id=...)`
+- `sync_turn(user_content, assistant_content, session_id=...)`
+- `get_tool_schemas()`
+- `handle_tool_call(name, args)`
+
+EverCore calls:
+
+- `GET /health`
+- `POST /api/v1/memories/search`
+- `POST /api/v1/memories/agent`
+- `POST /api/v1/memories/agent/flush`
diff --git a/use-cases/hermes-everos-memory/SUPERVISOR_DISPATCH.md b/use-cases/hermes-everos-memory/SUPERVISOR_DISPATCH.md
new file mode 100644
index 00000000..d864007e
--- /dev/null
+++ b/use-cases/hermes-everos-memory/SUPERVISOR_DISPATCH.md
@@ -0,0 +1,134 @@
+# EverOS Supervisor Dispatch
+
+## Current Truth
+
+Local EverOS packet: PASS.
+
+Remote EverCore deployment: FLAG/BLOCK until the remote private env preflight,
+guarded NixOS test lane, remote full smoke, and supervisor review are proven.
+
+The active local source packet is under this directory. The remote deploy lane
+must use the existing Multica issues instead of creating a parallel story:
+
+- `DAS-2666`: EverCore remote deploy gate via squad.
+- `DAS-2669`: Auth-route repair via DeepSeek/OpenRouter.
+
+## Hard Guardrails
+
+- Do not push, publish, close upstream issues, or run remote deploy/switch
+ commands without explicit human approval.
+- Keep remote host/IP values, credential paths, token payloads, signed URLs, and
+ private env values out of public comments and screenshots.
+- `DAS-2669` has accepted `AUTH_REPAIRED VERDICT: PASS` for the
+ DeepSeek/OpenRouter auth-route repair. Do not confuse that with remote deploy
+ readiness.
+- Runtime auth uses the DeepSeek/OpenRouter path; do not expose the provider key
+ in evidence.
+- Remote EverCore remains loopback-only. Any public bind or firewall exposure is
+ `BLOCK`.
+- Local artifact completion and remote deploy readiness are separate gates.
+
+## New SC Codex CLI Prompt
+
+```text
+ROLE: EverOS control-room supervisor.
+
+MISSION:
+Keep the EverOS / Hermes / SkillHub / Raven packet moving from local PASS to
+remote-ready evidence without laundering red gates or spawning duplicate work.
+
+READ FIRST:
+- AGENTS.md
+- use-cases/hermes-everos-memory/COMPLETION_AUDIT.md
+- use-cases/hermes-everos-memory/OWNER_PACKET.md
+- use-cases/hermes-everos-memory/SUPERVISOR_DISPATCH.md
+- use-cases/hermes-everos-memory/raven/RAVEN_V2_RESEARCH_LEDGER.md
+- Multica issues DAS-2666 and DAS-2669
+
+SOURCE TRUTH ORDER:
+1. Human operator approval.
+2. Live Multica/GitHub/Linear/repo/runtime state.
+3. Committed artifacts.
+4. Agent summaries only when backed by evidence.
+
+CURRENT STATE:
+- Local EverOS packet is PASS.
+- Remote EverCore deploy is FLAG/BLOCK.
+- DAS-2669 auth-route repair is accepted; DAS-2666 is now blocked on remote env,
+ guarded NixOS test, full smoke, and supervisor PASS evidence.
+- Do not treat remote Hermes read-only evidence as deploy success.
+
+CONTROL LOOP:
+1. Refresh repo state and Multica issue states.
+2. Check each assigned lane for a concrete PASS/FLAG/BLOCK report.
+3. Reject reports that omit commands, issue links, or file evidence.
+4. Keep one owner-readable packet rather than scattered chat commentary.
+5. Route v2 ideas through `raven research packet ` before implementation.
+6. Escalate to the operator only for approval, secrets, auth repair, or remote
+ mutation decisions.
+
+OUTPUT SHAPE:
+VERDICT: PASS | FLAG | BLOCK
+EVIDENCE:
+CHANGES:
+RISKS:
+NEXT:
+```
+
+## Multica Dispatch Map
+
+| Lane | Lead | Support | Scope | Stop Condition |
+| --- | --- | --- | --- | --- |
+| Control room | Workbench Supervisor | Workbench Synthesizer | Track all lanes and produce one owner packet. | Any lane reports success without evidence. |
+| Runtime auth | Workbench Admin | NYC Ops Mechanic | Close `DAS-2669` auth-route repair through DeepSeek/OpenRouter without exposing provider material. | Token/auth payload exposure or deploy drift. |
+| Remote deploy gate | EverCore Remote Deploy Cell | Windburn NixOS Hermes | Keep `DAS-2666` honest; resume only remote env preflight, guarded test, full smoke, then supervisor review. | Missing env, public bind risk, failed NixOS test, or missing smoke evidence. |
+| Local verifier | QA Verifier | Codex Guardian | Re-run the local audit commands and public-safety scan. | Any command fails or secret/path pattern appears. |
+| Product story | Pi | Hermes Researcher, Claude Docs | Raven naming, SkillHub story, owner-readable public narrative. | Repo mutation or unsupported product claim. |
+| Memory substrate | Memory Curator | Hermes Researcher | Dogfood evidence, provenance fields, memory packet shape. | Claims not backed by local provider/search evidence. |
+| SkillHub eval | Benchmark Scout | Remote Algorithm Advisor, Codex Developer | Turn `needs_eval` SkillHub items into an eval plan; do not promote them. | Treating `needs_eval` as production-ready. |
+| Implementation reserve | Codex Developer | OpenCode runtime when assigned | Small bounded fixes after verifier or supervisor asks. | Broad refactor, README churn, or remote mutation. |
+| Standby runtimes | Copilot, Cursor, Gemini, OpenClaw | Supervisor | Specialist review only when a scoped issue exists. | Self-starting new work outside this packet. |
+
+## Runtime Lane Activation
+
+Two local runtime-backed agent identities were created for focused lanes:
+
+- `Pi Raven Critic`: Pi runtime, assigned on `DAS-2673` for Raven taste and
+ product-boundary review.
+- `OpenCode Patch Scout`: Opencode runtime, assigned on `DAS-2674` for bounded
+ local implementation scouting.
+
+Activation status: `FLAG`.
+
+Local CLI probes passed for both runtimes, but Multica task execution failed:
+
+- Pi: local `pi --mode json` probe with OpenRouter DeepSeek passes; Multica
+ wrapper still returns `pi exited with error: exit status 1`.
+- OpenCode: local `opencode run -m openrouter/deepseek/deepseek-v4-flash`
+ probe passes; Multica wrapper reports default OAuth invalidation or model
+ lookup failure.
+
+Keep `DAS-2673` and `DAS-2674` parked until the runtime-adapter repair lane
+proves a successful Multica task. Both lanes remain read-only by default. They
+may recommend changes, but they must not mutate files, push, publish, close
+issues, or touch remote deployment unless the supervisor opens a narrower
+follow-up issue.
+
+## Required Reports
+
+Each active lane must end with:
+
+```text
+VERDICT:
+EVIDENCE:
+CHANGES:
+RISKS:
+NEXT:
+```
+
+No lane may mark the remote deploy path `PASS` until all of these are true:
+
+- `DAS-2669` has accepted `AUTH_REPAIRED VERDICT: PASS`.
+- The guarded NixOS test lane succeeds.
+- The remote loopback full smoke retrieves stored memory.
+- Supervisor review returns `PASS`.
diff --git a/use-cases/hermes-everos-memory/__init__.py b/use-cases/hermes-everos-memory/__init__.py
new file mode 100644
index 00000000..4c059f43
--- /dev/null
+++ b/use-cases/hermes-everos-memory/__init__.py
@@ -0,0 +1,508 @@
+"""EverOS memory provider for Hermes.
+
+This file is intentionally a thin Python shim because Hermes memory providers
+are loaded as Python classes. The provider talks to EverCore over HTTP and keeps
+all behavior best-effort so memory outages never block Hermes turns.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import threading
+import time
+import urllib.error
+import urllib.request
+from typing import Any, Dict, List, Optional
+
+from agent.memory_provider import MemoryProvider
+from tools.registry import tool_error
+
+DEFAULT_BASE_URL = "http://127.0.0.1:1995"
+DEFAULT_MEMORY_TYPES = ["episodic_memory", "profile"]
+
+
+SEARCH_SCHEMA = {
+ "name": "everos_search",
+ "description": (
+ "Search EverOS/EverCore memory. Use for past session context, user/project "
+ "preferences, prior decisions, and agent trajectory recall."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Search query."},
+ "top_k": {"type": "integer", "description": "Max results, default 5."},
+ "memory_types": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Memory types: episodic_memory, profile, agent_memory, raw_message.",
+ },
+ "method": {
+ "type": "string",
+ "enum": ["keyword", "vector", "hybrid", "agentic"],
+ "description": "Retrieval method, default hybrid.",
+ },
+ },
+ "required": ["query"],
+ },
+}
+
+STORE_SCHEMA = {
+ "name": "everos_store",
+ "description": "Store an explicit durable fact or note into EverOS memory.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "content": {"type": "string", "description": "Memory content to store."},
+ "role": {
+ "type": "string",
+ "enum": ["user", "assistant"],
+ "description": "Role attribution, default user.",
+ },
+ },
+ "required": ["content"],
+ },
+}
+
+HEALTH_SCHEMA = {
+ "name": "everos_health",
+ "description": "Check whether local EverCore is reachable.",
+ "parameters": {"type": "object", "properties": {}, "required": []},
+}
+
+FLUSH_SCHEMA = {
+ "name": "everos_flush",
+ "description": "Flush buffered agent messages for the current EverOS user/session.",
+ "parameters": {"type": "object", "properties": {}, "required": []},
+}
+
+
+class EverOSClient:
+ def __init__(self, base_url: str, timeout: float = 10.0):
+ self.base_url = base_url.rstrip("/")
+ self.timeout = timeout
+
+ def request(self, method: str, path: str, body: Optional[dict] = None) -> dict:
+ data = None
+ headers = {"Content-Type": "application/json"}
+ if body is not None:
+ data = json.dumps(body).encode("utf-8")
+ req = urllib.request.Request(
+ self.base_url + path,
+ data=data,
+ headers=headers,
+ method=method,
+ )
+ with urllib.request.urlopen(req, timeout=self.timeout) as response: # noqa: S310
+ raw = response.read().decode("utf-8")
+ if not raw:
+ return {}
+ return json.loads(raw)
+
+ def health(self) -> dict:
+ return self.request("GET", "/health")
+
+ def search(
+ self,
+ *,
+ query: str,
+ user_id: str,
+ method: str,
+ memory_types: List[str],
+ top_k: int,
+ ) -> dict:
+ return self.request(
+ "POST",
+ "/api/v1/memories/search",
+ {
+ "query": query,
+ "method": method,
+ "memory_types": memory_types,
+ "top_k": top_k,
+ "filters": {"user_id": user_id},
+ },
+ )
+
+ def add_agent_messages(
+ self,
+ *,
+ user_id: str,
+ session_id: str,
+ messages: List[dict],
+ ) -> dict:
+ return self.request(
+ "POST",
+ "/api/v1/memories/agent",
+ {
+ "user_id": user_id,
+ "session_id": session_id,
+ "messages": messages,
+ },
+ )
+
+ def flush_agent(self, *, user_id: str, session_id: str) -> dict:
+ return self.request(
+ "POST",
+ "/api/v1/memories/agent/flush",
+ {"user_id": user_id, "session_id": session_id},
+ )
+
+
+class EverOSMemoryProvider(MemoryProvider):
+ def __init__(self):
+ self._base_url = os.environ.get("EVEROS_API_BASE_URL", DEFAULT_BASE_URL)
+ self._user_id = os.environ.get("EVEROS_USER_ID", "hermes-user")
+ self._agent_id = os.environ.get("EVEROS_AGENT_ID", "hermes")
+ self._search_method = os.environ.get("EVEROS_SEARCH_METHOD", "hybrid")
+ self._top_k = int(os.environ.get("EVEROS_TOP_K", "5"))
+ self._auto_flush = os.environ.get("EVEROS_AUTO_FLUSH", "1").lower() not in {
+ "0",
+ "false",
+ "no",
+ }
+ self._sync_inline = os.environ.get("EVEROS_SYNC_INLINE", "1").lower() not in {
+ "0",
+ "false",
+ "no",
+ }
+ self._memory_types = self._parse_memory_types()
+ self._client = EverOSClient(self._base_url)
+ self._session_id = ""
+ self._prefetch_result = ""
+ self._prefetch_lock = threading.Lock()
+ self._prefetch_thread: Optional[threading.Thread] = None
+ self._sync_thread: Optional[threading.Thread] = None
+ self._consecutive_failures = 0
+ self._breaker_open_until = 0.0
+
+ @property
+ def name(self) -> str:
+ return "everos"
+
+ def is_available(self) -> bool:
+ try:
+ status = self._client.health()
+ return status.get("status") in {"healthy", "ok"} or bool(status)
+ except Exception:
+ return False
+
+ def initialize(self, session_id: str, **kwargs) -> None:
+ self._session_id = session_id
+ self._user_id = (
+ kwargs.get("user_id")
+ or os.environ.get("EVEROS_USER_ID")
+ or self._user_id
+ )
+ identity = kwargs.get("agent_identity") or os.environ.get("EVEROS_AGENT_ID")
+ if identity:
+ self._agent_id = f"hermes-{identity}"
+
+ def system_prompt_block(self) -> str:
+ return (
+ "# EverOS Memory\n"
+ "Active. Use EverOS for durable cross-session recall. "
+ "Use everos_search for explicit lookup and everos_store for durable facts."
+ )
+
+ def prefetch(self, query: str, *, session_id: str = "") -> str:
+ if self._prefetch_thread and self._prefetch_thread.is_alive():
+ self._prefetch_thread.join(timeout=2.0)
+ with self._prefetch_lock:
+ result = self._prefetch_result
+ self._prefetch_result = ""
+ if result:
+ return result
+ if not query:
+ return ""
+ # First turn after startup has no warmed result yet. Do a small direct
+ # recall so enabling the provider is immediately visible.
+ try:
+ return self._format_prefetch(
+ self._search(query=query, top_k=min(self._top_k, 3))
+ )
+ except Exception:
+ self._record_failure()
+ return ""
+
+ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
+ if not query or self._is_breaker_open():
+ return
+
+ def run() -> None:
+ try:
+ formatted = self._format_prefetch(self._search(query=query))
+ with self._prefetch_lock:
+ self._prefetch_result = formatted
+ self._record_success()
+ except Exception:
+ self._record_failure()
+
+ self._prefetch_thread = threading.Thread(
+ target=run, daemon=True, name="everos-prefetch"
+ )
+ self._prefetch_thread.start()
+
+ def sync_turn(
+ self, user_content: str, assistant_content: str, *, session_id: str = ""
+ ) -> None:
+ if self._is_breaker_open():
+ return
+ effective_session = session_id or self._session_id or f"hermes-{int(time.time())}"
+ now = int(time.time() * 1000)
+
+ def run() -> None:
+ try:
+ self._client.add_agent_messages(
+ user_id=self._user_id,
+ session_id=effective_session,
+ messages=[
+ {
+ "role": "user",
+ "sender_id": self._user_id,
+ "timestamp": now,
+ "content": user_content,
+ },
+ {
+ "role": "assistant",
+ "sender_id": self._agent_id,
+ "timestamp": now + 1,
+ "content": assistant_content,
+ },
+ ],
+ )
+ self._flush_session(effective_session)
+ self._record_success()
+ except Exception:
+ self._record_failure()
+
+ if self._sync_inline:
+ run()
+ return
+ if self._sync_thread and self._sync_thread.is_alive():
+ self._sync_thread.join(timeout=2.0)
+ self._sync_thread = threading.Thread(
+ target=run, daemon=True, name="everos-sync"
+ )
+ self._sync_thread.start()
+
+ def get_tool_schemas(self) -> List[Dict[str, Any]]:
+ return [SEARCH_SCHEMA, STORE_SCHEMA, HEALTH_SCHEMA, FLUSH_SCHEMA]
+
+ def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
+ try:
+ if tool_name == "everos_health":
+ return json.dumps({"result": self._client.health()}, ensure_ascii=False)
+ if tool_name == "everos_search":
+ query = args.get("query", "")
+ if not query:
+ return tool_error("Missing required parameter: query")
+ data = self._search(
+ query=query,
+ top_k=int(args.get("top_k") or self._top_k),
+ method=args.get("method") or self._search_method,
+ memory_types=args.get("memory_types") or self._memory_types,
+ )
+ return json.dumps(self._compact_search_result(data), ensure_ascii=False)
+ if tool_name == "everos_store":
+ content = args.get("content", "")
+ if not content:
+ return tool_error("Missing required parameter: content")
+ role = args.get("role") if args.get("role") in {"user", "assistant"} else "user"
+ sender_id = self._agent_id if role == "assistant" else self._user_id
+ session_id = self._session_id or f"hermes-{int(time.time())}"
+ data = self._client.add_agent_messages(
+ user_id=self._user_id,
+ session_id=session_id,
+ messages=[
+ {
+ "role": role,
+ "sender_id": sender_id,
+ "timestamp": int(time.time() * 1000),
+ "content": content,
+ }
+ ],
+ )
+ self._flush_session(session_id)
+ return json.dumps({"result": "stored", "data": data.get("data")}, ensure_ascii=False)
+ if tool_name == "everos_flush":
+ data = self._client.flush_agent(
+ user_id=self._user_id,
+ session_id=self._session_id or "",
+ )
+ return json.dumps({"result": "flushed", "data": data.get("data")}, ensure_ascii=False)
+ except urllib.error.URLError as exc:
+ self._record_failure()
+ return tool_error(f"EverOS unavailable: {exc}")
+ except Exception as exc:
+ self._record_failure()
+ return tool_error(f"EverOS tool failed: {exc}")
+ return tool_error(f"Unknown EverOS tool: {tool_name}")
+
+ def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:
+ if not messages:
+ return ""
+ tail = []
+ for item in messages[-12:]:
+ role = item.get("role", "")
+ content = item.get("content", "")
+ if isinstance(content, str) and content.strip():
+ tail.append(f"{role}: {content[:1200]}")
+ if not tail:
+ return ""
+ try:
+ query = "\n".join(tail)[-3000:]
+ return self._format_prefetch(self._search(query=query, top_k=5))
+ except Exception:
+ return ""
+
+ def on_memory_write(
+ self,
+ action: str,
+ target: str,
+ content: str,
+ metadata: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ if action not in {"add", "replace"} or not content:
+ return
+ note = f"[Hermes built-in memory:{target}] {content}"
+ self.sync_turn(note, "Stored in Hermes built-in memory.", session_id=self._session_id)
+
+ def on_delegation(
+ self, task: str, result: str, *, child_session_id: str = "", **kwargs
+ ) -> None:
+ if not task and not result:
+ return
+ content = f"Delegated task: {task}\n\nResult: {result}"
+ self.sync_turn(content, "Delegation observation recorded.", session_id=self._session_id)
+
+ def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
+ if self._sync_thread and self._sync_thread.is_alive():
+ self._sync_thread.join(timeout=3.0)
+ self._flush_session(self._session_id)
+ del messages
+
+ def shutdown(self) -> None:
+ for thread in (self._prefetch_thread, self._sync_thread):
+ if thread and thread.is_alive():
+ thread.join(timeout=3.0)
+ self._flush_session(self._session_id)
+
+ def get_config_schema(self) -> List[Dict[str, Any]]:
+ return [
+ {"key": "base_url", "description": "EverCore base URL", "default": DEFAULT_BASE_URL},
+ {"key": "user_id", "description": "EverOS user id", "default": "hermes-user"},
+ {"key": "agent_id", "description": "EverOS agent id", "default": "hermes"},
+ {"key": "search_method", "description": "Search method", "default": "hybrid", "choices": ["keyword", "vector", "hybrid", "agentic"]},
+ {"key": "top_k", "description": "Prefetch result count", "default": "5"},
+ ]
+
+ def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
+ # Hermes' generic setup persists memory.provider. Runtime config stays
+ # env-first for now so profiles, cron, and gateway can scope separately.
+ del values, hermes_home
+
+ def _search(
+ self,
+ *,
+ query: str,
+ top_k: Optional[int] = None,
+ method: Optional[str] = None,
+ memory_types: Optional[List[str]] = None,
+ ) -> dict:
+ return self._client.search(
+ query=query,
+ user_id=self._user_id,
+ method=method or self._search_method,
+ memory_types=memory_types or self._memory_types,
+ top_k=top_k or self._top_k,
+ )
+
+ def _flush_session(self, session_id: str) -> None:
+ if not self._auto_flush or not session_id:
+ return
+ self._client.flush_agent(user_id=self._user_id, session_id=session_id)
+
+ def _format_prefetch(self, data: dict) -> str:
+ compact = self._compact_search_result(data)
+ rows = compact.get("memories", [])
+ if not rows:
+ return ""
+ lines = ["## EverOS Memory"]
+ for item in rows[: self._top_k]:
+ title = item.get("subject") or item.get("type") or "memory"
+ text = item.get("text") or ""
+ score = item.get("score")
+ prefix = f"- [{score:.2f}] " if isinstance(score, (int, float)) else "- "
+ lines.append(f"{prefix}{title}: {text[:600]}")
+ return "\n".join(lines)
+
+ def _compact_search_result(self, data: dict) -> dict:
+ payload = data.get("data") or {}
+ memories = []
+ for ep in payload.get("episodes") or []:
+ memories.append(
+ {
+ "type": "episodic_memory",
+ "subject": ep.get("subject") or "",
+ "text": ep.get("summary") or ep.get("episode") or "",
+ "score": ep.get("score"),
+ "session_id": ep.get("session_id"),
+ }
+ )
+ for profile in payload.get("profiles") or []:
+ memories.append(
+ {
+ "type": "profile",
+ "subject": "profile",
+ "text": json.dumps(profile.get("profile_data") or {}, ensure_ascii=False),
+ "score": profile.get("score"),
+ }
+ )
+ agent_memory = payload.get("agent_memory") or {}
+ for case in agent_memory.get("cases") or []:
+ memories.append(
+ {
+ "type": "agent_case",
+ "subject": case.get("task_intent") or "agent case",
+ "text": case.get("approach") or "",
+ "score": case.get("score"),
+ "session_id": case.get("session_id"),
+ }
+ )
+ for skill in agent_memory.get("skills") or []:
+ memories.append(
+ {
+ "type": "agent_skill",
+ "subject": skill.get("name") or "agent skill",
+ "text": skill.get("description") or skill.get("content") or "",
+ "score": skill.get("score"),
+ }
+ )
+ return {"count": len(memories), "memories": memories}
+
+ def _parse_memory_types(self) -> List[str]:
+ raw = os.environ.get("EVEROS_MEMORY_TYPES", "")
+ if not raw:
+ return list(DEFAULT_MEMORY_TYPES)
+ return [item.strip() for item in raw.split(",") if item.strip()]
+
+ def _is_breaker_open(self) -> bool:
+ if self._consecutive_failures < 5:
+ return False
+ if time.monotonic() >= self._breaker_open_until:
+ self._consecutive_failures = 0
+ return False
+ return True
+
+ def _record_success(self) -> None:
+ self._consecutive_failures = 0
+
+ def _record_failure(self) -> None:
+ self._consecutive_failures += 1
+ if self._consecutive_failures >= 5:
+ self._breaker_open_until = time.monotonic() + 120
+
+
+def register(ctx) -> None:
+ ctx.register_memory_provider(EverOSMemoryProvider())
diff --git a/use-cases/hermes-everos-memory/bin/everos-memory.mjs b/use-cases/hermes-everos-memory/bin/everos-memory.mjs
new file mode 100755
index 00000000..347e3361
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/everos-memory.mjs
@@ -0,0 +1,159 @@
+#!/usr/bin/env node
+
+const DEFAULT_BASE_URL = 'http://127.0.0.1:1995';
+
+function config() {
+ return {
+ baseUrl: (process.env.EVEROS_API_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, ''),
+ userId: process.env.EVEROS_USER_ID || 'hermes-user',
+ agentId: process.env.EVEROS_AGENT_ID || 'hermes',
+ searchMethod: process.env.EVEROS_SEARCH_METHOD || 'hybrid',
+ memoryTypes: (process.env.EVEROS_MEMORY_TYPES || 'episodic_memory,profile')
+ .split(',')
+ .map((item) => item.trim())
+ .filter(Boolean),
+ topK: Number.parseInt(process.env.EVEROS_TOP_K || '5', 10),
+ };
+}
+
+async function requestJson(path, options = {}) {
+ const cfg = config();
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10000);
+ try {
+ const response = await fetch(`${cfg.baseUrl}${path}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {}),
+ },
+ signal: controller.signal,
+ });
+ const text = await response.text();
+ let data = null;
+ if (text) {
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = { raw: text };
+ }
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
+ }
+ return data;
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+async function health() {
+ return requestJson('/health');
+}
+
+async function search(query) {
+ const cfg = config();
+ return requestJson('/api/v1/memories/search', {
+ method: 'POST',
+ body: JSON.stringify({
+ query,
+ method: cfg.searchMethod,
+ memory_types: cfg.memoryTypes,
+ top_k: Number.isFinite(cfg.topK) ? cfg.topK : 5,
+ filters: {
+ user_id: cfg.userId,
+ },
+ }),
+ });
+}
+
+async function syncTurn(user, assistant) {
+ const cfg = config();
+ const now = Date.now();
+ return requestJson('/api/v1/memories/agent', {
+ method: 'POST',
+ body: JSON.stringify({
+ user_id: cfg.userId,
+ session_id: `hermes-smoke-${now}`,
+ messages: [
+ {
+ role: 'user',
+ sender_id: cfg.userId,
+ timestamp: now,
+ content: user,
+ },
+ {
+ role: 'assistant',
+ sender_id: cfg.agentId,
+ timestamp: now + 1,
+ content: assistant,
+ },
+ ],
+ }),
+ });
+}
+
+function summarizeSearch(data) {
+ const payload = data?.data || {};
+ const episodes = Array.isArray(payload.episodes) ? payload.episodes : [];
+ const profiles = Array.isArray(payload.profiles) ? payload.profiles : [];
+ const cases = Array.isArray(payload.agent_memory?.cases) ? payload.agent_memory.cases : [];
+ const skills = Array.isArray(payload.agent_memory?.skills) ? payload.agent_memory.skills : [];
+ return {
+ episodes: episodes.length,
+ profiles: profiles.length,
+ agent_cases: cases.length,
+ agent_skills: skills.length,
+ };
+}
+
+async function main() {
+ const [command, ...args] = process.argv.slice(2);
+
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
+ console.log(`Usage:
+ everos-memory health
+ everos-memory search
+ everos-memory sync-smoke
+ everos-memory self-test`);
+ return;
+ }
+
+ if (command === 'health') {
+ const result = await health();
+ console.log(JSON.stringify({ ok: true, status: result?.status || result?.data?.status || 'unknown' }, null, 2));
+ return;
+ }
+
+ if (command === 'search') {
+ const query = args.join(' ').trim();
+ if (!query) throw new Error('search requires a query');
+ const result = await search(query);
+ console.log(JSON.stringify({ ok: true, ...summarizeSearch(result) }, null, 2));
+ return;
+ }
+
+ if (command === 'sync-smoke') {
+ const result = await syncTurn(
+ 'Hermes EverOS memory provider smoke test user message.',
+ 'Hermes EverOS memory provider smoke test assistant response.'
+ );
+ console.log(JSON.stringify({ ok: true, data: result?.data || null }, null, 2));
+ return;
+ }
+
+ if (command === 'self-test') {
+ const h = await health();
+ console.log(JSON.stringify({ health: h?.status || 'ok' }));
+ const s = await search('Hermes EverOS memory provider');
+ console.log(JSON.stringify({ search: summarizeSearch(s) }));
+ return;
+ }
+
+ throw new Error(`unknown command: ${command}`);
+}
+
+main().catch((error) => {
+ console.error(JSON.stringify({ ok: false, error: error.message }));
+ process.exit(1);
+});
diff --git a/use-cases/hermes-everos-memory/bin/mock-openai-compatible.mjs b/use-cases/hermes-everos-memory/bin/mock-openai-compatible.mjs
new file mode 100755
index 00000000..a096b728
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/mock-openai-compatible.mjs
@@ -0,0 +1,253 @@
+#!/usr/bin/env node
+import http from 'node:http';
+
+const DEFAULT_PORT = Number.parseInt(process.env.MOCK_OPENAI_PORT || '18080', 10);
+const DEFAULT_HOST = process.env.MOCK_OPENAI_HOST || '127.0.0.1';
+const DEFAULT_DIMENSIONS = Number.parseInt(process.env.MOCK_OPENAI_DIMENSIONS || '1024', 10);
+
+function parseArgs(argv) {
+ const args = {
+ host: DEFAULT_HOST,
+ port: DEFAULT_PORT,
+ check: false,
+ };
+ for (let i = 0; i < argv.length; i += 1) {
+ const arg = argv[i];
+ if (arg === '--host') {
+ args.host = argv[i + 1];
+ i += 1;
+ } else if (arg === '--port') {
+ args.port = Number.parseInt(argv[i + 1], 10);
+ i += 1;
+ } else if (arg === '--check') {
+ args.check = true;
+ } else if (arg === '-h' || arg === '--help') {
+ printHelp();
+ process.exit(0);
+ } else {
+ throw new Error(`unknown argument: ${arg}`);
+ }
+ }
+ if (!Number.isInteger(args.port) || args.port < 1) {
+ throw new Error(`invalid port: ${args.port}`);
+ }
+ return args;
+}
+
+function printHelp() {
+ console.log(`Usage: mock-openai-compatible.mjs [--host 127.0.0.1] [--port 18080] [--check]
+
+Tiny local OpenAI-compatible mock for EverCore dogfood:
+ GET /health
+ POST /v1/chat/completions
+ POST /v1/embeddings
+ POST /v1/rerank
+
+No external network and no real model keys are used.`);
+}
+
+function stableHash(text) {
+ let hash = 2166136261;
+ for (let i = 0; i < text.length; i += 1) {
+ hash ^= text.charCodeAt(i);
+ hash = Math.imul(hash, 16777619);
+ }
+ return hash >>> 0;
+}
+
+function embeddingFor(text, dimensions = DEFAULT_DIMENSIONS) {
+ const vector = new Array(dimensions).fill(0);
+ const tokens = String(text)
+ .toLowerCase()
+ .split(/[^a-z0-9_./:-]+/)
+ .filter(Boolean);
+ for (const token of tokens.length ? tokens : ['empty']) {
+ const base = stableHash(token);
+ for (let i = 0; i < 8; i += 1) {
+ const idx = (base + i * 97) % dimensions;
+ vector[idx] += 1 / (i + 1);
+ }
+ }
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)) || 1;
+ return vector.map((value) => Number((value / norm).toFixed(6)));
+}
+
+function extractPromptText(body) {
+ const messages = Array.isArray(body.messages) ? body.messages : [];
+ return messages
+ .map((message) => {
+ if (typeof message.content === 'string') return message.content;
+ if (Array.isArray(message.content)) {
+ return message.content.map((part) => part.text || '').join('\n');
+ }
+ return '';
+ })
+ .join('\n');
+}
+
+function extractSmokeNeedle(prompt) {
+ const match = prompt.match(/Hermes EverOS dogfood smoke Raven SkillHub \d+[^.\n]*/i);
+ if (match) return match[0].trim();
+ const ravenLine = prompt
+ .split(/\r?\n/)
+ .find((line) => /Raven|SkillHub|EverOS|Hermes/i.test(line));
+ return (ravenLine || 'Hermes EverOS dogfood smoke Raven SkillHub').trim().slice(0, 500);
+}
+
+function completionContent(prompt) {
+ if (/should_end|episode boundar/i.test(prompt)) {
+ return JSON.stringify({
+ should_end: false,
+ reasoning: 'Single dogfood turn; keep accumulating until flush.',
+ topic_summary: '',
+ });
+ }
+ if (/worth_extracting/i.test(prompt)) {
+ return JSON.stringify({
+ worth_extracting: false,
+ reason: 'Smoke fixture only',
+ });
+ }
+
+ const needle = extractSmokeNeedle(prompt);
+ return JSON.stringify({
+ title: 'Hermes EverOS Raven SkillHub dogfood',
+ content: `${needle}. Provider-level store, flush, extract, index, search, and recall smoke for Raven and EverMe SkillHub.`,
+ summary: `${needle}. EverOS memory dogfood smoke.`,
+ });
+}
+
+function chatResponse(body) {
+ const prompt = extractPromptText(body);
+ const content = completionContent(prompt);
+ return {
+ id: `chatcmpl-mock-${Date.now()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: body.model || 'mock-openai-compatible',
+ choices: [
+ {
+ index: 0,
+ finish_reason: 'stop',
+ message: {
+ role: 'assistant',
+ content,
+ },
+ },
+ ],
+ usage: {
+ prompt_tokens: prompt.length,
+ completion_tokens: content.length,
+ total_tokens: prompt.length + content.length,
+ },
+ };
+}
+
+function embeddingsResponse(body) {
+ const input = Array.isArray(body.input) ? body.input : [body.input ?? ''];
+ const dimensions = Number.isInteger(body.dimensions) && body.dimensions > 0
+ ? body.dimensions
+ : DEFAULT_DIMENSIONS;
+ return {
+ object: 'list',
+ data: input.map((item, index) => ({
+ object: 'embedding',
+ index,
+ embedding: embeddingFor(String(item), dimensions),
+ })),
+ model: body.model || 'mock-embedding',
+ usage: {
+ prompt_tokens: input.join(' ').length,
+ total_tokens: input.join(' ').length,
+ },
+ };
+}
+
+function rerankResponse(body) {
+ const documents = Array.isArray(body.documents) ? body.documents : [];
+ const queryTokens = new Set(
+ String(body.query || '')
+ .toLowerCase()
+ .split(/[^a-z0-9_./:-]+/)
+ .filter(Boolean),
+ );
+ const results = documents
+ .map((document, index) => {
+ const docTokens = String(document)
+ .toLowerCase()
+ .split(/[^a-z0-9_./:-]+/)
+ .filter(Boolean);
+ const overlap = docTokens.filter((token) => queryTokens.has(token)).length;
+ return { index, relevance_score: Math.min(1, 0.55 + overlap / 20) };
+ })
+ .sort((a, b) => b.relevance_score - a.relevance_score);
+ return { results };
+}
+
+function readJson(req) {
+ return new Promise((resolve, reject) => {
+ const chunks = [];
+ req.on('data', (chunk) => chunks.push(chunk));
+ req.on('end', () => {
+ const raw = Buffer.concat(chunks).toString('utf8') || '{}';
+ try {
+ resolve(JSON.parse(raw));
+ } catch (error) {
+ reject(error);
+ }
+ });
+ req.on('error', reject);
+ });
+}
+
+function sendJson(res, status, payload) {
+ const body = JSON.stringify(payload);
+ res.writeHead(status, {
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(body),
+ });
+ res.end(body);
+}
+
+function createServer() {
+ return http.createServer(async (req, res) => {
+ try {
+ const path = new URL(req.url || '/', 'http://localhost').pathname;
+ if (req.method === 'GET' && path === '/health') {
+ sendJson(res, 200, { status: 'ok' });
+ return;
+ }
+ if (req.method !== 'POST') {
+ sendJson(res, 404, { error: { message: 'not found' } });
+ return;
+ }
+
+ const body = await readJson(req);
+ if (path === '/v1/chat/completions' || path === '/chat/completions') {
+ sendJson(res, 200, chatResponse(body));
+ } else if (path === '/v1/embeddings' || path === '/embeddings') {
+ sendJson(res, 200, embeddingsResponse(body));
+ } else if (path === '/v1/rerank' || path === '/rerank') {
+ sendJson(res, 200, rerankResponse(body));
+ } else {
+ sendJson(res, 404, { error: { message: `unknown path: ${path}` } });
+ }
+ } catch (error) {
+ sendJson(res, 500, { error: { message: error.message } });
+ }
+ });
+}
+
+function main() {
+ const args = parseArgs(process.argv.slice(2));
+ if (args.check) {
+ console.log('PASS mock-openai-compatible syntax');
+ return;
+ }
+ const server = createServer();
+ server.listen(args.port, args.host, () => {
+ console.log(`mock-openai-compatible listening host=${args.host} port=${args.port}`);
+ });
+}
+
+main();
diff --git a/use-cases/hermes-everos-memory/bin/raven b/use-cases/hermes-everos-memory/bin/raven
new file mode 100755
index 00000000..2865f8b8
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/raven
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+binary="raven-console/target/debug/raven"
+changed=""
+if [[ -x "$binary" ]]; then
+ changed="$(find raven-console/src raven-console/Cargo.toml raven-console/Cargo.lock -type f -newer "$binary" -print -quit)"
+fi
+
+if [[ ! -x "$binary" || -n "$changed" ]]; then
+ cargo build --quiet --manifest-path raven-console/Cargo.toml
+fi
+
+exec "$binary" "$@"
diff --git a/use-cases/hermes-everos-memory/bin/raven-run.mjs b/use-cases/hermes-everos-memory/bin/raven-run.mjs
new file mode 100755
index 00000000..05070bca
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/raven-run.mjs
@@ -0,0 +1,245 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import path from 'node:path';
+
+const REQUIRED = [
+ 'id',
+ 'title',
+ 'goal',
+ 'status',
+ 'owners',
+ 'memory_providers',
+ 'lanes',
+ 'gates',
+ 'artifacts',
+ 'evidence_refs',
+ 'next_actions',
+];
+
+const ENUMS = {
+ status: ['captured', 'dispatching', 'executing', 'reviewing', 'done', 'blocked'],
+ owner: ['codex', 'pi', 'opencode', 'hermes', 'muw', 'human'],
+ mutation_policy: ['read_only', 'local_only', 'external_requires_approval'],
+ lane_verdict: ['pass', 'flag', 'block', 'active'],
+ gate_status: ['pass', 'flag', 'block', 'not_run'],
+};
+
+function usage() {
+ console.log(`Usage:
+ raven-run validate
+ raven-run render
+ raven-run verify
+ raven-run summary
+ raven-run sample`);
+}
+
+function readJson(file) {
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
+}
+
+function validateRun(run) {
+ const errors = [];
+ for (const field of REQUIRED) {
+ if (!(field in run)) errors.push(`missing ${field}`);
+ }
+ for (const field of ['id', 'title', 'goal', 'status']) {
+ if (field in run && typeof run[field] !== 'string') errors.push(`${field} must be string`);
+ }
+ if (typeof run.id === 'string' && !/^[a-z0-9][a-z0-9._-]{2,127}$/.test(run.id)) {
+ errors.push('id has invalid format');
+ }
+ if (run.status && !ENUMS.status.includes(run.status)) errors.push('status invalid');
+
+ for (const field of ['owners', 'memory_providers', 'lanes', 'gates', 'artifacts', 'evidence_refs', 'next_actions']) {
+ if (field in run && !Array.isArray(run[field])) errors.push(`${field} must be array`);
+ }
+
+ if (Array.isArray(run.owners)) {
+ for (const owner of run.owners) {
+ if (!ENUMS.owner.includes(owner)) errors.push(`owner invalid: ${owner}`);
+ }
+ }
+
+ if (Array.isArray(run.lanes)) {
+ for (const lane of run.lanes) {
+ if (!lane.id) errors.push('lane missing id');
+ if (!ENUMS.owner.includes(lane.owner)) errors.push(`lane owner invalid: ${lane.owner}`);
+ if (!ENUMS.mutation_policy.includes(lane.mutation_policy)) {
+ errors.push(`lane mutation policy invalid: ${lane.mutation_policy}`);
+ }
+ if (!ENUMS.lane_verdict.includes(lane.verdict)) {
+ errors.push(`lane verdict invalid: ${lane.verdict}`);
+ }
+ }
+ }
+
+ if (Array.isArray(run.gates)) {
+ for (const gate of run.gates) {
+ if (!gate.id) errors.push('gate missing id');
+ if (!gate.name) errors.push(`gate missing name: ${gate.id || 'unknown'}`);
+ if (!ENUMS.gate_status.includes(gate.status)) errors.push(`gate status invalid: ${gate.status}`);
+ if (typeof gate.blocks_completion !== 'boolean') {
+ errors.push(`gate blocks_completion must be boolean: ${gate.id || 'unknown'}`);
+ }
+ }
+ }
+
+ return errors;
+}
+
+function verdict(run) {
+ const lanes = Array.isArray(run.lanes) ? run.lanes : [];
+ const gates = Array.isArray(run.gates) ? run.gates : [];
+
+ if (lanes.some((lane) => lane.verdict === 'block')) return 'BLOCK';
+ if (gates.some((gate) => gate.blocks_completion && gate.status === 'block')) return 'BLOCK';
+ if (lanes.some((lane) => lane.verdict === 'flag' || lane.verdict === 'active')) return 'FLAG';
+ if (gates.some((gate) => gate.blocks_completion && ['flag', 'not_run'].includes(gate.status))) return 'FLAG';
+ return 'PASS';
+}
+
+function table(rows) {
+ return rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
+}
+
+function renderRun(run) {
+ const laneRows = [
+ ['Lane', 'Owner', 'Policy', 'Verdict', 'Scope'],
+ ['---', '---', '---', '---', '---'],
+ ...run.lanes.map((lane) => [
+ lane.id,
+ lane.owner,
+ lane.mutation_policy,
+ lane.verdict.toUpperCase(),
+ lane.scope,
+ ]),
+ ];
+ const gateRows = [
+ ['Gate', 'Status', 'Blocks', 'Evidence'],
+ ['---', '---', '---', '---'],
+ ...run.gates.map((gate) => [
+ gate.name,
+ gate.status.toUpperCase(),
+ gate.blocks_completion ? 'yes' : 'no',
+ gate.evidence,
+ ]),
+ ];
+ const artifactRows = [
+ ['Artifact', 'Public Safe', 'Purpose'],
+ ['---', '---', '---'],
+ ...run.artifacts.map((artifact) => [
+ artifact.path,
+ artifact.public_safe ? 'yes' : 'no',
+ artifact.purpose,
+ ]),
+ ];
+
+ return [
+ `# ${run.title}`,
+ '',
+ `VERDICT: ${verdict(run)}.`,
+ '',
+ `Status: ${run.status}`,
+ `Run id: ${run.id}`,
+ `Owners: ${run.owners.join(', ')}`,
+ `Memory providers: ${run.memory_providers.join(', ')}`,
+ '',
+ '## Goal',
+ '',
+ run.goal,
+ '',
+ '## Lanes',
+ '',
+ table(laneRows),
+ '',
+ '## Gates',
+ '',
+ table(gateRows),
+ '',
+ '## Artifacts',
+ '',
+ table(artifactRows),
+ '',
+ '## Next',
+ '',
+ ...run.next_actions.map((action) => `- ${action}`),
+ ].join('\n');
+}
+
+function summary(run) {
+ return {
+ ok: true,
+ id: run.id,
+ status: run.status,
+ verdict: verdict(run),
+ lanes: run.lanes.length,
+ gates: run.gates.length,
+ blocking_gates_open: run.gates.filter(
+ (gate) => gate.blocks_completion && gate.status !== 'pass',
+ ).map((gate) => gate.id),
+ };
+}
+
+function renderGateVerification(run) {
+ const gateRows = [
+ ['Gate', 'Status', 'Blocks', 'Command', 'Evidence'],
+ ['---', '---', '---', '---', '---'],
+ ...run.gates.map((gate) => [
+ gate.id,
+ gate.status.toUpperCase(),
+ gate.blocks_completion ? 'yes' : 'no',
+ gate.command || 'manual',
+ gate.evidence,
+ ]),
+ ];
+ return [
+ `VERDICT: ${verdict(run)}.`,
+ '',
+ table(gateRows),
+ '',
+ `Blocking gates open: ${summary(run).blocking_gates_open.join(', ') || 'none'}`,
+ ].join('\n');
+}
+
+function main() {
+ const [command, file] = process.argv.slice(2);
+ if (!command || command === '--help' || command === '-h') {
+ usage();
+ return;
+ }
+ if (command === 'sample') {
+ const sample = new URL('../raven/fixtures/doomsday-run.json', import.meta.url);
+ console.log(fs.readFileSync(sample, 'utf8'));
+ return;
+ }
+ if (!file) throw new Error(`${command} requires run.json`);
+ const run = readJson(file);
+ const errors = validateRun(run);
+ if (errors.length) {
+ console.error(JSON.stringify({ ok: false, errors }, null, 2));
+ process.exit(1);
+ }
+ if (command === 'validate') {
+ console.log(JSON.stringify(summary(run), null, 2));
+ return;
+ }
+ if (command === 'summary') {
+ console.log(JSON.stringify(summary(run), null, 2));
+ return;
+ }
+ if (command === 'render') {
+ console.log(renderRun(run));
+ return;
+ }
+ if (command === 'verify') {
+ console.log(renderGateVerification(run));
+ const result = verdict(run);
+ if (result === 'BLOCK') process.exit(2);
+ if (result === 'FLAG') process.exit(1);
+ return;
+ }
+ throw new Error(`unknown command: ${command}`);
+}
+
+main();
diff --git a/use-cases/hermes-everos-memory/bin/skillhub-mock-api.mjs b/use-cases/hermes-everos-memory/bin/skillhub-mock-api.mjs
new file mode 100755
index 00000000..82bf9e22
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/skillhub-mock-api.mjs
@@ -0,0 +1,298 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import http from 'node:http';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { readJson, renderPacket, renderSkillViews, validatePacket } from './skillhub-packet.mjs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const DEFAULT_PACKET_DIR = path.resolve(__dirname, '../skillhub/fixtures');
+const DEFAULT_HOST = process.env.SKILLHUB_HOST || '127.0.0.1';
+const DEFAULT_PORT = Number(process.env.SKILLHUB_PORT || 18765);
+
+function usage() {
+ console.log(`Usage:
+ skillhub-mock-api [--dir ] [--host ] [--port ]
+ skillhub-mock-api --check [--dir ]`);
+}
+
+function parseArgs(args) {
+ const options = {
+ dir: DEFAULT_PACKET_DIR,
+ host: DEFAULT_HOST,
+ port: DEFAULT_PORT,
+ check: false,
+ };
+ for (let i = 0; i < args.length; i += 1) {
+ const item = args[i];
+ if (item === '--dir') options.dir = path.resolve(args[++i]);
+ else if (item === '--host') options.host = args[++i];
+ else if (item === '--port') options.port = Number(args[++i]);
+ else if (item === '--check') options.check = true;
+ else if (item === '--help' || item === '-h') {
+ usage();
+ process.exit(0);
+ } else {
+ throw new Error(`unknown option: ${item}`);
+ }
+ }
+ if (!Number.isInteger(options.port) || options.port <= 0) {
+ throw new Error('port must be a positive integer');
+ }
+ return options;
+}
+
+function packetSummary(packet) {
+ return {
+ id: packet.id,
+ name: packet.name,
+ summary: packet.summary,
+ visibility: packet.visibility,
+ status: packet.status,
+ version: packet.version,
+ source: packet.source,
+ domains: packet.domains,
+ install_targets: packet.install_targets,
+ evidence_refs: packet.evidence_refs,
+ eval_score: packet.eval_score,
+ rating: packet.rating,
+ votes: packet.votes,
+ };
+}
+
+function installPacket(packet, target) {
+ if (target && !packet.install_targets.includes(target)) {
+ return null;
+ }
+ return {
+ id: packet.id,
+ name: packet.name,
+ version: packet.version,
+ target: target || packet.install_targets[0],
+ compatible_targets: packet.install_targets,
+ source: packet.source,
+ visibility: packet.visibility,
+ status: packet.status,
+ domains: packet.domains,
+ summary: packet.summary,
+ body_markdown: packet.body_markdown,
+ evidence_refs: packet.evidence_refs,
+ eval_score: packet.eval_score,
+ rating: packet.rating,
+ votes: packet.votes,
+ };
+}
+
+function loadPackets(dir) {
+ const files = fs
+ .readdirSync(dir, { withFileTypes: true })
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
+ .map((entry) => path.join(dir, entry.name))
+ .sort();
+
+ const packets = new Map();
+ for (const file of files) {
+ const packet = readJson(file);
+ const errors = validatePacket(packet);
+ if (errors.length) {
+ throw new Error(`${path.relative(process.cwd(), file)} invalid: ${errors.join('; ')}`);
+ }
+ if (packets.has(packet.id)) {
+ throw new Error(`duplicate packet id: ${packet.id}`);
+ }
+ packets.set(packet.id, { file, packet });
+ }
+ return packets;
+}
+
+function listPackets(packets, url) {
+ const target = url.searchParams.get('target');
+ const domain = url.searchParams.get('domain');
+ const includeBody = url.searchParams.get('include_body') === 'true';
+
+ return [...packets.values()]
+ .map(({ packet }) => packet)
+ .filter((packet) => !target || packet.install_targets.includes(target))
+ .filter((packet) => !domain || packet.domains.includes(domain))
+ .map((packet) => (includeBody ? packet : packetSummary(packet)));
+}
+
+function sendJson(res, statusCode, payload) {
+ const body = JSON.stringify(payload, null, 2);
+ res.writeHead(statusCode, {
+ 'content-type': 'application/json; charset=utf-8',
+ 'cache-control': 'no-store',
+ });
+ res.end(`${body}\n`);
+}
+
+function sendText(res, statusCode, body) {
+ res.writeHead(statusCode, {
+ 'content-type': 'text/markdown; charset=utf-8',
+ 'cache-control': 'no-store',
+ });
+ res.end(`${body}\n`);
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.setEncoding('utf8');
+ req.on('data', (chunk) => {
+ body += chunk;
+ if (body.length > 2_000_000) {
+ reject(new Error('request body too large'));
+ req.destroy();
+ }
+ });
+ req.on('end', () => resolve(body));
+ req.on('error', reject);
+ });
+}
+
+async function route(req, res, packets) {
+ if (req.method === 'OPTIONS') {
+ res.writeHead(204, {
+ 'access-control-allow-origin': 'http://127.0.0.1',
+ 'access-control-allow-methods': 'GET,POST,OPTIONS',
+ 'access-control-allow-headers': 'content-type',
+ });
+ res.end();
+ return;
+ }
+
+ const url = new URL(req.url || '/', 'http://skillhub.local');
+ const segments = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
+
+ if (req.method === 'GET' && url.pathname === '/health') {
+ sendJson(res, 200, {
+ ok: true,
+ service: 'everme-skillhub-mock',
+ packet_count: packets.size,
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && segments.length === 1 && segments[0] === 'skills') {
+ sendJson(res, 200, { ok: true, skills: listPackets(packets, url) });
+ return;
+ }
+
+ if (req.method === 'GET' && segments.length === 2 && segments[0] === 'skills') {
+ const item = packets.get(segments[1]);
+ if (!item) {
+ sendJson(res, 404, { ok: false, error: 'skill not found' });
+ return;
+ }
+ sendJson(res, 200, { ok: true, skill: item.packet });
+ return;
+ }
+
+ if (
+ req.method === 'GET'
+ && segments.length === 3
+ && segments[0] === 'skills'
+ && segments[2] === 'render'
+ ) {
+ const item = packets.get(segments[1]);
+ if (!item) {
+ sendJson(res, 404, { ok: false, error: 'skill not found' });
+ return;
+ }
+ sendText(res, 200, renderPacket(item.packet));
+ return;
+ }
+
+ if (
+ req.method === 'GET'
+ && segments.length === 3
+ && segments[0] === 'skills'
+ && segments[2] === 'views'
+ ) {
+ const item = packets.get(segments[1]);
+ if (!item) {
+ sendJson(res, 404, { ok: false, error: 'skill not found' });
+ return;
+ }
+ sendJson(res, 200, {
+ ok: true,
+ skill_id: item.packet.id,
+ views_markdown: renderSkillViews(item.packet),
+ });
+ return;
+ }
+
+ if (
+ req.method === 'GET'
+ && segments.length === 3
+ && segments[0] === 'skills'
+ && segments[2] === 'install-packet'
+ ) {
+ const item = packets.get(segments[1]);
+ if (!item) {
+ sendJson(res, 404, { ok: false, error: 'skill not found' });
+ return;
+ }
+ const target = url.searchParams.get('target');
+ const packet = installPacket(item.packet, target);
+ if (!packet) {
+ sendJson(res, 422, {
+ ok: false,
+ error: 'target not supported',
+ supported_targets: item.packet.install_targets,
+ });
+ return;
+ }
+ sendJson(res, 200, { ok: true, install_packet: packet });
+ return;
+ }
+
+ if (req.method === 'POST' && url.pathname === '/skills/validate') {
+ const body = await readBody(req);
+ const packet = JSON.parse(body);
+ const errors = validatePacket(packet);
+ sendJson(res, errors.length ? 422 : 200, { ok: errors.length === 0, errors });
+ return;
+ }
+
+ sendJson(res, 404, { ok: false, error: 'not found' });
+}
+
+function writeCheckResult(packets) {
+ process.stdout.write(JSON.stringify({
+ ok: true,
+ service: 'everme-skillhub-mock',
+ packet_count: packets.size,
+ packet_ids: [...packets.keys()],
+ }, null, 2) + '\n');
+}
+
+function main() {
+ const options = parseArgs(process.argv.slice(2));
+ const packets = loadPackets(options.dir);
+
+ if (options.check) {
+ writeCheckResult(packets);
+ return;
+ }
+
+ const server = http.createServer((req, res) => {
+ route(req, res, packets).catch((error) => {
+ sendJson(res, 500, { ok: false, error: error.message });
+ });
+ });
+
+ server.listen(options.port, options.host, () => {
+ const address = server.address();
+ process.stdout.write(JSON.stringify({
+ ok: true,
+ service: 'everme-skillhub-mock',
+ url: `http://${address.address}:${address.port}`,
+ packet_count: packets.size,
+ }) + '\n');
+ });
+}
+
+main();
diff --git a/use-cases/hermes-everos-memory/bin/skillhub-packet.mjs b/use-cases/hermes-everos-memory/bin/skillhub-packet.mjs
new file mode 100755
index 00000000..c49b2813
--- /dev/null
+++ b/use-cases/hermes-everos-memory/bin/skillhub-packet.mjs
@@ -0,0 +1,295 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+const REQUIRED = [
+ 'id',
+ 'name',
+ 'summary',
+ 'visibility',
+ 'status',
+ 'version',
+ 'source',
+ 'domains',
+ 'install_targets',
+ 'evidence_refs',
+ 'body_markdown',
+];
+
+const ENUMS = {
+ visibility: ['private', 'link', 'community'],
+ status: ['draft', 'active', 'needs_eval', 'archived'],
+ source: ['manual', 'evercore_extracted', 'imported', 'community'],
+ install_targets: ['hermes', 'raven', 'claude_code', 'evercore', 'openclaw'],
+};
+
+function usage() {
+ console.log(`Usage:
+ skillhub-packet validate
+ skillhub-packet render
+ skillhub-packet views
+ skillhub-packet from-skill [--domain ] [--target ] [--owner ]
+ skillhub-packet sample`);
+}
+
+export function readJson(file) {
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
+}
+
+export function slugify(input) {
+ return String(input || '')
+ .toLowerCase()
+ .replace(/[^a-z0-9._-]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .slice(0, 120) || 'skill';
+}
+
+function findRepoRoot(startDir) {
+ let dir = path.resolve(startDir);
+ while (dir !== path.dirname(dir)) {
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
+ dir = path.dirname(dir);
+ }
+ return path.resolve(startDir);
+}
+
+function repoRelative(file) {
+ const absolute = path.resolve(file);
+ const root = findRepoRoot(process.cwd());
+ return path.relative(root, absolute);
+}
+
+export function parseFrontmatter(markdown) {
+ if (!markdown.startsWith('---\n')) {
+ return { frontmatter: {}, body: markdown.trim() };
+ }
+ const end = markdown.indexOf('\n---', 4);
+ if (end === -1) {
+ return { frontmatter: {}, body: markdown.trim() };
+ }
+ const raw = markdown.slice(4, end).split(/\r?\n/);
+ const body = markdown.slice(end + 4).trim();
+ const frontmatter = {};
+ let pendingKey = null;
+ for (const line of raw) {
+ if (!line.trim()) continue;
+ if (/^\s+/.test(line) && pendingKey) {
+ frontmatter[pendingKey] = `${frontmatter[pendingKey] || ''} ${line.trim()}`.trim();
+ continue;
+ }
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
+ if (!match) continue;
+ const [, key, value] = match;
+ pendingKey = key;
+ if (value === '>') {
+ frontmatter[key] = '';
+ } else if (value === 'true' || value === 'false') {
+ frontmatter[key] = value === 'true';
+ } else {
+ frontmatter[key] = value.replace(/^["']|["']$/g, '');
+ }
+ }
+ return { frontmatter, body };
+}
+
+export function validatePacket(packet) {
+ const errors = [];
+ for (const field of REQUIRED) {
+ if (!(field in packet)) errors.push(`missing ${field}`);
+ }
+ for (const field of ['id', 'name', 'summary', 'version', 'body_markdown']) {
+ if (field in packet && typeof packet[field] !== 'string') errors.push(`${field} must be string`);
+ }
+ if (typeof packet.id === 'string' && !/^[a-z0-9][a-z0-9._-]{2,127}$/.test(packet.id)) {
+ errors.push('id has invalid format');
+ }
+ if (typeof packet.version === 'string' && !/^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?$/.test(packet.version)) {
+ errors.push('version must be semver-like');
+ }
+ for (const field of ['domains', 'install_targets', 'evidence_refs']) {
+ if (field in packet && !Array.isArray(packet[field])) errors.push(`${field} must be array`);
+ }
+ for (const field of ['visibility', 'status', 'source']) {
+ if (field in packet && !ENUMS[field].includes(packet[field])) errors.push(`${field} invalid`);
+ }
+ if (Array.isArray(packet.install_targets)) {
+ for (const target of packet.install_targets) {
+ if (!ENUMS.install_targets.includes(target)) errors.push(`install target invalid: ${target}`);
+ }
+ }
+ return errors;
+}
+
+export function renderPacket(packet) {
+ return [
+ `# ${packet.name}`,
+ '',
+ `id: ${packet.id}`,
+ `status: ${packet.status}`,
+ `visibility: ${packet.visibility}`,
+ `version: ${packet.version}`,
+ `source: ${packet.source}`,
+ `domains: ${packet.domains.join(', ')}`,
+ `install_targets: ${packet.install_targets.join(', ')}`,
+ `evidence_refs: ${packet.evidence_refs.length}`,
+ '',
+ packet.summary,
+ ].join('\n');
+}
+
+function valueOrUnknown(value) {
+ if (value === undefined || value === null || value === '') return 'unknown';
+ return String(value);
+}
+
+export function renderSkillViews(packet) {
+ const evidence = packet.evidence_refs.length
+ ? packet.evidence_refs.map((ref) => `- ${ref}`).join('\n')
+ : '- no evidence refs yet';
+ const score = typeof packet.eval_score === 'number' ? packet.eval_score.toFixed(2) : 'not scored';
+ const rating = typeof packet.rating === 'number' ? packet.rating.toFixed(1) : 'not rated';
+ const votes = Number.isInteger(packet.votes) ? packet.votes : 0;
+ const lastEvolved = valueOrUnknown(packet.last_evolved_at);
+ return [
+ `# ${packet.name} SkillHub Views`,
+ '',
+ '## Skill Index Row',
+ '',
+ `- id: ${packet.id}`,
+ `- status: ${packet.status}`,
+ `- version: ${packet.version}`,
+ `- domains: ${packet.domains.join(', ')}`,
+ `- install targets: ${packet.install_targets.join(', ')}`,
+ '',
+ '## Skill Detail',
+ '',
+ packet.summary,
+ '',
+ packet.body_markdown.trim(),
+ '',
+ '## Evolution Queue',
+ '',
+ `- status: ${packet.status}`,
+ `- eval score: ${score}`,
+ `- last evolved: ${lastEvolved}`,
+ `- next action: ${packet.status === 'needs_eval' ? 'run eval before promoting' : 'watch for fresh evidence'}`,
+ '',
+ '## Install Packet',
+ '',
+ `- compatible targets: ${packet.install_targets.join(', ')}`,
+ `- source: ${packet.source}`,
+ `- visibility: ${packet.visibility}`,
+ `- version: ${packet.version}`,
+ '',
+ '## Trust Panel',
+ '',
+ `- rating: ${rating}`,
+ `- votes: ${votes}`,
+ `- evidence refs: ${packet.evidence_refs.length}`,
+ evidence,
+ ].join('\n');
+}
+
+export function packetFromSkill(file, options) {
+ const markdown = fs.readFileSync(file, 'utf8');
+ const { frontmatter, body } = parseFrontmatter(markdown);
+ const name = String(frontmatter.name || path.basename(path.dirname(file)) || 'skill');
+ return {
+ id: `${slugify(options.owner || 'everme-local')}.${slugify(name)}`,
+ name,
+ summary: String(frontmatter.description || name).replace(/\s+/g, ' ').trim(),
+ owner_id: options.owner || 'everme-local',
+ visibility: 'private',
+ status: 'needs_eval',
+ version: '0.1.0',
+ source: 'evercore_extracted',
+ domains: [options.domain || inferDomain(file)],
+ install_targets: [options.target || 'hermes'],
+ evidence_refs: [repoRelative(file)],
+ body_markdown: body || markdown.trim(),
+ frontmatter,
+ };
+}
+
+function inferDomain(file) {
+ const parts = file.split(path.sep);
+ const idx = parts.indexOf('skills_sample');
+ if (idx >= 0 && parts[idx + 1]) return parts[idx + 1].toLowerCase();
+ return 'general';
+}
+
+function parseOptions(args) {
+ const options = {};
+ for (let i = 0; i < args.length; i += 1) {
+ const item = args[i];
+ if (item === '--domain') options.domain = args[++i];
+ else if (item === '--target') options.target = args[++i];
+ else if (item === '--owner') options.owner = args[++i];
+ else throw new Error(`unknown option: ${item}`);
+ }
+ return options;
+}
+
+function main() {
+ const [command, file, ...rest] = process.argv.slice(2);
+ if (!command || command === '--help' || command === '-h') {
+ usage();
+ return;
+ }
+ if (command === 'sample') {
+ const sample = new URL('../skillhub/fixtures/raven-skillhub-sample.json', import.meta.url);
+ console.log(fs.readFileSync(sample, 'utf8'));
+ return;
+ }
+ if (command === 'validate') {
+ if (!file) throw new Error('validate requires packet.json');
+ const packet = readJson(file);
+ const errors = validatePacket(packet);
+ if (errors.length) {
+ console.error(JSON.stringify({ ok: false, errors }, null, 2));
+ process.exit(1);
+ }
+ console.log(JSON.stringify({ ok: true, id: packet.id, targets: packet.install_targets }, null, 2));
+ return;
+ }
+ if (command === 'render') {
+ if (!file) throw new Error('render requires packet.json');
+ const packet = readJson(file);
+ const errors = validatePacket(packet);
+ if (errors.length) {
+ console.error(JSON.stringify({ ok: false, errors }, null, 2));
+ process.exit(1);
+ }
+ console.log(renderPacket(packet));
+ return;
+ }
+ if (command === 'views') {
+ if (!file) throw new Error('views requires packet.json');
+ const packet = readJson(file);
+ const errors = validatePacket(packet);
+ if (errors.length) {
+ console.error(JSON.stringify({ ok: false, errors }, null, 2));
+ process.exit(1);
+ }
+ console.log(renderSkillViews(packet));
+ return;
+ }
+ if (command === 'from-skill') {
+ if (!file) throw new Error('from-skill requires SKILL.md');
+ const packet = packetFromSkill(file, parseOptions(rest));
+ const errors = validatePacket(packet);
+ if (errors.length) {
+ console.error(JSON.stringify({ ok: false, errors }, null, 2));
+ process.exit(1);
+ }
+ console.log(JSON.stringify(packet, null, 2));
+ return;
+ }
+ throw new Error(`unknown command: ${command}`);
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main();
+}
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/DEPLOY_PACKET.md b/use-cases/hermes-everos-memory/deploy/nixos/DEPLOY_PACKET.md
new file mode 100644
index 00000000..1740630d
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/DEPLOY_PACKET.md
@@ -0,0 +1,105 @@
+# EverCore Remote Deploy Packet v0
+
+## Verdict
+
+FLAG: deploy packet is ready for review and the remote NixOS workhorse is
+reachable, but EverCore is not yet running on the workhorse loopback service.
+
+## Decision
+
+Use the NixOS/workhorse lane for EverCore.
+
+CCR should stay out of the stateful service path. It can still run client-side
+smoke tests, propose patches, and review evidence packets.
+
+## Why
+
+- EverCore is a durable memory service; it belongs on the always-on host.
+- Hermes can dogfood it locally on the same host through `127.0.0.1`.
+- Keeping CCR as a helper preserves the mirror/patch-only boundary.
+- Loopback-first exposure prevents the data plane from becoming a public DB
+ surface.
+
+## Mutation Boundary
+
+This packet does not mutate the remote host.
+
+Before applying it remotely, confirm:
+
+- the target checkout is clean or intentionally dirty;
+- the remote env file exists outside git;
+- DeepSeek/OpenRouter provider key is installed only on the host;
+- `bindHost` remains `127.0.0.1` unless the operator explicitly approves a
+ private network exposure;
+- `nixos-rebuild test` passes before `switch`.
+
+## Apply Steps
+
+1. Copy `docker-compose.remote.yaml` and a filled `evercore.env` to the remote
+ runtime directory.
+2. Put an EverOS checkout at the configured `repoDir`.
+3. Import `evercore-remote-workhorse.nix` into the workhorse host config.
+4. Run the workhorse rebuild in test mode.
+5. Start or restart `evercore-compose.service`.
+6. Run `scripts/evercore-remote-smoke.sh --mode health`.
+7. After DeepSeek/OpenRouter LLM auth plus vector/rerank providers are
+ configured, run
+ `scripts/evercore-remote-smoke.sh --mode full`.
+8. Point Hermes at `EVEROS_API_BASE_URL=http://127.0.0.1:1995` on the same host,
+ or at an operator-controlled private route.
+
+## Red Gates
+
+Keep deployment blocked if any of these are true:
+
+- the API binds to a public interface without explicit approval;
+- any data service port is exposed outside Docker/private host boundaries;
+- the env file contains placeholder secrets during full smoke;
+- DeepSeek/OpenRouter auth preflight fails;
+- `evercore-api` starts without a mounted `/app/.env`;
+- health passes but full write/search fails and the provider is marked `PASS`;
+- full smoke search returns zero retrievable memories after flush;
+- host evidence includes raw public host/IP or credential paths.
+
+## Observed Remote Probe
+
+Latest read-only probe:
+
+- remote host is reachable through the existing workhorse SSH route;
+- remote OS is NixOS and system state is running;
+- failed systemd units reported as zero during the dry-run NixOS probe;
+- `evercore-compose.service` is inactive;
+- `evercore-health.timer` is inactive;
+- `http://127.0.0.1:1995/health` is unavailable on the remote host.
+
+Verdict: `FLAG`, because the target host is real and healthy enough for deploy
+work, but EverCore has not been applied or started there.
+
+## Verification Commands
+
+From this repo:
+
+```bash
+bash -n use-cases/hermes-everos-memory/deploy/nixos/scripts/evercore-remote-smoke.sh
+cd use-cases/hermes-everos-memory && just deepseek-auth-preflight
+EVERCORE_REPO_ROOT=$PWD \
+EVERCORE_ENV_FILE=$PWD/use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example \
+ docker-compose --env-file use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example \
+ -f use-cases/hermes-everos-memory/deploy/nixos/docker-compose.remote.yaml config
+```
+
+From the remote host after configuration:
+
+```bash
+repo/use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh --env evercore.env --require-key
+systemctl status evercore-compose.service
+systemctl status evercore-health.timer
+scripts/evercore-remote-smoke.sh --mode health
+scripts/evercore-remote-smoke.sh --mode full
+```
+
+## Next Concrete Action
+
+Repair the runtime lane by using the DeepSeek/OpenRouter auth path, prove it
+with `deepseek-auth-preflight.sh --require-key`, then resume the guarded
+`nixos-rebuild test` path. Keep `switch` blocked until `test` passes.
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/README.md b/use-cases/hermes-everos-memory/deploy/nixos/README.md
new file mode 100644
index 00000000..0ca43a75
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/README.md
@@ -0,0 +1,141 @@
+# EverCore Remote On NixOS
+
+This packet deploys EverCore as a remote memory backend for Hermes without
+turning CCR into the stateful service host.
+
+Decision:
+
+- NixOS/workhorse owns the long-running EverCore service.
+- CCR remains a client/helper lane for patch proposals, smoke tests, and review.
+- Public data ports stay closed. Hermes should reach EverCore through same-host
+ loopback, VPN, or an operator-controlled tunnel.
+
+## Files
+
+| File | Purpose |
+| --- | --- |
+| `docker-compose.remote.yaml` | EverCore API plus MongoDB, Redis, Elasticsearch, MinIO, and Milvus |
+| `evercore.env.example` | Sanitized remote env template; copy to `evercore.env` outside git |
+| `evercore-remote-workhorse.nix` | Optional NixOS module for the workhorse |
+| `scripts/evercore-remote-smoke.sh` | Public-safe health/write/search smoke helper |
+| `../../scripts/deepseek-auth-preflight.sh` | Public-safe DeepSeek/OpenRouter auth-shape check |
+
+## Security Contract
+
+- Do not expose MongoDB, Redis, Elasticsearch, MinIO, or Milvus on public
+ interfaces.
+- Do not commit `evercore.env`, provider keys, SSH targets, raw host values, or
+ local credential paths.
+- Keep the API bound to `127.0.0.1` unless the host is behind a private network
+ route and the operator explicitly opens it.
+- Run `nixos-rebuild test` before `switch` when this module is imported into a
+ live Windburn host.
+
+## Remote Layout
+
+Recommended host layout:
+
+```text
+/srv/windburn/evercore/
+ docker-compose.remote.yaml
+ evercore.env
+ repo/ # EverOS checkout, or symlink to the checkout
+ backups/
+```
+
+The compose file expects:
+
+- `EVERCORE_REPO_ROOT` to point at the EverOS checkout root.
+- `EVERCORE_ENV_FILE` to point at the secret-bearing env file.
+- `EVERCORE_BIND_HOST` and `EVERCORE_BIND_PORT` to control API exposure.
+
+The default bind is `127.0.0.1:1995`.
+
+## Manual Bring-Up
+
+On the remote host:
+
+```bash
+cp evercore.env.example evercore.env
+$EDITOR evercore.env
+repo/use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh \
+ --env evercore.env \
+ --require-key
+
+export EVERCORE_REPO_ROOT=/srv/windburn/evercore/repo
+export EVERCORE_ENV_FILE=/srv/windburn/evercore/evercore.env
+export EVERCORE_BIND_HOST=127.0.0.1
+export EVERCORE_BIND_PORT=1995
+
+docker-compose --env-file "$EVERCORE_ENV_FILE" \
+ -f docker-compose.remote.yaml up -d --build
+```
+
+Health-only smoke:
+
+```bash
+EVEROS_API_BASE_URL=http://127.0.0.1:1995 \
+ scripts/evercore-remote-smoke.sh --mode health
+```
+
+Full memory smoke, after LLM/vector/rerank providers are configured:
+
+```bash
+EVEROS_API_BASE_URL=http://127.0.0.1:1995 \
+ scripts/evercore-remote-smoke.sh --mode full
+```
+
+`full` mode checks health, writes one agent-memory turn, flushes the session,
+then blocks if search returns no retrievable memory.
+
+## NixOS Bring-Up
+
+Copy `evercore-remote-workhorse.nix` into the workhorse module set, import it in
+the host configuration, and override at least these options:
+
+```nix
+services.evercoreRemote = {
+ enable = true;
+ baseDir = "/srv/windburn/evercore";
+ repoDir = "/srv/windburn/evercore/repo";
+ envFile = "/srv/windburn/evercore/evercore.env";
+ composeFile = "/srv/windburn/evercore/docker-compose.remote.yaml";
+ bindHost = "127.0.0.1";
+ bindPort = 1995;
+};
+```
+
+Keep `openFirewall = false` for v0.
+
+## Hermes Provider
+
+For Hermes running on the same remote host:
+
+```bash
+export EVEROS_API_BASE_URL=http://127.0.0.1:1995
+export EVEROS_USER_ID=hermes-user
+export EVEROS_AGENT_ID=hermes
+```
+
+For local Hermes talking to remote EverCore, use a private route or tunnel and
+keep the provider config pointed at the local endpoint exposed by that route.
+
+## Gates
+
+`PASS` for deploy readiness requires:
+
+1. `docker-compose ps` shows every service healthy.
+2. `deepseek-auth-preflight.sh --env --require-key` passes
+ without printing secrets.
+3. `scripts/evercore-remote-smoke.sh --mode health` passes.
+4. `scripts/evercore-remote-smoke.sh --mode full` passes after provider keys are
+ installed.
+5. Hermes provider `everos_health`, `everos_store`, and `everos_search` all pass.
+6. No public data ports are reachable from outside the private host boundary.
+
+## Current Remote Disposition
+
+The workhorse route has been probed read-only and is reachable as a NixOS host,
+but EverCore is not yet active there. Treat remote deployment as `FLAG` until
+`evercore-compose.service`, `evercore-health.timer`, and `--mode full` pass on
+the remote host.
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/docker-compose.remote.yaml b/use-cases/hermes-everos-memory/deploy/nixos/docker-compose.remote.yaml
new file mode 100644
index 00000000..acacb174
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/docker-compose.remote.yaml
@@ -0,0 +1,179 @@
+name: evercore-remote
+
+services:
+ mongodb:
+ image: mongo:7.0
+ container_name: evercore-mongodb
+ restart: unless-stopped
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: ${EVERCORE_MONGODB_USERNAME:-admin}
+ MONGO_INITDB_ROOT_PASSWORD: ${EVERCORE_MONGODB_PASSWORD:?set EVERCORE_MONGODB_PASSWORD}
+ MONGO_INITDB_DATABASE: ${MONGODB_DATABASE:-memsys}
+ volumes:
+ - mongodb_data:/data/db
+ - ${EVERCORE_REPO_ROOT:-../../../..}/methods/EverCore/docker/mongodb/init:/docker-entrypoint-initdb.d:ro
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+ elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
+ container_name: evercore-elasticsearch
+ restart: unless-stopped
+ environment:
+ - discovery.type=single-node
+ - xpack.security.enabled=false
+ - ES_JAVA_OPTS=-Xms1g -Xmx1g
+ - bootstrap.memory_lock=true
+ ulimits:
+ memlock:
+ soft: -1
+ hard: -1
+ volumes:
+ - elasticsearch_data:/usr/share/elasticsearch/data
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+ milvus-etcd:
+ image: quay.io/coreos/etcd:v3.5.5
+ container_name: evercore-milvus-etcd
+ restart: unless-stopped
+ environment:
+ - ETCD_AUTO_COMPACTION_MODE=revision
+ - ETCD_AUTO_COMPACTION_RETENTION=1000
+ - ETCD_QUOTA_BACKEND_BYTES=4294967296
+ - ETCD_SNAPSHOT_COUNT=50000
+ command: etcd -advertise-client-urls=http://127.0.0.1:2479 -listen-client-urls http://0.0.0.0:2479 --data-dir /etcd
+ volumes:
+ - milvus_etcd_data:/etcd
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD", "etcdctl", "endpoint", "health"]
+ interval: 30s
+ timeout: 20s
+ retries: 3
+
+ milvus-minio:
+ image: minio/minio:RELEASE.2023-03-20T20-16-18Z
+ container_name: evercore-milvus-minio
+ restart: unless-stopped
+ environment:
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER}
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD}
+ MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER}
+ MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD}
+ command: minio server /minio_data --console-address ":9001"
+ volumes:
+ - milvus_minio_data:/minio_data
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
+ interval: 30s
+ timeout: 20s
+ retries: 3
+
+ milvus-standalone:
+ image: milvusdb/milvus:v2.5.2
+ container_name: evercore-milvus-standalone
+ restart: unless-stopped
+ command: ["milvus", "run", "standalone"]
+ environment:
+ ETCD_ENDPOINTS: milvus-etcd:2479
+ MINIO_ADDRESS: milvus-minio:9000
+ volumes:
+ - milvus_data:/var/lib/milvus
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD", "curl", "-fsS", "http://localhost:9091/healthz"]
+ interval: 30s
+ timeout: 20s
+ retries: 3
+ start_period: 90s
+ depends_on:
+ milvus-etcd:
+ condition: service_healthy
+ milvus-minio:
+ condition: service_healthy
+
+ redis:
+ image: redis:7.2-alpine
+ container_name: evercore-redis
+ restart: unless-stopped
+ volumes:
+ - redis_data:/data
+ networks:
+ - evercore-network
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
+
+ evercore-api:
+ build:
+ context: ${EVERCORE_REPO_ROOT:-../../../..}/methods/EverCore
+ dockerfile: Dockerfile
+ image: everos/evercore-api:remote
+ container_name: evercore-api
+ restart: unless-stopped
+ command: ["uv", "run", "python", "src/run.py", "--host", "0.0.0.0", "--port", "1995"]
+ env_file:
+ - ${EVERCORE_ENV_FILE:-./evercore.env}
+ environment:
+ MEMSYS_HOST: 0.0.0.0
+ MEMSYS_PORT: 1995
+ API_BASE_URL: http://127.0.0.1:1995
+ ports:
+ - "${EVERCORE_BIND_HOST:-127.0.0.1}:${EVERCORE_BIND_PORT:-1995}:1995"
+ volumes:
+ - ${EVERCORE_ENV_FILE:-./evercore.env}:/app/.env:ro
+ networks:
+ - evercore-network
+ depends_on:
+ mongodb:
+ condition: service_healthy
+ elasticsearch:
+ condition: service_healthy
+ milvus-standalone:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://localhost:1995/health >/dev/null || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 120s
+
+volumes:
+ mongodb_data:
+ driver: local
+ elasticsearch_data:
+ driver: local
+ milvus_etcd_data:
+ driver: local
+ milvus_minio_data:
+ driver: local
+ milvus_data:
+ driver: local
+ redis_data:
+ driver: local
+
+networks:
+ evercore-network:
+ driver: bridge
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/evercore-remote-workhorse.nix b/use-cases/hermes-everos-memory/deploy/nixos/evercore-remote-workhorse.nix
new file mode 100644
index 00000000..9c002fe3
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/evercore-remote-workhorse.nix
@@ -0,0 +1,196 @@
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.services.evercoreRemote;
+ composeBin = lib.getExe pkgs.docker-compose;
+ healthScript = pkgs.writeShellApplication {
+ name = "evercore-remote-health";
+ runtimeInputs = [ pkgs.coreutils pkgs.curl pkgs.jq ];
+ text = ''
+ set -euo pipefail
+
+ evidence_dir="${cfg.evidenceDir}"
+ mkdir -p "$evidence_dir"
+ tmp="$(mktemp)"
+ trap 'rm -f "$tmp"' EXIT
+
+ checked_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ if curl -fsS --max-time 10 "http://${cfg.bindHost}:${toString cfg.bindPort}/health" > "$tmp"; then
+ status="$(jq -r '.status // "unknown"' "$tmp" 2>/dev/null || printf 'unknown')"
+ jq -n \
+ --arg checked_at "$checked_at" \
+ --arg status "$status" \
+ --slurpfile health "$tmp" \
+ '{checked_at: $checked_at, status: $status, health: ($health[0] // {})}' \
+ > "$evidence_dir/current.json"
+ test "$status" = "healthy"
+ else
+ jq -n \
+ --arg checked_at "$checked_at" \
+ '{checked_at: $checked_at, status: "unreachable"}' \
+ > "$evidence_dir/current.json"
+ exit 1
+ fi
+ '';
+ };
+in
+{
+ options.services.evercoreRemote = {
+ enable = lib.mkEnableOption "EverCore remote memory backend";
+
+ baseDir = lib.mkOption {
+ type = lib.types.str;
+ default = "/srv/evercore";
+ description = "Runtime directory containing compose/env files and backups.";
+ };
+
+ repoDir = lib.mkOption {
+ type = lib.types.str;
+ default = "/srv/evercore/repo";
+ description = "EverOS checkout root used as Docker build context.";
+ };
+
+ envFile = lib.mkOption {
+ type = lib.types.str;
+ default = "/srv/evercore/evercore.env";
+ description = "Secret-bearing EverCore env file, not committed to git.";
+ };
+
+ composeFile = lib.mkOption {
+ type = lib.types.str;
+ default = "/srv/evercore/docker-compose.remote.yaml";
+ description = "Remote Docker Compose file.";
+ };
+
+ evidenceDir = lib.mkOption {
+ type = lib.types.str;
+ default = "/srv/evercore/evidence";
+ description = "Directory for local health evidence.";
+ };
+
+ bindHost = lib.mkOption {
+ type = lib.types.str;
+ default = "127.0.0.1";
+ description = "Host interface for the EverCore API port.";
+ };
+
+ bindPort = lib.mkOption {
+ type = lib.types.port;
+ default = 1995;
+ description = "Host port for EverCore API.";
+ };
+
+ openFirewall = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Open bindPort in the NixOS firewall. Keep false for v0.";
+ };
+
+ allowPublicBind = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Allow bindHost=0.0.0.0. Requires explicit operator intent.";
+ };
+
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = "evercore";
+ description = "Owner for runtime directories.";
+ };
+
+ group = lib.mkOption {
+ type = lib.types.str;
+ default = "evercore";
+ description = "Group for runtime directories.";
+ };
+
+ createUser = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Create the runtime user/group. Disable when reusing windburn.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.allowPublicBind || cfg.bindHost != "0.0.0.0";
+ message = "EverCore remote refuses bindHost=0.0.0.0 unless allowPublicBind=true.";
+ }
+ ];
+
+ virtualisation.docker.enable = true;
+
+ users.groups = lib.mkIf cfg.createUser {
+ ${cfg.group} = { };
+ };
+
+ users.users = lib.mkIf cfg.createUser {
+ ${cfg.user} = {
+ isSystemUser = true;
+ group = cfg.group;
+ extraGroups = [ "docker" ];
+ };
+ };
+
+ environment.systemPackages = [
+ pkgs.curl
+ pkgs.docker-compose
+ pkgs.jq
+ ];
+
+ networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.bindPort ];
+
+ systemd.tmpfiles.rules = [
+ "d ${cfg.baseDir} 0750 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.evidenceDir} 0750 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.baseDir}/backups 0750 ${cfg.user} ${cfg.group} - -"
+ ];
+
+ systemd.services.evercore-compose = {
+ description = "EverCore remote memory backend";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "docker.service" "network-online.target" ];
+ requires = [ "docker.service" ];
+ environment = {
+ EVERCORE_REPO_ROOT = cfg.repoDir;
+ EVERCORE_ENV_FILE = cfg.envFile;
+ EVERCORE_BIND_HOST = cfg.bindHost;
+ EVERCORE_BIND_PORT = toString cfg.bindPort;
+ };
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ WorkingDirectory = cfg.baseDir;
+ ExecStartPre = [
+ "${pkgs.coreutils}/bin/test -f ${cfg.composeFile}"
+ "${pkgs.coreutils}/bin/test -f ${cfg.envFile}"
+ "${pkgs.coreutils}/bin/test -d ${cfg.repoDir}"
+ ];
+ ExecStart = "${composeBin} --env-file ${cfg.envFile} -f ${cfg.composeFile} up -d --build --remove-orphans";
+ ExecStop = "${composeBin} --env-file ${cfg.envFile} -f ${cfg.composeFile} down";
+ TimeoutStartSec = 900;
+ TimeoutStopSec = 180;
+ };
+ };
+
+ systemd.services.evercore-health = {
+ description = "EverCore remote health evidence";
+ after = [ "evercore-compose.service" ];
+ serviceConfig = {
+ Type = "oneshot";
+ ExecStart = "${healthScript}/bin/evercore-remote-health";
+ };
+ };
+
+ systemd.timers.evercore-health = {
+ description = "Run EverCore remote health evidence check";
+ wantedBy = [ "timers.target" ];
+ timerConfig = {
+ OnBootSec = "5min";
+ OnUnitActiveSec = "5min";
+ Unit = "evercore-health.service";
+ };
+ };
+ };
+}
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example b/use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example
new file mode 100644
index 00000000..d81de44d
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example
@@ -0,0 +1,89 @@
+# EverCore remote env template.
+#
+# Copy to evercore.env on the remote host and fill in real values there.
+# Do not commit the filled file.
+
+# Compose-only secrets. Keep these values identical to the application values
+# below where the names overlap.
+EVERCORE_MONGODB_USERNAME=admin
+EVERCORE_MONGODB_PASSWORD=change-me
+MINIO_ROOT_USER=evercore-minio
+MINIO_ROOT_PASSWORD=change-me
+
+# API service.
+MEMSYS_HOST=0.0.0.0
+MEMSYS_PORT=1995
+API_BASE_URL=http://127.0.0.1:1995
+
+# Tenant scope. Keep v0 single-tenant unless a real multi-tenant gate exists.
+TENANT_SINGLE_TENANT_ID=t_everos_remote
+
+# LLM provider. Remote auth is pinned to DeepSeek through OpenRouter.
+LLM_PROVIDER=openrouter
+LLM_MODEL=deepseek/deepseek-chat
+LLM_TEMPERATURE=0.3
+LLM_MAX_TOKENS=32768
+LLM_API_KEY=change-me
+LLM_BASE_URL=https://openrouter.ai/api/v1
+LLM_OPENROUTER_PROVIDER=deepseek
+OPENROUTER_API_KEY=change-me
+OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
+OPENAI_API_KEY=change-me
+OPENAI_BASE_URL=https://api.openai.com/v1
+
+# Embedding and rerank providers. DeepInfra placeholders are remote-friendly;
+# replace with a private vLLM endpoint if the workhorse gets local models later.
+VECTORIZE_PROVIDER=deepinfra
+VECTORIZE_API_KEY=change-me
+VECTORIZE_BASE_URL=https://api.deepinfra.com/v1/openai
+VECTORIZE_MODEL=Qwen/Qwen3-Embedding-4B
+VECTORIZE_FALLBACK_PROVIDER=none
+VECTORIZE_FALLBACK_API_KEY=
+VECTORIZE_FALLBACK_BASE_URL=
+VECTORIZE_TIMEOUT=30
+VECTORIZE_MAX_RETRIES=3
+VECTORIZE_BATCH_SIZE=10
+VECTORIZE_MAX_CONCURRENT=5
+VECTORIZE_ENCODING_FORMAT=float
+VECTORIZE_DIMENSIONS=1024
+
+RERANK_PROVIDER=deepinfra
+RERANK_API_KEY=change-me
+RERANK_BASE_URL=https://api.deepinfra.com/v1/inference
+RERANK_MODEL=Qwen/Qwen3-Reranker-4B
+RERANK_FALLBACK_PROVIDER=none
+RERANK_FALLBACK_API_KEY=
+RERANK_FALLBACK_BASE_URL=
+RERANK_TIMEOUT=30
+RERANK_MAX_RETRIES=3
+RERANK_BATCH_SIZE=10
+RERANK_MAX_CONCURRENT=5
+
+# Data services are internal Docker service names, not public host bindings.
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_DB=8
+REDIS_SSL=false
+
+MONGODB_HOST=mongodb
+MONGODB_PORT=27017
+MONGODB_USERNAME=admin
+MONGODB_PASSWORD=change-me
+MONGODB_URI_PARAMS=socketTimeoutMS=15000&authSource=admin
+MONGODB_DATABASE=memsys
+
+ES_HOSTS=http://elasticsearch:9200
+ES_USERNAME=
+ES_PASSWORD=
+ES_VERIFY_CERTS=false
+
+MILVUS_HOST=milvus-standalone
+MILVUS_PORT=19530
+
+# Retrieval defaults.
+DEFAULT_SEARCH_METHOD=hybrid
+TOPK_LIMIT=100
+RECALL_MULTIPLIER=2
+MILVUS_SIMILARITY_THRESHOLD=0.6
+RERANK_SCORE_THRESHOLD=0.6
+AGENTIC_ROUND1_RERANK_TOP_N=10
diff --git a/use-cases/hermes-everos-memory/deploy/nixos/scripts/evercore-remote-smoke.sh b/use-cases/hermes-everos-memory/deploy/nixos/scripts/evercore-remote-smoke.sh
new file mode 100755
index 00000000..64011736
--- /dev/null
+++ b/use-cases/hermes-everos-memory/deploy/nixos/scripts/evercore-remote-smoke.sh
@@ -0,0 +1,169 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+mode="health"
+base_url="${EVEROS_API_BASE_URL:-http://127.0.0.1:1995}"
+user_id="${EVEROS_USER_ID:-hermes-remote-smoke}"
+agent_id="${EVEROS_AGENT_ID:-hermes}"
+
+usage() {
+ cat <<'USAGE'
+Usage: evercore-remote-smoke.sh [--mode health|write|full]
+
+Modes:
+ health Check /health only.
+ write Check /health and POST one agent-memory smoke turn.
+ full Check /health, write one turn, then search for it.
+
+Environment:
+ EVEROS_API_BASE_URL Default http://127.0.0.1:1995
+ EVEROS_USER_ID Default hermes-remote-smoke
+ EVEROS_AGENT_ID Default hermes
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --mode)
+ mode="${2:-}"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "unknown argument: $1" >&2
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+case "$mode" in
+ health|write|full) ;;
+ *)
+ echo "invalid mode: $mode" >&2
+ exit 2
+ ;;
+esac
+
+need() {
+ command -v "$1" >/dev/null 2>&1 || {
+ echo "missing required command: $1" >&2
+ exit 127
+ }
+}
+
+need curl
+need jq
+
+tmpdir="$(mktemp -d)"
+trap 'rm -rf "$tmpdir"' EXIT
+
+curl_json() {
+ local method="$1"
+ local path="$2"
+ local body="${3:-}"
+ local out="$4"
+
+ if [[ -n "$body" ]]; then
+ curl -fsS --max-time 30 \
+ -X "$method" \
+ -H 'Content-Type: application/json' \
+ --data-binary @"$body" \
+ "$base_url$path" > "$out"
+ else
+ curl -fsS --max-time 30 \
+ -X "$method" \
+ -H 'Content-Type: application/json' \
+ "$base_url$path" > "$out"
+ fi
+}
+
+health_out="$tmpdir/health.json"
+curl_json GET /health "" "$health_out"
+status="$(jq -r '.status // .data.status // "unknown"' "$health_out")"
+if [[ "$status" != "healthy" ]]; then
+ echo "BLOCK health status=$status"
+ exit 1
+fi
+echo "PASS health status=healthy"
+
+if [[ "$mode" == "health" ]]; then
+ exit 0
+fi
+
+session_id="hermes-remote-smoke-$(date +%s)"
+now_ms="$(($(date +%s) * 1000))"
+body="$tmpdir/agent-add.json"
+jq -n \
+ --arg user_id "$user_id" \
+ --arg agent_id "$agent_id" \
+ --arg session_id "$session_id" \
+ --argjson now_ms "$now_ms" \
+ '{
+ user_id: $user_id,
+ session_id: $session_id,
+ messages: [
+ {
+ role: "user",
+ sender_id: $user_id,
+ timestamp: $now_ms,
+ content: "EverCore remote Hermes provider smoke write."
+ },
+ {
+ role: "assistant",
+ sender_id: $agent_id,
+ timestamp: ($now_ms + 1),
+ content: "EverCore remote smoke response persisted for provider validation."
+ }
+ ]
+ }' > "$body"
+
+write_out="$tmpdir/write.json"
+curl_json POST /api/v1/memories/agent "$body" "$write_out"
+write_status="$(jq -r '.data.status // .status // "accepted"' "$write_out")"
+echo "PASS write status=$write_status"
+
+if [[ "$mode" == "write" ]]; then
+ exit 0
+fi
+
+flush_body="$tmpdir/agent-flush.json"
+jq -n \
+ --arg user_id "$user_id" \
+ --arg session_id "$session_id" \
+ '{user_id: $user_id, session_id: $session_id}' > "$flush_body"
+
+flush_out="$tmpdir/flush.json"
+curl_json POST /api/v1/memories/agent/flush "$flush_body" "$flush_out"
+flush_status="$(jq -r '.data.status // .status // "flushed"' "$flush_out")"
+echo "PASS flush status=$flush_status"
+
+search_body="$tmpdir/search.json"
+jq -n \
+ --arg query "EverCore remote Hermes provider smoke write" \
+ --arg user_id "$user_id" \
+ '{
+ query: $query,
+ method: "hybrid",
+ memory_types: ["episodic_memory", "raw_message", "profile", "agent_memory"],
+ top_k: 5,
+ filters: {user_id: $user_id}
+ }' > "$search_body"
+
+search_out="$tmpdir/search-out.json"
+curl_json POST /api/v1/memories/search "$search_body" "$search_out"
+episodes="$(jq '.data.episodes // [] | length' "$search_out")"
+raw_messages="$(jq '.data.raw_messages // [] | length' "$search_out")"
+profiles="$(jq '.data.profiles // [] | length' "$search_out")"
+agent_cases="$(jq '.data.agent_memory.cases // [] | length' "$search_out")"
+agent_skills="$(jq '.data.agent_memory.skills // [] | length' "$search_out")"
+
+total="$((episodes + raw_messages + profiles + agent_cases + agent_skills))"
+if [[ "$total" -lt 1 ]]; then
+ echo "BLOCK search returned no retrievable memories"
+ exit 1
+fi
+echo "PASS search episodes=$episodes raw_messages=$raw_messages profiles=$profiles agent_cases=$agent_cases agent_skills=$agent_skills"
diff --git a/use-cases/hermes-everos-memory/justfile b/use-cases/hermes-everos-memory/justfile
new file mode 100644
index 00000000..ef1051a8
--- /dev/null
+++ b/use-cases/hermes-everos-memory/justfile
@@ -0,0 +1,135 @@
+set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
+
+health:
+ bun run bin/everos-memory.mjs health
+
+search query:
+ bun run bin/everos-memory.mjs search "{{query}}"
+
+sync-smoke:
+ bun run bin/everos-memory.mjs sync-smoke
+
+self-test:
+ bun run bin/everos-memory.mjs self-test
+
+provider-load:
+ scripts/check-provider-load.sh
+
+dogfood-smoke mode="provider-only":
+ scripts/dogfood-smoke.sh --mode "{{mode}}"
+
+deepseek-auth-preflight file="deploy/nixos/evercore.env.example":
+ scripts/deepseek-auth-preflight.sh --env "{{file}}"
+
+skillhub-sample:
+ node bin/skillhub-packet.mjs validate skillhub/fixtures/raven-skillhub-sample.json
+
+skillhub-render file="skillhub/fixtures/raven-skillhub-sample.json":
+ node bin/skillhub-packet.mjs render "{{file}}"
+
+skillhub-views file="skillhub/fixtures/raven-skillhub-sample.json":
+ node bin/skillhub-packet.mjs views "{{file}}"
+
+skillhub-from-skill file:
+ node bin/skillhub-packet.mjs from-skill "{{file}}"
+
+skillhub-import-sample:
+ node bin/skillhub-packet.mjs validate skillhub/fixtures/evoagentbench-musician-life-event.json
+
+skillhub-api-check:
+ node bin/skillhub-mock-api.mjs --check
+
+skillhub-api port="18765":
+ node bin/skillhub-mock-api.mjs --port "{{port}}"
+
+skillhub-api-smoke:
+ scripts/skillhub-api-smoke.sh
+
+raven-sample:
+ node bin/raven-run.mjs validate raven/fixtures/doomsday-run.json
+
+raven-render:
+ node bin/raven-run.mjs render raven/fixtures/doomsday-run.json
+
+raven-verify:
+ node bin/raven-run.mjs verify raven/fixtures/doomsday-run.json
+
+raven-help:
+ bin/raven --help
+
+raven-status:
+ bin/raven status
+
+raven-packet:
+ bin/raven packet show
+
+raven-gates:
+ bin/raven gates
+
+raven-research-lanes:
+ bin/raven research lanes
+
+raven-research-packet-smoke:
+ bin/raven research packet native-feel --output -
+
+raven-research-synthesis:
+ bin/raven research synthesize
+
+raven-agents:
+ bin/raven agents list
+
+raven-doctor:
+ bin/raven doctor
+
+raven-native-audit:
+ bin/raven native-audit
+
+raven-runs:
+ bin/raven runs list
+
+raven-sc:
+ bin/raven sc
+
+raven-sc-status:
+ bin/raven sc status
+
+raven-sc-sessions:
+ bin/raven sc sessions
+
+raven-sc-providers:
+ bin/raven sc providers
+
+raven-sc-worktree:
+ bin/raven sc worktree
+
+raven-run-verify:
+ bin/raven run verify
+
+raven-chat-smoke:
+ RAVEN_HERMES_BIN=/bin/echo bin/raven chat send raven chat smoke
+
+raven-chat-receipt-smoke:
+ RAVEN_HERMES_BIN=/bin/echo bin/raven chat send --receipt - raven chat smoke
+
+raven-repl-smoke:
+ printf '/status\n/gates\n/research native-feel\n/chat raven chat smoke\n/memory raven\n/agents\n/runs\n/audit\n/quit\n' | RAVEN_HERMES_BIN=/bin/echo bin/raven repl
+
+raven-tui-smoke:
+ RAVEN_TUI_ONCE=1 bin/raven tui
+
+raven-console-check:
+ cargo fmt --manifest-path raven-console/Cargo.toml --check
+ cargo clippy --manifest-path raven-console/Cargo.toml -- -D warnings
+ cargo test --manifest-path raven-console/Cargo.toml
+
+mock-openai-check:
+ node bin/mock-openai-compatible.mjs --check
+
+mock-openai port="18080":
+ node bin/mock-openai-compatible.mjs --port "{{port}}"
+
+remote-smoke mode="health":
+ deploy/nixos/scripts/evercore-remote-smoke.sh --mode "{{mode}}"
+
+install-local:
+ scripts/install-local.sh
diff --git a/use-cases/hermes-everos-memory/package.json b/use-cases/hermes-everos-memory/package.json
new file mode 100644
index 00000000..f3e9704e
--- /dev/null
+++ b/use-cases/hermes-everos-memory/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "hermes-everos-memory",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "description": "Hermes memory-provider helper CLI for EverOS/EverCore",
+ "scripts": {
+ "health": "node bin/everos-memory.mjs health",
+ "search": "node bin/everos-memory.mjs search",
+ "sync-smoke": "node bin/everos-memory.mjs sync-smoke",
+ "deepseek:auth-preflight": "scripts/deepseek-auth-preflight.sh --env deploy/nixos/evercore.env.example",
+ "skillhub:sample": "node bin/skillhub-packet.mjs validate skillhub/fixtures/raven-skillhub-sample.json",
+ "skillhub:check": "node bin/skillhub-mock-api.mjs --check",
+ "skillhub:smoke": "scripts/skillhub-api-smoke.sh",
+ "skillhub:serve": "node bin/skillhub-mock-api.mjs",
+ "raven:sample": "node bin/raven-run.mjs validate raven/fixtures/doomsday-run.json",
+ "raven:render": "node bin/raven-run.mjs render raven/fixtures/doomsday-run.json",
+ "raven:status": "bin/raven status",
+ "raven:doctor": "bin/raven doctor",
+ "raven:gates": "bin/raven gates",
+ "raven:agents": "bin/raven agents list",
+ "raven:research-lanes": "bin/raven research lanes",
+ "raven:research-packet": "bin/raven research packet native-feel --output -",
+ "raven:research-synthesis": "bin/raven research synthesize",
+ "raven:runs": "bin/raven runs list",
+ "raven:native-audit": "bin/raven native-audit",
+ "raven:verify": "bin/raven run verify",
+ "raven:chat-smoke": "RAVEN_HERMES_BIN=/bin/echo bin/raven chat send raven chat smoke",
+ "raven:repl-smoke": "printf '/status\\n/gates\\n/research native-feel\\n/chat raven chat smoke\\n/memory raven\\n/agents\\n/runs\\n/audit\\n/quit\\n' | RAVEN_HERMES_BIN=/bin/echo bin/raven repl",
+ "raven:tui-smoke": "RAVEN_TUI_ONCE=1 bin/raven tui",
+ "mock-openai:check": "node bin/mock-openai-compatible.mjs --check",
+ "mock-openai": "node bin/mock-openai-compatible.mjs",
+ "test": "node bin/everos-memory.mjs self-test"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/use-cases/hermes-everos-memory/plugin.yaml b/use-cases/hermes-everos-memory/plugin.yaml
new file mode 100644
index 00000000..4f052a83
--- /dev/null
+++ b/use-cases/hermes-everos-memory/plugin.yaml
@@ -0,0 +1,7 @@
+name: everos
+version: 0.1.0
+description: "EverOS memory provider for Hermes - local EverCore recall, turn sync, and memory tools."
+hooks:
+ - on_pre_compress
+ - on_memory_write
+ - on_delegation
diff --git a/use-cases/hermes-everos-memory/raven-console/Cargo.lock b/use-cases/hermes-everos-memory/raven-console/Cargo.lock
new file mode 100644
index 00000000..24c537a2
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/Cargo.lock
@@ -0,0 +1,925 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "clipboard-win"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+dependencies = [
+ "error-code",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "error-code"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+
+[[package]]
+name = "fd-lock"
+version = "4.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
+dependencies = [
+ "cfg-if",
+ "rustix 1.1.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "instability"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
+dependencies = [
+ "darling",
+ "indoc",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "ratatui"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "indoc",
+ "instability",
+ "itertools",
+ "lru",
+ "paste",
+ "strum",
+ "unicode-segmentation",
+ "unicode-truncate",
+ "unicode-width 0.2.0",
+]
+
+[[package]]
+name = "raven-console"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "crossterm",
+ "ratatui",
+ "regex",
+ "rustyline",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.12.1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "rustyline"
+version = "15.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "clipboard-win",
+ "fd-lock",
+ "home",
+ "libc",
+ "log",
+ "memchr",
+ "nix",
+ "radix_trie",
+ "unicode-segmentation",
+ "unicode-width 0.2.0",
+ "utf8parse",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-truncate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
+dependencies = [
+ "itertools",
+ "unicode-segmentation",
+ "unicode-width 0.1.14",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/use-cases/hermes-everos-memory/raven-console/Cargo.toml b/use-cases/hermes-everos-memory/raven-console/Cargo.toml
new file mode 100644
index 00000000..e6b8cebf
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "raven-console"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "raven"
+path = "src/main.rs"
+
+[dependencies]
+clap = { version = "4.5", features = ["derive"] }
+crossterm = "0.28"
+ratatui = "0.29"
+regex = "1.11"
+rustyline = "15.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs
new file mode 100644
index 00000000..45c51e28
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs
@@ -0,0 +1,364 @@
+use crate::context::Context;
+use crate::model::{HermesChatTranscriptLine, HermesChatTurn, Verdict};
+use crate::sanitizer::sanitize_text;
+use crate::util::one_line;
+use crate::RavenResult;
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::time::Instant;
+
+const MAX_PROMPT_CHARS: usize = 4_000;
+const MAX_RESPONSE_CHARS: usize = 8_000;
+const MAX_EVIDENCE_CHARS: usize = 1_200;
+
+#[derive(Default)]
+pub struct HermesOptions {
+ pub cwd: Option,
+}
+
+#[derive(Clone)]
+pub(crate) struct HermesTurnMeta {
+ command: Vec,
+ workspace: String,
+ runtime: String,
+}
+
+pub fn ask(ctx: &Context, prompt: &str) -> RavenResult {
+ ask_with_options(ctx, prompt, HermesOptions::default())
+}
+
+pub fn ask_with_options(
+ ctx: &Context,
+ prompt: &str,
+ options: HermesOptions,
+) -> RavenResult {
+ let prompt = prompt.trim();
+ let runtime = detect_runtime();
+ let command = command_label();
+ let cwd = resolve_cwd(ctx, options.cwd.as_deref());
+ let workspace = cwd
+ .as_ref()
+ .map(|cwd| workspace_label(ctx, cwd))
+ .unwrap_or_else(|err| sanitize_text(err));
+ let meta = HermesTurnMeta {
+ command,
+ workspace,
+ runtime,
+ };
+
+ if prompt.is_empty() {
+ return Ok(HermesChatTurn {
+ prompt: String::new(),
+ command: meta.command,
+ workspace: meta.workspace,
+ runtime: meta.runtime,
+ verdict: Verdict::Flag,
+ exit_code: 0,
+ duration_ms: 0,
+ response: "Empty prompt.".to_string(),
+ evidence: "no Hermes call was made".to_string(),
+ transcript: Vec::new(),
+ });
+ }
+
+ let binary = env::var("RAVEN_HERMES_BIN").unwrap_or_else(|_| "hermes".to_string());
+ let bounded_prompt = clamp_chars(prompt, MAX_PROMPT_CHARS);
+
+ let cwd = match cwd {
+ Ok(cwd) => cwd,
+ Err(err) => {
+ return Ok(flag_turn(
+ &bounded_prompt,
+ meta,
+ 1,
+ 0,
+ "Hermes cwd is unavailable for this turn.",
+ &format!("invalid Hermes cwd: {err}"),
+ ))
+ }
+ };
+
+ let start = Instant::now();
+ let output = Command::new(binary)
+ .arg("-z")
+ .arg(build_raven_prompt(
+ &bounded_prompt,
+ &meta.workspace,
+ &meta.runtime,
+ ))
+ .current_dir(&cwd)
+ .env("RAVEN_WORKSPACE_ROOT", &ctx.root)
+ .env("RAVEN_OPERATOR_CWD", &cwd)
+ .env("RAVEN_HERMES_RUNTIME", &meta.runtime)
+ .output();
+
+ match output {
+ Ok(output) => {
+ let exit_code = output.status.code().unwrap_or(1);
+ Ok(turn_from_output(
+ &bounded_prompt,
+ exit_code,
+ &String::from_utf8_lossy(&output.stdout),
+ &String::from_utf8_lossy(&output.stderr),
+ start.elapsed().as_millis(),
+ meta,
+ ))
+ }
+ Err(err) => Ok(flag_turn(
+ &bounded_prompt,
+ meta,
+ 127,
+ start.elapsed().as_millis(),
+ "Hermes is unavailable for this turn.",
+ &format!("failed to launch Hermes: {err}"),
+ )),
+ }
+}
+
+fn build_raven_prompt(prompt: &str, workspace: &str, runtime: &str) -> String {
+ format!(
+ "You are Hermes inside Raven's local operator console.\n\
+Keep the answer concise and operational.\n\
+Do not mutate files, remote issues, deploy targets, or credentials unless the operator explicitly asks.\n\
+Keep public-surface safety: do not reveal local absolute paths, tokens, private hosts/IPs, or credential paths.\n\
+Runtime context: hermes_openai_runtime={runtime}; raven_workspace={workspace}.\n\
+If you use terminal tools, operate from the process cwd or from the RAVEN_OPERATOR_CWD/RAVEN_WORKSPACE_ROOT env vars, but do not print those env values.\n\
+If Raven gates are discussed, preserve the current truth: DAS-2669 auth-route repair is accepted through DeepSeek/OpenRouter, while DAS-2666 remains blocked until remote env preflight, guarded NixOS test, full smoke, and supervisor PASS exist.\n\n\
+Operator prompt:\n{prompt}"
+ )
+}
+
+pub(crate) fn turn_from_output(
+ prompt: &str,
+ exit_code: i32,
+ stdout: &str,
+ stderr: &str,
+ duration_ms: u128,
+ meta: HermesTurnMeta,
+) -> HermesChatTurn {
+ let stdout = stdout.trim();
+ let stderr = stderr.trim();
+ let raw_response = if stdout.is_empty() && !stderr.is_empty() {
+ stderr
+ } else {
+ stdout
+ };
+ let response = clamp_chars(&sanitize_text(raw_response), MAX_RESPONSE_CHARS);
+ let evidence = if exit_code == 0 {
+ format!(
+ "Hermes oneshot completed in {duration_ms}ms; runtime={}; cwd={}",
+ meta.runtime, meta.workspace
+ )
+ } else if stderr.is_empty() {
+ format!(
+ "Hermes exited {exit_code} with no stderr; runtime={}; cwd={}",
+ meta.runtime, meta.workspace
+ )
+ } else {
+ format!(
+ "Hermes exited {exit_code}: {}; runtime={}; cwd={}",
+ one_line(stderr),
+ meta.runtime,
+ meta.workspace
+ )
+ };
+ let response = if response.is_empty() {
+ "(no response text)".to_string()
+ } else {
+ response
+ };
+ let prompt = sanitize_text(&clamp_chars(prompt, MAX_PROMPT_CHARS));
+
+ HermesChatTurn {
+ transcript: vec![
+ HermesChatTranscriptLine {
+ role: "operator".to_string(),
+ content: prompt.clone(),
+ },
+ HermesChatTranscriptLine {
+ role: "assistant".to_string(),
+ content: response.clone(),
+ },
+ ],
+ prompt,
+ command: meta.command,
+ workspace: meta.workspace,
+ runtime: meta.runtime,
+ verdict: if exit_code == 0 {
+ Verdict::Pass
+ } else {
+ Verdict::Flag
+ },
+ exit_code,
+ duration_ms,
+ response,
+ evidence: clamp_chars(&sanitize_text(&evidence), MAX_EVIDENCE_CHARS),
+ }
+}
+
+fn flag_turn(
+ prompt: &str,
+ meta: HermesTurnMeta,
+ exit_code: i32,
+ duration_ms: u128,
+ response: &str,
+ evidence: &str,
+) -> HermesChatTurn {
+ let prompt = sanitize_text(&clamp_chars(prompt, MAX_PROMPT_CHARS));
+ let response = response.to_string();
+ HermesChatTurn {
+ transcript: vec![
+ HermesChatTranscriptLine {
+ role: "operator".to_string(),
+ content: prompt.clone(),
+ },
+ HermesChatTranscriptLine {
+ role: "assistant".to_string(),
+ content: response.clone(),
+ },
+ ],
+ prompt,
+ command: meta.command,
+ workspace: meta.workspace,
+ runtime: meta.runtime,
+ verdict: Verdict::Flag,
+ exit_code,
+ duration_ms,
+ response,
+ evidence: clamp_chars(&sanitize_text(evidence), MAX_EVIDENCE_CHARS),
+ }
+}
+
+fn resolve_cwd(ctx: &Context, requested: Option<&Path>) -> Result {
+ let cwd = requested.map_or_else(
+ || ctx.root.clone(),
+ |path| {
+ if path.is_absolute() {
+ path.to_path_buf()
+ } else {
+ ctx.root.join(path)
+ }
+ },
+ );
+
+ if cwd.is_dir() {
+ Ok(cwd)
+ } else {
+ Err(cwd.to_string_lossy().to_string())
+ }
+}
+
+fn workspace_label(ctx: &Context, cwd: &Path) -> String {
+ if cwd == ctx.root {
+ return "case-root".to_string();
+ }
+
+ if let Ok(relative) = cwd.strip_prefix(&ctx.root) {
+ let relative = relative.to_string_lossy().replace('\\', "/");
+ return format!("case-root/{relative}");
+ }
+
+ sanitize_text(&cwd.to_string_lossy())
+}
+
+fn command_label() -> Vec {
+ let binary = env::var("RAVEN_HERMES_BIN").unwrap_or_else(|_| "hermes".to_string());
+ let label = Path::new(&binary)
+ .file_name()
+ .and_then(|name| name.to_str())
+ .unwrap_or(&binary);
+ vec![
+ sanitize_text(label),
+ "-z".to_string(),
+ "[raven-prompt]".to_string(),
+ ]
+}
+
+fn detect_runtime() -> String {
+ if let Ok(runtime) = env::var("RAVEN_HERMES_RUNTIME") {
+ return sanitize_text(runtime.trim());
+ }
+
+ let Some(home) = env::var_os("HOME") else {
+ return "unknown".to_string();
+ };
+ let config = PathBuf::from(home).join(".hermes/config.yaml");
+ let Ok(text) = fs::read_to_string(config) else {
+ return "unknown".to_string();
+ };
+
+ text.lines()
+ .find_map(|line| {
+ line.trim()
+ .strip_prefix("openai_runtime:")
+ .map(|value| value.trim().trim_matches('"').trim_matches('\''))
+ })
+ .filter(|value| !value.is_empty())
+ .map(sanitize_text)
+ .unwrap_or_else(|| "unknown".to_string())
+}
+
+fn clamp_chars(value: &str, max_chars: usize) -> String {
+ let mut chars = value.chars();
+ let mut output = chars.by_ref().take(max_chars).collect::();
+ if chars.next().is_some() {
+ output.push_str(" ...[truncated]");
+ }
+ output
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{turn_from_output, HermesTurnMeta};
+ use crate::model::Verdict;
+
+ fn meta() -> HermesTurnMeta {
+ HermesTurnMeta {
+ command: vec!["hermes".to_string(), "-z".to_string()],
+ workspace: "case-root".to_string(),
+ runtime: "codex_app_server".to_string(),
+ }
+ }
+
+ #[test]
+ fn successful_turn_sanitizes_output() {
+ let turn = turn_from_output(
+ "inspect status",
+ 0,
+ "ready from /Users/alice/work and token sk-proj-abcdefghijklmnopqrstuvwxyz123456",
+ "",
+ 42,
+ meta(),
+ );
+
+ assert_eq!(turn.verdict, Verdict::Pass);
+ assert_eq!(turn.workspace, "case-root");
+ assert_eq!(turn.runtime, "codex_app_server");
+ assert!(!turn.response.contains("/Users/alice"));
+ assert!(!turn.response.contains("sk-proj-"));
+ assert!(turn.response.contains("[redacted-path]"));
+ assert!(turn.response.contains("[redacted-token]"));
+ assert_eq!(turn.transcript.len(), 2);
+ }
+
+ #[test]
+ fn failed_turn_is_flag_and_sanitizes_stderr() {
+ let turn = turn_from_output(
+ "ask",
+ 2,
+ "",
+ "failed on 127.0.0.1:8080 with token=secret-value",
+ 7,
+ meta(),
+ );
+
+ assert_eq!(turn.verdict, Verdict::Flag);
+ assert_eq!(turn.exit_code, 2);
+ assert!(!turn.evidence.contains("127.0.0.1"));
+ assert!(!turn.evidence.contains("secret-value"));
+ assert!(turn.evidence.contains("[redacted-ip]"));
+ assert!(turn.evidence.contains("token=[redacted-secret]"));
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/memory.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/memory.rs
new file mode 100644
index 00000000..9c960286
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/memory.rs
@@ -0,0 +1,108 @@
+use crate::context::Context;
+use crate::model::{MemoryHealth, MemorySearchResult, Verdict};
+use crate::sanitizer::{sanitize_text, sanitize_value};
+use crate::util::{one_line, truncate};
+use serde_json::Value;
+use std::process::{Command, Stdio};
+
+pub fn health(ctx: &Context) -> MemoryHealth {
+ let output = Command::new("node")
+ .arg("bin/everos-memory.mjs")
+ .arg("health")
+ .current_dir(&ctx.root)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+
+ match output {
+ Ok(output) if output.status.success() => {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let status = serde_json::from_str::(&stdout)
+ .ok()
+ .and_then(|value| {
+ value
+ .get("status")
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ .or_else(|| {
+ value
+ .get("data")
+ .and_then(|data| data.get("status"))
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ })
+ })
+ .unwrap_or_else(|| "available".to_string());
+ MemoryHealth {
+ verdict: Verdict::Pass,
+ status: sanitize_text(&status),
+ evidence: sanitize_text(&truncate(&one_line(&stdout), 260)),
+ }
+ }
+ Ok(output) => MemoryHealth {
+ verdict: Verdict::Flag,
+ status: "unavailable".to_string(),
+ evidence: sanitize_text(&format!(
+ "everos-memory health exited {}; {}",
+ output.status,
+ one_line(&String::from_utf8_lossy(&output.stderr))
+ )),
+ },
+ Err(err) => MemoryHealth {
+ verdict: Verdict::Flag,
+ status: "unavailable".to_string(),
+ evidence: sanitize_text(&format!("memory bridge unavailable: {err}")),
+ },
+ }
+}
+
+pub fn search(ctx: &Context, query: &str) -> MemorySearchResult {
+ if query.trim().is_empty() {
+ return MemorySearchResult {
+ query: String::new(),
+ verdict: Verdict::Flag,
+ evidence: "no query supplied".to_string(),
+ result: None,
+ };
+ }
+
+ let output = Command::new("node")
+ .arg("bin/everos-memory.mjs")
+ .arg("search")
+ .arg(query)
+ .current_dir(&ctx.root)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+
+ match output {
+ Ok(output) if output.status.success() => {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let result = serde_json::from_str::(&stdout)
+ .ok()
+ .map(sanitize_value);
+ MemorySearchResult {
+ query: sanitize_text(query),
+ verdict: Verdict::Pass,
+ evidence: sanitize_text(&truncate(&one_line(&stdout), 500)),
+ result,
+ }
+ }
+ Ok(output) => MemorySearchResult {
+ query: sanitize_text(query),
+ verdict: Verdict::Flag,
+ evidence: sanitize_text(&format!(
+ "everos-memory search exited {}; {}",
+ output.status,
+ one_line(&String::from_utf8_lossy(&output.stderr))
+ )),
+ result: None,
+ },
+ Err(err) => MemorySearchResult {
+ query: sanitize_text(query),
+ verdict: Verdict::Flag,
+ evidence: sanitize_text(&format!("memory bridge unavailable: {err}")),
+ result: None,
+ },
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/mod.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/mod.rs
new file mode 100644
index 00000000..6c2d78f4
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/mod.rs
@@ -0,0 +1,6 @@
+pub mod hermes;
+pub mod memory;
+pub mod muw;
+pub mod packet;
+pub mod sc;
+pub mod verify;
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/muw.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/muw.rs
new file mode 100644
index 00000000..d10d50c5
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/muw.rs
@@ -0,0 +1,460 @@
+use crate::constants::{
+ ISSUE_ADAPTER_REPAIR, ISSUE_AUTH_BLOCKER, ISSUE_CONTROL_ROOM, ISSUE_LOCAL_VERIFIER,
+ ISSUE_MEMORY_WATCH, ISSUE_REMOTE_DEPLOY, WATCHLIST_ISSUES,
+};
+use crate::model::{AgentView, IssueView, RemoteGate, Verdict};
+use crate::sanitizer::sanitize_text;
+use crate::util::{one_line, truncate};
+use serde_json::Value;
+use std::process::Command;
+
+pub fn load_watchlist() -> Vec {
+ if Command::new("multica").arg("--version").output().is_err() {
+ return WATCHLIST_ISSUES
+ .iter()
+ .map(|id| fallback_issue(id, "multica CLI unavailable"))
+ .collect();
+ }
+
+ WATCHLIST_ISSUES.iter().map(|id| load_issue(id)).collect()
+}
+
+pub fn remote_gates(issues: &[IssueView]) -> Vec {
+ let auth_issue = issue(issues, ISSUE_AUTH_BLOCKER);
+ let deploy_issue = issue(issues, ISSUE_REMOTE_DEPLOY);
+ let adapter_issue = issue(issues, ISSUE_ADAPTER_REPAIR);
+
+ let auth_repaired = auth_issue.map(has_auth_repaired).unwrap_or(false);
+ let guarded_nixos = deploy_issue
+ .map(|issue| contains_any(issue, &["guarded NixOS test", "nixos-rebuild test"]))
+ .unwrap_or(false);
+ let remote_full_smoke = deploy_issue
+ .map(|issue| {
+ contains_any(
+ issue,
+ &[
+ "remote loopback full smoke",
+ "remote-smoke full",
+ "--mode full",
+ ],
+ )
+ })
+ .unwrap_or(false);
+ let supervisor_pass = deploy_issue
+ .map(|issue| contains_any(issue, &["supervisor PASS", "VERDICT: PASS"]))
+ .unwrap_or(false);
+
+ let mut missing = Vec::new();
+ if !auth_repaired {
+ missing.push("AUTH_REPAIRED on DAS-2669");
+ }
+ if !guarded_nixos {
+ missing.push("guarded NixOS test");
+ }
+ if !remote_full_smoke {
+ missing.push("remote loopback full smoke");
+ }
+ if !supervisor_pass {
+ missing.push("supervisor PASS");
+ }
+
+ let adapter_verdict = adapter_issue
+ .map(|issue| Verdict::from_packet_word(&issue.status))
+ .unwrap_or(Verdict::Flag);
+
+ vec![
+ RemoteGate {
+ id: ISSUE_AUTH_BLOCKER.to_string(),
+ name: "DeepSeek/OpenRouter auth-route repair".to_string(),
+ verdict: if auth_repaired {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ blocks_completion: true,
+ hard_gate: true,
+ evidence: if auth_repaired {
+ "AUTH_REPAIRED present in live issue/comment evidence.".to_string()
+ } else {
+ "AUTH_REPAIRED not present in live issue/comment evidence.".to_string()
+ },
+ gate_effect: if auth_repaired {
+ "Auth block cleared; DAS-2666 still waits on deploy evidence.".to_string()
+ } else {
+ "Remote deploy lane remains blocked until this passes.".to_string()
+ },
+ },
+ RemoteGate {
+ id: ISSUE_REMOTE_DEPLOY.to_string(),
+ name: "EverCore remote deploy".to_string(),
+ verdict: if missing.is_empty() {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ blocks_completion: true,
+ hard_gate: true,
+ evidence: if missing.is_empty() {
+ "Auth repair, guarded NixOS test, remote loopback full smoke, and supervisor PASS are present.".to_string()
+ } else {
+ format!("Missing: {}.", missing.join(", "))
+ },
+ gate_effect: "Overall Raven status may only be FLAG while this remote gate is red."
+ .to_string(),
+ },
+ RemoteGate {
+ id: ISSUE_ADAPTER_REPAIR.to_string(),
+ name: "Pi/OpenCode adapter repair".to_string(),
+ verdict: adapter_verdict,
+ blocks_completion: false,
+ hard_gate: false,
+ evidence: "Adapter repair can unlock Pi/OpenCode lanes but cannot green remote deploy."
+ .to_string(),
+ gate_effect: "No effect on DAS-2666 remote deploy verdict.".to_string(),
+ },
+ ]
+}
+
+pub fn agent_views(issues: &[IssueView]) -> Vec {
+ [
+ (
+ "Workbench control room",
+ ISSUE_CONTROL_ROOM,
+ "Track lane truth and owner packet.",
+ ),
+ (
+ "Local verifier",
+ ISSUE_LOCAL_VERIFIER,
+ "Re-run local Raven and public-safety gates.",
+ ),
+ (
+ "Memory watch",
+ ISSUE_MEMORY_WATCH,
+ "Keep memory bridge and evidence state visible.",
+ ),
+ (
+ "Auth route repair",
+ ISSUE_AUTH_BLOCKER,
+ "DeepSeek/OpenRouter auth-route repair; parent deploy proof remains separate.",
+ ),
+ (
+ "EverCore remote deploy",
+ ISSUE_REMOTE_DEPLOY,
+ "Guarded NixOS test and loopback full smoke only after auth repair.",
+ ),
+ (
+ "Adapter repair",
+ ISSUE_ADAPTER_REPAIR,
+ "Repair Pi/OpenCode wrapper lanes without changing remote deploy verdict.",
+ ),
+ ]
+ .into_iter()
+ .map(|(name, id, scope)| {
+ let issue = issue(issues, id);
+ AgentView {
+ name: name.to_string(),
+ issue_id: id.to_string(),
+ status: issue
+ .map(|issue| issue.status.clone())
+ .unwrap_or_else(|| "unavailable".to_string()),
+ verdict: issue
+ .map(|issue| Verdict::from_packet_word(&issue.status))
+ .unwrap_or(Verdict::Flag),
+ scope: scope.to_string(),
+ }
+ })
+ .collect()
+}
+
+fn load_issue(id: &str) -> IssueView {
+ let output = Command::new("multica")
+ .arg("issue")
+ .arg("get")
+ .arg(id)
+ .arg("--output")
+ .arg("json")
+ .output();
+
+ let mut issue = match output {
+ Ok(output) if output.status.success() => {
+ match serde_json::from_slice::(&output.stdout) {
+ Ok(value) => issue_from_value(id, &value),
+ Err(err) => fallback_issue(id, &format!("multica JSON parse failed: {err}")),
+ }
+ }
+ Ok(output) => fallback_issue(id, &format!("multica issue get exited {}", output.status)),
+ Err(err) => fallback_issue(id, &err.to_string()),
+ };
+
+ if issue.available {
+ match load_comments(id) {
+ Some(comments) => {
+ issue.comments_checked = true;
+ let auth_repair_prefix =
+ if id == ISSUE_AUTH_BLOCKER && has_auth_repaired_text(&comments) {
+ "AUTH_REPAIRED VERDICT: PASS "
+ } else {
+ ""
+ };
+ issue.evidence_excerpt = sanitize_text(&truncate(
+ &one_line(&format!(
+ "{auth_repair_prefix}{} {}",
+ issue.evidence_excerpt, comments
+ )),
+ 900,
+ ));
+ }
+ None => {
+ issue.comments_checked = false;
+ }
+ }
+ }
+
+ issue
+}
+
+fn load_comments(id: &str) -> Option {
+ let output = Command::new("multica")
+ .arg("issue")
+ .arg("comment")
+ .arg("list")
+ .arg(id)
+ .arg("--output")
+ .arg("json")
+ .output()
+ .ok()?;
+
+ if !output.status.success() {
+ return None;
+ }
+
+ let value = serde_json::from_slice::(&output.stdout).ok()?;
+ let mut parts = Vec::new();
+ collect_comment_text(&value, &mut parts);
+ if parts.is_empty() {
+ None
+ } else {
+ Some(parts.join(" "))
+ }
+}
+
+fn collect_comment_text(value: &Value, out: &mut Vec) {
+ match value {
+ Value::Array(items) => {
+ for item in items {
+ collect_comment_text(item, out);
+ }
+ }
+ Value::Object(map) => {
+ for key in ["body", "content", "text", "markdown", "message"] {
+ if let Some(text) = map.get(key).and_then(Value::as_str) {
+ out.push(text.to_string());
+ }
+ }
+ for key in ["comments", "items", "data", "nodes"] {
+ if let Some(child) = map.get(key) {
+ collect_comment_text(child, out);
+ }
+ }
+ }
+ _ => {}
+ }
+}
+
+fn issue_from_value(id: &str, value: &Value) -> IssueView {
+ let identifier = string_field(value, &["identifier", "id", "key"]).unwrap_or(id);
+ let title =
+ string_field(value, &["title", "name", "summary"]).unwrap_or_else(|| fallback_title(id));
+ let status = string_field(
+ value,
+ &["status", "state", "workflow_state", "workflowStatus"],
+ )
+ .unwrap_or("unknown");
+ let priority = string_field(value, &["priority"]).unwrap_or("unknown");
+ let updated_at =
+ string_field(value, &["updated_at", "updatedAt", "updated"]).unwrap_or("unknown");
+ let description = string_field(value, &["description", "body", "content"]).unwrap_or("");
+
+ IssueView {
+ id: identifier.to_string(),
+ title: sanitize_text(title),
+ status: sanitize_text(status),
+ priority: sanitize_text(priority),
+ updated_at: sanitize_text(updated_at),
+ available: true,
+ source: "live".to_string(),
+ comments_checked: false,
+ evidence_excerpt: sanitize_text(&truncate(
+ &one_line(&format!("{title} {status} {description}")),
+ 900,
+ )),
+ }
+}
+
+fn fallback_issue(id: &str, reason: &str) -> IssueView {
+ IssueView {
+ id: id.to_string(),
+ title: fallback_title(id).to_string(),
+ status: if id == ISSUE_REMOTE_DEPLOY || id == ISSUE_AUTH_BLOCKER {
+ "blocked".to_string()
+ } else {
+ "unavailable".to_string()
+ },
+ priority: "unknown".to_string(),
+ updated_at: "unknown".to_string(),
+ available: false,
+ source: "fallback".to_string(),
+ comments_checked: false,
+ evidence_excerpt: sanitize_text(reason),
+ }
+}
+
+fn fallback_title(id: &str) -> &'static str {
+ match id {
+ ISSUE_REMOTE_DEPLOY => "EverCore remote deploy gate",
+ ISSUE_AUTH_BLOCKER => "Repair Windburn NixOS Codex runtime auth",
+ ISSUE_CONTROL_ROOM => "Raven control-room watch",
+ ISSUE_LOCAL_VERIFIER => "Raven local verifier watch",
+ ISSUE_MEMORY_WATCH => "Raven memory evidence watch",
+ ISSUE_ADAPTER_REPAIR => "Pi/OpenCode adapter repair",
+ _ => "Unknown watch issue",
+ }
+}
+
+fn string_field<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
+ let object = value.as_object()?;
+ for key in keys {
+ if let Some(text) = object.get(*key).and_then(Value::as_str) {
+ return Some(text);
+ }
+ if let Some(inner) = object.get(*key).and_then(Value::as_object) {
+ if let Some(text) = inner.get("name").and_then(Value::as_str) {
+ return Some(text);
+ }
+ if let Some(text) = inner.get("title").and_then(Value::as_str) {
+ return Some(text);
+ }
+ }
+ }
+ None
+}
+
+fn issue<'a>(issues: &'a [IssueView], id: &str) -> Option<&'a IssueView> {
+ issues.iter().find(|issue| issue.id == id)
+}
+
+fn has_auth_repaired(issue: &IssueView) -> bool {
+ has_auth_repaired_text(&issue.evidence_excerpt)
+}
+
+fn has_auth_repaired_text(text: &str) -> bool {
+ let evidence = text.to_ascii_uppercase();
+ evidence.contains("AUTH_REPAIRED")
+ && (evidence.contains("VERDICT: PASS")
+ || evidence.contains("AUTH_REPAIRED: PASS")
+ || evidence.contains("AUTH_REPAIR_PROOF: PASS"))
+}
+
+fn contains_any(issue: &IssueView, needles: &[&str]) -> bool {
+ let haystack = issue.evidence_excerpt.to_ascii_lowercase();
+ needles
+ .iter()
+ .any(|needle| haystack.contains(&needle.to_ascii_lowercase()))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::remote_gates;
+ use crate::constants::{ISSUE_ADAPTER_REPAIR, ISSUE_AUTH_BLOCKER, ISSUE_REMOTE_DEPLOY};
+ use crate::model::{IssueView, Verdict};
+
+ #[test]
+ fn missing_auth_repaired_keeps_remote_gates_blocked() {
+ let gates = remote_gates(&[
+ issue(
+ ISSUE_AUTH_BLOCKER,
+ "blocked",
+ "read-only proof still failing",
+ ),
+ issue(
+ ISSUE_REMOTE_DEPLOY,
+ "blocked",
+ "guarded NixOS test remote loopback full smoke supervisor PASS",
+ ),
+ ]);
+
+ assert_eq!(gate(&gates, ISSUE_AUTH_BLOCKER), Verdict::Block);
+ assert_eq!(gate(&gates, ISSUE_REMOTE_DEPLOY), Verdict::Block);
+ }
+
+ #[test]
+ fn deploy_needs_every_hard_evidence_marker() {
+ let gates = remote_gates(&[
+ issue(ISSUE_AUTH_BLOCKER, "closed", "AUTH_REPAIRED VERDICT: PASS"),
+ issue(ISSUE_REMOTE_DEPLOY, "blocked", "guarded NixOS test only"),
+ ]);
+
+ assert_eq!(gate(&gates, ISSUE_REMOTE_DEPLOY), Verdict::Block);
+ }
+
+ #[test]
+ fn future_auth_repair_mentions_do_not_pass_auth_gate() {
+ let gates = remote_gates(&[
+ issue(
+ ISSUE_AUTH_BLOCKER,
+ "blocked",
+ "VERDICT: FLAG post AUTH_REPAIRED only after proof succeeds",
+ ),
+ issue(
+ ISSUE_REMOTE_DEPLOY,
+ "blocked",
+ "guarded NixOS test remote loopback full smoke supervisor PASS",
+ ),
+ ]);
+
+ assert_eq!(gate(&gates, ISSUE_AUTH_BLOCKER), Verdict::Block);
+ assert_eq!(gate(&gates, ISSUE_REMOTE_DEPLOY), Verdict::Block);
+ }
+
+ #[test]
+ fn auth_repair_detector_handles_marker_after_stale_prefix() {
+ let stale_prefix = "VERDICT: BLOCK old refresh token failure ".repeat(80);
+ let evidence = format!("{stale_prefix} AUTH_REPAIRED VERDICT: PASS");
+
+ assert!(super::has_auth_repaired_text(&evidence));
+ }
+
+ #[test]
+ fn adapter_repair_pass_does_not_green_remote_deploy() {
+ let gates = remote_gates(&[
+ issue(ISSUE_AUTH_BLOCKER, "blocked", "runtime auth still broken"),
+ issue(ISSUE_REMOTE_DEPLOY, "blocked", "waiting on auth"),
+ issue(ISSUE_ADAPTER_REPAIR, "closed", "adapter PASS"),
+ ]);
+
+ assert_eq!(gate(&gates, ISSUE_ADAPTER_REPAIR), Verdict::Pass);
+ assert_eq!(gate(&gates, ISSUE_REMOTE_DEPLOY), Verdict::Block);
+ }
+
+ fn issue(id: &str, status: &str, evidence: &str) -> IssueView {
+ IssueView {
+ id: id.to_string(),
+ title: id.to_string(),
+ status: status.to_string(),
+ priority: "unknown".to_string(),
+ updated_at: "unknown".to_string(),
+ available: true,
+ source: "test".to_string(),
+ comments_checked: true,
+ evidence_excerpt: evidence.to_string(),
+ }
+ }
+
+ fn gate(gates: &[crate::model::RemoteGate], id: &str) -> Verdict {
+ gates
+ .iter()
+ .find(|gate| gate.id == id)
+ .map(|gate| gate.verdict)
+ .unwrap()
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/packet.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/packet.rs
new file mode 100644
index 00000000..86d86788
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/packet.rs
@@ -0,0 +1,101 @@
+use crate::model::{DocSummary, LocalGateView, RunPacket, Verdict};
+use crate::sanitizer::sanitize_text;
+use crate::util::{one_line, truncate};
+use crate::RavenResult;
+use std::fs::{self, File};
+use std::io::BufReader;
+use std::path::Path;
+
+pub fn read_packet(path: &Path) -> RavenResult {
+ let file = File::open(path)?;
+ let reader = BufReader::new(file);
+ Ok(serde_json::from_reader(reader)?)
+}
+
+pub fn packet_verdict(packet: &RunPacket) -> Verdict {
+ if packet
+ .lanes
+ .iter()
+ .any(|lane| lane.verdict.eq_ignore_ascii_case("block"))
+ || packet
+ .gates
+ .iter()
+ .any(|gate| gate.blocks_completion && gate.status.eq_ignore_ascii_case("block"))
+ {
+ return Verdict::Block;
+ }
+
+ if packet.lanes.iter().any(|lane| {
+ matches!(
+ lane.verdict.to_ascii_lowercase().as_str(),
+ "flag" | "active"
+ )
+ }) || packet.gates.iter().any(|gate| {
+ gate.blocks_completion
+ && matches!(
+ gate.status.to_ascii_lowercase().as_str(),
+ "flag" | "not_run"
+ )
+ }) {
+ return Verdict::Flag;
+ }
+
+ Verdict::Pass
+}
+
+pub fn local_gates(packet: &RunPacket) -> Vec {
+ packet
+ .gates
+ .iter()
+ .map(|gate| LocalGateView {
+ id: gate.id.clone(),
+ name: gate.name.clone(),
+ verdict: Verdict::from_packet_word(&gate.status),
+ command: gate.command.clone().unwrap_or_else(|| "manual".to_string()),
+ evidence: sanitize_text(&one_line(&gate.evidence)),
+ blocks_completion: gate.blocks_completion,
+ })
+ .collect()
+}
+
+pub fn doc_summaries(root: &Path) -> Vec {
+ [
+ "COMPLETION_AUDIT.md",
+ "OWNER_PACKET.md",
+ "SUPERVISOR_DISPATCH.md",
+ "raven/NATIVE_FEEL_AUDIT.md",
+ ]
+ .into_iter()
+ .map(|path| doc_summary(root, path))
+ .collect()
+}
+
+fn doc_summary(root: &Path, relative: &str) -> DocSummary {
+ let path = root.join(relative);
+ match fs::read_to_string(&path) {
+ Ok(text) => {
+ let title = text
+ .lines()
+ .next()
+ .unwrap_or(relative)
+ .trim_start_matches("# ")
+ .to_string();
+ let line = text
+ .lines()
+ .find(|line| {
+ line.contains("PASS") || line.contains("FLAG") || line.contains("BLOCK")
+ })
+ .unwrap_or("verdict not found");
+ DocSummary {
+ path: relative.to_string(),
+ verdict: Verdict::from_packet_word(line),
+ evidence: sanitize_text(&truncate(&format!("{title}; {}", one_line(line)), 260)),
+ }
+ }
+ Err(err) => DocSummary {
+ path: relative.to_string(),
+ verdict: Verdict::Block,
+ evidence: format!("read failed: {err}"),
+ },
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/sc.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/sc.rs
new file mode 100644
index 00000000..763049b2
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/sc.rs
@@ -0,0 +1,382 @@
+use crate::model::{
+ ScProviderView, ScReport, ScSessionView, ScStatusView, ScWorktreeView, Verdict,
+};
+use crate::sanitizer::sanitize_text;
+use crate::util::{one_line, truncate};
+use serde_json::Value;
+use std::env;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use std::thread;
+use std::time::{Duration, Instant};
+
+const SC_TIMEOUT: Duration = Duration::from_secs(5);
+
+struct ScOutput {
+ exit_code: i32,
+ stdout: String,
+ stderr: String,
+ duration_ms: u128,
+ timed_out: bool,
+}
+
+pub fn report() -> ScReport {
+ let status = status();
+ let providers = providers();
+ let sessions = sessions();
+ let worktree = worktree();
+ let verdict = if status.verdict == Verdict::Block || worktree.verdict == Verdict::Block {
+ Verdict::Block
+ } else if status.verdict == Verdict::Flag || worktree.verdict == Verdict::Flag {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ };
+
+ ScReport {
+ verdict,
+ status,
+ providers,
+ sessions,
+ worktree,
+ }
+}
+
+pub fn boot_report() -> ScReport {
+ ScReport {
+ verdict: Verdict::Flag,
+ status: ScStatusView {
+ verdict: Verdict::Flag,
+ ok: false,
+ api_version: None,
+ app_version: "unknown".to_string(),
+ evidence: "TUI boot snapshot skips sc socket calls; press u for live refresh."
+ .to_string(),
+ },
+ providers: Vec::new(),
+ sessions: Vec::new(),
+ worktree: ScWorktreeView {
+ verdict: Verdict::Flag,
+ branch: "unknown".to_string(),
+ target_branch: "unknown".to_string(),
+ dirty: None,
+ evidence: "refresh pending".to_string(),
+ },
+ }
+}
+
+pub fn status() -> ScStatusView {
+ let output = run_sc(&["status", "--json"]);
+ if output.exit_code != 0 || output.timed_out {
+ return ScStatusView {
+ verdict: if output.timed_out {
+ Verdict::Block
+ } else {
+ Verdict::Flag
+ },
+ ok: false,
+ api_version: None,
+ app_version: "unknown".to_string(),
+ evidence: output_evidence("sc status", &output),
+ };
+ }
+
+ let Ok(value) = serde_json::from_str::(&output.stdout) else {
+ return ScStatusView {
+ verdict: Verdict::Flag,
+ ok: false,
+ api_version: None,
+ app_version: "unknown".to_string(),
+ evidence: "sc status returned non-json output".to_string(),
+ };
+ };
+
+ let ok = value.get("ok").and_then(Value::as_bool).unwrap_or(false);
+ let api_version = value.get("api_version").and_then(Value::as_u64);
+ let app_version = value
+ .get("app_version")
+ .and_then(Value::as_str)
+ .unwrap_or("unknown")
+ .to_string();
+
+ ScStatusView {
+ verdict: if ok { Verdict::Pass } else { Verdict::Flag },
+ ok,
+ api_version,
+ evidence: sanitize_text(&format!(
+ "sc socket responded in {}ms; api={}; app={}",
+ output.duration_ms,
+ api_version
+ .map(|version| version.to_string())
+ .unwrap_or_else(|| "unknown".to_string()),
+ app_version
+ )),
+ app_version,
+ }
+}
+
+pub fn providers() -> Vec {
+ let output = run_sc(&["chat", "providers", "--json"]);
+ if output.exit_code != 0 || output.timed_out {
+ return Vec::new();
+ }
+
+ let Ok(value) = serde_json::from_str::(&output.stdout) else {
+ return Vec::new();
+ };
+
+ value
+ .get("providers")
+ .and_then(Value::as_array)
+ .into_iter()
+ .flatten()
+ .map(|provider| ScProviderView {
+ provider_key: string_field(provider, "provider_key"),
+ display_name: string_field(provider, "display_name"),
+ enabled: provider
+ .get("enabled")
+ .and_then(Value::as_bool)
+ .unwrap_or(false),
+ model_count: provider
+ .get("models")
+ .and_then(Value::as_array)
+ .map(|models| models.len())
+ .unwrap_or(0),
+ reasoning_efforts: provider
+ .get("supported_reasoning_efforts")
+ .and_then(Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(Value::as_str)
+ .map(ToString::to_string)
+ .collect(),
+ })
+ .collect()
+}
+
+pub fn sessions() -> Vec {
+ let output = run_sc(&["chat", "list", "--json"]);
+ if output.exit_code != 0 || output.timed_out {
+ return Vec::new();
+ }
+
+ let Ok(value) = serde_json::from_str::(&output.stdout) else {
+ return Vec::new();
+ };
+
+ value
+ .get("sessions")
+ .and_then(Value::as_array)
+ .into_iter()
+ .flatten()
+ .map(|session| ScSessionView {
+ thread_id: string_field(session, "thread_id"),
+ provider_key: string_field(session, "provider_key"),
+ title: string_field(session, "title"),
+ model: string_field(session, "model"),
+ reasoning_effort: string_field(session, "reasoning_effort"),
+ active_turn: session
+ .get("active_turn")
+ .and_then(Value::as_bool)
+ .unwrap_or(false),
+ closed: session
+ .get("closed")
+ .and_then(Value::as_bool)
+ .unwrap_or(false),
+ branch: string_field(session, "branch"),
+ worktree: public_worktree_label(&string_field(session, "worktree_path")),
+ })
+ .collect()
+}
+
+pub fn worktree() -> ScWorktreeView {
+ let output = run_sc(&["worktree", "status", "--json"]);
+ if output.exit_code != 0 || output.timed_out {
+ return ScWorktreeView {
+ verdict: if output.timed_out {
+ Verdict::Block
+ } else {
+ Verdict::Flag
+ },
+ branch: "unknown".to_string(),
+ target_branch: "unknown".to_string(),
+ dirty: None,
+ evidence: output_evidence("sc worktree status", &output),
+ };
+ }
+
+ let Ok(value) = serde_json::from_str::(&output.stdout) else {
+ return ScWorktreeView {
+ verdict: Verdict::Flag,
+ branch: "unknown".to_string(),
+ target_branch: "unknown".to_string(),
+ dirty: None,
+ evidence: "sc worktree status returned non-json output".to_string(),
+ };
+ };
+
+ let branch = first_string(&value, &["branch", "current_branch", "head_branch"]);
+ let target_branch = first_string(&value, &["target_branch", "base_branch"]);
+ let dirty = value
+ .get("dirty")
+ .or_else(|| value.get("has_uncommitted_changes"))
+ .and_then(Value::as_bool);
+
+ ScWorktreeView {
+ verdict: Verdict::Pass,
+ branch: branch.unwrap_or_else(|| "unknown".to_string()),
+ target_branch: target_branch.unwrap_or_else(|| "unknown".to_string()),
+ dirty,
+ evidence: format!("sc worktree status completed in {}ms", output.duration_ms),
+ }
+}
+
+fn run_sc(args: &[&str]) -> ScOutput {
+ let start = Instant::now();
+ let mut child = match Command::new(sc_binary())
+ .args(args)
+ .current_dir(sc_cwd())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ {
+ Ok(child) => child,
+ Err(err) => {
+ return ScOutput {
+ exit_code: 127,
+ stdout: String::new(),
+ stderr: err.to_string(),
+ duration_ms: start.elapsed().as_millis(),
+ timed_out: false,
+ }
+ }
+ };
+
+ loop {
+ match child.try_wait() {
+ Ok(Some(_status)) => break,
+ Ok(None) if start.elapsed() >= SC_TIMEOUT => {
+ let _ = child.kill();
+ let output = child.wait_with_output().ok();
+ return ScOutput {
+ exit_code: 124,
+ stdout: output
+ .as_ref()
+ .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
+ .unwrap_or_default(),
+ stderr: output
+ .as_ref()
+ .map(|output| String::from_utf8_lossy(&output.stderr).to_string())
+ .unwrap_or_else(|| "sc command timed out".to_string()),
+ duration_ms: start.elapsed().as_millis(),
+ timed_out: true,
+ };
+ }
+ Ok(None) => thread::sleep(Duration::from_millis(25)),
+ Err(err) => {
+ let _ = child.kill();
+ return ScOutput {
+ exit_code: 1,
+ stdout: String::new(),
+ stderr: err.to_string(),
+ duration_ms: start.elapsed().as_millis(),
+ timed_out: false,
+ };
+ }
+ }
+ }
+
+ match child.wait_with_output() {
+ Ok(output) => ScOutput {
+ exit_code: output.status.code().unwrap_or(1),
+ stdout: String::from_utf8_lossy(&output.stdout).to_string(),
+ stderr: String::from_utf8_lossy(&output.stderr).to_string(),
+ duration_ms: start.elapsed().as_millis(),
+ timed_out: false,
+ },
+ Err(err) => ScOutput {
+ exit_code: 1,
+ stdout: String::new(),
+ stderr: err.to_string(),
+ duration_ms: start.elapsed().as_millis(),
+ timed_out: false,
+ },
+ }
+}
+
+fn sc_binary() -> PathBuf {
+ if let Ok(path) = env::var("RAVEN_SC_BIN") {
+ return PathBuf::from(path);
+ }
+
+ env::var_os("HOME")
+ .map(PathBuf::from)
+ .map(|home| home.join(".superconductor/bin/sc"))
+ .unwrap_or_else(|| PathBuf::from("sc"))
+}
+
+fn sc_cwd() -> PathBuf {
+ let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+ for candidate in cwd.ancestors() {
+ if candidate.join(".git").exists() {
+ return candidate.to_path_buf();
+ }
+ }
+ cwd
+}
+
+fn output_evidence(label: &str, output: &ScOutput) -> String {
+ if output.timed_out {
+ return format!("{label} timed out after {}ms", output.duration_ms);
+ }
+
+ let text = if output.stderr.trim().is_empty() {
+ output.stdout.trim()
+ } else {
+ output.stderr.trim()
+ };
+ sanitize_text(&format!(
+ "{label} exited {} in {}ms: {}",
+ output.exit_code,
+ output.duration_ms,
+ truncate(&one_line(text), 240)
+ ))
+}
+
+fn string_field(value: &Value, key: &str) -> String {
+ value
+ .get(key)
+ .and_then(Value::as_str)
+ .unwrap_or("unknown")
+ .to_string()
+}
+
+fn first_string(value: &Value, keys: &[&str]) -> Option {
+ keys.iter()
+ .find_map(|key| value.get(*key).and_then(Value::as_str))
+ .map(ToString::to_string)
+}
+
+fn public_worktree_label(path: &str) -> String {
+ let sanitized = sanitize_text(path);
+ if sanitized == path {
+ return sanitized;
+ }
+
+ path.trim_end_matches('/')
+ .rsplit('/')
+ .next()
+ .filter(|value| !value.is_empty())
+ .unwrap_or("worktree")
+ .to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::public_worktree_label;
+
+ #[test]
+ fn worktree_label_avoids_absolute_paths() {
+ assert_eq!(public_worktree_label("/Users/alice/EverOS/"), "EverOS");
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/adapters/verify.rs b/use-cases/hermes-everos-memory/raven-console/src/adapters/verify.rs
new file mode 100644
index 00000000..66c29b18
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/verify.rs
@@ -0,0 +1,125 @@
+use crate::constants::RUNS_DIR;
+use crate::context::Context;
+use crate::model::{RunView, Verdict};
+use crate::sanitizer::{sanitize_json, sanitize_text};
+use crate::util::{one_line, path_for_display, truncate};
+use serde_json::Value;
+use std::fs;
+use std::process::{Command, Stdio};
+use std::time::Instant;
+
+pub struct VerifyResult {
+ pub command: Vec,
+ pub exit_code: i32,
+ pub duration_ms: u128,
+ pub verdict: Verdict,
+ pub stdout: String,
+ pub stderr: String,
+}
+
+pub fn run_verify(ctx: &Context) -> VerifyResult {
+ let command = vec![
+ "node".to_string(),
+ "bin/raven-run.mjs".to_string(),
+ "verify".to_string(),
+ "raven/fixtures/doomsday-run.json".to_string(),
+ ];
+
+ let started = Instant::now();
+ let output = Command::new("node")
+ .arg("bin/raven-run.mjs")
+ .arg("verify")
+ .arg("raven/fixtures/doomsday-run.json")
+ .current_dir(&ctx.root)
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output();
+ let duration_ms = started.elapsed().as_millis();
+
+ match output {
+ Ok(output) => {
+ let exit_code = output.status.code().unwrap_or(1);
+ let verdict = match exit_code {
+ 0 => Verdict::Pass,
+ 2 => Verdict::Block,
+ _ => Verdict::Flag,
+ };
+ VerifyResult {
+ command,
+ exit_code,
+ duration_ms,
+ verdict,
+ stdout: sanitize_text(&String::from_utf8_lossy(&output.stdout)),
+ stderr: sanitize_text(&String::from_utf8_lossy(&output.stderr)),
+ }
+ }
+ Err(err) => VerifyResult {
+ command,
+ exit_code: 1,
+ duration_ms,
+ verdict: Verdict::Flag,
+ stdout: String::new(),
+ stderr: sanitize_text(&format!("failed to spawn verifier: {err}")),
+ },
+ }
+}
+
+pub fn list_runs(ctx: &Context) -> Vec {
+ let dir = ctx.root.join(RUNS_DIR);
+ let mut saved = Vec::new();
+
+ if let Ok(entries) = fs::read_dir(&dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.extension().and_then(|item| item.to_str()) != Some("json") {
+ continue;
+ }
+ if let Ok(text) = fs::read_to_string(&path) {
+ if let Ok(value) = serde_json::from_str::(&text) {
+ let safe = sanitize_json(&value).unwrap_or(Value::Null);
+ saved.push(RunView {
+ id: safe
+ .get("id")
+ .and_then(Value::as_str)
+ .unwrap_or("saved-receipt")
+ .to_string(),
+ command: safe
+ .get("command")
+ .map(|value| one_line(&value.to_string()))
+ .unwrap_or_else(|| "unknown".to_string()),
+ verdict: safe
+ .get("verdict")
+ .and_then(Value::as_str)
+ .map(Verdict::from_packet_word)
+ .unwrap_or(Verdict::Flag),
+ source: "saved-receipt".to_string(),
+ evidence: safe
+ .get("evidence_excerpt")
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ .unwrap_or_else(|| "receipt present".to_string()),
+ receipt_path: Some(path_for_display(&path)),
+ });
+ }
+ }
+ }
+ }
+
+ if !saved.is_empty() {
+ saved.sort_by(|left, right| left.id.cmp(&right.id));
+ return saved;
+ }
+
+ ctx.packet
+ .gates
+ .iter()
+ .map(|gate| RunView {
+ id: gate.id.clone(),
+ command: gate.command.clone().unwrap_or_else(|| "manual".to_string()),
+ verdict: Verdict::from_packet_word(&gate.status),
+ source: "configured-gate".to_string(),
+ evidence: sanitize_text(&truncate(&one_line(&gate.evidence), 260)),
+ receipt_path: None,
+ })
+ .collect()
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/audit.rs b/use-cases/hermes-everos-memory/raven-console/src/audit.rs
new file mode 100644
index 00000000..16770eb1
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/audit.rs
@@ -0,0 +1,164 @@
+use crate::context::Context;
+use crate::model::{NativeAuditItem, NativeAuditReport, Verdict};
+use std::fs;
+
+pub fn run(ctx: &Context) -> NativeAuditReport {
+ let doc_exists = ctx.root.join("raven/NATIVE_FEEL_AUDIT.md").exists();
+ let cargo = fs::read_to_string(ctx.root.join("raven-console/Cargo.toml")).unwrap_or_default();
+ let source = fs::read_to_string(ctx.root.join("raven-console/src/tui.rs")).unwrap_or_default();
+ let repl = fs::read_to_string(ctx.root.join("raven-console/src/repl.rs")).unwrap_or_default();
+ let sanitizer =
+ fs::read_to_string(ctx.root.join("raven-console/src/sanitizer.rs")).unwrap_or_default();
+ let gitignore = fs::read_to_string(ctx.root.join(".gitignore")).unwrap_or_default();
+
+ let items = vec![
+ item(
+ "latency",
+ Verdict::Pass,
+ "TUI boots from a local snapshot and refreshes live Multica/memory data asynchronously.",
+ false,
+ ),
+ item(
+ "keybindings",
+ if source.contains("KeyCode::Char('q')")
+ && source.contains("KeyCode::Char('?')")
+ && source.contains("KeyCode::Char('h')")
+ && source.contains("KeyCode::Char('i')")
+ && source.contains("KeyCode::Char('o')")
+ {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "TUI exposes h/c chat, i prompt input, q, ?, :, /, s, p, m, a, g, r, o, d, n, Esc, and Ctrl-C paths.",
+ true,
+ ),
+ item(
+ "focus",
+ if source.contains("Panel::") {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "Active panel is explicit state, not screen-position inference.",
+ true,
+ ),
+ item(
+ "scrollback",
+ Verdict::Pass,
+ "Evidence drawer stays fixed; historical run receipts live in raven/.local-runs/.",
+ false,
+ ),
+ item(
+ "interrupt behavior",
+ if source.contains("KeyCode::Esc") && source.contains("KeyCode::Char('c')") {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "Esc cancels prompt modes; Ctrl-C exits safely.",
+ true,
+ ),
+ item(
+ "REPL history",
+ if cargo.contains("rustyline") && repl.contains("add_history_entry") {
+ Verdict::Pass
+ } else {
+ Verdict::Flag
+ },
+ "rustyline backs interactive REPL history; piped smoke remains deterministic.",
+ false,
+ ),
+ item(
+ "pane stability",
+ if cargo.contains("ratatui") && source.contains("Layout::default") {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "ratatui renders fixed status, rail, panel, evidence, and input regions.",
+ true,
+ ),
+ item(
+ "command grammar",
+ Verdict::Pass,
+ "clap command tree mirrors Raven v1 public interface, including chat send and REPL slash commands.",
+ false,
+ ),
+ item(
+ "typed IPC",
+ Verdict::Pass,
+ "RavenSnapshot, RavenReceipt, HermesChatTurn, and ScReport are serde-typed JSON contracts.",
+ false,
+ ),
+ item(
+ "evidence visibility",
+ Verdict::Pass,
+ "remote hard gates, local gates, runs, docs, and watchlist evidence are visible.",
+ false,
+ ),
+ item(
+ "public-safety redaction",
+ if sanitizer.contains("redacted-token") && sanitizer.contains("redacted-signed-url") {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "JSON and human output run through sanitizer for token/path/IP/signed URL shapes.",
+ true,
+ ),
+ item(
+ "receipt hygiene",
+ if gitignore.contains("raven/.local-runs/") {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "Saved run receipts land under gitignored raven/.local-runs/.",
+ true,
+ ),
+ item(
+ "audit doc",
+ if doc_exists {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ "raven/NATIVE_FEEL_AUDIT.md is the repo-local UX/safety contract.",
+ true,
+ ),
+ ];
+
+ let verdict = if items
+ .iter()
+ .any(|item| item.hard_failure && item.verdict == Verdict::Block)
+ {
+ Verdict::Block
+ } else if items.iter().any(|item| item.verdict == Verdict::Flag) {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ };
+
+ NativeAuditReport {
+ verdict,
+ items,
+ blocks_pass_on: vec![
+ "missing hard keybindings".to_string(),
+ "unstable pane layout".to_string(),
+ "unsafe interrupt behavior".to_string(),
+ "missing typed JSON contracts".to_string(),
+ "unredacted public output".to_string(),
+ "non-gitignored saved receipts".to_string(),
+ ],
+ }
+}
+
+fn item(category: &str, verdict: Verdict, evidence: &str, hard_failure: bool) -> NativeAuditItem {
+ NativeAuditItem {
+ category: category.to_string(),
+ verdict,
+ evidence: evidence.to_string(),
+ hard_failure,
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/commands.rs b/use-cases/hermes-everos-memory/raven-console/src/commands.rs
new file mode 100644
index 00000000..a4e53b69
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/commands.rs
@@ -0,0 +1,635 @@
+use crate::adapters::{hermes, memory, sc, verify};
+use crate::audit;
+use crate::context::Context;
+use crate::model::{DoctorCheck, DoctorReport, Verdict};
+use crate::{output, receipt, repl, research, snapshot, tui, RavenResult};
+use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+use std::process::Command;
+
+#[derive(Parser)]
+#[command(name = "raven")]
+#[command(about = "Raven v1 local-first EverOS operating console")]
+#[command(version)]
+pub struct Cli {
+ #[arg(long, global = true)]
+ pub json: bool,
+ #[command(subcommand)]
+ pub command: Option,
+}
+
+#[derive(Subcommand)]
+pub enum Commands {
+ /// Print local packet, remote gate, memory, and watchlist status.
+ Status,
+ /// Start the terminal console view.
+ Tui,
+ /// Start the slash-command REPL.
+ Repl,
+ /// Send a bounded prompt through Hermes.
+ Chat {
+ #[command(subcommand)]
+ command: ChatCommand,
+ },
+ /// Show or export owner packet material.
+ Packet {
+ #[command(subcommand)]
+ command: PacketCommand,
+ },
+ /// Query the EverOS memory bridge.
+ Memory {
+ #[command(subcommand)]
+ command: MemoryCommand,
+ },
+ /// Show agent lane status.
+ Agents {
+ #[command(subcommand)]
+ command: Option,
+ },
+ /// Show hard gates and stop conditions.
+ Gates,
+ /// Inspect bounded Raven v2 research lanes and packets.
+ Research {
+ #[command(subcommand)]
+ command: ResearchCommand,
+ },
+ /// Show saved receipts or configured verification runs.
+ Runs {
+ #[command(subcommand)]
+ command: RunsCommand,
+ },
+ /// Inspect Superconductor session/worktree state.
+ Sc {
+ #[command(subcommand)]
+ command: Option,
+ },
+ /// Execute local Raven run commands.
+ Run {
+ #[command(subcommand)]
+ command: RunCommand,
+ },
+ /// Check local dependencies and bridge availability.
+ Doctor,
+ /// Audit native terminal UX and public-safety discipline.
+ NativeAudit,
+}
+
+#[derive(Subcommand)]
+pub enum PacketCommand {
+ /// Show the current owner-readable packet summary.
+ Show,
+ /// Export a sanitized owner packet.
+ Export {
+ #[arg(long)]
+ output: Option,
+ },
+}
+
+#[derive(Subcommand)]
+pub enum MemoryCommand {
+ /// Check EverOS memory-provider health.
+ Health,
+ /// Search through the EverOS provider bridge.
+ Search { query: Vec },
+}
+
+#[derive(Subcommand)]
+pub enum ChatCommand {
+ /// Send one prompt through Hermes and print the sanitized turn.
+ Send {
+ /// Override the Hermes process working directory.
+ #[arg(long)]
+ cwd: Option,
+ /// Write a sanitized chat receipt to a path, or print it with "-".
+ #[arg(long)]
+ receipt: Option,
+ /// Save a sanitized chat receipt under raven/.local-runs/.
+ #[arg(long)]
+ save: bool,
+ prompt: Vec,
+ },
+}
+
+#[derive(Subcommand)]
+pub enum AgentsCommand {
+ /// List agent/watch lanes.
+ List,
+}
+
+#[derive(Subcommand)]
+pub enum RunsCommand {
+ /// List saved receipts or configured verification commands.
+ List,
+}
+
+#[derive(Subcommand)]
+pub enum ScCommand {
+ /// Show the full Superconductor report.
+ All,
+ /// Check the Superconductor socket and API version.
+ Status,
+ /// List active Superconductor chat sessions.
+ Sessions,
+ /// List enabled Superconductor providers.
+ Providers,
+ /// Show current Superconductor worktree status.
+ Worktree,
+}
+
+#[derive(Subcommand)]
+pub enum ResearchCommand {
+ /// List bounded v2 research lanes.
+ Lanes,
+ /// Render one lane as a live-gate-calibrated decision packet.
+ Packet {
+ lane: String,
+ #[arg(long)]
+ output: Option,
+ },
+ /// Check whether architecture synthesis has enough packet evidence.
+ Synthesize {
+ #[arg(long)]
+ output: Option,
+ },
+}
+
+#[derive(Subcommand)]
+pub enum RunCommand {
+ /// Verify the local Raven packet gates.
+ Verify {
+ #[arg(long)]
+ receipt: Option,
+ #[arg(long)]
+ save: bool,
+ },
+}
+
+pub fn execute(cli: Cli, ctx: &Context) -> RavenResult<()> {
+ match cli.command.unwrap_or(Commands::Status) {
+ Commands::Status => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot)
+ } else {
+ output::status(&snapshot);
+ Ok(())
+ }
+ }
+ Commands::Tui => tui::run(ctx),
+ Commands::Repl => repl::run(ctx),
+ Commands::Chat { command } => match command {
+ ChatCommand::Send {
+ cwd,
+ receipt: receipt_target,
+ save,
+ prompt,
+ } => run_chat_command(ctx, cli.json, cwd, receipt_target, save, &prompt.join(" ")),
+ },
+ Commands::Packet { command } => match command {
+ PacketCommand::Show => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot.packet)
+ } else {
+ output::packet(&snapshot);
+ Ok(())
+ }
+ }
+ PacketCommand::Export { output: target } => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot)
+ } else {
+ output::write_text(
+ target.as_deref(),
+ &output::packet_export_markdown(&snapshot),
+ )
+ }
+ }
+ },
+ Commands::Memory { command } => match command {
+ MemoryCommand::Health => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot.memory)
+ } else {
+ output::memory_health(&snapshot);
+ Ok(())
+ }
+ }
+ MemoryCommand::Search { query } => {
+ let result = memory::search(ctx, &query.join(" "));
+ if cli.json {
+ output::json(&result)
+ } else {
+ output::memory_search(&result);
+ Ok(())
+ }
+ }
+ },
+ Commands::Agents { command: _ } => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot.agents)
+ } else {
+ output::agents(&snapshot);
+ Ok(())
+ }
+ }
+ Commands::Gates => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&serde_json::json!({
+ "remote": snapshot.remote_gates,
+ "local": snapshot.local_gates,
+ }))
+ } else {
+ output::gates(&snapshot);
+ Ok(())
+ }
+ }
+ Commands::Research { command } => match command {
+ ResearchCommand::Lanes => {
+ let lanes = research::list_lanes();
+ if cli.json {
+ output::json(&lanes)
+ } else {
+ output::research_lanes(&lanes);
+ Ok(())
+ }
+ }
+ ResearchCommand::Packet {
+ lane,
+ output: target,
+ } => {
+ let snapshot = snapshot::build(ctx);
+ let packet = research::packet_for_lane(&lane, &snapshot.remote_gates)
+ .ok_or_else(|| format!("unknown research lane `{lane}`"))?;
+ if cli.json {
+ output::json(&packet)
+ } else if target.is_some() {
+ output::write_text(target.as_deref(), &research::packet_markdown(&packet))
+ } else {
+ output::research_packet(&packet);
+ Ok(())
+ }
+ }
+ ResearchCommand::Synthesize { output: target } => {
+ let synthesis = research::synthesis_readiness(&[]);
+ if cli.json {
+ output::json(&synthesis)
+ } else if target.is_some() {
+ output::write_text(target.as_deref(), &research::synthesis_markdown(&synthesis))
+ } else {
+ output::research_synthesis(&synthesis);
+ Ok(())
+ }
+ }
+ },
+ Commands::Runs { command: _ } => {
+ let snapshot = snapshot::build(ctx);
+ if cli.json {
+ output::json(&snapshot.runs)
+ } else {
+ output::runs(&snapshot);
+ Ok(())
+ }
+ }
+ Commands::Sc { command } => match command.unwrap_or(ScCommand::All) {
+ ScCommand::All => {
+ let report = sc::report();
+ if cli.json {
+ output::json(&report)
+ } else {
+ output::sc_report(&report);
+ Ok(())
+ }
+ }
+ ScCommand::Status => {
+ let status = sc::status();
+ if cli.json {
+ output::json(&status)
+ } else {
+ output::sc_status(&status);
+ Ok(())
+ }
+ }
+ ScCommand::Sessions => {
+ let sessions = sc::sessions();
+ if cli.json {
+ output::json(&sessions)
+ } else {
+ output::sc_sessions(&sessions);
+ Ok(())
+ }
+ }
+ ScCommand::Providers => {
+ let providers = sc::providers();
+ if cli.json {
+ output::json(&providers)
+ } else {
+ output::sc_providers(&providers);
+ Ok(())
+ }
+ }
+ ScCommand::Worktree => {
+ let worktree = sc::worktree();
+ if cli.json {
+ output::json(&worktree)
+ } else {
+ output::sc_worktree(&worktree);
+ Ok(())
+ }
+ }
+ },
+ Commands::Run { command } => match command {
+ RunCommand::Verify {
+ receipt: receipt_target,
+ save,
+ } => run_verify_command(ctx, cli.json, receipt_target, save),
+ },
+ Commands::Doctor => {
+ let report = doctor(ctx);
+ if cli.json {
+ output::json(&report)
+ } else {
+ output::doctor(&report);
+ Ok(())
+ }
+ }
+ Commands::NativeAudit => {
+ let report = audit::run(ctx);
+ if cli.json {
+ output::json(&report)
+ } else {
+ output::native_audit(&report);
+ Ok(())
+ }
+ }
+ }
+}
+
+fn run_chat_command(
+ ctx: &Context,
+ json: bool,
+ cwd: Option,
+ receipt_target: Option,
+ save: bool,
+ prompt: &str,
+) -> RavenResult<()> {
+ let turn = hermes::ask_with_options(ctx, prompt, hermes::HermesOptions { cwd })?;
+ let chat_receipt = receipt::from_chat(&turn);
+
+ if receipt_target.as_deref() == Some("-") {
+ output::json(&chat_receipt)?;
+ } else if json {
+ output::json(&turn)?;
+ } else {
+ output::chat_turn(&turn);
+ }
+
+ if let Some(path) = receipt_target.as_deref().filter(|path| *path != "-") {
+ let written = receipt::save_receipt(ctx, &chat_receipt, Some(path))?;
+ output::line(&format!("RECEIPT: {}", written.display()));
+ }
+
+ if save {
+ let written = receipt::save_receipt(ctx, &chat_receipt, None)?;
+ output::line(&format!("SAVED: {}", written.display()));
+ }
+
+ Ok(())
+}
+
+pub fn dispatch_repl(ctx: &Context, input: &str) -> RavenResult {
+ match input {
+ "/help" => {
+ println!("RAVEN_REPL_COMMANDS");
+ println!("/help");
+ println!("/status");
+ println!("/packet");
+ println!("/chat ");
+ println!("/memory ");
+ println!("/agents");
+ println!("/gates");
+ println!("/research [lane]");
+ println!("/runs");
+ println!("/sc [status|sessions|providers|worktree]");
+ println!("/doctor");
+ println!("/audit");
+ println!("/quit");
+ }
+ "/status" => output::status(&snapshot::build(ctx)),
+ "/packet" => output::packet(&snapshot::build(ctx)),
+ "/agents" => output::agents(&snapshot::build(ctx)),
+ "/gates" => output::gates(&snapshot::build(ctx)),
+ "/research" => output::research_lanes(&research::list_lanes()),
+ "/runs" => output::runs(&snapshot::build(ctx)),
+ "/sc" => output::sc_report(&sc::report()),
+ "/sc status" => output::sc_status(&sc::status()),
+ "/sc sessions" => output::sc_sessions(&sc::sessions()),
+ "/sc providers" => output::sc_providers(&sc::providers()),
+ "/sc worktree" => output::sc_worktree(&sc::worktree()),
+ "/doctor" => output::doctor(&doctor(ctx)),
+ "/audit" => output::native_audit(&audit::run(ctx)),
+ "/quit" | "/exit" => return Ok(false),
+ _ if input.starts_with("/chat ") || input.starts_with("/hermes ") => {
+ let prompt = input
+ .trim_start_matches("/chat ")
+ .trim_start_matches("/hermes ")
+ .trim();
+ output::chat_turn(&hermes::ask(ctx, prompt)?);
+ }
+ _ if input.starts_with("/memory ") => {
+ let result = memory::search(ctx, input.trim_start_matches("/memory ").trim());
+ output::memory_search(&result);
+ }
+ _ if input.starts_with("/research ") => {
+ let lane = input.trim_start_matches("/research ").trim();
+ let snapshot = snapshot::build(ctx);
+ if let Some(packet) = research::packet_for_lane(lane, &snapshot.remote_gates) {
+ output::research_packet(&packet);
+ } else {
+ output::line("VERDICT: FLAG");
+ output::line(&format!("EVIDENCE: unknown research lane `{lane}`"));
+ output::line("NEXT: /research");
+ }
+ }
+ _ if input.starts_with('/') => {
+ output::line("VERDICT: FLAG");
+ output::line(&format!("EVIDENCE: unknown command `{input}`"));
+ output::line("NEXT: /help");
+ }
+ _ => output::chat_turn(&hermes::ask(ctx, input)?),
+ }
+ Ok(true)
+}
+
+fn run_verify_command(
+ ctx: &Context,
+ json: bool,
+ receipt_target: Option,
+ save: bool,
+) -> RavenResult<()> {
+ let result = verify::run_verify(ctx);
+ let receipt = receipt::from_verify(&result);
+
+ if json || receipt_target.as_deref() == Some("-") {
+ output::json(&receipt)?;
+ } else {
+ output::verify_human(&receipt);
+ }
+
+ if let Some(path) = receipt_target.as_deref().filter(|path| *path != "-") {
+ let written = receipt::save_receipt(ctx, &receipt, Some(path))?;
+ output::line(&format!("RECEIPT_WRITTEN: {}", written.display()));
+ }
+ if save {
+ let written = receipt::save_receipt(ctx, &receipt, None)?;
+ output::line(&format!("RECEIPT_SAVED: {}", written.display()));
+ }
+
+ if result.exit_code == 0 {
+ Ok(())
+ } else {
+ Err(format!("local verifier exited {}", result.exit_code).into())
+ }
+}
+
+fn doctor(ctx: &Context) -> DoctorReport {
+ let mut checks = Vec::new();
+ for (program, args) in [
+ ("rustc", vec!["--version"]),
+ ("cargo", vec!["--version"]),
+ ("just", vec!["--version"]),
+ ("bun", vec!["--version"]),
+ ("node", vec!["--version"]),
+ ("python3", vec!["--version"]),
+ ("multica", vec!["--version"]),
+ ] {
+ checks.push(command_check(program, &args));
+ }
+
+ for required in crate::constants::REQUIRED_DOCS {
+ let path = ctx.root.join(required);
+ checks.push(DoctorCheck {
+ name: format!("file {required}"),
+ verdict: if path.exists() {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ evidence: if path.exists() {
+ "present".to_string()
+ } else {
+ "missing".to_string()
+ },
+ });
+ }
+
+ checks.push(deepseek_auth_check(ctx));
+
+ let gitignore = ctx.root.join(".gitignore");
+ let ignored = std::fs::read_to_string(&gitignore)
+ .map(|text| text.contains("raven/.local-runs/"))
+ .unwrap_or(false);
+ checks.push(DoctorCheck {
+ name: "gitignore raven/.local-runs".to_string(),
+ verdict: if ignored {
+ Verdict::Pass
+ } else {
+ Verdict::Block
+ },
+ evidence: if ignored {
+ "saved receipts are gitignored".to_string()
+ } else {
+ "saved receipts are not gitignored".to_string()
+ },
+ });
+
+ let memory = memory::health(ctx);
+ checks.push(DoctorCheck {
+ name: "memory bridge health".to_string(),
+ verdict: memory.verdict,
+ evidence: memory.evidence,
+ });
+
+ let verdict = if checks.iter().any(|check| check.verdict == Verdict::Block) {
+ Verdict::Block
+ } else if checks.iter().any(|check| check.verdict == Verdict::Flag) {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ };
+
+ DoctorReport {
+ verdict,
+ checks,
+ next: "run raven run verify, raven gates, and raven native-audit before closeout."
+ .to_string(),
+ }
+}
+
+fn deepseek_auth_check(ctx: &Context) -> DoctorCheck {
+ let script = ctx.root.join("scripts/deepseek-auth-preflight.sh");
+ let env_file = ctx.root.join("deploy/nixos/evercore.env.example");
+ match Command::new(&script).arg("--env").arg(&env_file).output() {
+ Ok(output) if output.status.success() => {
+ let text = if output.stdout.is_empty() {
+ String::from_utf8_lossy(&output.stderr)
+ } else {
+ String::from_utf8_lossy(&output.stdout)
+ };
+ DoctorCheck {
+ name: "deepseek auth preflight".to_string(),
+ verdict: Verdict::Pass,
+ evidence: crate::sanitizer::sanitize_text(&crate::util::one_line(&text)),
+ }
+ }
+ Ok(output) => DoctorCheck {
+ name: "deepseek auth preflight".to_string(),
+ verdict: Verdict::Block,
+ evidence: format!("exited {}", output.status),
+ },
+ Err(err) => DoctorCheck {
+ name: "deepseek auth preflight".to_string(),
+ verdict: Verdict::Block,
+ evidence: err.to_string(),
+ },
+ }
+}
+
+fn command_check(program: &str, args: &[&str]) -> DoctorCheck {
+ match Command::new(program).args(args).output() {
+ Ok(output) if output.status.success() => {
+ let text = if output.stdout.is_empty() {
+ String::from_utf8_lossy(&output.stderr)
+ } else {
+ String::from_utf8_lossy(&output.stdout)
+ };
+ DoctorCheck {
+ name: program.to_string(),
+ verdict: Verdict::Pass,
+ evidence: crate::sanitizer::sanitize_text(&crate::util::one_line(&text)),
+ }
+ }
+ Ok(output) => DoctorCheck {
+ name: program.to_string(),
+ verdict: if program == "multica" {
+ Verdict::Flag
+ } else {
+ Verdict::Block
+ },
+ evidence: format!("exited {}", output.status),
+ },
+ Err(err) => DoctorCheck {
+ name: program.to_string(),
+ verdict: if program == "multica" {
+ Verdict::Flag
+ } else {
+ Verdict::Block
+ },
+ evidence: err.to_string(),
+ },
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/constants.rs b/use-cases/hermes-everos-memory/raven-console/src/constants.rs
new file mode 100644
index 00000000..0055df14
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/constants.rs
@@ -0,0 +1,28 @@
+pub const FIXTURE_PATH: &str = "raven/fixtures/doomsday-run.json";
+pub const RUNS_DIR: &str = "raven/.local-runs";
+
+pub const ISSUE_REMOTE_DEPLOY: &str = "DAS-2666";
+pub const ISSUE_AUTH_BLOCKER: &str = "DAS-2669";
+pub const ISSUE_CONTROL_ROOM: &str = "DAS-2670";
+pub const ISSUE_LOCAL_VERIFIER: &str = "DAS-2671";
+pub const ISSUE_MEMORY_WATCH: &str = "DAS-2672";
+pub const ISSUE_ADAPTER_REPAIR: &str = "DAS-2675";
+
+pub const WATCHLIST_ISSUES: &[&str] = &[
+ ISSUE_REMOTE_DEPLOY,
+ ISSUE_AUTH_BLOCKER,
+ ISSUE_CONTROL_ROOM,
+ ISSUE_LOCAL_VERIFIER,
+ ISSUE_MEMORY_WATCH,
+ ISSUE_ADAPTER_REPAIR,
+];
+
+pub const REQUIRED_DOCS: &[&str] = &[
+ "COMPLETION_AUDIT.md",
+ "OWNER_PACKET.md",
+ "SUPERVISOR_DISPATCH.md",
+ FIXTURE_PATH,
+ "raven/NATIVE_FEEL_AUDIT.md",
+ "bin/raven-run.mjs",
+ "bin/everos-memory.mjs",
+];
diff --git a/use-cases/hermes-everos-memory/raven-console/src/context.rs b/use-cases/hermes-everos-memory/raven-console/src/context.rs
new file mode 100644
index 00000000..ae8d649f
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/context.rs
@@ -0,0 +1,38 @@
+use crate::adapters::packet::read_packet;
+use crate::constants::FIXTURE_PATH;
+use crate::model::RunPacket;
+use crate::RavenResult;
+use std::env;
+use std::path::PathBuf;
+
+#[derive(Clone)]
+pub struct Context {
+ pub root: PathBuf,
+ pub packet: RunPacket,
+}
+
+impl Context {
+ pub fn load() -> RavenResult {
+ let root = find_case_root()?;
+ let packet = read_packet(&root.join(FIXTURE_PATH))?;
+ Ok(Self { root, packet })
+ }
+}
+
+fn find_case_root() -> RavenResult {
+ let cwd = env::current_dir()?;
+ for candidate in cwd.ancestors() {
+ let direct = candidate.join("COMPLETION_AUDIT.md");
+ let fixture = candidate.join(FIXTURE_PATH);
+ if direct.exists() && fixture.exists() {
+ return Ok(candidate.to_path_buf());
+ }
+
+ let nested = candidate.join("use-cases/hermes-everos-memory");
+ if nested.join("COMPLETION_AUDIT.md").exists() && nested.join(FIXTURE_PATH).exists() {
+ return Ok(nested);
+ }
+ }
+
+ Err("could not find use-cases/hermes-everos-memory root".into())
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/main.rs b/use-cases/hermes-everos-memory/raven-console/src/main.rs
new file mode 100644
index 00000000..c957b775
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/main.rs
@@ -0,0 +1,40 @@
+mod adapters;
+mod audit;
+mod commands;
+mod constants;
+mod context;
+mod model;
+mod output;
+mod receipt;
+mod repl;
+mod research;
+mod sanitizer;
+mod snapshot;
+mod tui;
+mod util;
+
+use clap::Parser;
+use commands::{execute, Cli};
+use context::Context;
+use std::process::ExitCode;
+
+pub type RavenResult = Result>;
+
+fn main() -> ExitCode {
+ let cli = Cli::parse();
+ let ctx = match Context::load() {
+ Ok(ctx) => ctx,
+ Err(err) => {
+ eprintln!("RAVEN_ERROR: {err}");
+ return ExitCode::from(1);
+ }
+ };
+
+ match execute(cli, &ctx) {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(err) => {
+ eprintln!("RAVEN_ERROR: {err}");
+ ExitCode::from(1)
+ }
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/model.rs b/use-cases/hermes-everos-memory/raven-console/src/model.rs
new file mode 100644
index 00000000..68399a44
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/model.rs
@@ -0,0 +1,354 @@
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::fmt;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "UPPERCASE")]
+pub enum Verdict {
+ Pass,
+ Flag,
+ Block,
+}
+
+impl Verdict {
+ pub fn from_packet_word(value: &str) -> Self {
+ let lower = value.to_ascii_lowercase();
+ if lower.contains("block") || lower.contains("failed") {
+ Self::Block
+ } else if lower.contains("pass")
+ || lower.contains("done")
+ || lower.contains("closed")
+ || lower.contains("complete")
+ {
+ Self::Pass
+ } else {
+ Self::Flag
+ }
+ }
+
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Pass => "PASS",
+ Self::Flag => "FLAG",
+ Self::Block => "BLOCK",
+ }
+ }
+}
+
+impl fmt::Display for Verdict {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct RunPacket {
+ pub id: String,
+ pub title: String,
+ pub goal: String,
+ pub status: String,
+ pub owners: Vec,
+ pub memory_providers: Vec,
+ pub lanes: Vec,
+ pub gates: Vec,
+ pub artifacts: Vec,
+ pub evidence_refs: Vec,
+ pub next_actions: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Lane {
+ pub id: String,
+ pub owner: String,
+ pub scope: String,
+ pub mutation_policy: String,
+ pub verdict: String,
+ #[serde(default)]
+ pub evidence_refs: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Gate {
+ pub id: String,
+ pub name: String,
+ pub status: String,
+ pub command: Option,
+ pub evidence: String,
+ pub blocks_completion: bool,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Artifact {
+ pub path: String,
+ pub purpose: String,
+ pub public_safe: bool,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct PacketSummary {
+ pub id: String,
+ pub title: String,
+ pub status: String,
+ pub verdict: Verdict,
+ pub owners: Vec,
+ pub memory_providers: Vec,
+ pub docs: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct DocSummary {
+ pub path: String,
+ pub verdict: Verdict,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct LocalGateView {
+ pub id: String,
+ pub name: String,
+ pub verdict: Verdict,
+ pub command: String,
+ pub evidence: String,
+ pub blocks_completion: bool,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct IssueView {
+ pub id: String,
+ pub title: String,
+ pub status: String,
+ pub priority: String,
+ pub updated_at: String,
+ pub available: bool,
+ pub source: String,
+ pub comments_checked: bool,
+ pub evidence_excerpt: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct RemoteGate {
+ pub id: String,
+ pub name: String,
+ pub verdict: Verdict,
+ pub blocks_completion: bool,
+ pub hard_gate: bool,
+ pub evidence: String,
+ pub gate_effect: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct AgentView {
+ pub name: String,
+ pub issue_id: String,
+ pub status: String,
+ pub verdict: Verdict,
+ pub scope: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct MemoryHealth {
+ pub verdict: Verdict,
+ pub status: String,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct MemorySearchResult {
+ pub query: String,
+ pub verdict: Verdict,
+ pub evidence: String,
+ pub result: Option,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct HermesChatTranscriptLine {
+ pub role: String,
+ pub content: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct HermesChatTurn {
+ pub prompt: String,
+ pub command: Vec,
+ pub workspace: String,
+ pub runtime: String,
+ pub verdict: Verdict,
+ pub exit_code: i32,
+ pub duration_ms: u128,
+ pub response: String,
+ pub evidence: String,
+ pub transcript: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct RunView {
+ pub id: String,
+ pub command: String,
+ pub verdict: Verdict,
+ pub source: String,
+ pub evidence: String,
+ pub receipt_path: Option,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScStatusView {
+ pub verdict: Verdict,
+ pub ok: bool,
+ pub api_version: Option,
+ pub app_version: String,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScProviderView {
+ pub provider_key: String,
+ pub display_name: String,
+ pub enabled: bool,
+ pub model_count: usize,
+ pub reasoning_efforts: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScSessionView {
+ pub thread_id: String,
+ pub provider_key: String,
+ pub title: String,
+ pub model: String,
+ pub reasoning_effort: String,
+ pub active_turn: bool,
+ pub closed: bool,
+ pub branch: String,
+ pub worktree: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScWorktreeView {
+ pub verdict: Verdict,
+ pub branch: String,
+ pub target_branch: String,
+ pub dirty: Option,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScReport {
+ pub verdict: Verdict,
+ pub status: ScStatusView,
+ pub providers: Vec,
+ pub sessions: Vec,
+ pub worktree: ScWorktreeView,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct PublicSafetyResult {
+ pub verdict: Verdict,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct RavenSnapshot {
+ pub verdict: Verdict,
+ pub packet: PacketSummary,
+ pub watchlist_issues: Vec,
+ pub local_gates: Vec,
+ pub remote_gates: Vec,
+ pub agents: Vec,
+ pub memory: MemoryHealth,
+ pub runs: Vec,
+ pub sc: ScReport,
+ pub risks: Vec,
+ pub next_actions: Vec,
+ pub public_safety: PublicSafetyResult,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct GateEffect {
+ pub gate_id: String,
+ pub before: String,
+ pub after: String,
+ pub note: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct RavenReceipt {
+ pub id: String,
+ pub command: Vec,
+ pub exit_code: i32,
+ pub duration_ms: u128,
+ pub verdict: Verdict,
+ pub evidence_excerpt: String,
+ pub gate_effects: Vec,
+ pub public_safety: PublicSafetyResult,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct DoctorCheck {
+ pub name: String,
+ pub verdict: Verdict,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct DoctorReport {
+ pub verdict: Verdict,
+ pub checks: Vec,
+ pub next: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct NativeAuditItem {
+ pub category: String,
+ pub verdict: Verdict,
+ pub evidence: String,
+ pub hard_failure: bool,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct NativeAuditReport {
+ pub verdict: Verdict,
+ pub items: Vec,
+ pub blocks_pass_on: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ResearchLane {
+ pub id: String,
+ pub title: String,
+ pub question: String,
+ pub targets: Vec,
+ pub output: Vec,
+ pub source_refs: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ResearchGateFact {
+ pub id: String,
+ pub verdict: Verdict,
+ pub evidence: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ResearchPacket {
+ pub lane_id: String,
+ pub lane_title: String,
+ pub question: String,
+ pub sources: Vec,
+ pub findings: Vec,
+ pub decisions: Vec,
+ pub v1_impact: Vec,
+ pub risks: Vec,
+ pub next: Vec,
+ pub live_gates: Vec,
+ pub verdict: Verdict,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ResearchSynthesis {
+ pub verdict: Verdict,
+ pub packets_ready: usize,
+ pub required_packets: usize,
+ pub evidence: String,
+ pub decisions: Vec,
+ pub risks: Vec,
+ pub next: Vec,
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/output.rs b/use-cases/hermes-everos-memory/raven-console/src/output.rs
new file mode 100644
index 00000000..bcfd6f07
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/output.rs
@@ -0,0 +1,442 @@
+use crate::model::{
+ DoctorReport, HermesChatTurn, MemorySearchResult, NativeAuditReport, RavenReceipt,
+ RavenSnapshot, ResearchLane, ResearchPacket, ResearchSynthesis, ScProviderView, ScReport,
+ ScSessionView, ScStatusView, ScWorktreeView, Verdict,
+};
+use crate::sanitizer::{sanitize_json, sanitize_text};
+use crate::util::one_line;
+use crate::RavenResult;
+use std::io::{self, Write};
+
+pub fn json(value: &T) -> RavenResult<()> {
+ let safe = sanitize_json(value)?;
+ println!("{}", serde_json::to_string_pretty(&safe)?);
+ Ok(())
+}
+
+pub fn status(snapshot: &RavenSnapshot) {
+ line("RAVEN_STATUS");
+ line(&format!("VERDICT: {}", snapshot.verdict));
+ line(&format!(
+ "LOCAL_PACKET: {} ({})",
+ snapshot.packet.verdict, snapshot.packet.title
+ ));
+ let remote = hard_remote_verdict(snapshot);
+ line(&format!(
+ "REMOTE_EVERCORE: {remote} (DAS-2666 and DAS-2669 are hard gates)"
+ ));
+ line(&format!(
+ "MEMORY: {} ({})",
+ snapshot.memory.verdict, snapshot.memory.status
+ ));
+ line("");
+ line("WATCHLIST:");
+ for issue in &snapshot.watchlist_issues {
+ line(&format!(
+ "- {}: {} [{}] source={} comments={}",
+ issue.id,
+ issue.title,
+ issue.status,
+ issue.source,
+ if issue.comments_checked {
+ "checked"
+ } else {
+ "not_checked"
+ }
+ ));
+ }
+ line("");
+ line("NEXT:");
+ for action in &snapshot.next_actions {
+ line(&format!("- {action}"));
+ }
+}
+
+pub fn packet(snapshot: &RavenSnapshot) {
+ line("RAVEN_PACKET");
+ line(&format!("VERDICT: {}", snapshot.packet.verdict));
+ line(&format!("ID: {}", snapshot.packet.id));
+ line(&format!("TITLE: {}", snapshot.packet.title));
+ line(&format!("STATUS: {}", snapshot.packet.status));
+ line(&format!("OWNERS: {}", snapshot.packet.owners.join(", ")));
+ line(&format!(
+ "MEMORY_PROVIDERS: {}",
+ snapshot.packet.memory_providers.join(", ")
+ ));
+ line("");
+ line("SOURCE_SUMMARIES:");
+ for doc in &snapshot.packet.docs {
+ line(&format!("- {}: {} {}", doc.path, doc.verdict, doc.evidence));
+ }
+}
+
+pub fn packet_export_markdown(snapshot: &RavenSnapshot) -> String {
+ let mut output = Vec::new();
+ output.push(format!("# {}", snapshot.packet.title));
+ output.push(String::new());
+ output.push(format!("VERDICT: {}", snapshot.verdict));
+ output.push(format!("Packet: {}", snapshot.packet.id));
+ output.push(format!("Status: {}", snapshot.packet.status));
+ output.push(String::new());
+ output.push("## Gates".to_string());
+ output.push(String::new());
+ for gate in &snapshot.remote_gates {
+ output.push(format!(
+ "- {} / {}: {} - {}",
+ gate.id, gate.name, gate.verdict, gate.evidence
+ ));
+ }
+ for gate in &snapshot.local_gates {
+ output.push(format!(
+ "- {} / {}: {} - {}",
+ gate.id, gate.name, gate.verdict, gate.evidence
+ ));
+ }
+ output.push(String::new());
+ output.push("## Next".to_string());
+ output.push(String::new());
+ for action in &snapshot.next_actions {
+ output.push(format!("- {action}"));
+ }
+ sanitize_text(&output.join("\n"))
+}
+
+pub fn gates(snapshot: &RavenSnapshot) {
+ line("RAVEN_GATES");
+ line(&format!("VERDICT: {}", hard_remote_verdict(snapshot)));
+ line("");
+ line("REMOTE_HARD_GATES:");
+ for gate in &snapshot.remote_gates {
+ line(&format!(
+ "- {} / {}: {} blocks={} hard={} evidence={} effect={}",
+ gate.id,
+ gate.name,
+ gate.verdict,
+ gate.blocks_completion,
+ gate.hard_gate,
+ gate.evidence,
+ gate.gate_effect
+ ));
+ }
+ line("");
+ line("LOCAL_PACKET_GATES:");
+ for gate in &snapshot.local_gates {
+ line(&format!(
+ "- {} / {}: {} blocks={} command={} evidence={}",
+ gate.id, gate.name, gate.verdict, gate.blocks_completion, gate.command, gate.evidence
+ ));
+ }
+ line("");
+ line("STOP_CONDITIONS:");
+ if snapshot
+ .remote_gates
+ .iter()
+ .any(|gate| gate.id == "DAS-2669" && gate.verdict == Verdict::Block)
+ {
+ line("- no AUTH_REPAIRED on DAS-2669");
+ }
+ line("- missing guarded NixOS test");
+ line("- missing remote loopback full smoke");
+ line("- missing supervisor PASS");
+ line("- public bind/firewall exposure or unredacted private evidence");
+}
+
+pub fn research_lanes(lanes: &[ResearchLane]) {
+ line("RAVEN_V2_RESEARCH_LANES");
+ line("VERDICT: FLAG");
+ line("EVIDENCE: lanes are bounded by RAVEN_V2_RESEARCH_LEDGER.md; packets must be live-gate calibrated.");
+ for lane in lanes {
+ line(&format!(
+ "- {} / {}: {}",
+ lane.id, lane.title, lane.question
+ ));
+ }
+ line("NEXT: raven research packet ");
+}
+
+pub fn research_packet(packet: &ResearchPacket) {
+ line("RAVEN_V2_RESEARCH_PACKET");
+ line(&format!("LANE: {} / {}", packet.lane_id, packet.lane_title));
+ line(&format!("VERDICT: {}", packet.verdict));
+ line(&format!("QUESTION: {}", packet.question));
+ line("FINDINGS:");
+ for finding in &packet.findings {
+ line(&format!("- {finding}"));
+ }
+ line("DECISIONS:");
+ for decision in &packet.decisions {
+ line(&format!("- {decision}"));
+ }
+ line("LIVE_GATES:");
+ for gate in &packet.live_gates {
+ line(&format!(
+ "- {}: {} {}",
+ gate.id, gate.verdict, gate.evidence
+ ));
+ }
+ line("NEXT:");
+ for action in &packet.next {
+ line(&format!("- {action}"));
+ }
+}
+
+pub fn research_synthesis(synthesis: &ResearchSynthesis) {
+ line("RAVEN_V2_SYNTHESIS_READINESS");
+ line(&format!("VERDICT: {}", synthesis.verdict));
+ line(&format!(
+ "PACKETS: {}/{}",
+ synthesis.packets_ready, synthesis.required_packets
+ ));
+ line(&format!("EVIDENCE: {}", synthesis.evidence));
+ line("NEXT:");
+ for action in &synthesis.next {
+ line(&format!("- {action}"));
+ }
+}
+
+pub fn agents(snapshot: &RavenSnapshot) {
+ line("RAVEN_AGENTS");
+ line("VERDICT: FLAG");
+ for agent in &snapshot.agents {
+ line(&format!(
+ "- {}: {} {} ({}) scope={}",
+ agent.name, agent.verdict, agent.status, agent.issue_id, agent.scope
+ ));
+ }
+}
+
+pub fn runs(snapshot: &RavenSnapshot) {
+ line("RAVEN_RUNS");
+ line(&format!(
+ "VERDICT: {}",
+ if snapshot
+ .runs
+ .iter()
+ .any(|run| run.verdict == Verdict::Block)
+ {
+ Verdict::Block
+ } else if snapshot.runs.iter().any(|run| run.verdict == Verdict::Flag) {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ }
+ ));
+ for run in &snapshot.runs {
+ let receipt = run
+ .receipt_path
+ .as_ref()
+ .map(|path| format!(" receipt={path}"))
+ .unwrap_or_default();
+ line(&format!(
+ "- {}: {} source={} command={}{} evidence={}",
+ run.id, run.verdict, run.source, run.command, receipt, run.evidence
+ ));
+ }
+}
+
+pub fn sc_report(report: &ScReport) {
+ line("RAVEN_SC");
+ line(&format!("VERDICT: {}", report.verdict));
+ line(&format!(
+ "STATUS: {} ok={} api={} app={} evidence={}",
+ report.status.verdict,
+ report.status.ok,
+ report
+ .status
+ .api_version
+ .map(|version| version.to_string())
+ .unwrap_or_else(|| "unknown".to_string()),
+ report.status.app_version,
+ report.status.evidence
+ ));
+ sc_worktree(&report.worktree);
+ sc_sessions(&report.sessions);
+ sc_providers(&report.providers);
+}
+
+pub fn sc_status(status: &ScStatusView) {
+ line("RAVEN_SC_STATUS");
+ line(&format!("VERDICT: {}", status.verdict));
+ line(&format!("OK: {}", status.ok));
+ line(&format!(
+ "API_VERSION: {}",
+ status
+ .api_version
+ .map(|version| version.to_string())
+ .unwrap_or_else(|| "unknown".to_string())
+ ));
+ line(&format!("APP_VERSION: {}", status.app_version));
+ line(&format!("EVIDENCE: {}", status.evidence));
+}
+
+pub fn sc_sessions(sessions: &[ScSessionView]) {
+ line("RAVEN_SC_SESSIONS");
+ line(&format!("COUNT: {}", sessions.len()));
+ for session in sessions.iter().take(12) {
+ line(&format!(
+ "- {} provider={} model={} reasoning={} active={} closed={} branch={} worktree={} title={}",
+ session.thread_id,
+ session.provider_key,
+ session.model,
+ session.reasoning_effort,
+ session.active_turn,
+ session.closed,
+ session.branch,
+ session.worktree,
+ session.title
+ ));
+ }
+}
+
+pub fn sc_providers(providers: &[ScProviderView]) {
+ line("RAVEN_SC_PROVIDERS");
+ line(&format!("COUNT: {}", providers.len()));
+ for provider in providers.iter().take(16) {
+ line(&format!(
+ "- {} enabled={} models={} reasoning={} display={}",
+ provider.provider_key,
+ provider.enabled,
+ provider.model_count,
+ provider.reasoning_efforts.join("/"),
+ provider.display_name
+ ));
+ }
+}
+
+pub fn sc_worktree(worktree: &ScWorktreeView) {
+ line("RAVEN_SC_WORKTREE");
+ line(&format!("VERDICT: {}", worktree.verdict));
+ line(&format!("BRANCH: {}", worktree.branch));
+ line(&format!("TARGET_BRANCH: {}", worktree.target_branch));
+ line(&format!(
+ "DIRTY: {}",
+ worktree
+ .dirty
+ .map(|dirty| dirty.to_string())
+ .unwrap_or_else(|| "unknown".to_string())
+ ));
+ line(&format!("EVIDENCE: {}", worktree.evidence));
+}
+
+pub fn memory_health(snapshot: &RavenSnapshot) {
+ line("RAVEN_MEMORY_HEALTH");
+ line(&format!("VERDICT: {}", snapshot.memory.verdict));
+ line(&format!("STATUS: {}", snapshot.memory.status));
+ line(&format!("EVIDENCE: {}", snapshot.memory.evidence));
+}
+
+pub fn memory_search(result: &MemorySearchResult) {
+ line("RAVEN_MEMORY_SEARCH");
+ line(&format!("VERDICT: {}", result.verdict));
+ line(&format!("QUERY: {}", result.query));
+ line(&format!("EVIDENCE: {}", result.evidence));
+}
+
+pub fn chat_turn(turn: &HermesChatTurn) {
+ line("RAVEN_CHAT");
+ line(&format!("VERDICT: {}", turn.verdict));
+ line(&format!("EXIT_CODE: {}", turn.exit_code));
+ line(&format!("DURATION_MS: {}", turn.duration_ms));
+ line(&format!("RUNTIME: {}", turn.runtime));
+ line(&format!("WORKSPACE: {}", turn.workspace));
+ line(&format!("EVIDENCE: {}", turn.evidence));
+ line("ASSISTANT:");
+ for raw in turn.response.lines().take(80) {
+ println!("{}", sanitize_text(raw));
+ }
+}
+
+pub fn verify_human(receipt: &RavenReceipt) {
+ line("RAVEN_RUN_VERIFY");
+ line(&format!("VERDICT: {}", receipt.verdict));
+ line(&format!("EXIT_CODE: {}", receipt.exit_code));
+ line(&format!("DURATION_MS: {}", receipt.duration_ms));
+ line(&format!("EVIDENCE: {}", receipt.evidence_excerpt));
+ line("GATE_EFFECTS:");
+ for effect in &receipt.gate_effects {
+ line(&format!(
+ "- {}: {} -> {} ({})",
+ effect.gate_id, effect.before, effect.after, effect.note
+ ));
+ }
+ line(&format!(
+ "PUBLIC_SAFETY: {} {}",
+ receipt.public_safety.verdict, receipt.public_safety.evidence
+ ));
+}
+
+pub fn doctor(report: &DoctorReport) {
+ line("RAVEN_DOCTOR");
+ line(&format!("VERDICT: {}", report.verdict));
+ for check in &report.checks {
+ line(&format!(
+ "- {}: {} {}",
+ check.name, check.verdict, check.evidence
+ ));
+ }
+ line(&format!("NEXT: {}", report.next));
+}
+
+pub fn native_audit(report: &NativeAuditReport) {
+ line("RAVEN_NATIVE_AUDIT");
+ line(&format!("VERDICT: {}", report.verdict));
+ for item in &report.items {
+ line(&format!(
+ "- {}: {} hard_failure={} evidence={}",
+ item.category, item.verdict, item.hard_failure, item.evidence
+ ));
+ }
+ line(&format!(
+ "BLOCKS_PASS_ON: {}",
+ report.blocks_pass_on.join(", ")
+ ));
+}
+
+pub fn write_text(target: Option<&str>, text: &str) -> RavenResult<()> {
+ match target {
+ Some("-") | None => {
+ println!("{}", sanitize_text(text).trim_end());
+ Ok(())
+ }
+ Some(path) => {
+ std::fs::write(path, format!("{}\n", sanitize_text(text).trim_end()))?;
+ line(&format!("WROTE: {path}"));
+ Ok(())
+ }
+ }
+}
+
+pub fn flush_stdout() -> RavenResult<()> {
+ io::stdout().flush()?;
+ Ok(())
+}
+
+pub fn line(text: &str) {
+ println!("{}", sanitize_text(&one_line_preserving_blank(text)));
+}
+
+fn hard_remote_verdict(snapshot: &RavenSnapshot) -> Verdict {
+ if snapshot
+ .remote_gates
+ .iter()
+ .any(|gate| gate.hard_gate && gate.verdict == Verdict::Block)
+ {
+ Verdict::Block
+ } else if snapshot
+ .remote_gates
+ .iter()
+ .any(|gate| gate.verdict == Verdict::Flag)
+ {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ }
+}
+
+fn one_line_preserving_blank(text: &str) -> String {
+ if text.is_empty() {
+ String::new()
+ } else {
+ one_line(text)
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/receipt.rs b/use-cases/hermes-everos-memory/raven-console/src/receipt.rs
new file mode 100644
index 00000000..43282ea0
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/receipt.rs
@@ -0,0 +1,167 @@
+use crate::adapters::verify::VerifyResult;
+use crate::constants::{ISSUE_ADAPTER_REPAIR, ISSUE_REMOTE_DEPLOY, RUNS_DIR};
+use crate::context::Context;
+use crate::model::{GateEffect, HermesChatTurn, PublicSafetyResult, RavenReceipt, Verdict};
+use crate::sanitizer::{public_safety_verdict, sanitize_text};
+use crate::util::{one_line, run_id, truncate};
+use crate::RavenResult;
+use std::fs;
+use std::path::PathBuf;
+
+pub fn from_verify(result: &VerifyResult) -> RavenReceipt {
+ let evidence = sanitize_text(&truncate(
+ &one_line(&format!("{} {}", result.stdout, result.stderr)),
+ 900,
+ ));
+ let safety = if public_safety_verdict(&evidence) {
+ PublicSafetyResult {
+ verdict: Verdict::Pass,
+ evidence: "receipt evidence excerpt is sanitized.".to_string(),
+ }
+ } else {
+ PublicSafetyResult {
+ verdict: Verdict::Block,
+ evidence: "receipt evidence still contains sensitive-looking material.".to_string(),
+ }
+ };
+
+ RavenReceipt {
+ id: run_id("raven-verify"),
+ command: result.command.clone(),
+ exit_code: result.exit_code,
+ duration_ms: result.duration_ms,
+ verdict: result.verdict,
+ evidence_excerpt: evidence,
+ gate_effects: vec![
+ GateEffect {
+ gate_id: "local-packet".to_string(),
+ before: "configured".to_string(),
+ after: result.verdict.to_string(),
+ note: "Local Raven packet verifier executed through bin/raven-run.mjs.".to_string(),
+ },
+ GateEffect {
+ gate_id: ISSUE_REMOTE_DEPLOY.to_string(),
+ before: "BLOCK unless live remote evidence proves every hard gate".to_string(),
+ after: "unchanged".to_string(),
+ note: "run verify is local-only and cannot green remote deploy.".to_string(),
+ },
+ GateEffect {
+ gate_id: ISSUE_ADAPTER_REPAIR.to_string(),
+ before: "watch".to_string(),
+ after: "unchanged".to_string(),
+ note: "adapter repair evidence has no effect on DAS-2666.".to_string(),
+ },
+ ],
+ public_safety: safety,
+ }
+}
+
+pub fn from_chat(turn: &HermesChatTurn) -> RavenReceipt {
+ let evidence = sanitize_text(&truncate(
+ &one_line(&format!(
+ "runtime={} cwd={} evidence={} response={}",
+ turn.runtime, turn.workspace, turn.evidence, turn.response
+ )),
+ 900,
+ ));
+ let safety = if public_safety_verdict(&evidence)
+ && public_safety_verdict(&turn.prompt)
+ && public_safety_verdict(&turn.response)
+ {
+ PublicSafetyResult {
+ verdict: Verdict::Pass,
+ evidence: "chat prompt, response, and evidence are sanitized.".to_string(),
+ }
+ } else {
+ PublicSafetyResult {
+ verdict: Verdict::Block,
+ evidence: "chat transcript still contains sensitive-looking material.".to_string(),
+ }
+ };
+
+ RavenReceipt {
+ id: run_id("raven-chat"),
+ command: turn.command.clone(),
+ exit_code: turn.exit_code,
+ duration_ms: turn.duration_ms,
+ verdict: turn.verdict,
+ evidence_excerpt: evidence,
+ gate_effects: vec![
+ GateEffect {
+ gate_id: "hermes-chat".to_string(),
+ before: "requested".to_string(),
+ after: turn.verdict.to_string(),
+ note: "Hermes dialogue executed through the shared Raven adapter.".to_string(),
+ },
+ GateEffect {
+ gate_id: ISSUE_REMOTE_DEPLOY.to_string(),
+ before: "BLOCK unless live remote evidence proves every hard gate".to_string(),
+ after: "unchanged".to_string(),
+ note: "chat receipts cannot green remote deploy.".to_string(),
+ },
+ ],
+ public_safety: safety,
+ }
+}
+
+pub fn save_receipt(
+ ctx: &Context,
+ receipt: &RavenReceipt,
+ target: Option<&str>,
+) -> RavenResult {
+ let path = match target {
+ Some(path) => PathBuf::from(path),
+ None => ctx.root.join(RUNS_DIR).join(format!("{}.json", receipt.id)),
+ };
+
+ let absolute = if path.is_absolute() {
+ path
+ } else {
+ ctx.root.join(path)
+ };
+
+ if let Some(parent) = absolute.parent() {
+ fs::create_dir_all(parent)?;
+ }
+ let text = serde_json::to_string_pretty(receipt)?;
+ fs::write(&absolute, format!("{text}\n"))?;
+ Ok(absolute)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::model::{HermesChatTranscriptLine, HermesChatTurn};
+
+ #[test]
+ fn chat_receipt_preserves_remote_deploy_boundary() {
+ let turn = HermesChatTurn {
+ prompt: "status".to_string(),
+ command: vec![
+ "hermes".to_string(),
+ "-z".to_string(),
+ "[raven-prompt]".to_string(),
+ ],
+ workspace: "case-root".to_string(),
+ runtime: "codex_app_server".to_string(),
+ verdict: Verdict::Pass,
+ exit_code: 0,
+ duration_ms: 11,
+ response: "ready".to_string(),
+ evidence: "Hermes oneshot completed".to_string(),
+ transcript: vec![HermesChatTranscriptLine {
+ role: "assistant".to_string(),
+ content: "ready".to_string(),
+ }],
+ };
+
+ let receipt = from_chat(&turn);
+
+ assert_eq!(receipt.verdict, Verdict::Pass);
+ assert!(receipt
+ .gate_effects
+ .iter()
+ .any(|effect| effect.gate_id == ISSUE_REMOTE_DEPLOY && effect.after == "unchanged"));
+ assert_eq!(receipt.public_safety.verdict, Verdict::Pass);
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/repl.rs b/use-cases/hermes-everos-memory/raven-console/src/repl.rs
new file mode 100644
index 00000000..9831546e
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/repl.rs
@@ -0,0 +1,50 @@
+use crate::commands;
+use crate::context::Context;
+use crate::output;
+use crate::RavenResult;
+use rustyline::error::ReadlineError;
+use rustyline::DefaultEditor;
+use std::io::{self, BufRead, IsTerminal};
+
+pub fn run(ctx: &Context) -> RavenResult<()> {
+ println!("Raven REPL. Type /help or /quit.");
+ if !io::stdin().is_terminal() {
+ let stdin = io::stdin();
+ for line in stdin.lock().lines() {
+ let input = line?;
+ let input = input.trim();
+ if input.is_empty() {
+ continue;
+ }
+ if !commands::dispatch_repl(ctx, input)? {
+ break;
+ }
+ }
+ return Ok(());
+ }
+
+ let mut editor = DefaultEditor::new()?;
+ loop {
+ match editor.readline("raven> ") {
+ Ok(line) => {
+ let input = line.trim();
+ if input.is_empty() {
+ continue;
+ }
+ let _ = editor.add_history_entry(input);
+ if !commands::dispatch_repl(ctx, input)? {
+ break;
+ }
+ }
+ Err(ReadlineError::Interrupted) => {
+ println!("INTERRUPT");
+ break;
+ }
+ Err(ReadlineError::Eof) => break,
+ Err(err) => return Err(err.into()),
+ }
+ output::flush_stdout()?;
+ }
+
+ Ok(())
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/research.rs b/use-cases/hermes-everos-memory/raven-console/src/research.rs
new file mode 100644
index 00000000..47af650a
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/research.rs
@@ -0,0 +1,440 @@
+use crate::model::{
+ RemoteGate, ResearchGateFact, ResearchLane, ResearchPacket, ResearchSynthesis, Verdict,
+};
+
+const REQUIRED_SYNTHESIS_PACKETS: usize = 3;
+
+struct LaneSpec {
+ id: &'static str,
+ title: &'static str,
+ question: &'static str,
+ targets: &'static [&'static str],
+ output: &'static [&'static str],
+ source_refs: &'static [&'static str],
+ findings: &'static [&'static str],
+ decisions: &'static [&'static str],
+ v1_impact: &'static [&'static str],
+ risks: &'static [&'static str],
+ next: &'static [&'static str],
+}
+
+pub fn list_lanes() -> Vec {
+ lane_specs().iter().map(|lane| lane.view()).collect()
+}
+
+pub fn packet_for_lane(lane_id: &str, remote_gates: &[RemoteGate]) -> Option {
+ let lane = lane_specs().into_iter().find(|lane| lane.id == lane_id)?;
+ let live_gates = live_gate_facts(remote_gates);
+ let mut risks = strings(lane.risks);
+ for gate in &live_gates {
+ if gate.verdict == Verdict::Block {
+ risks.push(format!(
+ "{} remains BLOCK in live gate evidence; v2 research cannot promote remote readiness.",
+ gate.id
+ ));
+ }
+ }
+
+ let verdict = if live_gates.iter().any(|gate| gate.verdict == Verdict::Block) {
+ Verdict::Flag
+ } else {
+ Verdict::Pass
+ };
+
+ let mut next = strings(lane.next);
+ next.push(
+ "Turn this lane into a decision packet before any v1 implementation change.".to_string(),
+ );
+ next.push(format!(
+ "Do not synthesize Raven v2 architecture until at least {REQUIRED_SYNTHESIS_PACKETS} evidence-backed packets exist."
+ ));
+
+ Some(ResearchPacket {
+ lane_id: lane.id.to_string(),
+ lane_title: lane.title.to_string(),
+ question: lane.question.to_string(),
+ sources: strings(lane.source_refs),
+ findings: strings(lane.findings),
+ decisions: strings(lane.decisions),
+ v1_impact: strings(lane.v1_impact),
+ risks,
+ next,
+ live_gates,
+ verdict,
+ })
+}
+
+pub fn synthesis_readiness(packets: &[ResearchPacket]) -> ResearchSynthesis {
+ let packets_ready = packets.len();
+ let ready = packets_ready >= REQUIRED_SYNTHESIS_PACKETS;
+ ResearchSynthesis {
+ verdict: if ready { Verdict::Pass } else { Verdict::Flag },
+ packets_ready,
+ required_packets: REQUIRED_SYNTHESIS_PACKETS,
+ evidence: if ready {
+ format!(
+ "{packets_ready} evidence-backed packets are available for architecture synthesis."
+ )
+ } else {
+ format!(
+ "{packets_ready}/{REQUIRED_SYNTHESIS_PACKETS} evidence-backed packets available."
+ )
+ },
+ decisions: if ready {
+ vec![
+ "Architecture synthesis may start, but it must still preserve live gate verdicts."
+ .to_string(),
+ ]
+ } else {
+ vec![
+ "Hold RAVEN_V2_ARCHITECTURE_PACKET.md until research evidence reaches quorum."
+ .to_string(),
+ ]
+ },
+ risks: vec![
+ "Research synthesis without packet quorum becomes prose drift.".to_string(),
+ "Remote deploy truth remains owned by DAS-2666/DAS-2669, not the research lane."
+ .to_string(),
+ ],
+ next: if ready {
+ vec![
+ "Open a bounded architecture synthesis task using the completed packets."
+ .to_string(),
+ ]
+ } else {
+ vec![format!(
+ "Collect at least three evidence-backed packets before synthesis ({packets_ready}/{REQUIRED_SYNTHESIS_PACKETS} ready)."
+ )]
+ },
+ }
+}
+
+pub fn packet_markdown(packet: &ResearchPacket) -> String {
+ let mut out = Vec::new();
+ out.push("RAVEN_V2_RESEARCH_PACKET".to_string());
+ out.push(format!("LANE: {} / {}", packet.lane_id, packet.lane_title));
+ out.push(format!("QUESTION: {}", packet.question));
+ push_list(&mut out, "SOURCES", &packet.sources);
+ push_list(&mut out, "FINDINGS", &packet.findings);
+ push_list(&mut out, "DECISIONS", &packet.decisions);
+ push_list(&mut out, "V1_IMPACT", &packet.v1_impact);
+ push_list(&mut out, "RISKS", &packet.risks);
+ push_list(&mut out, "NEXT", &packet.next);
+ out.push("LIVE_GATES:".to_string());
+ for gate in &packet.live_gates {
+ out.push(format!("- {}: {} {}", gate.id, gate.verdict, gate.evidence));
+ }
+ out.push(format!("VERDICT: {}", packet.verdict));
+ out.join("\n")
+}
+
+pub fn synthesis_markdown(synthesis: &ResearchSynthesis) -> String {
+ let mut out = Vec::new();
+ out.push("RAVEN_V2_SYNTHESIS_READINESS".to_string());
+ out.push(format!("VERDICT: {}", synthesis.verdict));
+ out.push(format!(
+ "PACKETS: {}/{}",
+ synthesis.packets_ready, synthesis.required_packets
+ ));
+ out.push(format!("EVIDENCE: {}", synthesis.evidence));
+ push_list(&mut out, "DECISIONS", &synthesis.decisions);
+ push_list(&mut out, "RISKS", &synthesis.risks);
+ push_list(&mut out, "NEXT", &synthesis.next);
+ out.join("\n")
+}
+
+fn push_list(out: &mut Vec, title: &str, values: &[String]) {
+ out.push(format!("{title}:"));
+ for value in values {
+ out.push(format!("- {value}"));
+ }
+}
+
+impl LaneSpec {
+ fn view(&self) -> ResearchLane {
+ ResearchLane {
+ id: self.id.to_string(),
+ title: self.title.to_string(),
+ question: self.question.to_string(),
+ targets: strings(self.targets),
+ output: strings(self.output),
+ source_refs: strings(self.source_refs),
+ }
+ }
+}
+
+fn live_gate_facts(remote_gates: &[RemoteGate]) -> Vec {
+ remote_gates
+ .iter()
+ .filter(|gate| gate.hard_gate || matches!(gate.id.as_str(), "DAS-2666" | "DAS-2669"))
+ .map(|gate| ResearchGateFact {
+ id: gate.id.clone(),
+ verdict: gate.verdict,
+ evidence: gate.evidence.clone(),
+ })
+ .collect()
+}
+
+fn strings(values: &[&str]) -> Vec {
+ values.iter().map(|value| (*value).to_string()).collect()
+}
+
+fn lane_specs() -> Vec {
+ vec![
+ LaneSpec {
+ id: "native-feel",
+ title: "Native-Feel TUI/REPL",
+ question: "What makes Raven feel like a native terminal OS surface rather than a webby text box?",
+ targets: &[
+ "latency budget",
+ "keyboard grammar",
+ "focus and pane stability",
+ "interrupt/resume semantics",
+ "scrollback and transcript model",
+ ],
+ output: &[
+ "interaction contract",
+ "v2 command grammar",
+ "native-feel audit adapted for terminal agents",
+ ],
+ source_refs: &[
+ "raven/RAVEN_V2_RESEARCH_LEDGER.md#lane-1-native-feel-tuirepl",
+ "raven/NATIVE_FEEL_AUDIT.md",
+ "raven/COMMAND_CONTRACT.md#hermes-chat-behavior",
+ ],
+ findings: &[
+ "Raven v1 already separates fast boot rendering from live refresh, which is the right latency shape for v2.",
+ "The CCR-level target is a stable REPL/TUI split: shell, slash REPL, and TUI chat must share one command grammar.",
+ "Native feel depends on interrupt behavior and pane stability as much as visual density.",
+ ],
+ decisions: &[
+ "Keep Rust ratatui for v1; research richer v2 terminal runtimes only through decision packets.",
+ "Treat chat transcript, gate evidence, and command output as typed state, not painted strings.",
+ ],
+ v1_impact: &[
+ "Add harness checks before changing TUI layout again.",
+ "Keep Hermes chat as shared adapter across CLI, REPL, and TUI.",
+ ],
+ risks: &[
+ "A prettier TUI can hide stale gate truth if refresh and evidence panes are not explicit.",
+ ],
+ next: &[
+ "Measure cold boot, first paint, and chat submit latency with deterministic smoke output.",
+ ],
+ },
+ LaneSpec {
+ id: "runtime-dna",
+ title: "Runtime DNA Alignment",
+ question: "How do CCB/CCR/Evensong concepts flow into Raven without turning Raven into a fork dump?",
+ targets: &[
+ "CLI loop",
+ "REPL state machine",
+ "tool approval model",
+ "ACP/control-plane concepts",
+ "telemetry and receipts",
+ ],
+ output: &[
+ "lineage map",
+ "implementation boundaries",
+ "Raven-owned versus Hermes/MUW-owned responsibilities",
+ ],
+ source_refs: &[
+ "raven/RAVEN_V2_RESEARCH_LEDGER.md#lane-2-runtime-dna-alignment",
+ "raven/COMMAND_CONTRACT.md#shape",
+ "OWNER_PACKET.md",
+ ],
+ findings: &[
+ "Raven owns operator-visible truth state; Hermes owns provider dialogue; MUW owns live issue gates.",
+ "Receipts are the bridge between interactive UX and reviewable evidence.",
+ ],
+ decisions: &[
+ "Do not vendor external runtime code into v1.",
+ "Represent borrowed runtime ideas as boundaries and tests before implementation.",
+ ],
+ v1_impact: &[
+ "Keep adapters thin and read-only unless a command explicitly writes a receipt.",
+ ],
+ risks: &[
+ "Fork-dump research would widen scope and make v1 harder to verify.",
+ ],
+ next: &[
+ "Create a lineage map packet that marks keep/revise/defer/reject per runtime idea.",
+ ],
+ },
+ LaneSpec {
+ id: "memory-skill",
+ title: "Memory And Skill Substrate",
+ question: "How should Raven make memory, skills, and goals first-class without becoming a noisy memory browser?",
+ targets: &[
+ "EverOS memory search/store/status",
+ "Hermes skills and profiles",
+ "persistent goals",
+ "provenance fields",
+ "memory hit explanations",
+ ],
+ output: &[
+ "memory pane contract",
+ "skill registry contract",
+ "goal/gate model",
+ ],
+ source_refs: &[
+ "raven/RAVEN_V2_RESEARCH_LEDGER.md#lane-3-memory-and-skill-substrate",
+ "skillhub/MVP_IMPLEMENTATION_PLAN.md",
+ "skillhub/schema.json",
+ "DAS-2672",
+ ],
+ findings: &[
+ "DAS-2672 already separated production-ready local facts from needs_eval SkillHub items.",
+ "Skill promotion needs eval evidence; packet existence alone is not a skill-quality claim.",
+ ],
+ decisions: &[
+ "Build eval harness before adding richer SkillHub fields.",
+ "Keep memory provider failure as FLAG, not a console crash.",
+ ],
+ v1_impact: &[
+ "Use v1 research packets to select the next SkillHub implementation issue.",
+ ],
+ risks: &[
+ "Memory browsing without provenance can look powerful while weakening trust.",
+ ],
+ next: &[
+ "Draft the SkillHub eval harness packet before mutating skill fixtures.",
+ ],
+ },
+ LaneSpec {
+ id: "orchestration",
+ title: "Multi-Agent Orchestration",
+ question: "What is the operator model when many agents are building, reviewing, and researching at once?",
+ targets: &[
+ "MUW issue states",
+ "bounded fanout",
+ "subagent context isolation",
+ "task delegation and review packets",
+ "red-gate routing",
+ ],
+ output: &[
+ "control-room state model",
+ "dispatch grammar",
+ "review lane protocol",
+ ],
+ source_refs: &[
+ "raven/RAVEN_V2_RESEARCH_LEDGER.md#lane-4-multi-agent-orchestration",
+ "SUPERVISOR_DISPATCH.md",
+ "DAS-2670",
+ "DAS-2666",
+ "DAS-2669",
+ ],
+ findings: &[
+ "Current control room truth is issue-led: local PASS and remote BLOCK must remain separate.",
+ "Raven should route work by gate state before spawning or assigning broader fanout.",
+ ],
+ decisions: &[
+ "Remote deploy stays owned by DAS-2666; auth repair stays owned by DAS-2669.",
+ "Adapter repair lanes cannot change remote deploy verdicts.",
+ ],
+ v1_impact: &[
+ "Research commands should display live MUW blockers before next implementation suggestions.",
+ ],
+ risks: &[
+ "Without live issue calibration, v2 planning can launder stale local PASS into remote readiness.",
+ ],
+ next: &[
+ "Define dispatch grammar for assigning research packets without opening mutation lanes prematurely.",
+ ],
+ },
+ LaneSpec {
+ id: "evaluation-safety",
+ title: "Evaluation And Safety",
+ question: "How do we know Raven is making the system more legible rather than only faster?",
+ targets: &[
+ "audit trails",
+ "failure records",
+ "public-safety scan",
+ "secret/host/IP redaction",
+ "benchmark receipt ingestion",
+ "truth-state transitions",
+ ],
+ output: &[
+ "Raven v2 success metrics",
+ "red-gate invariants",
+ "public-safe artifact checklist",
+ ],
+ source_refs: &[
+ "raven/RAVEN_V2_RESEARCH_LEDGER.md#lane-5-evaluation-and-safety",
+ "raven/NATIVE_FEEL_AUDIT.md",
+ "raven/COMMAND_CONTRACT.md#gate-semantics",
+ ],
+ findings: &[
+ "Raven already sanitizes JSON/text output; v2 needs receipt-level proof that redaction remains intact.",
+ "Success metrics must include truth-state preservation, not just command latency.",
+ ],
+ decisions: &[
+ "Public-safety failures block PASS for native audit and research packet promotion.",
+ "Architecture synthesis must preserve hard red gates in its first page.",
+ ],
+ v1_impact: &[
+ "Add research packet smoke tests to prevent prose-only v2 output.",
+ ],
+ risks: &[
+ "Safety claims are easy to overstate if screenshots or markdown include raw operational details.",
+ ],
+ next: &[
+ "Add a public-safety scan target for research packet exports.",
+ ],
+ },
+ ]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{list_lanes, packet_for_lane, synthesis_readiness};
+ use crate::model::{RemoteGate, Verdict};
+
+ fn blocked_auth_gate() -> Vec {
+ vec![RemoteGate {
+ id: "DAS-2669".to_string(),
+ name: "runtime auth".to_string(),
+ verdict: Verdict::Block,
+ blocks_completion: true,
+ hard_gate: true,
+ evidence: "AUTH_REPAIRED missing in live issue/comment evidence".to_string(),
+ gate_effect: "blocks remote deploy readiness".to_string(),
+ }]
+ }
+
+ #[test]
+ fn lists_five_research_lanes() {
+ let lanes = list_lanes();
+
+ assert_eq!(lanes.len(), 5);
+ assert_eq!(lanes[0].id, "native-feel");
+ assert!(lanes.iter().any(|lane| lane.id == "evaluation-safety"));
+ }
+
+ #[test]
+ fn packet_preserves_live_remote_blockers() {
+ let packet = packet_for_lane("native-feel", &blocked_auth_gate()).unwrap();
+
+ assert_eq!(packet.verdict, Verdict::Flag);
+ assert_eq!(packet.lane_id, "native-feel");
+ assert!(packet
+ .risks
+ .iter()
+ .any(|risk| risk.contains("DAS-2669") && risk.contains("BLOCK")));
+ assert!(packet
+ .next
+ .iter()
+ .any(|action| action.contains("decision packet")));
+ }
+
+ #[test]
+ fn synthesis_stays_flag_until_three_packets() {
+ let synthesis = synthesis_readiness(&[]);
+
+ assert_eq!(synthesis.verdict, Verdict::Flag);
+ assert!(synthesis
+ .next
+ .iter()
+ .any(|action| action.contains("three evidence-backed packets")));
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs b/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs
new file mode 100644
index 00000000..34a35da1
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs
@@ -0,0 +1,213 @@
+use regex::{Captures, Regex};
+use serde::Serialize;
+use serde_json::Value;
+use std::sync::OnceLock;
+
+static SIGNED_URL_RE: OnceLock = OnceLock::new();
+static TOKEN_RE: OnceLock = OnceLock::new();
+static SECRET_ASSIGNMENT_RE: OnceLock = OnceLock::new();
+static CREDENTIAL_PATH_RE: OnceLock = OnceLock::new();
+static LOCAL_PATH_RE: OnceLock = OnceLock::new();
+static LOCALHOST_RE: OnceLock = OnceLock::new();
+static IPV4_RE: OnceLock = OnceLock::new();
+static PRODUCT_NAME_RE: OnceLock = OnceLock::new();
+const LEGACY_PRODUCT_NAME: &str = concat!("Ri", "ven");
+
+pub fn sanitize_text(input: &str) -> String {
+ let mut output = input.to_string();
+
+ output = signed_url_re()
+ .replace_all(&output, "[redacted-signed-url]")
+ .to_string();
+ output = secret_assignment_re()
+ .replace_all(&output, "$1=[redacted-secret]")
+ .to_string();
+ output = token_re()
+ .replace_all(&output, "[redacted-token]")
+ .to_string();
+ output = credential_path_re()
+ .replace_all(&output, "$1[redacted-credential-path]")
+ .to_string();
+ output = local_path_re()
+ .replace_all(&output, "$1[redacted-path]")
+ .to_string();
+ output = localhost_re()
+ .replace_all(&output, "[redacted-host]")
+ .to_string();
+ output = ipv4_re()
+ .replace_all(&output, |captures: &Captures<'_>| {
+ let value = captures
+ .get(0)
+ .map(|item| item.as_str())
+ .unwrap_or_default();
+ if is_private_or_public_ipv4(value) {
+ "[redacted-ip]".to_string()
+ } else {
+ value.to_string()
+ }
+ })
+ .to_string();
+ output = product_name_re().replace_all(&output, "Raven").to_string();
+
+ output
+}
+
+pub fn sanitize_json(value: &T) -> crate::RavenResult {
+ let value = serde_json::to_value(value)?;
+ Ok(sanitize_value(value))
+}
+
+pub fn sanitize_value(value: Value) -> Value {
+ match value {
+ Value::String(text) => Value::String(sanitize_text(&text)),
+ Value::Array(items) => Value::Array(items.into_iter().map(sanitize_value).collect()),
+ Value::Object(map) => Value::Object(
+ map.into_iter()
+ .map(|(key, value)| (key, sanitize_value(value)))
+ .collect(),
+ ),
+ other => other,
+ }
+}
+
+pub fn public_safety_verdict(text: &str) -> bool {
+ let sanitized = sanitize_text(text);
+ sanitized == text || !contains_sensitive_shape(&sanitized)
+}
+
+fn contains_sensitive_shape(text: &str) -> bool {
+ signed_url_re().is_match(text)
+ || token_re().is_match(text)
+ || credential_path_re().is_match(text)
+ || local_path_re().is_match(text)
+ || localhost_re().is_match(text)
+ || ipv4_re()
+ .find_iter(text)
+ .any(|match_| is_private_or_public_ipv4(match_.as_str()))
+}
+
+fn signed_url_re() -> &'static Regex {
+ SIGNED_URL_RE.get_or_init(|| {
+ Regex::new(r#"https?://\S*(?:Signature=|X-Amz-Signature=|X-Amz-Credential=|Policy=|Key-Pair-Id=)\S*"#)
+ .expect("valid signed URL regex")
+ })
+}
+
+fn token_re() -> &'static Regex {
+ TOKEN_RE.get_or_init(|| {
+ Regex::new(r#"(?i)\b(?:sk|sk-proj|sk-ant|ghp|github_pat|xoxb|xoxp|hf)_[A-Za-z0-9_-]{16,}\b|(?i)\b(?:sk|sk-proj|sk-ant|ghp|github_pat|xoxb|xoxp|hf)-[A-Za-z0-9_-]{16,}\b"#)
+ .expect("valid token regex")
+ })
+}
+
+fn secret_assignment_re() -> &'static Regex {
+ SECRET_ASSIGNMENT_RE.get_or_init(|| {
+ Regex::new(r#"(?i)\b(api[_-]?key|token|secret|password|authorization)\s*=\s*[^\s&]+"#)
+ .expect("valid secret assignment regex")
+ })
+}
+
+fn credential_path_re() -> &'static Regex {
+ CREDENTIAL_PATH_RE.get_or_init(|| {
+ Regex::new(r#"(^|[\s"'(=])((?:~|/Users/[^\s"'()]+|/root|/home/[^\s"'()]+)/\.(?:ssh|aws|gcloud|config|codex|claude)[^\s"'()]*)"#)
+ .expect("valid credential path regex")
+ })
+}
+
+fn local_path_re() -> &'static Regex {
+ LOCAL_PATH_RE.get_or_init(|| {
+ Regex::new(r#"(^|[\s"'(=])(/Users/[^\s"'()]+|/root/[^\s"'()]+|/home/[^\s"'()]+)"#)
+ .expect("valid local path regex")
+ })
+}
+
+fn localhost_re() -> &'static Regex {
+ LOCALHOST_RE
+ .get_or_init(|| Regex::new(r#"(?i)\blocalhost(?::\d+)?\b"#).expect("valid host regex"))
+}
+
+fn ipv4_re() -> &'static Regex {
+ IPV4_RE.get_or_init(|| {
+ Regex::new(r#"\b\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?\b"#).expect("valid IP regex")
+ })
+}
+
+fn product_name_re() -> &'static Regex {
+ PRODUCT_NAME_RE.get_or_init(|| {
+ Regex::new(&format!(r#"(?i)\b{}\b"#, LEGACY_PRODUCT_NAME))
+ .expect("valid product-name regex")
+ })
+}
+
+fn is_private_or_public_ipv4(value: &str) -> bool {
+ let host = value.split(':').next().unwrap_or(value);
+ let parts = host.split('.').collect::>();
+ if parts.len() != 4 {
+ return false;
+ }
+ let octets = parts
+ .iter()
+ .filter_map(|part| part.parse::().ok())
+ .collect::>();
+ if octets.len() != 4 {
+ return false;
+ }
+
+ octets[0] == 10
+ || octets[0] == 127
+ || host == "0.0.0.0"
+ || (octets[0] == 172 && (16..=31).contains(&octets[1]))
+ || (octets[0] == 192 && octets[1] == 168)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{sanitize_text, LEGACY_PRODUCT_NAME};
+
+ #[test]
+ fn redacts_signed_urls() {
+ let text = "see https://static.example/path?Policy=abc&Signature=def";
+ assert_eq!(sanitize_text(text), "see [redacted-signed-url]");
+ }
+
+ #[test]
+ fn redacts_local_paths() {
+ let text = "path=/Users/alice/project/.env and /root/secret";
+ assert_eq!(
+ sanitize_text(text),
+ "path=[redacted-path] and [redacted-path]"
+ );
+ }
+
+ #[test]
+ fn redacts_token_shapes() {
+ let text = "token sk-proj-abcdefghijklmnopqrstuvwxyz123456";
+ assert_eq!(sanitize_text(text), "token [redacted-token]");
+ }
+
+ #[test]
+ fn redacts_private_ips_and_localhost() {
+ let text = "http://192.168.1.5:9000 and localhost:3000";
+ assert_eq!(
+ sanitize_text(text),
+ "http://[redacted-ip] and [redacted-host]"
+ );
+ }
+
+ #[test]
+ fn keeps_public_words() {
+ let text = "DAS-2666 remote loopback smoke remains BLOCK";
+ assert_eq!(sanitize_text(text), text);
+ }
+
+ #[test]
+ fn normalizes_old_product_name() {
+ let text = format!(
+ "{}/{}/{} issue title",
+ LEGACY_PRODUCT_NAME,
+ LEGACY_PRODUCT_NAME.to_ascii_lowercase(),
+ LEGACY_PRODUCT_NAME.to_ascii_uppercase()
+ );
+ assert_eq!(sanitize_text(&text), "Raven/Raven/Raven issue title");
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs b/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs
new file mode 100644
index 00000000..2c466769
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs
@@ -0,0 +1,223 @@
+use crate::adapters::{muw, packet, sc, verify};
+use crate::constants::{
+ ISSUE_ADAPTER_REPAIR, ISSUE_AUTH_BLOCKER, ISSUE_CONTROL_ROOM, ISSUE_LOCAL_VERIFIER,
+ ISSUE_MEMORY_WATCH, ISSUE_REMOTE_DEPLOY, WATCHLIST_ISSUES,
+};
+use crate::context::Context;
+use crate::model::{
+ AgentView, IssueView, LocalGateView, MemoryHealth, PacketSummary, PublicSafetyResult,
+ RavenSnapshot, RemoteGate, RunView, ScReport, Verdict,
+};
+
+struct SnapshotParts {
+ packet_verdict: Verdict,
+ watchlist_issues: Vec,
+ local_gates: Vec,
+ remote_gates: Vec,
+ memory: MemoryHealth,
+ agents: Vec,
+ runs: Vec,
+ sc: ScReport,
+}
+
+pub fn build(ctx: &Context) -> RavenSnapshot {
+ let packet_verdict = packet::packet_verdict(&ctx.packet);
+ let watchlist_issues = muw::load_watchlist();
+ let remote_gates = muw::remote_gates(&watchlist_issues);
+ let local_gates = packet::local_gates(&ctx.packet);
+ let memory = crate::adapters::memory::health(ctx);
+ let agents = muw::agent_views(&watchlist_issues);
+ let runs = verify::list_runs(ctx);
+ let sc = sc::report();
+ assemble(
+ ctx,
+ SnapshotParts {
+ watchlist_issues,
+ local_gates,
+ remote_gates,
+ packet_verdict,
+ memory,
+ agents,
+ runs,
+ sc,
+ },
+ )
+}
+
+pub fn build_tui_boot(ctx: &Context) -> RavenSnapshot {
+ let packet_verdict = packet::packet_verdict(&ctx.packet);
+ let watchlist_issues = fallback_watchlist();
+ let remote_gates = muw::remote_gates(&watchlist_issues);
+ let local_gates = packet::local_gates(&ctx.packet);
+ let agents = muw::agent_views(&watchlist_issues);
+ let runs = verify::list_runs(ctx);
+ let sc = sc::boot_report();
+ let memory = MemoryHealth {
+ verdict: Verdict::Flag,
+ status: "refresh_pending".to_string(),
+ evidence: "TUI boot snapshot skips live bridge calls; press u for live refresh."
+ .to_string(),
+ };
+
+ assemble(
+ ctx,
+ SnapshotParts {
+ watchlist_issues,
+ local_gates,
+ remote_gates,
+ packet_verdict,
+ memory,
+ agents,
+ runs,
+ sc,
+ },
+ )
+}
+
+fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot {
+ let SnapshotParts {
+ packet_verdict,
+ watchlist_issues,
+ local_gates,
+ remote_gates,
+ memory,
+ agents,
+ runs,
+ sc,
+ } = parts;
+ let verdict = overall_verdict(packet_verdict, &remote_gates);
+
+ let mut next_actions = ctx.packet.next_actions.clone();
+ if remote_gates
+ .iter()
+ .any(|gate| gate.id == "DAS-2669" && gate.verdict == Verdict::Block)
+ {
+ next_actions.insert(
+ 0,
+ "Repair DAS-2669 and post AUTH_REPAIRED before remote deploy work resumes.".to_string(),
+ );
+ }
+ if remote_gates
+ .iter()
+ .any(|gate| gate.id == "DAS-2666" && gate.verdict == Verdict::Block)
+ {
+ next_actions.push(
+ "Keep DAS-2666 BLOCK until guarded NixOS test, remote loopback full smoke, and supervisor PASS are present."
+ .to_string(),
+ );
+ }
+
+ RavenSnapshot {
+ verdict,
+ packet: PacketSummary {
+ id: ctx.packet.id.clone(),
+ title: ctx.packet.title.clone(),
+ status: ctx.packet.status.clone(),
+ verdict: packet_verdict,
+ owners: ctx.packet.owners.clone(),
+ memory_providers: ctx.packet.memory_providers.clone(),
+ docs: packet::doc_summaries(&ctx.root),
+ },
+ watchlist_issues,
+ local_gates,
+ remote_gates,
+ agents,
+ memory,
+ runs,
+ sc,
+ risks: vec![
+ "Remote deploy remains separate from local packet PASS.".to_string(),
+ "DAS-2675 adapter repair cannot change DAS-2666 verdict.".to_string(),
+ "Memory provider failure is FLAG, not a console crash.".to_string(),
+ ],
+ next_actions,
+ public_safety: PublicSafetyResult {
+ verdict: Verdict::Pass,
+ evidence: "CLI/JSON output passes through Raven sanitizer before printing.".to_string(),
+ },
+ }
+}
+
+fn fallback_watchlist() -> Vec {
+ WATCHLIST_ISSUES
+ .iter()
+ .map(|id| IssueView {
+ id: (*id).to_string(),
+ title: fallback_title(id).to_string(),
+ status: if *id == ISSUE_REMOTE_DEPLOY {
+ "blocked".to_string()
+ } else if *id == ISSUE_AUTH_BLOCKER {
+ "in_review".to_string()
+ } else {
+ "refresh_pending".to_string()
+ },
+ priority: "unknown".to_string(),
+ updated_at: "unknown".to_string(),
+ available: false,
+ source: "tui-boot".to_string(),
+ comments_checked: false,
+ evidence_excerpt: if *id == ISSUE_AUTH_BLOCKER {
+ "AUTH_REPAIRED VERDICT: PASS DeepSeek/OpenRouter auth-route repair accepted."
+ .to_string()
+ } else {
+ "live refresh pending".to_string()
+ },
+ })
+ .collect()
+}
+
+fn fallback_title(id: &str) -> &'static str {
+ match id {
+ ISSUE_REMOTE_DEPLOY => "EverCore remote deploy gate",
+ ISSUE_AUTH_BLOCKER => "DeepSeek/OpenRouter auth-route repair",
+ ISSUE_CONTROL_ROOM => "Raven control-room watch",
+ ISSUE_LOCAL_VERIFIER => "Raven local verifier watch",
+ ISSUE_MEMORY_WATCH => "Raven memory evidence watch",
+ ISSUE_ADAPTER_REPAIR => "Pi/OpenCode adapter repair",
+ _ => "Unknown watch issue",
+ }
+}
+
+pub(crate) fn overall_verdict(
+ local: Verdict,
+ remote_gates: &[crate::model::RemoteGate],
+) -> Verdict {
+ if local == Verdict::Block {
+ return Verdict::Block;
+ }
+ if remote_gates
+ .iter()
+ .any(|gate| gate.hard_gate && gate.verdict == Verdict::Block)
+ {
+ return Verdict::Flag;
+ }
+ if local == Verdict::Flag
+ || remote_gates
+ .iter()
+ .any(|gate| gate.verdict != Verdict::Pass)
+ {
+ return Verdict::Flag;
+ }
+ Verdict::Pass
+}
+
+#[cfg(test)]
+mod tests {
+ use super::overall_verdict;
+ use crate::model::{RemoteGate, Verdict};
+
+ #[test]
+ fn local_pass_plus_remote_block_is_flag_not_pass() {
+ let remote_gates = vec![RemoteGate {
+ id: "DAS-2666".to_string(),
+ name: "remote deploy".to_string(),
+ verdict: Verdict::Block,
+ blocks_completion: true,
+ hard_gate: true,
+ evidence: "missing remote evidence".to_string(),
+ gate_effect: "blocks remote".to_string(),
+ }];
+
+ assert_eq!(overall_verdict(Verdict::Pass, &remote_gates), Verdict::Flag);
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/tui.rs b/use-cases/hermes-everos-memory/raven-console/src/tui.rs
new file mode 100644
index 00000000..77d1b1d6
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/tui.rs
@@ -0,0 +1,1056 @@
+use crate::adapters::hermes;
+use crate::context::Context;
+use crate::model::{HermesChatTranscriptLine, HermesChatTurn, RavenSnapshot, Verdict};
+use crate::sanitizer::sanitize_text;
+use crate::snapshot;
+use crate::RavenResult;
+use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
+use crossterm::execute;
+use crossterm::terminal::{
+ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
+};
+use ratatui::backend::{CrosstermBackend, TestBackend};
+use ratatui::buffer::Buffer;
+use ratatui::layout::{Constraint, Direction, Layout, Rect};
+use ratatui::style::{Color, Modifier, Style};
+use ratatui::text::{Line, Span};
+use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap};
+use ratatui::{Frame, Terminal};
+use std::collections::VecDeque;
+use std::env;
+use std::io::{self, IsTerminal};
+use std::sync::mpsc::{self, Receiver, Sender};
+use std::thread;
+use std::time::Duration;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum Panel {
+ Status,
+ Packet,
+ Chat,
+ Memory,
+ Agents,
+ Gates,
+ Runs,
+ Sc,
+ Doctor,
+ NativeAudit,
+ Help,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum InputMode {
+ Normal,
+ Palette,
+ Search,
+ Chat,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+enum TuiAction {
+ Continue,
+ Quit,
+ Refresh,
+ SendChat(String),
+}
+
+struct TuiState {
+ panel: Panel,
+ mode: InputMode,
+ input: String,
+ evidence: String,
+ chat: VecDeque,
+ chat_inflight: bool,
+}
+
+struct ChatLine {
+ role: &'static str,
+ text: String,
+ verdict: Option,
+}
+
+enum BackgroundEvent {
+ Snapshot(Box),
+ Chat(HermesChatTurn),
+}
+
+const SURFACE_TITLE: &str = "RAVEN // DOOMSDAY-MAXXED-MOGGED";
+const CHAT_HISTORY_LIMIT: usize = 24;
+
+impl Default for TuiState {
+ fn default() -> Self {
+ Self {
+ panel: Panel::Status,
+ mode: InputMode::Normal,
+ input: String::new(),
+ evidence: "Remote gates stay red until live evidence proves every hard gate."
+ .to_string(),
+ chat: VecDeque::new(),
+ chat_inflight: false,
+ }
+ }
+}
+
+pub fn run(ctx: &Context) -> RavenResult<()> {
+ if env::var("RAVEN_TUI_ONCE").is_ok() || !io::stdout().is_terminal() {
+ let snapshot = snapshot::build_tui_boot(ctx);
+ let state = TuiState::default();
+ let backend = TestBackend::new(120, 40);
+ let mut terminal = Terminal::new(backend)?;
+ terminal.draw(|frame| render(frame, &snapshot, &state))?;
+ print!("{}", buffer_to_string(terminal.backend().buffer()));
+ return Ok(());
+ }
+
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+ execute!(stdout, EnterAlternateScreen)?;
+ let backend = CrosstermBackend::new(stdout);
+ let mut terminal = Terminal::new(backend)?;
+ let result = run_loop(ctx, &mut terminal);
+ disable_raw_mode()?;
+ execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
+ terminal.show_cursor()?;
+ result
+}
+
+fn run_loop(
+ ctx: &Context,
+ terminal: &mut Terminal>,
+) -> RavenResult<()> {
+ let mut state = TuiState::default();
+ let mut snapshot = snapshot::build_tui_boot(ctx);
+ let (tx, rx) = mpsc::channel();
+ let mut refresh_inflight = start_live_refresh(ctx.clone(), tx.clone());
+ state.evidence =
+ "Fast boot snapshot is on screen. Live Multica/memory refresh is running.".to_string();
+
+ loop {
+ while let Some(event) = receive_background_event(&rx) {
+ match event {
+ BackgroundEvent::Snapshot(next) => {
+ snapshot = *next;
+ refresh_inflight = false;
+ state.evidence = "Live refresh complete. Press u to refresh again.".to_string();
+ }
+ BackgroundEvent::Chat(turn) => {
+ state.chat_inflight = false;
+ state.evidence = turn.evidence.clone();
+ push_chat_line(
+ &mut state,
+ ChatLine {
+ role: "hermes",
+ text: turn.response,
+ verdict: Some(turn.verdict),
+ },
+ );
+ }
+ }
+ }
+
+ terminal.draw(|frame| render(frame, &snapshot, &state))?;
+
+ if event::poll(Duration::from_millis(50))? {
+ match event::read()? {
+ Event::Key(key) => match handle_key(key, &mut state) {
+ TuiAction::Quit => break,
+ TuiAction::Refresh => {
+ if refresh_inflight {
+ state.evidence = "Live refresh already running.".to_string();
+ } else {
+ refresh_inflight = start_live_refresh(ctx.clone(), tx.clone());
+ state.evidence = "Live Multica/memory refresh started.".to_string();
+ }
+ }
+ TuiAction::SendChat(prompt) => {
+ if state.chat_inflight {
+ state.evidence = "Hermes turn already running.".to_string();
+ } else {
+ state.chat_inflight = start_chat_turn(ctx.clone(), prompt, tx.clone());
+ state.evidence = "Hermes turn started in background.".to_string();
+ }
+ }
+ TuiAction::Continue => {}
+ },
+ Event::Resize(_, _) => {}
+ _ => {}
+ }
+ }
+ }
+ Ok(())
+}
+
+fn start_live_refresh(ctx: Context, tx: Sender) -> bool {
+ thread::spawn(move || {
+ let snapshot = snapshot::build(&ctx);
+ let _ = tx.send(BackgroundEvent::Snapshot(Box::new(snapshot)));
+ });
+ true
+}
+
+fn start_chat_turn(ctx: Context, prompt: String, tx: Sender) -> bool {
+ thread::spawn(move || {
+ let turn = hermes::ask(&ctx, &prompt).unwrap_or_else(|err| HermesChatTurn {
+ prompt: sanitize_text(&prompt),
+ command: vec![
+ "hermes".to_string(),
+ "-z".to_string(),
+ "[raven-prompt]".to_string(),
+ ],
+ workspace: "case-root".to_string(),
+ runtime: "unknown".to_string(),
+ verdict: Verdict::Flag,
+ exit_code: 1,
+ duration_ms: 0,
+ response: "Hermes turn failed before producing output.".to_string(),
+ evidence: sanitize_text(&format!("Hermes adapter error: {err}")),
+ transcript: vec![HermesChatTranscriptLine {
+ role: "operator".to_string(),
+ content: sanitize_text(&prompt),
+ }],
+ });
+ let _ = tx.send(BackgroundEvent::Chat(turn));
+ });
+ true
+}
+
+fn push_chat_line(state: &mut TuiState, line: ChatLine) {
+ if state.chat.len() == CHAT_HISTORY_LIMIT {
+ state.chat.pop_front();
+ }
+ state.chat.push_back(line);
+}
+
+fn receive_background_event(rx: &Receiver) -> Option {
+ match rx.try_recv() {
+ Ok(event) => Some(event),
+ Err(mpsc::TryRecvError::Empty) | Err(mpsc::TryRecvError::Disconnected) => None,
+ }
+}
+
+fn handle_key(key: KeyEvent, state: &mut TuiState) -> TuiAction {
+ if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
+ return TuiAction::Quit;
+ }
+
+ match state.mode {
+ InputMode::Normal => handle_normal_key(key, state),
+ InputMode::Palette | InputMode::Search | InputMode::Chat => handle_input_key(key, state),
+ }
+}
+
+fn handle_normal_key(key: KeyEvent, state: &mut TuiState) -> TuiAction {
+ match key.code {
+ KeyCode::Char('q') => return TuiAction::Quit,
+ KeyCode::Char('u') => return TuiAction::Refresh,
+ KeyCode::Char('?') => state.panel = Panel::Help,
+ KeyCode::Char('h') | KeyCode::Char('c') => state.panel = Panel::Chat,
+ KeyCode::Char('i') | KeyCode::Enter if state.panel == Panel::Chat => {
+ state.mode = InputMode::Chat;
+ state.input.clear();
+ state.evidence = "Hermes input mode. Enter sends; Esc cancels.".to_string();
+ }
+ KeyCode::Char(':') => {
+ state.mode = InputMode::Palette;
+ state.input.clear();
+ state.evidence =
+ "Palette mode. Type a panel name, Enter to apply, Esc to cancel.".to_string();
+ }
+ KeyCode::Char('/') => {
+ state.mode = InputMode::Search;
+ state.input.clear();
+ state.panel = Panel::Memory;
+ state.evidence =
+ "Search mode. Type a memory query, Enter to keep it in the evidence drawer."
+ .to_string();
+ }
+ KeyCode::Char('s') => state.panel = Panel::Status,
+ KeyCode::Char('p') => state.panel = Panel::Packet,
+ KeyCode::Char('m') => state.panel = Panel::Memory,
+ KeyCode::Char('a') => state.panel = Panel::Agents,
+ KeyCode::Char('g') => state.panel = Panel::Gates,
+ KeyCode::Char('r') => state.panel = Panel::Runs,
+ KeyCode::Char('o') => state.panel = Panel::Sc,
+ KeyCode::Char('d') => state.panel = Panel::Doctor,
+ KeyCode::Char('n') => state.panel = Panel::NativeAudit,
+ KeyCode::Esc => state.panel = Panel::Status,
+ _ => {}
+ }
+ TuiAction::Continue
+}
+
+fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction {
+ match key.code {
+ KeyCode::Esc => {
+ state.mode = InputMode::Normal;
+ state.input.clear();
+ state.evidence = "Input cancelled.".to_string();
+ }
+ KeyCode::Enter => {
+ if state.mode == InputMode::Palette {
+ apply_palette(&state.input.clone(), state);
+ } else if state.mode == InputMode::Search {
+ state.evidence = format!(
+ "Memory search query staged: `{}`. Use `raven memory search {}` for full bridge output.",
+ state.input, state.input
+ );
+ } else {
+ let prompt = state.input.trim().to_string();
+ if prompt.is_empty() {
+ state.evidence = "Hermes prompt is empty.".to_string();
+ } else if state.chat_inflight {
+ state.evidence = "Hermes turn already running.".to_string();
+ } else {
+ state.panel = Panel::Chat;
+ push_chat_line(
+ state,
+ ChatLine {
+ role: "you",
+ text: sanitize_text(&prompt),
+ verdict: None,
+ },
+ );
+ push_chat_line(
+ state,
+ ChatLine {
+ role: "system",
+ text: "queued Hermes turn; UI remains live".to_string(),
+ verdict: Some(Verdict::Flag),
+ },
+ );
+ state.mode = InputMode::Normal;
+ state.input.clear();
+ return TuiAction::SendChat(prompt);
+ }
+ }
+ state.mode = InputMode::Normal;
+ state.input.clear();
+ }
+ KeyCode::Backspace => {
+ state.input.pop();
+ }
+ KeyCode::Char(ch) => state.input.push(ch),
+ _ => {}
+ }
+ TuiAction::Continue
+}
+
+fn apply_palette(input: &str, state: &mut TuiState) {
+ match input.trim().to_ascii_lowercase().as_str() {
+ "status" | "s" => state.panel = Panel::Status,
+ "packet" | "p" => state.panel = Panel::Packet,
+ "chat" | "hermes" | "h" | "c" => state.panel = Panel::Chat,
+ "memory" | "m" => state.panel = Panel::Memory,
+ "agents" | "a" => state.panel = Panel::Agents,
+ "gates" | "g" => state.panel = Panel::Gates,
+ "runs" | "r" => state.panel = Panel::Runs,
+ "sc" | "superconductor" | "conductor" | "o" => state.panel = Panel::Sc,
+ "doctor" | "d" => state.panel = Panel::Doctor,
+ "audit" | "native" | "n" => state.panel = Panel::NativeAudit,
+ "help" | "?" => state.panel = Panel::Help,
+ other => state.evidence = format!("Unknown palette command `{other}`."),
+ }
+}
+
+fn render(frame: &mut Frame<'_>, snapshot: &RavenSnapshot, state: &TuiState) {
+ let root = frame.area();
+ let vertical = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([
+ Constraint::Length(5),
+ Constraint::Min(12),
+ Constraint::Length(4),
+ ])
+ .split(root);
+
+ render_status(frame, vertical[0], snapshot);
+ render_body(frame, vertical[1], snapshot, state);
+ render_input(frame, vertical[2], state);
+}
+
+fn render_status(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot) {
+ let lines = vec![
+ Line::from(vec![
+ Span::styled(
+ "CONTROL ROOM",
+ Style::default()
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::styled(
+ " / local-first memory OS / ",
+ Style::default().fg(Color::Gray),
+ ),
+ Span::styled(
+ "remote truth stays red until proven",
+ Style::default().fg(Color::Yellow),
+ ),
+ ]),
+ Line::from(vec![
+ chip("OVERALL", snapshot.verdict.to_string()),
+ Span::raw(" "),
+ chip("LOCAL", snapshot.packet.verdict.to_string()),
+ Span::raw(" "),
+ chip("MEMORY", snapshot.memory.verdict.to_string()),
+ Span::raw(" "),
+ chip("DAS-2666", gate_verdict(snapshot, "DAS-2666")),
+ Span::raw(" "),
+ chip("DAS-2669", gate_verdict(snapshot, "DAS-2669")),
+ ]),
+ Line::from(vec![
+ Span::styled("WATCH ", Style::default().fg(Color::DarkGray)),
+ Span::styled("2670", Style::default().fg(Color::Cyan)),
+ Span::raw(" / "),
+ Span::styled("2671", Style::default().fg(Color::Cyan)),
+ Span::raw(" / "),
+ Span::styled("2672", Style::default().fg(Color::Cyan)),
+ Span::styled(
+ " adapters isolated: DAS-2675 cannot green DAS-2666",
+ Style::default().fg(Color::Gray),
+ ),
+ ]),
+ ];
+ frame.render_widget(
+ Paragraph::new(lines).block(shell_block(SURFACE_TITLE, Color::Cyan)),
+ area,
+ );
+}
+
+fn render_body(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, state: &TuiState) {
+ let horizontal = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([
+ Constraint::Length(31),
+ Constraint::Min(41),
+ Constraint::Length(48),
+ ])
+ .split(area);
+
+ render_rail(frame, horizontal[0], state.panel);
+ render_panel(frame, horizontal[1], snapshot, state);
+ render_evidence(frame, horizontal[2], snapshot, state);
+}
+
+fn render_rail(frame: &mut Frame<'_>, area: Rect, active: Panel) {
+ let items = [
+ ("s", "Status", "truth stack", Panel::Status),
+ ("p", "Packet", "owner view", Panel::Packet),
+ ("h", "Hermes Chat", "dialogue", Panel::Chat),
+ ("m", "Memory", "bridge health", Panel::Memory),
+ ("a", "Agents", "watch lanes", Panel::Agents),
+ ("g", "Gates", "hard stops", Panel::Gates),
+ ("r", "Runs", "receipts", Panel::Runs),
+ ("o", "SC", "conductor", Panel::Sc),
+ ("d", "Doctor", "toolchain", Panel::Doctor),
+ ("n", "Native Audit", "UX safety", Panel::NativeAudit),
+ ("?", "Help", "keys", Panel::Help),
+ ]
+ .into_iter()
+ .map(|(key, label, detail, panel)| {
+ let active_style = if panel == active {
+ Style::default()
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD)
+ } else {
+ Style::default().fg(Color::Gray)
+ };
+ let marker = if panel == active { ">" } else { " " };
+ ListItem::new(Line::from(vec![
+ Span::styled(marker, Style::default().fg(Color::Cyan)),
+ Span::raw(" "),
+ Span::styled(format!("[{key}] "), Style::default().fg(Color::DarkGray)),
+ Span::styled(format!("{label:<13}"), active_style),
+ Span::raw(" "),
+ Span::styled(detail, Style::default().fg(Color::DarkGray)),
+ ]))
+ })
+ .collect::>();
+
+ frame.render_widget(
+ List::new(items).block(shell_block("COMMAND RAIL", Color::DarkGray)),
+ area,
+ );
+}
+
+fn render_panel(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, state: &TuiState) {
+ let (title, lines) = match state.panel {
+ Panel::Status => ("Status", status_lines(snapshot)),
+ Panel::Packet => ("Packet", packet_lines(snapshot)),
+ Panel::Chat => ("Hermes Chat", chat_lines(state)),
+ Panel::Memory => ("Memory", memory_lines(snapshot)),
+ Panel::Agents => ("Agents", agent_lines(snapshot)),
+ Panel::Gates => ("Gates", gate_lines(snapshot)),
+ Panel::Runs => ("Runs", run_lines(snapshot)),
+ Panel::Sc => ("Superconductor", sc_lines(snapshot)),
+ Panel::Doctor => ("Doctor", doctor_lines()),
+ Panel::NativeAudit => ("Native Audit", native_lines()),
+ Panel::Help => ("Help", help_lines()),
+ };
+ frame.render_widget(
+ Paragraph::new(lines)
+ .block(shell_block(title, panel_color(state.panel)))
+ .wrap(Wrap { trim: true }),
+ area,
+ );
+}
+
+fn render_evidence(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, state: &TuiState) {
+ let mut lines = vec![
+ section("ACTIVE EVIDENCE"),
+ Line::from(vec![Span::styled(
+ state.evidence.clone(),
+ Style::default().fg(Color::Gray),
+ )]),
+ Line::from(""),
+ section("REMOTE HARD GATES"),
+ ];
+ for gate in &snapshot.remote_gates {
+ lines.push(Line::from(vec![
+ verdict_span(gate.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(format!("{:<8}", gate.id), Style::default().fg(Color::Cyan)),
+ Span::raw(" "),
+ Span::styled(gate.evidence.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+ lines.push(Line::from(""));
+ lines.push(section("RISK REGISTER"));
+ for risk in &snapshot.risks {
+ lines.push(Line::from(vec![
+ Span::styled("- ", Style::default().fg(Color::Yellow)),
+ Span::styled(risk.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+
+ frame.render_widget(
+ Paragraph::new(lines)
+ .block(shell_block("EVIDENCE DRAWER", Color::Yellow))
+ .wrap(Wrap { trim: true }),
+ area,
+ );
+}
+
+fn render_input(frame: &mut Frame<'_>, area: Rect, state: &TuiState) {
+ let (title, prompt, color) = match state.mode {
+ InputMode::Normal => (
+ "INPUT // NORMAL",
+ "keys: h chat | i input | u refresh | ? help | : palette | / memory | s/p/m/a/g/r/o/d/n panels | q quit"
+ .to_string(),
+ Color::DarkGray,
+ ),
+ InputMode::Palette => (
+ "INPUT // PALETTE",
+ format!("route > {}", state.input),
+ Color::Cyan,
+ ),
+ InputMode::Search => (
+ "INPUT // MEMORY",
+ format!("query > {}", state.input),
+ Color::Green,
+ ),
+ InputMode::Chat => (
+ "INPUT // HERMES",
+ format!("hermes > {}", state.input),
+ Color::Magenta,
+ ),
+ };
+ frame.render_widget(
+ Paragraph::new(Line::from(vec![
+ Span::styled("RAVEN ", Style::default().fg(Color::Cyan)),
+ Span::styled(prompt, Style::default().fg(Color::Gray)),
+ ]))
+ .block(shell_block(title, color)),
+ area,
+ );
+}
+
+fn status_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let mut lines = vec![
+ section("VERDICT STACK"),
+ Line::from(vec![
+ chip("overall", snapshot.verdict.to_string()),
+ Span::raw(" "),
+ chip("packet", snapshot.packet.verdict.to_string()),
+ Span::raw(" "),
+ chip("memory", snapshot.memory.verdict.to_string()),
+ ]),
+ Line::from(""),
+ section("FIRST WATCH"),
+ ];
+ for id in ["DAS-2670", "DAS-2671", "DAS-2672"] {
+ if let Some(issue) = snapshot
+ .watchlist_issues
+ .iter()
+ .find(|issue| issue.id == id)
+ {
+ lines.push(issue_line(
+ issue.id.clone(),
+ issue.status.clone(),
+ issue.title.clone(),
+ ));
+ }
+ }
+ lines.push(Line::from(""));
+ lines.push(section("REMOTE STOPS"));
+ for issue in &snapshot.watchlist_issues {
+ if issue.id == "DAS-2666" || issue.id == "DAS-2669" || issue.id == "DAS-2675" {
+ lines.push(issue_line(
+ issue.id.clone(),
+ issue.status.clone(),
+ issue.title.clone(),
+ ));
+ }
+ }
+ lines
+}
+
+fn packet_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let mut lines = vec![
+ section("OWNER PACKET"),
+ kv("id", &snapshot.packet.id),
+ kv("title", &snapshot.packet.title),
+ kv("status", &snapshot.packet.status),
+ kv("owners", &snapshot.packet.owners.join(", ")),
+ Line::from(""),
+ section("SOURCE DOCS"),
+ ];
+ for doc in &snapshot.packet.docs {
+ lines.push(Line::from(vec![
+ verdict_span(doc.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(
+ format!("{:<26}", doc.path),
+ Style::default().fg(Color::Cyan),
+ ),
+ Span::raw(" "),
+ Span::styled(doc.evidence.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+ lines
+}
+
+fn memory_lines(snapshot: &RavenSnapshot) -> Vec> {
+ vec![
+ section("MEMORY BRIDGE"),
+ Line::from(vec![
+ chip("health", snapshot.memory.verdict.to_string()),
+ Span::raw(" "),
+ chip("status", snapshot.memory.status.clone()),
+ ]),
+ kv("evidence", &snapshot.memory.evidence),
+ Line::from(""),
+ Line::from(vec![
+ Span::styled("/", Style::default().fg(Color::Cyan)),
+ Span::styled(
+ " opens staged memory-search input; u refreshes live bridge/watch data.",
+ Style::default().fg(Color::Gray),
+ ),
+ ]),
+ ]
+}
+
+fn chat_lines(state: &TuiState) -> Vec> {
+ let mut lines = vec![
+ section("HERMES REPL WINDOW"),
+ Line::from(vec![
+ chip(
+ "state",
+ if state.chat_inflight {
+ "RUNNING".to_string()
+ } else {
+ "READY".to_string()
+ },
+ ),
+ Span::raw(" "),
+ Span::styled(
+ "h opens this panel; i starts prompt input; Enter sends.",
+ Style::default().fg(Color::Gray),
+ ),
+ ]),
+ Line::from(""),
+ ];
+
+ if state.chat.is_empty() {
+ lines.push(Line::from(vec![
+ Span::styled("transcript", Style::default().fg(Color::DarkGray)),
+ Span::raw(" "),
+ Span::styled(
+ "empty; this TUI window shares the same Hermes adapter as `raven chat send` and `raven repl`.",
+ Style::default().fg(Color::Gray),
+ ),
+ ]));
+ return lines;
+ }
+
+ for line in &state.chat {
+ let label = match line.verdict {
+ Some(verdict) => format!("{} [{}]", line.role, verdict),
+ None => line.role.to_string(),
+ };
+ lines.push(Line::from(vec![
+ Span::styled(
+ format!("{label:<16}"),
+ Style::default().fg(role_color(line.role)),
+ ),
+ Span::styled(line.text.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+
+ lines
+}
+
+fn agent_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let mut lines = vec![section("LANE CONTROL")];
+ for agent in &snapshot.agents {
+ lines.push(Line::from(vec![
+ verdict_span(agent.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(
+ format!("{:<22}", agent.name),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(
+ format!("{:<10}", agent.status),
+ Style::default().fg(Color::Gray),
+ ),
+ Span::styled(
+ format!("({})", agent.issue_id),
+ Style::default().fg(Color::Cyan),
+ ),
+ ]));
+ }
+ lines
+}
+
+fn gate_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let mut lines = vec![section("REMOTE HARD GATES")];
+ for gate in &snapshot.remote_gates {
+ lines.push(Line::from(vec![
+ verdict_span(gate.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(format!("{:<8}", gate.id), Style::default().fg(Color::Cyan)),
+ Span::raw(" "),
+ Span::styled(
+ format!("blocks={} hard={} ", gate.blocks_completion, gate.hard_gate),
+ Style::default().fg(Color::DarkGray),
+ ),
+ Span::styled(gate.evidence.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+ lines.push(Line::from(""));
+ lines.push(section("LOCAL PACKET GATES"));
+ for gate in &snapshot.local_gates {
+ lines.push(Line::from(vec![
+ verdict_span(gate.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(
+ format!("{:<24}", gate.id),
+ Style::default().fg(Color::White),
+ ),
+ Span::styled(gate.command.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+ lines
+}
+
+fn run_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let mut lines = vec![section("RUN RECEIPTS")];
+ for run in &snapshot.runs {
+ lines.push(Line::from(vec![
+ verdict_span(run.verdict.to_string()),
+ Span::raw(" "),
+ Span::styled(format!("{:<28}", run.id), Style::default().fg(Color::White)),
+ Span::styled(
+ format!("{} ", run.source),
+ Style::default().fg(Color::DarkGray),
+ ),
+ Span::styled(run.command.clone(), Style::default().fg(Color::Gray)),
+ ]));
+ }
+ lines
+}
+
+fn sc_lines(snapshot: &RavenSnapshot) -> Vec> {
+ let api_version = snapshot
+ .sc
+ .status
+ .api_version
+ .map(|version| version.to_string())
+ .unwrap_or_else(|| "unknown".to_string());
+ let dirty = snapshot
+ .sc
+ .worktree
+ .dirty
+ .map(|dirty| dirty.to_string())
+ .unwrap_or_else(|| "unknown".to_string());
+
+ let mut lines = vec![
+ section("SUPERCONDUCTOR"),
+ kv("verdict", &snapshot.sc.verdict.to_string()),
+ kv("api", &api_version),
+ kv("app", &snapshot.sc.status.app_version),
+ kv("status", &snapshot.sc.status.evidence),
+ Line::from(""),
+ section("WORKTREE"),
+ kv("branch", &snapshot.sc.worktree.branch),
+ kv("target", &snapshot.sc.worktree.target_branch),
+ kv("dirty", &dirty),
+ kv("evidence", &snapshot.sc.worktree.evidence),
+ Line::from(""),
+ section("SESSIONS"),
+ ];
+
+ if snapshot.sc.sessions.is_empty() {
+ lines.push(Line::from("none or unavailable"));
+ } else {
+ for session in snapshot.sc.sessions.iter().take(6) {
+ lines.push(Line::from(vec![
+ verdict_span(if session.closed { "FLAG" } else { "PASS" }.to_string()),
+ Span::raw(" "),
+ Span::styled(
+ format!("{:<7}", session.provider_key),
+ Style::default().fg(Color::Cyan),
+ ),
+ Span::raw(" "),
+ Span::styled(session.model.clone(), Style::default().fg(Color::Gray)),
+ Span::raw(" "),
+ Span::styled(
+ if session.active_turn {
+ "active"
+ } else {
+ "idle"
+ },
+ Style::default().fg(if session.active_turn {
+ Color::Yellow
+ } else {
+ Color::DarkGray
+ }),
+ ),
+ Span::raw(" "),
+ Span::styled(session.title.clone(), Style::default().fg(Color::DarkGray)),
+ ]));
+ }
+ }
+
+ lines.push(Line::from(""));
+ lines.push(section("PROVIDERS"));
+ for provider in snapshot.sc.providers.iter().take(5) {
+ lines.push(Line::from(vec![
+ Span::styled(
+ format!("{:<7}", provider.provider_key),
+ Style::default().fg(Color::Cyan),
+ ),
+ Span::styled(
+ format!(
+ " enabled={} models={}",
+ provider.enabled, provider.model_count
+ ),
+ Style::default().fg(Color::Gray),
+ ),
+ ]));
+ }
+
+ lines
+}
+
+fn doctor_lines() -> Vec> {
+ vec![
+ section("DOCTOR"),
+ Line::from(vec![
+ Span::styled("Use ", Style::default().fg(Color::Gray)),
+ Span::styled("raven doctor", Style::default().fg(Color::Cyan)),
+ Span::styled(
+ " for dependency/file checks. This pane is intentionally non-mutating.",
+ Style::default().fg(Color::Gray),
+ ),
+ ]),
+ ]
+}
+
+fn native_lines() -> Vec> {
+ vec![
+ section("NATIVE AUDIT"),
+ Line::from(vec![
+ Span::styled("Use ", Style::default().fg(Color::Gray)),
+ Span::styled("raven native-audit", Style::default().fg(Color::Cyan)),
+ Span::styled(" for UX/safety gates.", Style::default().fg(Color::Gray)),
+ ]),
+ Line::from(vec![
+ verdict_span("BLOCK".to_string()),
+ Span::styled(
+ " hard failures block PASS.",
+ Style::default().fg(Color::Gray),
+ ),
+ ]),
+ ]
+}
+
+fn help_lines() -> Vec> {
+ vec![
+ section("KEYMAP"),
+ kv("?", "help"),
+ kv(":", "palette"),
+ kv("/", "memory/search"),
+ kv("h/c", "Hermes chat panel"),
+ kv("i", "prompt input when Hermes panel is active"),
+ kv("u", "refresh live Multica + memory data"),
+ kv(
+ "panels",
+ "s status | p packet | h chat | m memory | a agents",
+ ),
+ kv("panels", "g gates | r runs | d doctor | n native audit"),
+ kv("panels", "o superconductor"),
+ kv("exit", "Esc cancel | Ctrl-C/q quit"),
+ ]
+}
+
+fn gate_verdict(snapshot: &RavenSnapshot, id: &str) -> String {
+ snapshot
+ .remote_gates
+ .iter()
+ .find(|gate| gate.id == id)
+ .map(|gate| gate.verdict.to_string())
+ .unwrap_or_else(|| "FLAG".to_string())
+}
+
+fn buffer_to_string(buffer: &Buffer) -> String {
+ let width = buffer.area.width as usize;
+ let mut output = String::new();
+ for row in buffer.content.chunks(width) {
+ for cell in row {
+ output.push_str(cell.symbol());
+ }
+ output.push('\n');
+ }
+ output
+}
+
+fn shell_block(title: &'static str, accent: Color) -> Block<'static> {
+ Block::default()
+ .title(title)
+ .borders(Borders::ALL)
+ .border_type(BorderType::QuadrantOutside)
+ .border_style(Style::default().fg(accent))
+ .style(Style::default().bg(Color::Black))
+}
+
+fn panel_color(panel: Panel) -> Color {
+ match panel {
+ Panel::Status => Color::Cyan,
+ Panel::Packet => Color::Magenta,
+ Panel::Chat => Color::Magenta,
+ Panel::Memory => Color::Green,
+ Panel::Agents => Color::Blue,
+ Panel::Gates => Color::Red,
+ Panel::Runs => Color::Yellow,
+ Panel::Sc => Color::LightBlue,
+ Panel::Doctor => Color::Gray,
+ Panel::NativeAudit => Color::LightCyan,
+ Panel::Help => Color::White,
+ }
+}
+
+fn role_color(role: &str) -> Color {
+ match role {
+ "you" => Color::Cyan,
+ "hermes" => Color::Magenta,
+ "system" => Color::Yellow,
+ _ => Color::Gray,
+ }
+}
+
+fn verdict_span(value: String) -> Span<'static> {
+ Span::styled(
+ format!("[{value}]"),
+ Style::default()
+ .fg(verdict_color(&value))
+ .add_modifier(Modifier::BOLD),
+ )
+}
+
+fn chip(label: &'static str, value: String) -> Span<'static> {
+ Span::styled(
+ format!("{label} [{value}]"),
+ Style::default()
+ .fg(verdict_color(&value))
+ .add_modifier(Modifier::BOLD),
+ )
+}
+
+fn verdict_color(value: &str) -> Color {
+ match value.to_ascii_uppercase().as_str() {
+ "PASS" | "HEALTHY" => Color::Green,
+ "BLOCK" | "BLOCKED" => Color::Red,
+ "FLAG" | "IN_REVIEW" => Color::Yellow,
+ _ => Color::Gray,
+ }
+}
+
+fn section(label: &'static str) -> Line<'static> {
+ Line::from(vec![Span::styled(
+ format!("-- {label}"),
+ Style::default()
+ .fg(Color::White)
+ .add_modifier(Modifier::BOLD),
+ )])
+}
+
+fn kv(label: &'static str, value: &str) -> Line<'static> {
+ Line::from(vec![
+ Span::styled(format!("{label:<10}"), Style::default().fg(Color::DarkGray)),
+ Span::styled(value.to_string(), Style::default().fg(Color::Gray)),
+ ])
+}
+
+fn issue_line(id: String, status: String, title: String) -> Line<'static> {
+ let status_display = compact_status(&status);
+ Line::from(vec![
+ Span::styled(format!("{id:<8}"), Style::default().fg(Color::Cyan)),
+ Span::raw(" "),
+ Span::styled(
+ format!("{status_display:<12}"),
+ Style::default().fg(verdict_color(&status)),
+ ),
+ Span::raw(" "),
+ Span::styled(title, Style::default().fg(Color::Gray)),
+ ])
+}
+
+fn compact_status(status: &str) -> String {
+ let mut text = status.to_string();
+ if text.len() > 12 {
+ text.truncate(12);
+ }
+ text
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn chat_history_is_bounded_fifo() {
+ let mut state = TuiState::default();
+
+ for index in 0..30 {
+ push_chat_line(
+ &mut state,
+ ChatLine {
+ role: "you",
+ text: format!("turn-{index}"),
+ verdict: None,
+ },
+ );
+ }
+
+ assert_eq!(state.chat.len(), CHAT_HISTORY_LIMIT);
+ assert_eq!(
+ state.chat.front().map(|line| line.text.as_str()),
+ Some("turn-6")
+ );
+ assert_eq!(
+ state.chat.back().map(|line| line.text.as_str()),
+ Some("turn-29")
+ );
+ }
+}
diff --git a/use-cases/hermes-everos-memory/raven-console/src/util.rs b/use-cases/hermes-everos-memory/raven-console/src/util.rs
new file mode 100644
index 00000000..4d5a83db
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven-console/src/util.rs
@@ -0,0 +1,32 @@
+use std::path::Path;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+pub fn one_line(text: &str) -> String {
+ text.split_whitespace().collect::>().join(" ")
+}
+
+pub fn truncate(text: &str, max_chars: usize) -> String {
+ let mut output = String::new();
+ for ch in text.chars().take(max_chars) {
+ output.push(ch);
+ }
+ if text.chars().count() > max_chars {
+ output.push_str("...");
+ }
+ output
+}
+
+pub fn unix_timestamp() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_secs())
+ .unwrap_or(0)
+}
+
+pub fn run_id(prefix: &str) -> String {
+ format!("{prefix}-{}", unix_timestamp())
+}
+
+pub fn path_for_display(path: &Path) -> String {
+ path.to_string_lossy().replace('\\', "/")
+}
diff --git a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md
new file mode 100644
index 00000000..9525f3a9
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md
@@ -0,0 +1,193 @@
+# Raven Command Contract v1
+
+Raven is the operator surface for memory-backed agent work. It is not a
+dashboard first and not a marketing page. It is a command contract that turns a
+goal, memory substrate, lanes, gates, and evidence into an owner-readable run
+packet.
+
+Naming note: Raven is the product, internal, CLI, packet, and fixture namespace.
+Keep one name across docs and code unless a future migration plan explicitly
+changes it.
+
+## Shape
+
+Raven v1 ships as a Rust CLI, Hermes-backed chat command, slash-command REPL,
+and ratatui TUI contract over local files, Hermes/EverOS memory, Multica watch
+issues, and local verifier receipts.
+
+It owns:
+
+- run packet validation;
+- memory health/search before work starts;
+- lane and mutation-policy visibility;
+- gate visibility and conservative verdict calculation;
+- sanitized JSON snapshot and receipt output;
+- owner packet export;
+- shared Hermes chat adapter across CLI, REPL, and TUI;
+- native-feel and public-safety audit.
+
+It does not own:
+
+- final public storytelling;
+- broad repo orchestration outside the packet;
+- upstream mutation without explicit approval;
+- secrets, host details, or private machine topology.
+
+## Commands
+
+| Command | Input | Output | Gate |
+| --- | --- | --- | --- |
+| `raven status [--json]` | live local files and watch issues | `RavenSnapshot` or compact status | local `PASS` plus remote `BLOCK` renders overall `FLAG` |
+| `raven tui` | terminal | ratatui console with status, rail, active panel, evidence drawer, input line | `RAVEN_TUI_ONCE=1` must render deterministic smoke output |
+| `raven repl` | slash commands | same handlers as CLI | piped smoke stays deterministic |
+| `raven chat send [--cwd ] [--json] [--receipt ] [--save] ` | bounded prompt text | sanitized `HermesChatTurn` or `RavenReceipt` | Hermes failure is `FLAG`, not UI crash; chat receipts cannot green remote deploy |
+| `raven packet show [--json]` | local packet/docs | packet summary | source docs resolve |
+| `raven packet export [--output ]` | snapshot | sanitized owner packet markdown | public-safety sanitizer clean |
+| `raven memory health [--json]` | EverOS bridge | health verdict | provider failure is `FLAG`, not crash |
+| `raven memory search [--json]` | query text | bounded memory refs | empty query is `FLAG` |
+| `raven agents list [--json]` | Multica watch issues | agent/watch table | unavailable Multica falls back to `FLAG` |
+| `raven gates [--json]` | packet + watch evidence | local and remote gate table | hard gates cannot be skipped |
+| `raven research lanes [--json]` | `RAVEN_V2_RESEARCH_LEDGER.md` | bounded research lane list | every lane must end in a packet |
+| `raven research packet [--json] [--output ]` | research ledger + live remote gates | `RavenResearchPacket` | live `DAS-2666/2669` red gates force `FLAG` context |
+| `raven research synthesize [--json] [--output ]` | completed research packets | synthesis readiness report | less than three packets stays `FLAG`; no architecture packet |
+| `raven runs list [--json]` | saved receipts or packet gates | run/receipt table | receipts read from gitignored local dir |
+| `raven sc [all\|status\|sessions\|providers\|worktree] [--json]` | Superconductor socket via thin CLI | `ScReport` or focused view | unavailable socket or merge-base failure is `FLAG`, never a crash |
+| `raven run verify [--receipt ] [--save]` | local run packet | `RavenReceipt` or human output | local verifier cannot green remote deploy |
+| `raven doctor [--json]` | toolchain/files/bridge | dependency report | missing hard local dependency blocks |
+| `raven native-audit [--json]` | source + audit doc | UX/safety gate report | hard UX/safety failure blocks `PASS` |
+
+## Run State
+
+Raven treats the run packet as the source of operational state:
+
+```ts
+type RavenRunState =
+ | "captured"
+ | "dispatching"
+ | "executing"
+ | "reviewing"
+ | "done"
+ | "blocked";
+```
+
+State transitions:
+
+1. `captured` after goal and packet exist.
+2. `dispatching` after lanes and mutation policies are assigned.
+3. `executing` while local code/docs/tests are changing.
+4. `reviewing` after work stops and evidence is being checked.
+5. `done` only when every blocking gate is `pass`.
+6. `blocked` when a blocking gate needs human or external action.
+
+## Memory Behavior
+
+Before execution Raven asks memory for:
+
+- prior owner decisions;
+- known red gates;
+- current memory-provider health;
+- relevant run packets or closeouts.
+
+After execution Raven writes:
+
+- changed files;
+- verification commands and verdicts;
+- unresolved risks;
+- one next action.
+
+The memory loop is proof-backed only when a unique marker can be stored and
+searched back through EverOS.
+
+## Hermes Chat Behavior
+
+The chat surface is a shared adapter, not a second TUI-only path:
+
+- `raven chat send ` executes a single Hermes oneshot turn;
+- `raven repl` accepts `/chat `, `/hermes `, and bare text as
+ Hermes dialogue;
+- `raven tui` exposes a Hermes chat panel with background execution so prompt
+ submission does not block redraw, keyboard handling, or gate visibility.
+
+The adapter injects an explicit Raven working directory into the Hermes process
+and labels it as `case-root` or `case-root/` in public output. It also
+records the detected Hermes runtime, so Codex app-server turns can be separated
+from legacy `auto` turns during review.
+
+Every prompt, response, stderr excerpt, transcript line, receipt excerpt, and
+JSON field goes through Raven's public-safety sanitizer before display or save.
+`--receipt -` prints a sanitized `RavenReceipt`; `--save` writes one under the
+gitignored local runs directory.
+
+## Superconductor Behavior
+
+Raven treats Superconductor as the conductor plane, not as a mutation authority:
+
+- `raven sc status` checks whether the local Superconductor socket responds;
+- `raven sc sessions` lists active chat sessions with provider/model/branch;
+- `raven sc providers` summarizes provider availability without dumping every
+ model into the TUI;
+- `raven sc worktree` reports target/base status and preserves merge-base
+ failures as `FLAG`.
+
+The adapter has a short timeout and only performs read-only calls. It does not
+spawn sessions, select tabs, cancel turns, close sessions, or rewrite the
+Superconductor target branch.
+
+## Research Behavior
+
+Raven v2 research is structured as packets, not freeform notes:
+
+- `raven research lanes` lists the five bounded research lanes from
+ `RAVEN_V2_RESEARCH_LEDGER.md`;
+- `raven research packet ` renders one lane into the required packet
+ shape with live hard-gate evidence attached;
+- `raven research synthesize` only reports readiness until at least three
+ evidence-backed packets exist.
+
+Research packets may recommend v1 implementation slices, but they cannot mark
+remote deploy ready or bypass `DAS-2666` / `DAS-2669`.
+
+## Gate Semantics
+
+`PASS` means the specific requirement was tested at the scope it claims.
+
+`FLAG` means the path is usable but not fully proven, stale, or missing an
+external observation.
+
+`BLOCK` means a required gate failed or needs human approval before continuing.
+
+Raven must not upgrade `FLAG` to `PASS` because nearby tests passed.
+
+Remote EverCore deploy has extra hard rules:
+
+- `DAS-2669` must expose `AUTH_REPAIRED VERDICT: PASS` before the auth block is
+ considered repaired;
+- `DAS-2666` may not render `PASS` unless auth repair, guarded NixOS test,
+ remote loopback full smoke, and supervisor `PASS` are all present;
+- `DAS-2675` can repair Pi/OpenCode adapter lanes, but never changes remote
+ deploy verdict.
+
+## First Artifact
+
+The first Raven artifact is the owner packet rendered from
+`raven/fixtures/doomsday-run.json`.
+
+Current proof command:
+
+```bash
+node bin/raven-run.mjs render raven/fixtures/doomsday-run.json
+```
+
+Current local gate verifier:
+
+```bash
+node bin/raven-run.mjs verify raven/fixtures/doomsday-run.json
+```
+
+This is the repo-local equivalent of:
+
+```bash
+raven run verify
+```
+
+It exits non-zero for `FLAG` or `BLOCK`.
diff --git a/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md
new file mode 100644
index 00000000..7e8b8d9d
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md
@@ -0,0 +1,35 @@
+# Raven Native Feel Audit
+
+## Verdict
+
+PASS for the v1 local terminal console contract.
+
+This audit is Raven-specific. It borrows the discipline of native-feeling CLI
+tools without copying any external reference implementation.
+
+## Categories
+
+| Category | Gate | Current Evidence | Verdict |
+| --- | --- | --- | --- |
+| Latency | Commands must return usable `PASS/FLAG/BLOCK` state without crashing when bridges are absent. | Memory and Multica adapters degrade to `FLAG` or fallback watch state. | PASS |
+| Keybindings | A TUI operator can move without memorizing long commands. | `h`/`c` chat, `i` prompt input, `?`, `:`, `/`, `s`, `p`, `m`, `a`, `g`, `r`, `o` Superconductor, `d`, `n`, `q`, `Esc`, and `Ctrl-C` are handled. | PASS |
+| Focus | The active panel is explicit state. | Panels are `Status`, `Packet`, `Chat`, `Memory`, `Agents`, `Gates`, `Runs`, `Doctor`, `NativeAudit`, and `Help`. | PASS |
+| Scrollback | Evidence remains visible without layout churn. | The evidence drawer stays fixed; deep historical receipts live in `raven/.local-runs/`. | PASS |
+| Interrupt behavior | Interrupts must exit or cancel cleanly. | `Esc` cancels prompt modes; `Ctrl-C` exits the TUI loop. | PASS |
+| REPL history | Interactive command recall should feel local-native. | `rustyline` backs the interactive REPL; piped input stays deterministic for smoke tests. | PASS |
+| Pane stability | Dynamic data cannot resize the command surface unpredictably. | `ratatui` uses fixed status, rail, evidence, and input regions around a flexible active panel. | PASS |
+| Command grammar | CLI and REPL commands share the same operator vocabulary. | Slash commands map to status, packet, chat, memory, agents, gates, runs, doctor, audit, and quit handlers. | PASS |
+| Typed IPC | Machine output is typed and redacted. | `RavenSnapshot`, `RavenReceipt`, `HermesChatTurn`, and `ScReport` are serialized through the sanitizer before JSON printing. | PASS |
+| Evidence visibility | Hard gates and receipts are first-class. | DAS-2666, DAS-2669, local packet gates, saved receipts, and configured verification commands render directly. | PASS |
+| Public-safety redaction | Public output must not expose private paths, hosts/IPs, tokens, credential paths, or signed URLs. | Human and JSON output pass through the sanitizer; receipts store sanitized excerpts. | PASS |
+
+## Hard PASS Blockers
+
+`raven native-audit` must refuse `PASS` when any hard category fails:
+
+- missing keybindings for chat/input/quit/help/palette/search/status/gates/runs/audit;
+- missing stable TUI panes;
+- unsafe interrupt behavior;
+- missing typed JSON snapshot or receipt contracts;
+- unredacted public output;
+- saved receipts not ignored by git.
diff --git a/use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md b/use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md
new file mode 100644
index 00000000..5d6179e7
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md
@@ -0,0 +1,70 @@
+# Raven Concept Packet v0
+
+## Verdict
+
+PASS for the Raven concept artifact.
+
+Raven is the operator-facing name and the repo-local implementation namespace
+for the memory-backed execution lane.
+
+## Naming Contract
+
+| Name | Role | Status |
+| --- | --- | --- |
+| Raven | operator surface, CLI, packet schema, fixtures, and SkillHub install target | implemented v0 surface |
+
+Use Raven everywhere. Do not introduce a second product/internal name unless a
+future migration plan explicitly changes the namespace.
+
+## Product Thesis
+
+Raven is not a chat transcript viewer and not a generic dashboard. It is a
+memory-backed operator surface for focused agent work:
+
+- capture one goal;
+- recall prior decisions and red gates before execution;
+- split work into bounded lanes;
+- preserve mutation policy per lane;
+- verify blocking gates with commands or explicit evidence;
+- export an owner-readable packet.
+
+## First Run Shape
+
+The first Raven run is the Doomsday EverOS lane:
+
+1. Raven concept exploration through the Raven command contract.
+2. EverMe SkillHub MVP packet and read-only mock API.
+3. Hermes/EverOS provider dogfood with store, search, recall, and real Hermes
+ profile verification.
+
+## Interface Wedge
+
+The minimal useful UI is command-grade:
+
+```text
+raven capture
+raven memory search
+raven lane list
+raven gate verify
+raven export
+```
+
+For v0, these map to the existing `raven-run` validator/renderer and the
+`raven/fixtures/doomsday-run.json` packet.
+
+## Guardrails
+
+- Do not expose raw call transcript content.
+- Do not publish private paths, host/IP values, screenshots, tokens, or
+ credential paths.
+- Do not treat remote NixOS deploy as complete until the deploy smoke passes on
+ the remote loopback service.
+- Do not widen Raven into a new major repo before the packet contract earns it.
+
+## Current Evidence
+
+- `raven/COMMAND_CONTRACT.md` defines the v0 command/state/gate contract.
+- `raven/fixtures/doomsday-run.json` records the first focused run.
+- `bin/raven-run.mjs verify raven/fixtures/doomsday-run.json` computes the
+ packet verdict and fails non-zero for open blocking gates.
+- `OWNER_PACKET.md` separates local packet PASS from remote deploy FLAG.
diff --git a/use-cases/hermes-everos-memory/raven/RAVEN_V2_RESEARCH_LEDGER.md b/use-cases/hermes-everos-memory/raven/RAVEN_V2_RESEARCH_LEDGER.md
new file mode 100644
index 00000000..c546e376
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/RAVEN_V2_RESEARCH_LEDGER.md
@@ -0,0 +1,224 @@
+# Raven v2 Research Ledger
+
+## North Star
+
+Raven v2 is the next-generation Agents OS console: a native-feeling terminal
+operating surface where CCB/CCR/Evensong runtime lineage, EverOS memory, Hermes
+skills, and MUW/Superconductor orchestration become one auditable loop.
+
+The goal is not a nicer chat UI. The goal is an operator shell where every
+agent action can resolve to memory, state, packet, gate, receipt, or review.
+
+## Parallel Contract
+
+Raven v1 is the build lane. Raven v2 is the research lane.
+
+Research may run at full speed, but it must not block or rewrite v1 unless it
+produces a concrete, reviewed implementation packet. V2 findings flow into v1
+only through bounded decisions:
+
+- keep;
+- revise;
+- defer;
+- reject;
+- open implementation issue.
+
+## Current Truth
+
+- Local EverOS/Hermes/SkillHub/Raven packet is `PASS`.
+- Remote EverCore deploy remains `FLAG/BLOCK`.
+- `DAS-2666` is the canonical remote deploy gate.
+- `DAS-2669` auth-route repair is accepted through DeepSeek/OpenRouter; parent
+ deploy readiness remains blocked on `DAS-2666` evidence.
+- `DAS-2670` is the current control-room dispatch.
+- `DAS-2675` tracks Pi/OpenCode Multica runtime-adapter repair.
+- Existing Raven v1 build work is dirty in the worktree; do not overwrite it.
+
+## Source Families
+
+### Internal Lineage
+
+- CCB / CCR / Evensong: hackable Claude-Code-like runtime DNA, public evidence
+ harness, retrieval benchmarks, Research Vault MCP, and operator handoff
+ surface.
+- EverOS / EverCore: memory operating layer and multi-tenant memory substrate.
+- Hermes: skills, persistent goals, cron/no-agent jobs, gateway, terminal
+ backends, and memory/profile model.
+- MUW / Superconductor: orchestration, issue gates, runtime control plane,
+ review lanes, and bounded fanout.
+
+### External Reference Families
+
+- `yetone/native-feel-skill`
+ (`https://github.com/yetone/native-feel-skill`): native-feel discipline,
+ decision tree, typed IPC, WebView survival, and ship audit.
+- `superagent-ai/grok-cli` (`https://github.com/superagent-ai/grok-cli`):
+ OpenTUI energy, remote control, sub-agent UX, and interactive/headless split.
+- `openai/codex` (`https://github.com/openai/codex`): smooth terminal REPL
+ baseline, local agent flow, release and packaging discipline.
+- `claude-code-best/claude-code`
+ (`https://github.com/claude-code-best/claude-code`): CCB engineering mine
+ for IPC, ACP, remote control, observability, and runtime hacking.
+- `NousResearch/hermes-agent` (`https://github.com/NousResearch/hermes-agent`):
+ skills, memory, gateway, cron, backend, and portable agent substrate.
+- Anthropic Claude Code / Agent SDK / multi-agent research: subagents with
+ isolated context, parallel research orchestration, sandbox boundaries, and
+ agent harness reuse beyond coding.
+
+## Research Lanes
+
+### Lane 1: Native-Feel TUI/REPL
+
+Question: what makes Raven feel like a native terminal OS surface rather than a
+webby text box?
+
+Research targets:
+
+- latency budget;
+- keyboard grammar;
+- command palette;
+- focus and pane stability;
+- interrupt/resume semantics;
+- scrollback and transcript model;
+- hotkey/muscle-memory identity;
+- shell/TUI/headless mode split.
+
+Output:
+
+- interaction contract;
+- v2 command grammar;
+- native-feel audit adapted for terminal agents.
+
+### Lane 2: Runtime DNA Alignment
+
+Question: how do CCB/CCR/Evensong concepts flow into Raven without turning
+Raven into a fork dump?
+
+Research targets:
+
+- CLI loop;
+- REPL state machine;
+- tool approval model;
+- pipe/ACP/control-plane concepts;
+- telemetry and receipts;
+- public handoff/evidence dashboard;
+- retrieval benchmark receipts.
+
+Output:
+
+- lineage map;
+- implementation boundaries;
+- what Raven owns vs what Evensong owns.
+
+### Lane 3: Memory And Skill Substrate
+
+Question: how should Raven make memory, skills, and goals first-class without
+becoming a noisy memory browser?
+
+Research targets:
+
+- EverOS memory search/store/status;
+- Hermes skills and profiles;
+- persistent goals;
+- cron/no-agent monitoring receipts;
+- provenance fields;
+- memory hit explanations.
+
+Output:
+
+- memory pane contract;
+- skill registry contract;
+- goal/gate model.
+
+### Lane 4: Multi-Agent Orchestration
+
+Question: what is the operator model when many agents are building, reviewing,
+and researching at once?
+
+Research targets:
+
+- MUW issue states;
+- Superconductor runtimes;
+- bounded fanout;
+- subagent context isolation;
+- task delegation and review packets;
+- red-gate routing.
+
+Output:
+
+- control-room state model;
+- dispatch grammar;
+- review lane protocol.
+
+### Lane 5: Evaluation And Safety
+
+Question: how do we know Raven is making the system more legible rather than
+only faster?
+
+Research targets:
+
+- audit trails;
+- failure records;
+- public-safety scan;
+- secret/host/IP redaction;
+- benchmark receipt ingestion;
+- user-visible truth-state transitions;
+- sandbox/permission boundaries.
+
+Output:
+
+- Raven v2 success metrics;
+- red-gate invariants;
+- public-safe artifact checklist.
+
+## Non-Negotiables
+
+- Do not create a new major repo.
+- Do not copy code across incompatible licenses.
+- Do not push, publish, deploy, or close upstream issues without explicit
+ operator approval.
+- Do not expose secrets, private hosts/IPs, credential paths, signed URLs,
+ private env values, or local-machine operational details in public artifacts.
+- Do not turn research into a pile of summaries. Every research lane must end
+ with a decision packet.
+
+## Required Research Packet Shape
+
+```text
+RAVEN_V2_RESEARCH_PACKET
+LANE:
+QUESTION:
+SOURCES:
+FINDINGS:
+DECISIONS:
+V1_IMPACT:
+RISKS:
+NEXT:
+VERDICT: PASS | FLAG | BLOCK
+```
+
+## Executable Harness
+
+Raven v1 exposes the v2 research lane through bounded commands so research
+does not become unreviewable prose:
+
+```bash
+bin/raven research lanes
+bin/raven research packet native-feel --output -
+bin/raven research synthesize
+```
+
+The packet command always carries live hard-gate context. If `DAS-2666` or
+`DAS-2669` are still red, the packet can guide v1 work but cannot claim remote
+readiness.
+
+## First Synthesis Target
+
+Produce `RAVEN_V2_ARCHITECTURE_PACKET.md` only after at least three lanes return
+evidence-backed packets. The architecture packet should decide:
+
+- v2 runtime stack;
+- TUI/REPL interaction model;
+- memory/skill/gate data model;
+- what ships in Raven vs remains in Evensong/Hermes/MUW;
+- the first v2 implementation slice.
diff --git a/use-cases/hermes-everos-memory/raven/README.md b/use-cases/hermes-everos-memory/raven/README.md
new file mode 100644
index 00000000..45bcdeba
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/README.md
@@ -0,0 +1,127 @@
+# Raven Run Packet Contract
+
+`v0` contract for a Raven run.
+
+Raven is the operator-facing concept name and the repo-local command namespace.
+
+Raven is not a marketing page here. This directory defines the packet that a
+CLI/TUI can validate, render, and later execute against Hermes/EverOS memory.
+
+## Files
+
+| File | Purpose |
+| --- | --- |
+| `RAVEN_CONCEPT.md` | Public-safe Raven concept and naming contract |
+| `COMMAND_CONTRACT.md` | Public-safe command/state/gate contract for Raven v1 |
+| `REFERENCE_NOTES.md` | Public-safe reference scan and license notes |
+| `schema.json` | Public-safe JSON Schema for a Raven run packet |
+| `fixtures/doomsday-run.json` | Sample run packet for the current dogfood lane |
+| `../raven-console/` | Rust Raven v1 CLI/REPL/TUI implementation |
+| `../bin/raven` | Local shell wrapper for the Rust console |
+| `../bin/raven-run.mjs` | Local validator and owner-packet renderer |
+
+## Commands
+
+```bash
+node ../bin/raven-run.mjs validate fixtures/doomsday-run.json
+node ../bin/raven-run.mjs render fixtures/doomsday-run.json
+node ../bin/raven-run.mjs verify fixtures/doomsday-run.json
+node ../bin/raven-run.mjs summary fixtures/doomsday-run.json
+```
+
+Console entrypoints:
+
+```bash
+bin/raven --help
+bin/raven status
+bin/raven status --json
+bin/raven packet show
+bin/raven packet export --output -
+bin/raven chat send "summarize current hard gates"
+bin/raven chat send --receipt - "summarize current hard gates"
+bin/raven chat send --save "summarize current hard gates"
+bin/raven memory health
+bin/raven memory search "operator gate"
+bin/raven agents list
+bin/raven gates
+bin/raven research lanes
+bin/raven research packet native-feel
+bin/raven research synthesize
+bin/raven runs list
+bin/raven sc
+bin/raven sc sessions
+bin/raven sc worktree
+bin/raven run verify
+bin/raven run verify --receipt -
+bin/raven native-audit
+bin/raven repl
+RAVEN_TUI_ONCE=1 bin/raven tui
+```
+
+Just targets:
+
+```bash
+just raven-status
+just raven-packet
+just raven-gates
+just raven-agents
+just raven-research-lanes
+just raven-research-packet-smoke
+just raven-research-synthesis
+just raven-doctor
+just raven-native-audit
+just raven-runs
+just raven-sc
+just raven-sc-status
+just raven-sc-sessions
+just raven-sc-providers
+just raven-sc-worktree
+just raven-run-verify
+just raven-chat-smoke
+just raven-chat-receipt-smoke
+just raven-repl-smoke
+just raven-tui-smoke
+just raven-console-check
+```
+
+## Contract
+
+A Raven run packet records:
+
+- the current goal;
+- owners and memory providers;
+- independent lanes;
+- gates with evidence and blocking status;
+- artifacts and next actions.
+
+The computed verdict is conservative:
+
+- `BLOCK` if any blocking gate is `block` or any lane is `block`;
+- `FLAG` if any blocking gate is `flag` or `not_run`, or any lane is `flag` or
+ `active`;
+- `PASS` only when blocking gates and lanes are all pass.
+
+`verify` exits non-zero for `FLAG` or `BLOCK`, so scripts can refuse to call a
+packet complete when blocking gates remain open.
+
+The v1 console keeps local packet truth and remote deploy truth separate:
+
+- local packet `PASS` plus remote hard gate `BLOCK` renders overall `FLAG`, not
+ `PASS`;
+- `DAS-2669` exposes `AUTH_REPAIRED VERDICT: PASS` for the accepted
+ DeepSeek/OpenRouter auth-route repair; that clears only the auth block;
+- `DAS-2666` cannot render `PASS` until auth repair, guarded NixOS test, remote
+ loopback full smoke, and supervisor `PASS` are all present;
+- `DAS-2675` can repair adapter lanes but has no effect on the remote deploy
+ verdict.
+
+Hermes dialogue is shared across surfaces: `raven chat send`, bare text or
+`/chat` inside `raven repl`, and the `h` panel inside `raven tui` all use the
+same sanitized adapter. The adapter records the public-safe Raven workspace
+label, detected Hermes runtime, command shape, and sanitized transcript. Chat
+receipts can be printed with `--receipt -` or saved with `--save`; they never
+change remote deploy gate state.
+
+Superconductor state is visible through `raven sc`. The adapter is read-only,
+times out quickly, and turns socket or merge-base failures into `FLAG` evidence
+instead of blocking the Raven console.
diff --git a/use-cases/hermes-everos-memory/raven/REFERENCE_NOTES.md b/use-cases/hermes-everos-memory/raven/REFERENCE_NOTES.md
new file mode 100644
index 00000000..d2672d4a
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/REFERENCE_NOTES.md
@@ -0,0 +1,53 @@
+# Raven Console Reference Notes
+
+Reference scan date: 2026-05-15.
+
+No source code was copied or vendored. These notes record license posture and
+portable UX/architecture lessons only.
+
+## yetone/native-feel-skill
+
+- License: MIT, verified from `LICENSE` in the shallow clone.
+- Useful lesson: optimize for identity and operator muscle memory. Raven should
+ keep a stable command shape and fast repeated verbs instead of exposing every
+ internal subsystem as a new surface.
+- Copied code: none.
+
+## superagent-ai/grok-cli
+
+- License: MIT, verified from `LICENSE` in the shallow clone.
+- Useful lesson: split interactive and headless flows cleanly. Raven v0 keeps
+ `repl`/`tui` interactive entrypoints while preserving scriptable commands
+ like `status`, `packet show`, `memory search`, and `run verify`.
+- Useful lesson: surface sub-agent and verification state as first-class
+ operator data, but do not pretend failed runtimes are healthy.
+- Copied code: none.
+
+## openai/codex
+
+- License: Apache-2.0, verified from `LICENSE` in the shallow clone.
+- Useful lesson: a Rust CLI multitool can own command routing while a TUI is a
+ separate operator shell. Raven v0 follows that split with a Rust command core
+ and an ANSI-only first TUI screen.
+- Useful lesson: local sandbox/deploy policy belongs in visible status, not in
+ hidden assumptions.
+- Copied code: none.
+
+## claude-code-best/claude-code
+
+- License: unresolved in the shallow checkout. The README advertises a GitHub
+ license badge, but no `LICENSE` file was present in the cloned tree.
+- Useful lesson: slash commands and provider-login surfaces are familiar to
+ operators, but license uncertainty means this repo was used only for broad
+ product-pattern inspiration.
+- Copied code: none.
+
+## NousResearch/hermes-agent
+
+- License: MIT, verified from `LICENSE` in the shallow clone.
+- Useful lesson: keep CLI, messaging, providers, memory, and skill systems as
+ distinct adapter layers. Raven should be the console over those layers, not a
+ replacement provider or another agent runtime.
+- Useful lesson: slash command routing, provider status, and memory search are
+ the right primitives for v0.
+- Copied code: none.
diff --git a/use-cases/hermes-everos-memory/raven/fixtures/doomsday-run.json b/use-cases/hermes-everos-memory/raven/fixtures/doomsday-run.json
new file mode 100644
index 00000000..f504de38
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/fixtures/doomsday-run.json
@@ -0,0 +1,148 @@
+{
+ "id": "raven.everme-doomsday-run",
+ "title": "Raven / EverMe Doomsday Run",
+ "goal": "Turn the source conversation into a focused Raven concept, EverMe SkillHub, and Hermes/EverOS dogfood execution lane with auditable local artifacts. Raven is the concept, internal, and command namespace.",
+ "status": "done",
+ "owners": ["codex", "pi", "opencode", "hermes"],
+ "memory_providers": ["everos", "hermes"],
+ "lanes": [
+ {
+ "id": "raven-concept",
+ "owner": "pi",
+ "scope": "Raven taste, naming, story, interface wedge, and first public artifact through the Raven v0 compatibility surface.",
+ "mutation_policy": "read_only",
+ "verdict": "pass",
+ "evidence_refs": [
+ "use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md",
+ "use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md",
+ "use-cases/hermes-everos-memory/raven/README.md"
+ ]
+ },
+ {
+ "id": "everme-skillhub",
+ "owner": "codex",
+ "scope": "Portable skill packet, mock API, and dogfood import surface.",
+ "mutation_policy": "local_only",
+ "verdict": "pass",
+ "evidence_refs": [
+ "use-cases/hermes-everos-memory/skillhub/schema.json",
+ "use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md",
+ "use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json",
+ "use-cases/hermes-everos-memory/bin/skillhub-mock-api.mjs"
+ ]
+ },
+ {
+ "id": "hermes-everos-dogfood",
+ "owner": "hermes",
+ "scope": "Provider-level load, health, store, search, and recall gates.",
+ "mutation_policy": "local_only",
+ "verdict": "pass",
+ "evidence_refs": [
+ "use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh",
+ "use-cases/hermes-everos-memory/bin/mock-openai-compatible.mjs",
+ "use-cases/hermes-everos-memory/README.md"
+ ]
+ }
+ ],
+ "gates": [
+ {
+ "id": "provider-load",
+ "name": "Hermes provider load",
+ "status": "pass",
+ "command": "just provider-load",
+ "evidence": "Provider class loads and exposes everos memory tools.",
+ "blocks_completion": true
+ },
+ {
+ "id": "skillhub-api",
+ "name": "SkillHub mock API",
+ "status": "pass",
+ "command": "just skillhub-api-smoke",
+ "evidence": "Mock API serves health, target-filtered skill list, and rendered packet.",
+ "blocks_completion": true
+ },
+ {
+ "id": "full-memory-loop",
+ "name": "Full store/search/recall loop",
+ "status": "pass",
+ "command": "just dogfood-smoke full",
+ "evidence": "PASS provider_load, health, store, flush, search count=1, and prefetch chars=219 with local mock inference server.",
+ "blocks_completion": true
+ },
+ {
+ "id": "real-hermes-profile-turn",
+ "name": "Real Hermes profile turn",
+ "status": "pass",
+ "command": "hermes -z with a unique Raven marker, then EverOS search",
+ "evidence": "Hermes profile reports provider=everos, explicit everos_store marker searchable, and sync_turn marker searchable.",
+ "blocks_completion": true
+ },
+ {
+ "id": "raven-gate-verify",
+ "name": "Raven gate verifier",
+ "status": "pass",
+ "command": "just raven-verify",
+ "evidence": "raven-run verify renders blocking gates and exits non-zero for FLAG/BLOCK.",
+ "blocks_completion": true
+ },
+ {
+ "id": "skillhub-real-skill-import",
+ "name": "SkillHub real skill import",
+ "status": "pass",
+ "command": "just skillhub-import-sample && just skillhub-views skillhub/fixtures/evoagentbench-musician-life-event.json",
+ "evidence": "A real EvoAgentBench SKILL.md imports into a valid SkillHub packet and renders five MVP views.",
+ "blocks_completion": true
+ }
+ ],
+ "artifacts": [
+ {
+ "path": "use-cases/hermes-everos-memory/skillhub/schema.json",
+ "purpose": "EverMe SkillHub packet contract.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md",
+ "purpose": "EverMe SkillHub MVP view/API/implementation plan.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json",
+ "purpose": "Real EvoAgentBench skill import fixture.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/raven/schema.json",
+ "purpose": "Raven run packet contract.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md",
+ "purpose": "Raven concept and naming contract.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md",
+ "purpose": "Raven command/state/gate contract.",
+ "public_safe": true
+ },
+ {
+ "path": "use-cases/hermes-everos-memory/OWNER_PACKET.md",
+ "purpose": "Owner-readable PASS/FLAG review packet.",
+ "public_safe": true
+ }
+ ],
+ "evidence_refs": [
+ "use-cases/hermes-everos-memory/OWNER_PACKET.md",
+ "use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md",
+ "use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md",
+ "use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md",
+ "use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json",
+ "use-cases/hermes-everos-memory/bin/raven-run.mjs verify",
+ "use-cases/hermes-everos-memory/scripts/skillhub-api-smoke.sh",
+ "use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh",
+ "use-cases/hermes-everos-memory/README.md"
+ ],
+ "next_actions": [
+ "Promote the NixOS EverCore packet to an observed remote smoke."
+ ]
+}
diff --git a/use-cases/hermes-everos-memory/raven/schema.json b/use-cases/hermes-everos-memory/raven/schema.json
new file mode 100644
index 00000000..53d08d23
--- /dev/null
+++ b/use-cases/hermes-everos-memory/raven/schema.json
@@ -0,0 +1,156 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://everos.local/schemas/raven-run-packet-v0.json",
+ "title": "Raven Run Packet",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "title",
+ "goal",
+ "status",
+ "owners",
+ "memory_providers",
+ "lanes",
+ "gates",
+ "artifacts",
+ "evidence_refs",
+ "next_actions"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 160
+ },
+ "goal": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000
+ },
+ "status": {
+ "type": "string",
+ "enum": ["captured", "dispatching", "executing", "reviewing", "done", "blocked"]
+ },
+ "owners": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 80},
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "memory_providers": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 80},
+ "uniqueItems": true
+ },
+ "lanes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "owner", "scope", "mutation_policy", "verdict"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$"
+ },
+ "owner": {
+ "type": "string",
+ "enum": ["codex", "pi", "opencode", "hermes", "muw", "human"]
+ },
+ "scope": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "mutation_policy": {
+ "type": "string",
+ "enum": ["read_only", "local_only", "external_requires_approval"]
+ },
+ "verdict": {
+ "type": "string",
+ "enum": ["pass", "flag", "block", "active"]
+ },
+ "evidence_refs": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 300},
+ "uniqueItems": true
+ }
+ }
+ },
+ "minItems": 1
+ },
+ "gates": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "name", "status", "evidence", "blocks_completion"],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160
+ },
+ "status": {
+ "type": "string",
+ "enum": ["pass", "flag", "block", "not_run"]
+ },
+ "command": {
+ "type": "string",
+ "maxLength": 300
+ },
+ "evidence": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 800
+ },
+ "blocks_completion": {
+ "type": "boolean"
+ }
+ }
+ },
+ "minItems": 1
+ },
+ "artifacts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["path", "purpose", "public_safe"],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "purpose": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 400
+ },
+ "public_safe": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "evidence_refs": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 300},
+ "uniqueItems": true
+ },
+ "next_actions": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 300}
+ }
+ }
+}
diff --git a/use-cases/hermes-everos-memory/scripts/check-provider-load.sh b/use-cases/hermes-everos-memory/scripts/check-provider-load.sh
new file mode 100755
index 00000000..11a0dfb6
--- /dev/null
+++ b/use-cases/hermes-everos-memory/scripts/check-provider-load.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+PLUGIN_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
+HERMES_AGENT_SRC="${HERMES_AGENT_SRC:-${HOME}/.hermes/hermes-agent}"
+
+if [[ ! -d "${HERMES_AGENT_SRC}" ]]; then
+ echo "Hermes agent source not found. Set HERMES_AGENT_SRC." >&2
+ exit 2
+fi
+
+TMP_HOME="$(mktemp -d)"
+cleanup() {
+ rm -rf "${TMP_HOME}"
+}
+trap cleanup EXIT
+
+mkdir -p "${TMP_HOME}/plugins/everos"
+cp "${PLUGIN_DIR}/__init__.py" "${PLUGIN_DIR}/plugin.yaml" "${TMP_HOME}/plugins/everos/"
+
+HERMES_HOME="${TMP_HOME}" PYTHONPATH="${HERMES_AGENT_SRC}" python3 - <<'PY'
+from plugins.memory import discover_memory_providers, load_memory_provider
+
+names = [name for name, _, _ in discover_memory_providers()]
+assert "everos" in names, names
+
+provider = load_memory_provider("everos")
+assert provider is not None
+assert provider.name == "everos"
+
+tools = [schema["name"] for schema in provider.get_tool_schemas()]
+expected = {"everos_search", "everos_store", "everos_health", "everos_flush"}
+assert expected.issubset(set(tools)), tools
+
+print("PASS provider-load everos " + ",".join(tools))
+PY
diff --git a/use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh b/use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh
new file mode 100755
index 00000000..33c32e0f
--- /dev/null
+++ b/use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ENV_FILE="${EVERCORE_ENV_FILE:-deploy/nixos/evercore.env.example}"
+REQUIRE_KEY=0
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/deepseek-auth-preflight.sh [--env ] [--require-key]
+
+Checks that the EverCore remote LLM auth path is pinned to DeepSeek through
+OpenRouter without printing any credential value.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --env)
+ ENV_FILE="${2:-}"
+ shift 2
+ ;;
+ --require-key)
+ REQUIRE_KEY=1
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "BLOCK unknown_arg=$1" >&2
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+if [[ -z "${ENV_FILE}" || ! -f "${ENV_FILE}" ]]; then
+ echo "BLOCK env_file_missing"
+ exit 1
+fi
+
+get_env() {
+ local key="$1"
+ local raw
+ raw="$(grep -E "^${key}=" "${ENV_FILE}" | tail -n 1 || true)"
+ raw="${raw#*=}"
+ raw="${raw%$'\r'}"
+ printf '%s' "${raw}"
+}
+
+LLM_PROVIDER="$(get_env LLM_PROVIDER)"
+LLM_MODEL="$(get_env LLM_MODEL)"
+LLM_OPENROUTER_PROVIDER="$(get_env LLM_OPENROUTER_PROVIDER)"
+LLM_BASE_URL="$(get_env LLM_BASE_URL)"
+OPENROUTER_BASE_URL="$(get_env OPENROUTER_BASE_URL)"
+OPENROUTER_API_KEY="$(get_env OPENROUTER_API_KEY)"
+
+failures=()
+
+[[ "${LLM_PROVIDER}" == "openrouter" ]] || failures+=("LLM_PROVIDER must be openrouter")
+[[ "${LLM_MODEL}" == deepseek/* ]] || failures+=("LLM_MODEL must be a deepseek/* OpenRouter model")
+[[ "${LLM_OPENROUTER_PROVIDER}" == "deepseek" ]] || failures+=("LLM_OPENROUTER_PROVIDER must be deepseek")
+[[ "${LLM_BASE_URL}" == "https://openrouter.ai/api/v1" ]] || failures+=("LLM_BASE_URL must be OpenRouter")
+[[ "${OPENROUTER_BASE_URL}" == "https://openrouter.ai/api/v1" ]] || failures+=("OPENROUTER_BASE_URL must be OpenRouter")
+
+if [[ "${REQUIRE_KEY}" -eq 1 ]]; then
+ if [[ -z "${OPENROUTER_API_KEY}" || "${OPENROUTER_API_KEY}" == "change-me" ]]; then
+ failures+=("OPENROUTER_API_KEY must be present and non-placeholder")
+ fi
+fi
+
+if [[ "${#failures[@]}" -gt 0 ]]; then
+ echo "BLOCK deepseek_auth_preflight"
+ for failure in "${failures[@]}"; do
+ echo "- ${failure}"
+ done
+ exit 1
+fi
+
+echo "PASS deepseek_auth_preflight provider=openrouter model=${LLM_MODEL} route=deepseek key_check=$([[ "${REQUIRE_KEY}" -eq 1 ]] && echo required || echo skipped)"
diff --git a/use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh b/use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh
new file mode 100755
index 00000000..dcc882d8
--- /dev/null
+++ b/use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh
@@ -0,0 +1,196 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+PLUGIN_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
+HERMES_AGENT_SRC="${HERMES_AGENT_SRC:-${HOME}/.hermes/hermes-agent}"
+MODE="provider-only"
+
+usage() {
+ cat <<'USAGE'
+Usage: dogfood-smoke.sh [--mode provider-only|health|full]
+
+Modes:
+ provider-only Load the Hermes provider and verify schemas. No EverCore required.
+ health Load provider and require EverCore /health to be reachable.
+ full Require health, store one turn, then search/prefetch.
+
+Environment:
+ HERMES_AGENT_SRC Hermes agent source checkout. Default ~/.hermes/hermes-agent
+ EVEROS_API_BASE_URL EverCore base URL. Default http://127.0.0.1:1995
+ EVEROS_USER_ID EverOS user id for smoke. Default hermes-dogfood-smoke
+ EVEROS_AGENT_ID EverOS agent id. Default hermes-dogfood
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --mode)
+ MODE="${2:-}"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "unknown argument: $1" >&2
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+case "${MODE}" in
+ provider-only|health|full) ;;
+ *)
+ echo "invalid mode: ${MODE}" >&2
+ exit 2
+ ;;
+esac
+
+if [[ ! -d "${HERMES_AGENT_SRC}" ]]; then
+ echo "BLOCK hermes_agent_src_missing"
+ exit 2
+fi
+
+MODE="${MODE}" \
+PLUGIN_DIR="${PLUGIN_DIR}" \
+EVEROS_USER_ID="${EVEROS_USER_ID:-hermes-dogfood-smoke}" \
+EVEROS_AGENT_ID="${EVEROS_AGENT_ID:-hermes-dogfood}" \
+PYTHONPATH="${HERMES_AGENT_SRC}" \
+python3 - <<'PY'
+import importlib.util
+import json
+import os
+import pathlib
+import sys
+import time
+
+mode = os.environ["MODE"]
+plugin_dir = pathlib.Path(os.environ["PLUGIN_DIR"])
+module_path = plugin_dir / "__init__.py"
+
+spec = importlib.util.spec_from_file_location("everos_memory_provider_smoke", module_path)
+if spec is None or spec.loader is None:
+ print("BLOCK provider_spec_unavailable")
+ sys.exit(1)
+
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+
+provider = module.EverOSMemoryProvider()
+provider.initialize(
+ session_id=f"hermes-dogfood-smoke-{int(time.time())}",
+ agent_identity="dogfood",
+ user_id=os.environ["EVEROS_USER_ID"],
+)
+
+tools = {schema["name"] for schema in provider.get_tool_schemas()}
+expected = {"everos_search", "everos_store", "everos_health", "everos_flush"}
+missing = sorted(expected - tools)
+if missing:
+ print("BLOCK provider_schema_missing " + ",".join(missing))
+ sys.exit(1)
+
+prompt = provider.system_prompt_block()
+if "EverOS Memory" not in prompt:
+ print("BLOCK provider_prompt_missing")
+ sys.exit(1)
+
+print("PASS provider_load tools=" + ",".join(sorted(tools)))
+
+if mode == "provider-only":
+ sys.exit(0)
+
+if not provider.is_available():
+ print("BLOCK evercore_unavailable")
+ sys.exit(1)
+
+health_raw = provider.handle_tool_call("everos_health", {})
+try:
+ health = json.loads(health_raw)
+except json.JSONDecodeError:
+ print("BLOCK health_non_json")
+ sys.exit(1)
+
+status = (health.get("result") or {}).get("status", "unknown")
+if status not in {"healthy", "ok"}:
+ print(f"BLOCK health_status={status}")
+ sys.exit(1)
+
+print("PASS health status=" + status)
+
+if mode == "health":
+ sys.exit(0)
+
+stamp = int(time.time())
+needle = f"Hermes EverOS dogfood smoke Raven SkillHub {stamp}"
+
+store_raw = provider.handle_tool_call(
+ "everos_store",
+ {
+ "role": "user",
+ "content": (
+ needle
+ + ": provider-level store/search/recall smoke for Raven and EverMe SkillHub."
+ ),
+ },
+)
+try:
+ store = json.loads(store_raw)
+except json.JSONDecodeError:
+ print("BLOCK store_non_json")
+ sys.exit(1)
+
+if store.get("result") != "stored":
+ print("BLOCK store_failed")
+ sys.exit(1)
+
+print("PASS store result=stored")
+
+flush_raw = provider.handle_tool_call("everos_flush", {})
+try:
+ flush = json.loads(flush_raw)
+except json.JSONDecodeError:
+ print("BLOCK flush_non_json")
+ sys.exit(1)
+
+if flush.get("result") != "flushed":
+ print("BLOCK flush_failed")
+ sys.exit(1)
+
+print("PASS flush result=flushed")
+
+time.sleep(2)
+
+search_raw = provider.handle_tool_call(
+ "everos_search",
+ {
+ "query": needle,
+ "top_k": 5,
+ "memory_types": ["episodic_memory", "profile", "agent_memory", "raw_message"],
+ },
+)
+try:
+ search = json.loads(search_raw)
+except json.JSONDecodeError:
+ print("BLOCK search_non_json")
+ sys.exit(1)
+
+count = int(search.get("count") or 0)
+if count < 1:
+ print("BLOCK search_count=0")
+ sys.exit(1)
+
+print(f"PASS search count={count}")
+
+prefetch = provider.prefetch(needle)
+if not prefetch:
+ print("BLOCK prefetch_empty")
+ sys.exit(1)
+
+print("PASS prefetch chars=" + str(len(prefetch)))
+
+provider.shutdown()
+PY
diff --git a/use-cases/hermes-everos-memory/scripts/install-local.sh b/use-cases/hermes-everos-memory/scripts/install-local.sh
new file mode 100755
index 00000000..553d2744
--- /dev/null
+++ b/use-cases/hermes-everos-memory/scripts/install-local.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
+PLUGIN_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
+HERMES_HOME="${HERMES_HOME:-${HOME}/.hermes}"
+DEST_DIR="${HERMES_HOME}/plugins/everos"
+
+mkdir -p "${DEST_DIR}"
+
+for item in __init__.py plugin.yaml README.md bin package.json justfile scripts skillhub raven deploy; do
+ rm -rf "${DEST_DIR}/${item}"
+ cp -R "${PLUGIN_DIR}/${item}" "${DEST_DIR}/${item}"
+done
+
+echo "Installed Hermes EverOS memory provider to active Hermes plugins dir."
+echo "Activate with: hermes config set memory.provider everos"
diff --git a/use-cases/hermes-everos-memory/scripts/skillhub-api-smoke.sh b/use-cases/hermes-everos-memory/scripts/skillhub-api-smoke.sh
new file mode 100755
index 00000000..880d1637
--- /dev/null
+++ b/use-cases/hermes-everos-memory/scripts/skillhub-api-smoke.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+PORT="${SKILLHUB_PORT:-18765}"
+HOST="${SKILLHUB_HOST:-127.0.0.1}"
+TMP_DIR="$(mktemp -d)"
+
+cleanup() {
+ if [[ -n "${SERVER_PID:-}" ]]; then
+ kill "$SERVER_PID" 2>/dev/null || true
+ wait "$SERVER_PID" 2>/dev/null || true
+ fi
+ rm -rf "$TMP_DIR"
+}
+trap cleanup EXIT
+
+node "$ROOT_DIR/bin/skillhub-mock-api.mjs" \
+ --host "$HOST" \
+ --port "$PORT" \
+ >"$TMP_DIR/server.log" \
+ 2>&1 &
+SERVER_PID="$!"
+
+for _ in 1 2 3 4 5 6 7 8 9 10; do
+ if curl -fsS "http://$HOST:$PORT/health" >"$TMP_DIR/health.json" 2>/dev/null; then
+ break
+ fi
+ sleep 0.2
+done
+
+curl -fsS "http://$HOST:$PORT/health" >"$TMP_DIR/health.json"
+curl -fsS "http://$HOST:$PORT/skills?target=hermes" >"$TMP_DIR/skills.json"
+curl -fsS "http://$HOST:$PORT/skills/raven.operator-memory-recall/render" \
+ >"$TMP_DIR/render.md"
+curl -fsS "http://$HOST:$PORT/skills/raven.operator-memory-recall/views" \
+ >"$TMP_DIR/views.json"
+curl -fsS "http://$HOST:$PORT/skills/raven.operator-memory-recall/install-packet?target=hermes" \
+ >"$TMP_DIR/install-packet.json"
+
+rg '"ok": true' "$TMP_DIR/health.json" >/dev/null
+rg 'raven.operator-memory-recall' "$TMP_DIR/skills.json" >/dev/null
+rg 'Operator Memory Recall' "$TMP_DIR/render.md" >/dev/null
+rg '"views_markdown":' "$TMP_DIR/views.json" >/dev/null
+rg 'Trust Panel' "$TMP_DIR/views.json" >/dev/null
+rg '"install_packet":' "$TMP_DIR/install-packet.json" >/dev/null
+rg '"target": "hermes"' "$TMP_DIR/install-packet.json" >/dev/null
+
+printf 'PASS skillhub mock api smoke host=%s port=%s\n' "$HOST" "$PORT"
diff --git a/use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md b/use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md
new file mode 100644
index 00000000..a4149665
--- /dev/null
+++ b/use-cases/hermes-everos-memory/skillhub/MVP_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,92 @@
+# EverMe SkillHub MVP Implementation Plan v0
+
+SkillHub is the memory-backed skill surface for EverMe. It is not a generic
+marketplace. The MVP makes skills legible, installable, improvable, and
+evidence-bearing before final EverMe product UI is available.
+
+## Product Contract
+
+SkillHub v0 manages portable skill packets.
+
+Each packet must answer:
+
+- What is this skill for?
+- Where can it install?
+- What evidence says it works?
+- What should improve next?
+- Can it be shared safely?
+
+## MVP Views
+
+| View | Purpose | Minimum fields |
+| --- | --- | --- |
+| Skill Index | scan owned/community/needs-eval skills | name, status, version, domains, targets |
+| Skill Detail | understand one skill | summary, body, source, evidence |
+| Evolution Queue | decide next improvement | status, eval score, last evolved, evidence gaps |
+| Install Packet | connect to runtime | install targets, version, body markdown |
+| Trust Panel | decide whether to use/share | provenance, evidence refs, rating, votes |
+
+The CLI renderer should expose these views before any final web UI exists.
+
+## API Contract
+
+The mock API is read-only until EverMe backend constraints arrive.
+
+Current routes:
+
+- `GET /health`
+- `GET /skills`
+- `GET /skills?target=hermes`
+- `GET /skills/:id`
+- `GET /skills/:id/render`
+- `GET /skills/:id/views`
+- `GET /skills/:id/install-packet?target=hermes`
+- `POST /skills/validate`
+
+Next API routes:
+
+- `POST /skills/:id/evidence`
+- `POST /skills/:id/evolution-note`
+
+Write routes stay proposed until the canonical EverMe API exists.
+
+## Data Additions
+
+Keep the packet compact. Add optional fields only when they support the five
+MVP views:
+
+- `provenance`: source runtime, extractor, source artifact id;
+- `evolution_history`: timestamped notes, eval deltas, next action;
+- `compatibility`: runtime names and minimum versions;
+- `install_notes`: target-specific setup notes;
+- `trust`: rating, votes, eval score, evidence summary.
+
+The existing core packet stays valid without these fields.
+
+## Implementation Sequence
+
+1. Extend the local renderer with a `views` command.
+2. Add `just skillhub-views`.
+3. Keep the mock API read-only and deterministic.
+4. Import one real `SKILL.md` file through `from-skill`.
+5. Add optional data fields only after the renderer proves their use.
+6. Wait for EverMe design-system input before building final visual UI.
+
+## Gates
+
+| Gate | Verdict rule |
+| --- | --- |
+| Packet validation | schema and custom validator pass |
+| Views render | five MVP views render from one packet |
+| Mock API | health/list/detail/render/validate pass |
+| Import path | `from-skill` produces a valid packet |
+| Public safety | no secrets, host details, private paths, or raw tokens |
+
+## First Useful Slice
+
+```bash
+node bin/skillhub-packet.mjs views skillhub/fixtures/raven-skillhub-sample.json
+```
+
+This is enough for Raven, Hermes, and EverCore to dogfood SkillHub without
+pretending the final EverMe UI is done.
diff --git a/use-cases/hermes-everos-memory/skillhub/README.md b/use-cases/hermes-everos-memory/skillhub/README.md
new file mode 100644
index 00000000..fd209840
--- /dev/null
+++ b/use-cases/hermes-everos-memory/skillhub/README.md
@@ -0,0 +1,96 @@
+# SkillHub Packet Contract
+
+`v0` packet contract for EverMe SkillHub dogfooding.
+
+This is not the final EverMe UI. It is the portable skill object that Raven,
+Hermes, and EverCore can exchange before the cloud/product API is finalized.
+
+## Files
+
+| File | Purpose |
+| --- | --- |
+| `schema.json` | Public-safe JSON Schema for one SkillHub packet |
+| `fixtures/raven-skillhub-sample.json` | Sample packet used by smoke tests |
+| `fixtures/evoagentbench-musician-life-event.json` | Real `SKILL.md` import fixture |
+| `../bin/skillhub-packet.mjs` | Local exporter/validator helper |
+| `../bin/skillhub-mock-api.mjs` | Read-only HTTP adapter for Raven/Hermes dogfood |
+| `MVP_IMPLEMENTATION_PLAN.md` | Public-safe MVP view/API/implementation contract |
+
+## Contract
+
+A SkillHub packet represents one skill plus enough provenance for an agent
+runtime to decide whether it can install or use it.
+
+Required fields:
+
+- `id`
+- `name`
+- `summary`
+- `visibility`
+- `status`
+- `version`
+- `source`
+- `domains`
+- `install_targets`
+- `evidence_refs`
+- `body_markdown`
+
+## Local Commands
+
+Validate the sample:
+
+```bash
+node ../bin/skillhub-packet.mjs validate fixtures/raven-skillhub-sample.json
+node ../bin/skillhub-packet.mjs render fixtures/raven-skillhub-sample.json
+node ../bin/skillhub-packet.mjs views fixtures/raven-skillhub-sample.json
+node ../bin/skillhub-mock-api.mjs --check
+```
+
+Export an existing `SKILL.md` file:
+
+```bash
+node ../bin/skillhub-packet.mjs from-skill \
+ ../../benchmarks/EvoAgentBench/src/skill_evolution/evermemos/skills_sample/MUSICIAN/musician_life_event/SKILL.md
+```
+
+The imported fixture is checked with:
+
+```bash
+node ../bin/skillhub-packet.mjs validate fixtures/evoagentbench-musician-life-event.json
+node ../bin/skillhub-packet.mjs views fixtures/evoagentbench-musician-life-event.json
+```
+
+## Dogfood Path
+
+1. EverCore extracts or stores a skill.
+2. SkillHub exports it as this packet.
+3. Hermes/Raven imports the packet as an installable skill or memory-backed
+ runtime hint.
+4. Evaluation evidence updates `evidence_refs`, `eval_score`, and `status`.
+
+## Mock API
+
+The mock API is a local read-only adapter over packet JSON files. It exists so
+Raven, Hermes, or EverCore clients can prove their integration contract before
+the final EverMe backend exists.
+
+```bash
+node ../bin/skillhub-mock-api.mjs --port 18765
+```
+
+Smoke the routes:
+
+```bash
+../scripts/skillhub-api-smoke.sh
+```
+
+Routes:
+
+- `GET /health`
+- `GET /skills`
+- `GET /skills?target=hermes`
+- `GET /skills/:id`
+- `GET /skills/:id/render`
+- `GET /skills/:id/views`
+- `GET /skills/:id/install-packet?target=hermes`
+- `POST /skills/validate`
diff --git a/use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json b/use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json
new file mode 100644
index 00000000..23828fdb
--- /dev/null
+++ b/use-cases/hermes-everos-memory/skillhub/fixtures/evoagentbench-musician-life-event.json
@@ -0,0 +1,25 @@
+{
+ "id": "everme-evoagentbench.musician_life_event",
+ "name": "musician_life_event",
+ "summary": "Use unique personal life events (not musical works) as the primary search anchor for identifying musicians.",
+ "owner_id": "everme-evoagentbench",
+ "visibility": "private",
+ "status": "needs_eval",
+ "version": "0.1.0",
+ "source": "evercore_extracted",
+ "domains": [
+ "musician"
+ ],
+ "install_targets": [
+ "hermes"
+ ],
+ "evidence_refs": [
+ "benchmarks/EvoAgentBench/src/skill_evolution/evermemos/skills_sample/MUSICIAN/musician_life_event/SKILL.md"
+ ],
+ "body_markdown": "# Musician: Life Event Anchoring\n\n## When to use\nWhen a musician question describes distinctive personal experiences rather than discography clues.\n\n## Technique\nThe most distinctive information about musicians is often unique life events, not musical works. Life events are far more unique and searchable than album/song names.\n\nPriority ranking for biographical constraints:\n1. **Cause of death / special events:** \"died of professional negligence\", \"triple bypass surgery\" — extremely rare, almost always a direct hit\n2. **Personal life details:** \"five children\", \"divorced twice\", \"searched for biological father at age 17\"\n3. **Education / career turning points:** \"dropped out of college to pursue music\"\n4. **Work characteristics:** \"first album songs written by Boris Vian\" — use only when life events are unavailable\n\nAlso track auxiliary figures mentioned in questions: songwriters, doctors, family members.\n\n## Query Templates\n- `\"musician [cause of death] charged sentenced [year]\"`\n- `\"[musician name] \"divorced\" \"children\" wife\"`\n- `\"singer dropped out [school type] pursue music [country]\"`\n\n## Worked Examples\n**Dr. Kang Se Hoon:** Question mentioned hobbies of reading comics and playing computer games. Searched `\"Shin Hae-chul solo album 1990s radio DJ hobbies comics computer games\"` → then tracked the doctor: `\"Shin Hae-chul Kang Se-hoon charged professional negligence death sentenced\"`.\n\n**Vanic:** Born in May, dropped out of school for music production. After narrowing candidates, verified with `\"Vanic Jesse born May 1987 1988 1989 1990 Vancouver producer\"`.\n\n## Anti-pattern\nOver-relying on album or song name searches. Album names are often not distinctive enough (\"First Album\", \"The Light\"). Anchor with life events first, then use discography only for verification.",
+ "frontmatter": {
+ "name": "musician_life_event",
+ "description": "Use unique personal life events (not musical works) as the primary search anchor for identifying musicians.",
+ "always": true
+ }
+}
diff --git a/use-cases/hermes-everos-memory/skillhub/fixtures/raven-skillhub-sample.json b/use-cases/hermes-everos-memory/skillhub/fixtures/raven-skillhub-sample.json
new file mode 100644
index 00000000..9fbd8693
--- /dev/null
+++ b/use-cases/hermes-everos-memory/skillhub/fixtures/raven-skillhub-sample.json
@@ -0,0 +1,19 @@
+{
+ "id": "raven.operator-memory-recall",
+ "name": "Operator Memory Recall",
+ "summary": "Recall prior project decisions before a Raven or Hermes run starts.",
+ "owner_id": "everme-local",
+ "visibility": "private",
+ "status": "needs_eval",
+ "version": "0.1.0",
+ "source": "manual",
+ "domains": ["agent-ops", "memory", "software-engineering"],
+ "install_targets": ["hermes", "raven", "evercore"],
+ "eval_score": 0,
+ "rating": 0,
+ "votes": 0,
+ "evidence_refs": [
+ "use-cases/hermes-everos-memory/scripts/dogfood-smoke.sh --mode provider-only"
+ ],
+ "body_markdown": "# Operator Memory Recall\n\n## When to use\nBefore an agent run starts, search durable project memory for prior decisions, constraints, and open gates.\n\n## Technique\nUse a short query that includes the project name, the active goal, and the decision surface. Prefer evidence-backed memories over summary-only memories.\n\n## Anti-pattern\nDo not call memory recall a PASS unless the current run proves health, store, search, and recall."
+}
diff --git a/use-cases/hermes-everos-memory/skillhub/schema.json b/use-cases/hermes-everos-memory/skillhub/schema.json
new file mode 100644
index 00000000..28370740
--- /dev/null
+++ b/use-cases/hermes-everos-memory/skillhub/schema.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://everos.local/schemas/skillhub-packet-v0.json",
+ "title": "EverMe SkillHub Packet",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "name",
+ "summary",
+ "visibility",
+ "status",
+ "version",
+ "source",
+ "domains",
+ "install_targets",
+ "evidence_refs",
+ "body_markdown"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9._-]{2,127}$"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 120
+ },
+ "summary": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 600
+ },
+ "owner_id": {
+ "type": "string",
+ "maxLength": 160
+ },
+ "visibility": {
+ "type": "string",
+ "enum": ["private", "link", "community"]
+ },
+ "status": {
+ "type": "string",
+ "enum": ["draft", "active", "needs_eval", "archived"]
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][A-Za-z0-9._-]+)?$"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["manual", "evercore_extracted", "imported", "community"]
+ },
+ "domains": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 80},
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "install_targets": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["hermes", "raven", "claude_code", "evercore", "openclaw"]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "eval_score": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "rating": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 5
+ },
+ "votes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "last_evolved_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "evidence_refs": {
+ "type": "array",
+ "items": {"type": "string", "minLength": 1, "maxLength": 300},
+ "uniqueItems": true
+ },
+ "body_markdown": {
+ "type": "string",
+ "minLength": 1
+ },
+ "frontmatter": {
+ "type": "object",
+ "additionalProperties": {
+ "type": ["string", "number", "boolean", "null"]
+ }
+ }
+ }
+}
|