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 ![banner-gif](https://github.com/user-attachments/assets/650b901b-c9ba-4001-bac7-626b009df830) + #### 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 -[![banner-gif](https://github.com/user-attachments/assets/57d8cda7-35a5-4561-b794-5520dffc917b)](https://github.com/golutra/golutra) +[![banner-gif](https://github.com/user-attachments/assets/57d8cda7-35a5-4561-b794-5520dffc917b)](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 -[![banner-gif](https://github.com/user-attachments/assets/93ac2a68-4f18-4fcb-8d87-80aeb00a9d7c)](https://github.com/kellyvv/OpenHer) +[![banner-gif](https://github.com/user-attachments/assets/93ac2a68-4f18-4fcb-8d87-80aeb00a9d7c)](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 -[![banner-gif](https://github.com/user-attachments/assets/c258a6c4-fe70-497a-98d1-3dade4a932f6)](https://github.com/nanxingw/EverMem) +[![banner-gif](https://github.com/user-attachments/assets/c258a6c4-fe70-497a-98d1-3dade4a932f6)](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 -[![banner-gif](https://github.com/user-attachments/assets/314c9126-8e08-4688-bbbb-8555ad58cf67)](https://github.com/onenewborn/StudyBuddy-public) +[![banner-gif](https://github.com/user-attachments/assets/314c9126-8e08-4688-bbbb-8555ad58cf67)](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. -[![banner-gif](https://github.com/user-attachments/assets/e2428df3-ea11-4e88-8f9c-dad437dd8998)](https://github.com/AlexL1024/NeuralConnect) +[![banner-gif](https://github.com/user-attachments/assets/e2428df3-ea11-4e88-8f9c-dad437dd8998)](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
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#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", []):
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -431,7 +429,7 @@ LoCoMo **92.73%**
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -465,7 +463,7 @@ Agent self-evolution evaluation through longitudinal growth curves, transfer eff
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -506,7 +504,7 @@ cat evaluation/results/locomo-everos/report.txt
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -540,7 +538,7 @@ If EverOS helps your research, please cite the relevant paper:
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -553,7 +551,7 @@ Star the repo or join the community links above to follow new architecture metho
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
@@ -596,6 +594,6 @@ Read the [Contribution Guidelines](.github/CONTRIBUTING.md) for setup, pull requ
-[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top) +[![Back to top](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#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"] + } + } + } +}