From 50f3e846019a70aa5a6de296c612090d67660eae Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 00:01:24 -0400 Subject: [PATCH 01/24] ci(templates): add PR + security tracker issue templates for Linear/Slack mirror sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two issue templates under .github/ISSUE_TEMPLATE/ for long-lived, auditable mirrors of in-flight upstream PRs: - pr_tracker.yml: general PR mirror (scope, evidence, decision log, closure) - security_tracker.yml: high-priority variant (CWE, severity, reachability, verification, disclosure hygiene) Both carry a `pr-mirror` label so the Linear evermind-dash project and the Slack #bots channel can subscribe by label. Bilingual EN + 中文. --- .github/ISSUE_TEMPLATE/pr_tracker.yml | 109 ++++++++++++++++++ .github/ISSUE_TEMPLATE/security_tracker.yml | 116 ++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/pr_tracker.yml create mode 100644 .github/ISSUE_TEMPLATE/security_tracker.yml diff --git a/.github/ISSUE_TEMPLATE/pr_tracker.yml b/.github/ISSUE_TEMPLATE/pr_tracker.yml new file mode 100644 index 00000000..1dbe050a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pr_tracker.yml @@ -0,0 +1,109 @@ +name: PR tracker (mirror) +description: Mirror an in-flight pull request as a tracking issue for Linear and Slack sync / 镜像一个进行中的 PR 作为追踪 issue +title: "[PR Track] #: " +labels: ["pr-mirror", "tracking"] +body: + - type: markdown + attributes: + value: | + Use this when you want a long-lived, auditable record of an upstream PR. + Linear and Slack subscribe to issues with the `pr-mirror` label. + 镜像一个 PR 用于 Linear / Slack 长期可审计追踪。带 `pr-mirror` 标签的 issue 会被订阅。 + - type: input + id: pr_number + attributes: + label: PR number + placeholder: "e.g. 196" + validations: + required: true + - type: input + id: pr_url + attributes: + label: PR URL + placeholder: https://github.com/EverMind-AI/EverOS/pull/ + validations: + required: true + - type: input + id: author + attributes: + label: Author handle + placeholder: "@github-login" + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - methods/EverCore + - methods/HyperMem + - benchmarks/EverMemBench + - benchmarks/EvoAgentBench + - use-cases + - documentation + - CI / build / release + - other + validations: + required: true + - type: dropdown + id: lane + attributes: + label: Review lane + description: How this PR should be triaged. / 该 PR 的优先级处理通道。 + options: + - hotfix (block release until merged) + - normal (standard review) + - docs-only (light review) + - exploratory (no merge intent) + validations: + required: true + - type: textarea + id: scope + attributes: + label: Scope summary + description: One paragraph. What does the PR change, and what is intentionally left out? + placeholder: | + Changes: + - ... + Out of scope: + - ... + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence snapshot + description: | + Required before this mirror can be closed. Paste the CI summary, test command output, + or the link to the run. "No mirror closes without evidence." + 关闭镜像 issue 前必填。粘贴 CI 摘要、测试命令输出或 run 链接。 + render: shell + validations: + required: true + - type: textarea + id: decisions + attributes: + label: Decision log + description: Notable review decisions (approvals, requested changes, deferrals). + placeholder: | + - 2026-05-13 @reviewer: requested change on tests/test_x.py + - 2026-05-13 @author: scoped follow-up to PR #... + - type: input + id: linear_issue + attributes: + label: Linear issue (optional) + placeholder: "EVE-123" + - type: input + id: slack_thread + attributes: + label: Slack thread (optional) + placeholder: "https://everminddash.slack.com/archives/.../p..." + - type: checkboxes + id: closure + attributes: + label: Closure criteria + description: Check all that apply before closing this mirror. + options: + - label: PR is merged, closed, or marked won't-fix upstream. + - label: Evidence snapshot above reflects the final state. + - label: Linear and Slack records have been updated (if linked). diff --git a/.github/ISSUE_TEMPLATE/security_tracker.yml b/.github/ISSUE_TEMPLATE/security_tracker.yml new file mode 100644 index 00000000..10844bf1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_tracker.yml @@ -0,0 +1,116 @@ +name: Security tracker (mirror) +description: Mirror a security PR or disclosure for Linear and Slack escalation / 镜像一个安全 PR 或披露 +title: "[Security Track] CWE-: " +labels: ["security", "pr-mirror", "tracking", "urgent"] +body: + - type: markdown + attributes: + value: | + Use this for any PR or disclosure that affects credentials, authn/authz, data exposure, + supply chain, or sandbox escape. The `urgent` label routes this to high-priority + notifications in Slack and Linear. + 用于凭证、认证授权、数据暴露、供应链、沙箱逃逸等安全 PR / 披露。`urgent` 标签会触发高优先级通知。 + Do NOT include exploit details that are not already public in the upstream PR. + 请勿写入未在 upstream PR 公开的利用细节。 + - type: input + id: cwe + attributes: + label: CWE id + placeholder: "CWE-798" + validations: + required: true + - type: input + id: pr_url + attributes: + label: Upstream PR or advisory URL + placeholder: https://github.com/EverMind-AI/EverOS/pull/ + validations: + required: true + - type: dropdown + id: severity + attributes: + label: Severity + options: + - Critical (full auth bypass / unauthenticated RCE / mass data loss) + - High (privileged data access / credential exposure / persistent compromise) + - Medium (limited data access / requires user interaction) + - Low (defense-in-depth / hardening) + validations: + required: true + - type: dropdown + id: exposure + attributes: + label: Reachability + description: How reachable is this in the documented quickstart / default config? + options: + - Default config (reproducible from a clean clone) + - Default config + network position + - Non-default config but documented + - Hypothetical / not yet reproducible + validations: + required: true + - type: textarea + id: affected + attributes: + label: Affected components + description: File paths, services, or versions impacted. + placeholder: | + - methods/EverCore/docker-compose.yaml (memsys-milvus-minio block) + - methods/EverCore/env.template + validations: + required: true + - type: textarea + id: fix_summary + attributes: + label: Proposed fix summary + description: One paragraph. What does the PR change? Cite the contract that makes it fail-closed. + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Verification evidence + description: | + Required before closure. Show the commands and output that prove the fix works AND + that the unpatched state was exploitable. "No security mirror closes without evidence." + 关闭前必填。展示证明 fix 生效以及未修复状态可利用的命令与输出。 + render: shell + validations: + required: true + - type: textarea + id: residual + attributes: + label: Residual risk / follow-ups + description: Anything intentionally out of scope, plus follow-up issues that should be filed. + placeholder: | + - docs/installation/ still references the old default in examples; follow-up sweep needed. + - Consider adding a CI lint to catch hardcoded secrets in docker-compose files. + - type: input + id: linear_issue + attributes: + label: Linear issue (optional) + placeholder: "EVE-123" + - type: input + id: slack_thread + attributes: + label: Slack thread (optional) + placeholder: "https://everminddash.slack.com/archives/.../p..." + - type: checkboxes + id: disclosure + attributes: + label: Disclosure hygiene + description: Confirm before submitting. + options: + - label: This mirror contains no exploit details beyond what is already public in the upstream PR. + required: true + - label: The upstream PR or advisory link is correct and reachable. + required: true + - label: A maintainer has been pinged in Slack #bots or via Linear EVE if Severity is Critical or High. + - type: checkboxes + id: closure + attributes: + label: Closure criteria + options: + - label: Upstream PR merged, advisory published, or risk formally accepted. + - label: Verification evidence above reflects the merged state. + - label: Residual-risk follow-ups have issues filed (or explicitly waived). From 8a121390bd2494826dc1cf109ff4cfa4f529bc57 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 01:26:59 -0400 Subject: [PATCH 02/24] ci(sync): add auto-rebase workflow keeping fork main current with upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs every 6 hours via cron + manual workflow_dispatch. - Rebases fork main onto upstream/main (preserves fork-only commits like the issue templates) - Force-pushes with --force-with-lease for safety - Opens a tracking issue on conflict instead of failing silently Uses default GITHUB_TOKEN — no PAT needed since we only push to fork. --- .github/workflows/sync-upstream.yml | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..bfd8d884 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,61 @@ +name: Sync fork from upstream + +on: + schedule: + - cron: '17 */6 * * *' # 每 6 小时跑一次,分钟 17 错开高峰 + workflow_dispatch: # 也支持手动触发 + +permissions: + contents: write + issues: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: git remote add upstream https://github.com/EverMind-AI/EverOS.git + + - name: Fetch upstream + run: git fetch upstream + + - name: Rebase fork main onto upstream/main + id: rebase + run: | + git checkout main + set +e + git rebase upstream/main + rc=$? + if [ $rc -ne 0 ]; then + git rebase --abort + echo "conflict=true" >> "$GITHUB_OUTPUT" + exit $rc + fi + echo "conflict=false" >> "$GITHUB_OUTPUT" + + - name: Push to fork main + if: steps.rebase.outputs.conflict == 'false' + run: git push origin main --force-with-lease + + - name: Open issue on conflict + if: failure() && steps.rebase.outputs.conflict == 'true' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `[sync] Rebase conflict syncing fork from upstream (${new Date().toISOString().slice(0,10)})`, + body: `Auto-sync from \`upstream/main\` failed due to rebase conflicts.\n\nRun id: ${context.runId}\nWorkflow: ${context.workflow}\n\nResolve manually:\n\n\`\`\`\ncd ~/EverOS && git fetch upstream && git rebase upstream/main\n# resolve conflicts, then:\ngit push origin main --force-with-lease\n\`\`\``, + labels: ['tracking'] + }); From 831bcd8945c92cd132fdd74ef966e1635cc033d3 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 01:41:46 -0400 Subject: [PATCH 03/24] ci(linear): add issue mirror workflow for pr-mirror labeled issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triggers on issues.opened and issues.labeled. When pr-mirror label is present, creates a corresponding Linear issue in the EverMind-Dash project via Linear GraphQL API. Comments back on GitHub with the EVE-id link. Idempotency: skips if a '🔗 Linear:' marker comment already exists. Priority: 'urgent' label -> Linear urgent (1); otherwise medium (3). On API failure: applies 'sync-failed' label for triage. Requires (configured separately): Secret: LINEAR_API_KEY (Linear Personal API key, lin_api_*) Vars: LINEAR_TEAM_ID (EverMind team UUID) LINEAR_PROJECT_ID (EverMind-Dash project UUID) --- .github/workflows/linear-sync.yml | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/linear-sync.yml diff --git a/.github/workflows/linear-sync.yml b/.github/workflows/linear-sync.yml new file mode 100644 index 00000000..82b01e7c --- /dev/null +++ b/.github/workflows/linear-sync.yml @@ -0,0 +1,118 @@ +name: Linear sync for tracking mirrors + +on: + issues: + types: [opened, labeled] + +permissions: + issues: write + contents: read + +jobs: + sync: + if: contains(github.event.issue.labels.*.name, 'pr-mirror') + runs-on: ubuntu-latest + steps: + - name: Mirror GitHub issue to Linear + uses: actions/github-script@v7 + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID }} + with: + script: | + const issue = context.payload.issue; + const repo = `${context.repo.owner}/${context.repo.repo}`; + + // Idempotency: skip if Linear marker comment already exists + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + per_page: 100 + }); + if (comments.some(c => c.body.includes('🔗 Linear:'))) { + core.info('Already synced (marker comment present). Skipping.'); + return; + } + + const isUrgent = issue.labels.some(l => l.name === 'urgent'); + const priority = isUrgent ? 1 : 3; // 1=Urgent, 3=Medium + + const description = [ + `**Source**: [${repo}#${issue.number}](${issue.html_url})`, + '', + '---', + '', + issue.body || '_(no body provided)_', + '', + '---', + '', + `_Auto-synced from GitHub by [linear-sync workflow](https://github.com/${repo}/actions)._` + ].join('\n'); + + const mutation = ` + mutation IssueCreate($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { id identifier url } + } + } + `; + + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Authorization': process.env.LINEAR_API_KEY, + 'Content-Type': 'application/json', + 'x-apollo-operation-name': 'IssueCreate' + }, + body: JSON.stringify({ + query: mutation, + variables: { + input: { + title: issue.title, + description: description, + teamId: process.env.LINEAR_TEAM_ID, + projectId: process.env.LINEAR_PROJECT_ID, + priority: priority + } + } + }) + }); + + const data = await response.json(); + + if (!response.ok || data.errors || !data?.data?.issueCreate?.success) { + const errMsg = 'Linear API error: ' + JSON.stringify(data, null, 2); + core.error(errMsg); + + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'sync-failed', + color: 'D93F0B', + description: 'Linear sync workflow failed for this issue' + }); + } catch (e) { /* exists already */ } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['sync-failed'] + }); + + throw new Error(errMsg); + } + + const linearIssue = data.data.issueCreate.issue; + core.info(`Created ${linearIssue.identifier} -> ${linearIssue.url}`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🔗 Linear: [${linearIssue.identifier}](${linearIssue.url})\n\n_Auto-created by linear-sync workflow._` + }); From bee6f1d4a0750fa54718baf769d81b9a3a682579 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 01:59:47 -0400 Subject: [PATCH 04/24] docs(templates): align security_tracker Slack reference to actual channel Update the disclosure-hygiene checkbox to reference #p-evermind-dash (the actual Slack channel linked to the EverMind-Dash Linear project) instead of the placeholder #bots. --- .github/ISSUE_TEMPLATE/security_tracker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/security_tracker.yml b/.github/ISSUE_TEMPLATE/security_tracker.yml index 10844bf1..2f2c96c8 100644 --- a/.github/ISSUE_TEMPLATE/security_tracker.yml +++ b/.github/ISSUE_TEMPLATE/security_tracker.yml @@ -105,7 +105,7 @@ body: required: true - label: The upstream PR or advisory link is correct and reachable. required: true - - label: A maintainer has been pinged in Slack #bots or via Linear EVE if Severity is Critical or High. + - label: A maintainer has been pinged in Slack #p-evermind-dash or via Linear EVE if Severity is Critical or High. - type: checkboxes id: closure attributes: From 63be3787b70630f92916da3957fd9cfae0f1ece1 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 02:03:39 -0400 Subject: [PATCH 05/24] fix(linear-sync): prevent duplicate Linear issues from multi-label events Two compounding fixes to avoid creating multiple Linear issues from a single GitHub issue creation: 1. concurrency group keyed on issue.number with cancel-in-progress=false serializes runs per issue. Second run will see the first run's comment and skip via existing idempotency check. 2. Tighten 'labeled' event filter to only fire when the added label is pr-mirror itself, not any other label. Eliminates the four extra runs that gh issue create --label A --label B ... triggers (one issues.opened + four issues.labeled = 5 events for a 4-label create). Reproduction: gh issue create with 4 labels including pr-mirror was firing the workflow 5 times concurrently. Idempotency check has a ~5s race window before the first run posts its bot comment, so 2-3 runs created duplicate Linear issues before the rest skipped. Verified via Issue #4 sync producing both EVE-3 and EVE-4. --- .github/workflows/linear-sync.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linear-sync.yml b/.github/workflows/linear-sync.yml index 82b01e7c..c2af07de 100644 --- a/.github/workflows/linear-sync.yml +++ b/.github/workflows/linear-sync.yml @@ -8,9 +8,15 @@ permissions: issues: write contents: read +concurrency: + group: linear-sync-issue-${{ github.event.issue.number }} + cancel-in-progress: false + jobs: sync: - if: contains(github.event.issue.labels.*.name, 'pr-mirror') + if: | + contains(github.event.issue.labels.*.name, 'pr-mirror') && + (github.event.action == 'opened' || github.event.label.name == 'pr-mirror') runs-on: ubuntu-latest steps: - name: Mirror GitHub issue to Linear From fe80ca1fd86f64ac27664aa58b41da73b3b2d00c Mon Sep 17 00:00:00 2001 From: 0xVox <35294173+Fearvox@users.noreply.github.com> Date: Wed, 13 May 2026 02:29:51 -0400 Subject: [PATCH 06/24] ci(watch): add overnight fork patrol Adds the fork overnight patrol workflow, Linear-aware tracking issue creation, and docs guard support for coming-soon use-case placeholders. Verified with local script checks and passing Docs CI. --- .github/scripts/overnight-watch.mjs | 371 ++++++++++++++++++++++++ .github/workflows/docs.yml | 3 + .github/workflows/overnight-watch.yml | 56 ++++ docs/fork-playground/overnight-watch.md | 54 ++++ 4 files changed, 484 insertions(+) create mode 100755 .github/scripts/overnight-watch.mjs create mode 100644 .github/workflows/overnight-watch.yml create mode 100644 docs/fork-playground/overnight-watch.md diff --git a/.github/scripts/overnight-watch.mjs b/.github/scripts/overnight-watch.mjs new file mode 100755 index 00000000..5fc2b1c4 --- /dev/null +++ b/.github/scripts/overnight-watch.mjs @@ -0,0 +1,371 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const config = { + repoOwner: process.env.REPO_OWNER || "Fearvox", + repoName: process.env.REPO_NAME || "EverOS", + upstreamRepo: process.env.UPSTREAM_REPO || "EverMind-AI/EverOS", + watchBranch: + process.env.WATCH_BRANCH || "codex-watch-overnight-2026-05-13", + ownerTimezone: process.env.OWNER_TIMEZONE || "America/Los_Angeles", + linearTeamId: + process.env.LINEAR_TEAM_ID || "233391d6-ec9e-4aa8-b534-16a221b8119a", + linearProjectId: + process.env.LINEAR_PROJECT_ID || "39aa3865-345c-4313-9dc0-ab3b509c5d21", + createTrackingIssue: + (process.env.CREATE_TRACKING_ISSUE || "false").toLowerCase() === "true", + githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "", + linearApiKey: process.env.LINEAR_API_KEY || "", +}; + +const repoSlug = `${config.repoOwner}/${config.repoName}`; +const recentWindowHours = Number(process.env.WATCH_WINDOW_HOURS || 24); +const since = new Date(Date.now() - recentWindowHours * 60 * 60 * 1000); +const now = new Date(); + +function run(command, args, options = {}) { + const result = execFileSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", options.inheritStderr ? "inherit" : "pipe"], + env: { + ...process.env, + GH_TOKEN: config.githubToken || process.env.GH_TOKEN, + }, + }); + return result.trim(); +} + +function tryRun(command, args, fallback = "") { + try { + return run(command, args); + } catch (error) { + return fallback; + } +} + +function ghJson(endpoint) { + const output = run("gh", [ + "api", + "-H", + "Accept: application/vnd.github+json", + endpoint, + ]); + return JSON.parse(output); +} + +function ensureRemote(name, url) { + const remotes = tryRun("git", ["remote"]).split("\n").filter(Boolean); + if (!remotes.includes(name)) { + run("git", ["remote", "add", name, url]); + } +} + +function fetchGitState() { + ensureRemote("upstream", `https://github.com/${config.upstreamRepo}.git`); + tryRun("git", ["fetch", "origin", "--prune"], ""); + tryRun("git", ["fetch", "upstream", "main", "--prune"], ""); + + const forkHead = tryRun("git", ["rev-parse", "--short", "origin/main"], "unknown"); + const upstreamHead = tryRun( + "git", + ["rev-parse", "--short", "upstream/main"], + "unknown", + ); + const counts = tryRun( + "git", + ["rev-list", "--left-right", "--count", "origin/main...upstream/main"], + "0\t0", + ) + .split(/\s+/) + .map((value) => Number(value)); + + const watchBranchSha = tryRun( + "git", + ["ls-remote", "--heads", "origin", config.watchBranch], + "", + ) + .split(/\s+/)[0] + ?.slice(0, 12); + + return { + forkHead, + upstreamHead, + forkAhead: counts[0] || 0, + forkBehind: counts[1] || 0, + watchBranchSha: watchBranchSha || "", + }; +} + +function fetchGitHubState() { + const runs = ghJson( + `/repos/${repoSlug}/actions/runs?per_page=50&status=completed`, + ).workflow_runs || []; + const failedRuns = runs + .filter((runInfo) => new Date(runInfo.created_at) >= since) + .filter((runInfo) => + ["failure", "cancelled", "timed_out"].includes(runInfo.conclusion), + ) + .slice(0, 10); + + const upstreamPulls = ghJson( + `/repos/${config.upstreamRepo}/pulls?state=open&sort=updated&direction=desc&per_page=30`, + ) + .filter((pull) => new Date(pull.updated_at) >= since) + .slice(0, 10); + + const forkPulls = ghJson( + `/repos/${repoSlug}/pulls?state=open&sort=updated&direction=desc&per_page=30`, + ) + .filter((pull) => new Date(pull.updated_at) >= since) + .slice(0, 10); + + return { failedRuns, upstreamPulls, forkPulls }; +} + +function lineForPull(pull) { + return `- #${pull.number} ${pull.title} (${pull.user.login}, updated ${pull.updated_at})`; +} + +function lineForRun(runInfo) { + return `- ${runInfo.name}: ${runInfo.conclusion} (${runInfo.html_url})`; +} + +function renderReport(gitState, githubState) { + const findings = []; + + if (!gitState.watchBranchSha) { + findings.push(`Watch branch is not on origin: ${config.watchBranch}`); + } + if (gitState.forkBehind > 0) { + findings.push(`Fork main is behind upstream/main by ${gitState.forkBehind} commit(s).`); + } + if (githubState.failedRuns.length > 0) { + findings.push( + `${githubState.failedRuns.length} completed workflow run(s) failed in the last ${recentWindowHours}h.`, + ); + } + + const verdict = findings.length > 0 ? "FLAG" : "PASS"; + const localNow = now.toLocaleString("en-US", { + timeZone: config.ownerTimezone, + hour12: false, + }); + + return { + verdict, + body: [ + `# Overnight Fork Watch: ${verdict}`, + "", + `Generated: ${now.toISOString()} (${config.ownerTimezone}: ${localNow})`, + `Repository: ${repoSlug}`, + `Upstream: ${config.upstreamRepo}`, + `Watch branch: \`${config.watchBranch}\``, + "", + "## Drift", + "", + `- origin/main: \`${gitState.forkHead}\``, + `- upstream/main: \`${gitState.upstreamHead}\``, + `- fork ahead: ${gitState.forkAhead}`, + `- fork behind: ${gitState.forkBehind}`, + `- watch branch on origin: ${ + gitState.watchBranchSha ? `yes (\`${gitState.watchBranchSha}\`)` : "no" + }`, + "", + "## Findings", + "", + findings.length ? findings.map((item) => `- ${item}`).join("\n") : "- None.", + "", + `## Fork Workflow Failures (${recentWindowHours}h)`, + "", + githubState.failedRuns.length + ? githubState.failedRuns.map(lineForRun).join("\n") + : "- None.", + "", + `## Upstream PRs Updated (${recentWindowHours}h)`, + "", + githubState.upstreamPulls.length + ? githubState.upstreamPulls.map(lineForPull).join("\n") + : "- None.", + "", + `## Fork PRs Updated (${recentWindowHours}h)`, + "", + githubState.forkPulls.length + ? githubState.forkPulls.map(lineForPull).join("\n") + : "- None.", + "", + "## Operator Notes", + "", + "- This issue is safe for public tracking: no local paths, host/IP values, or secrets are included.", + "- A GitHub issue created by `GITHUB_TOKEN` does not trigger secondary workflows, so this watch mirrors to Linear directly when `LINEAR_API_KEY` is available.", + ].join("\n"), + }; +} + +function ensureLabel(name, color, description) { + tryRun("gh", [ + "label", + "create", + name, + "--repo", + repoSlug, + "--color", + color, + "--description", + description, + ]); +} + +function issueHasLinearMarker(issueNumber) { + const comments = ghJson(`/repos/${repoSlug}/issues/${issueNumber}/comments?per_page=100`); + return comments.some((comment) => comment.body.includes("Linear:")); +} + +async function mirrorToLinear(issueNumber, title, body) { + if (!config.linearApiKey || issueHasLinearMarker(issueNumber)) { + return; + } + + const mutation = ` + mutation IssueCreate($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { id identifier url } + } + } + `; + + const response = await fetch("https://api.linear.app/graphql", { + method: "POST", + headers: { + Authorization: config.linearApiKey, + "Content-Type": "application/json", + "x-apollo-operation-name": "IssueCreate", + }, + body: JSON.stringify({ + query: mutation, + variables: { + input: { + title, + description: [ + `**Source**: https://github.com/${repoSlug}/issues/${issueNumber}`, + "", + "---", + "", + body, + ].join("\n"), + teamId: config.linearTeamId, + projectId: config.linearProjectId, + priority: 3, + }, + }, + }), + }); + + const data = await response.json(); + if (!response.ok || data.errors || !data?.data?.issueCreate?.success) { + ensureLabel("sync-failed", "D93F0B", "Linear sync workflow failed for this issue"); + tryRun("gh", [ + "issue", + "edit", + String(issueNumber), + "--repo", + repoSlug, + "--add-label", + "sync-failed", + ]); + throw new Error(`Linear API error: ${JSON.stringify(data)}`); + } + + const linearIssue = data.data.issueCreate.issue; + const marker = `Linear: [${linearIssue.identifier}](${linearIssue.url})\n\n_Auto-created by overnight-watch._`; + run("gh", ["issue", "comment", String(issueNumber), "--repo", repoSlug, "--body", marker]); +} + +function findExistingIssue(title) { + const issues = JSON.parse( + run("gh", [ + "issue", + "list", + "--repo", + repoSlug, + "--state", + "open", + "--label", + "overnight-watch", + "--json", + "number,title", + "--limit", + "20", + ]), + ); + return issues.find((issue) => issue.title === title); +} + +async function createOrUpdateTrackingIssue(report) { + if (!config.createTrackingIssue || report.verdict === "PASS") { + return; + } + + ensureLabel("overnight-watch", "1D76DB", "Automated overnight fork watch"); + ensureLabel("tracking", "5319E7", "Long-lived tracking item"); + ensureLabel("pr-mirror", "0E8A16", "Mirrored into Linear or Slack tracking"); + + const date = now.toISOString().slice(0, 10); + const title = `[watch] Overnight fork patrol: ${date}`; + const existing = findExistingIssue(title); + const tempDir = mkdtempSync(join(tmpdir(), "everos-watch-")); + const bodyFile = join(tempDir, "body.md"); + writeFileSync(bodyFile, report.body); + + if (existing) { + run("gh", [ + "issue", + "comment", + String(existing.number), + "--repo", + repoSlug, + "--body-file", + bodyFile, + ]); + await mirrorToLinear(existing.number, title, report.body); + return; + } + + const created = run("gh", [ + "issue", + "create", + "--repo", + repoSlug, + "--title", + title, + "--body-file", + bodyFile, + "--label", + "overnight-watch", + "--label", + "tracking", + "--label", + "pr-mirror", + ]); + const match = created.match(/\/issues\/(\d+)/); + if (match) { + await mirrorToLinear(Number(match[1]), title, report.body); + } +} + +async function main() { + const gitState = fetchGitState(); + const githubState = fetchGitHubState(); + const report = renderReport(gitState, githubState); + console.log(report.body); + await createOrUpdateTrackingIssue(report); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8eec180..aa74ce5f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -103,6 +103,9 @@ jobs: banner_match = re.search(r"\[!\[[^\]]*\]\([^)]+\)\]\(([^)]+)\)", cell) primary_match = re.search(r"^\[(?:Code|Plugin|Live Demo)\]\(([^)]+)\)", cell, flags=re.M) + if "Coming soon" in cell and not primary_match: + continue + if not banner_match: failures.append(f"{path}: {title}: missing linked banner") elif not primary_match: diff --git a/.github/workflows/overnight-watch.yml b/.github/workflows/overnight-watch.yml new file mode 100644 index 00000000..3a0e8489 --- /dev/null +++ b/.github/workflows/overnight-watch.yml @@ -0,0 +1,56 @@ +name: Fork overnight watch + +on: + schedule: + # 02:13 America/Los_Angeles during daylight time. GitHub cron is UTC. + - cron: "13 9 * * *" + workflow_dispatch: + inputs: + create_tracking_issue: + description: "Open/update a tracking issue when the watch verdict is FLAG" + required: false + type: boolean + default: true + watch_branch: + description: "Fork playground branch to verify on origin" + required: false + type: string + default: "codex-watch-overnight-2026-05-13" + +permissions: + actions: read + contents: read + issues: write + pull-requests: read + +concurrency: + group: overnight-watch-${{ github.ref }} + cancel-in-progress: false + +jobs: + watch: + if: github.repository == 'Fearvox/EverOS' + runs-on: ubuntu-latest + env: + REPO_OWNER: ${{ vars.REPO_OWNER || 'Fearvox' }} + REPO_NAME: ${{ vars.REPO_NAME || 'EverOS' }} + UPSTREAM_REPO: EverMind-AI/EverOS + WATCH_BRANCH: ${{ inputs.watch_branch || vars.WATCH_BRANCH || 'codex-watch-overnight-2026-05-13' }} + OWNER_TIMEZONE: ${{ vars.OWNER_TIMEZONE || 'America/Los_Angeles' }} + LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID || '233391d6-ec9e-4aa8-b534-16a221b8119a' }} + LINEAR_PROJECT_ID: ${{ vars.LINEAR_PROJECT_ID || '39aa3865-345c-4313-9dc0-ab3b509c5d21' }} + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + CREATE_TRACKING_ISSUE: ${{ inputs.create_tracking_issue || github.event_name == 'schedule' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run overnight watch + run: node .github/scripts/overnight-watch.mjs diff --git a/docs/fork-playground/overnight-watch.md b/docs/fork-playground/overnight-watch.md new file mode 100644 index 00000000..379fd21b --- /dev/null +++ b/docs/fork-playground/overnight-watch.md @@ -0,0 +1,54 @@ +# Overnight Fork Watch + +This fork can move fast, but the upstream feed should stay boring and auditable. +The overnight watch is a small GitHub Actions patrol for `Fearvox/EverOS`. + +## What It Checks + +- `origin/main` drift against `EverMind-AI/EverOS` `upstream/main`. +- Whether the active playground branch exists on the fork: + `codex-watch-overnight-2026-05-13`. +- Failed, cancelled, or timed-out fork workflow runs in the last 24 hours. +- Upstream and fork pull requests updated in the last 24 hours. + +## Tracking Behavior + +The workflow prints a public-safe report on every run. If the verdict is `FLAG`, +it opens or updates a GitHub issue labeled: + +- `overnight-watch` +- `tracking` +- `pr-mirror` + +Issues created by `GITHUB_TOKEN` do not trigger secondary workflows. Because of +that, the watch mirrors the tracking issue to Linear directly when +`LINEAR_API_KEY` is available. The target Linear team/project are: + +- Team: `233391d6-ec9e-4aa8-b534-16a221b8119a` +- Project: `39aa3865-345c-4313-9dc0-ab3b509c5d21` + +A `FLAG` verdict does not fail the watch workflow by itself. Runtime errors +still fail the workflow, but expected drift or downstream failures are reported +through the tracking issue so the watch does not poison its own next run. + +## Manual Run + +```bash +REPO_OWNER=Fearvox \ +REPO_NAME=EverOS \ +WATCH_BRANCH=codex-watch-overnight-2026-05-13 \ +OWNER_TIMEZONE=America/Los_Angeles \ +LINEAR_TEAM_ID=233391d6-ec9e-4aa8-b534-16a221b8119a \ +LINEAR_PROJECT_ID=39aa3865-345c-4313-9dc0-ab3b509c5d21 \ +CREATE_TRACKING_ISSUE=false \ +node .github/scripts/overnight-watch.mjs +``` + +Set `CREATE_TRACKING_ISSUE=true` only when you want the local run to mutate +GitHub issues. + +## Public-Surface Hygiene + +Reports intentionally avoid local absolute paths, host/IP values, token names +beyond the required GitHub secret names, and operator-only commands. They should +be safe to show in Discord or a screen share. From a1f4a4586e0fc1ad0438a4553b33978fbdfc281e Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 05:49:45 -0400 Subject: [PATCH 07/24] chore: add MUW agent review instructions --- .github/copilot-instructions.md | 42 +++++++++++++++++++++++++++++++++ AGENTS.md | 11 +++++++++ 2 files changed, 53 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..182b6691 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,42 @@ +# Copilot and Codex Review Instructions + +When reviewing pull requests in this repository, use the MUW review contract. +Start every review with this block: + +```text +VERDICT: PASS / FLAG / BLOCK +VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action +EVIDENCE: +``` + +Use the verdicts this way: + +- `PASS`: the pull request objective is met and the evidence is sufficient. +- `FLAG`: useful progress, but a non-blocking issue, missing evidence, or follow-up remains. +- `BLOCK`: the objective is unmet, unsafe, unverifiable, or materially wrong. + +Report findings first, ordered by severity. For each actionable finding, include: + +- Severity +- File/path +- Evidence from the actual diff, status check, command output, or linked issue +- Why it matters +- Fix guidance or the next verification required + +Review method: + +1. Identify the promised objective from the PR title, body, linked issue, and changed files. +2. Inspect the real diff and available checks before making a success claim. +3. Compare evidence against the objective; do not accept `done` from a summary alone. +4. Verify the smallest real path that proves the claim. +5. Keep evidence concise, reproducible, and repository-relative. + +EverOS-specific checks: + +- For `methods/EverCore/`, preserve async I/O, tenant scoping, and existing module boundaries. +- For prompts, keep EN/ZH variants aligned when both exist. +- For docs and community files, preserve the README reader journey and keep root uncluttered. +- For `.github/workflows/docs.yml`, keep the workflow lightweight and dependency-free unless the PR explicitly changes that contract. +- Do not expose secrets, credential paths, raw tokens, private host values, or operator-only commands in review text. + +For clean reviews, still return the MUW block with the evidence checked and any residual test gap. Keep the final review concise; prefer one clear judgment over a long menu of weak suggestions. diff --git a/AGENTS.md b/AGENTS.md index 2707448d..90793f32 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,3 +71,14 @@ uv run pyright # Type check, if pyright is installed variants. - Prefer existing repo patterns and component boundaries before adding new abstractions. + +## GitHub Agent Review Contract + +- GitHub Copilot, Codex, and other review agents should follow + `.github/copilot-instructions.md`. +- Start PR reviews with the MUW block: + `VERDICT: PASS / FLAG / BLOCK`, `VERDICT_SUMMARY:`, and `EVIDENCE:`. +- Do not mark a PR `PASS` from author summary alone; inspect the actual diff, + linked issue, and available checks first. +- Report actionable findings first, ordered by severity, with file/path, + evidence, impact, and fix guidance. From 0ae77fcf813bfe5be8417e9c418e3d43399c5aa4 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 13:24:31 -0400 Subject: [PATCH 08/24] chore: align Codex review guidelines header --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 90793f32..d5b6b160 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ uv run pyright # Type check, if pyright is installed - Prefer existing repo patterns and component boundaries before adding new abstractions. -## GitHub Agent Review Contract +## Review guidelines - GitHub Copilot, Codex, and other review agents should follow `.github/copilot-instructions.md`. From 5d42427162cdcd28a41656f1b8f49d44bb54c980 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 13:28:44 -0400 Subject: [PATCH 09/24] chore: add MUW review custom agent --- .github/agents/muw-review.md | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/agents/muw-review.md diff --git a/.github/agents/muw-review.md b/.github/agents/muw-review.md new file mode 100644 index 00000000..84237833 --- /dev/null +++ b/.github/agents/muw-review.md @@ -0,0 +1,50 @@ +--- +name: MUW Review +description: Review pull requests and repo changes using the MUW PASS/FLAG/BLOCK contract. +--- + +# MUW Review Agent + +You are a review-only agent for Fearvox/EverOS. Use this agent when the user asks +for a pull request review, workflow review, evidence check, or closeout check. + +Default behavior: + +- Do not edit files, push commits, or create a pull request unless the user + explicitly asks for fixes. +- Inspect the real pull request diff, linked issue, available checks, and + repository instructions before making a success claim. +- Prefer a concise GitHub comment or session summary over a long essay. + +Start every review with: + +```text +VERDICT: PASS / FLAG / BLOCK +VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action +EVIDENCE: +``` + +Verdicts: + +- `PASS`: the objective is met and evidence is sufficient. +- `FLAG`: useful progress, but a non-blocking issue, missing evidence, or follow-up remains. +- `BLOCK`: the objective is unmet, unsafe, unverifiable, or materially wrong. + +Findings: + +- List findings first, ordered by severity. +- For each finding, include severity, file/path, evidence, why it matters, and + required fix or next verification. +- If the review is clean, still include the evidence checked and residual test gap. + +EverOS focus: + +- Preserve async I/O, tenant scoping, and existing module boundaries in + `methods/EverCore/`. +- Keep EN/ZH prompt variants aligned when both exist. +- Treat broken links, failing docs checks, stale setup commands, missing + `.env.example` files, and unclear issue templates as DX bugs. +- Keep `.github/workflows/docs.yml` lightweight and dependency-free unless the + task explicitly changes that contract. +- Do not expose secrets, credential paths, raw tokens, private host values, or + operator-only commands in public comments. From 226df866e4252b82c421be49d02360ccbb3ddf3f Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 13:34:17 -0400 Subject: [PATCH 10/24] revert: remove MUW review custom agent --- .github/agents/muw-review.md | 50 ------------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 .github/agents/muw-review.md diff --git a/.github/agents/muw-review.md b/.github/agents/muw-review.md deleted file mode 100644 index 84237833..00000000 --- a/.github/agents/muw-review.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: MUW Review -description: Review pull requests and repo changes using the MUW PASS/FLAG/BLOCK contract. ---- - -# MUW Review Agent - -You are a review-only agent for Fearvox/EverOS. Use this agent when the user asks -for a pull request review, workflow review, evidence check, or closeout check. - -Default behavior: - -- Do not edit files, push commits, or create a pull request unless the user - explicitly asks for fixes. -- Inspect the real pull request diff, linked issue, available checks, and - repository instructions before making a success claim. -- Prefer a concise GitHub comment or session summary over a long essay. - -Start every review with: - -```text -VERDICT: PASS / FLAG / BLOCK -VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action -EVIDENCE: -``` - -Verdicts: - -- `PASS`: the objective is met and evidence is sufficient. -- `FLAG`: useful progress, but a non-blocking issue, missing evidence, or follow-up remains. -- `BLOCK`: the objective is unmet, unsafe, unverifiable, or materially wrong. - -Findings: - -- List findings first, ordered by severity. -- For each finding, include severity, file/path, evidence, why it matters, and - required fix or next verification. -- If the review is clean, still include the evidence checked and residual test gap. - -EverOS focus: - -- Preserve async I/O, tenant scoping, and existing module boundaries in - `methods/EverCore/`. -- Keep EN/ZH prompt variants aligned when both exist. -- Treat broken links, failing docs checks, stale setup commands, missing - `.env.example` files, and unclear issue templates as DX bugs. -- Keep `.github/workflows/docs.yml` lightweight and dependency-free unless the - task explicitly changes that contract. -- Do not expose secrets, credential paths, raw tokens, private host values, or - operator-only commands in public comments. From 35864fbcfb073d3823289411acc567bcfc4d576b Mon Sep 17 00:00:00 2001 From: 0xVox Date: Wed, 13 May 2026 15:18:26 -0400 Subject: [PATCH 11/24] chore: add MUW review comment lane --- .github/MUW_REVIEW_LANE.md | 58 +++++ .github/scripts/muw-review-lane.mjs | 325 ++++++++++++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 .github/MUW_REVIEW_LANE.md create mode 100644 .github/scripts/muw-review-lane.mjs diff --git a/.github/MUW_REVIEW_LANE.md b/.github/MUW_REVIEW_LANE.md new file mode 100644 index 00000000..77f01618 --- /dev/null +++ b/.github/MUW_REVIEW_LANE.md @@ -0,0 +1,58 @@ +# MUW Review Lane + +Use this lane when GitHub's native Codex review is useful but its fixed review +wrapper is too loose for MUW closeout work. + +The lane has three steps: + +1. Collect PR evidence. +2. Ask Codex to produce an exact MUW verdict from the generated prompt. +3. Post the verdict back to the PR with an idempotency marker. + +## Collect Evidence + +```bash +node .github/scripts/muw-review-lane.mjs collect --pr 24 --repo Fearvox/EverOS +``` + +The command prints paths like: + +```text +context=/tmp/muw-review-pr-24/pr-24-context.md +prompt=/tmp/muw-review-pr-24/pr-24-prompt.md +metadata=/tmp/muw-review-pr-24/pr-24-metadata.json +``` + +Give the prompt file to Codex. The context bundle includes PR metadata, changed +files, status checks, recent comments, existing reviews, and a redacted patch. + +## Post Verdict + +Save the Codex verdict to a file, then post it: + +```bash +node .github/scripts/muw-review-lane.mjs post \ + --pr 24 \ + --repo Fearvox/EverOS \ + --body-file /tmp/muw-review-pr-24/verdict.md +``` + +`post` refuses bodies that do not contain: + +```text +VERDICT: +VERDICT_SUMMARY: +EVIDENCE: +``` + +It also adds a hidden marker containing the PR head SHA. Re-running `post` for +the same head is a no-op unless `--force` is provided. + +## Why Not Native Review + +- GitHub's `@codex review` endpoint is useful, but it wraps responses in the + native Codex review shell. +- GitHub Agent tasks are mutation-oriented and may create draft PRs even for a + review-only prompt. +- This lane keeps review evidence gathering and comment publishing mechanical, + while leaving the verdict judgment to Codex. diff --git a/.github/scripts/muw-review-lane.mjs b/.github/scripts/muw-review-lane.mjs new file mode 100644 index 00000000..98d57ff3 --- /dev/null +++ b/.github/scripts/muw-review-lane.mjs @@ -0,0 +1,325 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { tmpdir } from "node:os"; + +const DEFAULT_REPO = process.env.GH_REPO || "Fearvox/EverOS"; +const DEFAULT_PATCH_BYTES = 120_000; +const MARKER = "muw-review-lane:v1"; + +function usage(exitCode = 0) { + const text = ` +Usage: + node .github/scripts/muw-review-lane.mjs collect --pr [--repo owner/name] [--out ] + node .github/scripts/muw-review-lane.mjs post --pr --body-file [--repo owner/name] [--force] + +Collect creates a redacted PR evidence bundle and a Codex prompt. +Post publishes a MUW-formatted issue comment with an idempotency marker. +`; + process.stderr.write(text.trimStart()); + process.exit(exitCode); +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (!command || command === "--help" || command === "-h") usage(0); + + const options = { command, repo: DEFAULT_REPO, force: false }; + for (let index = 0; index < rest.length; index += 1) { + const arg = rest[index]; + if (arg === "--repo") options.repo = rest[++index]; + else if (arg === "--pr") options.pr = Number(rest[++index]); + else if (arg === "--out") options.out = rest[++index]; + else if (arg === "--body-file") options.bodyFile = rest[++index]; + else if (arg === "--patch-bytes") options.patchBytes = Number(rest[++index]); + else if (arg === "--force") options.force = true; + else throw new Error(`Unknown argument: ${arg}`); + } + + if (!["collect", "post"].includes(command)) usage(1); + if (!Number.isInteger(options.pr) || options.pr <= 0) { + throw new Error("--pr must be a positive pull request number"); + } + return options; +} + +function run(command, args, options = {}) { + return execFileSync(command, args, { + encoding: "utf8", + input: options.input, + env: process.env, + stdio: ["pipe", "pipe", options.inheritStderr ? "inherit" : "pipe"], + maxBuffer: 20 * 1024 * 1024, + }).trim(); +} + +function gh(args, options = {}) { + return run("gh", args, options); +} + +function ghJson(args) { + return JSON.parse(gh(args)); +} + +function redact(value) { + return String(value ?? "") + .replace(/github_pat_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]") + .replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]") + .replace(/sk-[A-Za-z0-9_-]{20,}/g, "[REDACTED_API_KEY]") + .replace(/xox[baprs]-[A-Za-z0-9-]+/g, "[REDACTED_SLACK_TOKEN]") + .replace(/(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED_TOKEN]") + .replace(/(token=)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED_TOKEN]"); +} + +function truncate(value, maxBytes) { + const text = redact(value); + const bytes = Buffer.byteLength(text, "utf8"); + if (bytes <= maxBytes) return text; + return `${text.slice(0, maxBytes)}\n\n[TRUNCATED: ${bytes - maxBytes} bytes omitted]`; +} + +function summarizeChecks(items = []) { + if (!items.length) return "- No status checks reported."; + return items + .map((item) => { + const name = item.name || item.context || item.workflowName || item.__typename; + const state = item.conclusion || item.state || item.status || "unknown"; + const url = item.detailsUrl || item.targetUrl || ""; + return `- ${name}: ${state}${url ? ` (${url})` : ""}`; + }) + .join("\n"); +} + +function summarizeFiles(files = []) { + if (!files.length) return "- No changed files reported."; + return files + .map((file) => `- ${file.path} (+${file.additions}/-${file.deletions})`) + .join("\n"); +} + +function summarizeReviews(reviews = []) { + if (!reviews.length) return "- No reviews yet."; + return reviews + .map((review) => { + const author = review.author?.login || "unknown"; + const body = truncate(review.body || "", 700).replace(/\n/g, " "); + return `- ${review.submittedAt || "unknown"} ${author} ${review.state}: ${body}`; + }) + .join("\n"); +} + +function summarizeComments(comments = []) { + if (!comments.length) return "- No issue comments yet."; + return comments + .slice(-20) + .map((comment) => { + const author = comment.author?.login || "unknown"; + const body = truncate(comment.body || "", 700).replace(/\n/g, " "); + return `- ${comment.createdAt || "unknown"} ${author}: ${body}`; + }) + .join("\n"); +} + +function collect(options) { + const outDir = resolve( + options.out || join(tmpdir(), `muw-review-pr-${options.pr}`), + ); + mkdirSync(outDir, { recursive: true }); + + const fields = [ + "number", + "title", + "url", + "author", + "baseRefName", + "headRefName", + "headRefOid", + "isDraft", + "body", + "labels", + "mergeStateStatus", + "statusCheckRollup", + "files", + "commits", + "comments", + "reviews", + ].join(","); + + const pr = ghJson([ + "pr", + "view", + String(options.pr), + "--repo", + options.repo, + "--json", + fields, + ]); + const patch = gh([ + "pr", + "diff", + String(options.pr), + "--repo", + options.repo, + "--patch", + ]); + + const patchBytes = options.patchBytes || DEFAULT_PATCH_BYTES; + const contextPath = join(outDir, `pr-${options.pr}-context.md`); + const promptPath = join(outDir, `pr-${options.pr}-prompt.md`); + const metadataPath = join(outDir, `pr-${options.pr}-metadata.json`); + + const context = [ + `# MUW Review Context: ${options.repo}#${pr.number}`, + "", + `- Title: ${pr.title}`, + `- URL: ${pr.url}`, + `- Author: ${pr.author?.login || "unknown"}`, + `- Draft: ${pr.isDraft}`, + `- Base: ${pr.baseRefName}`, + `- Head: ${pr.headRefName}`, + `- Head SHA: ${pr.headRefOid}`, + `- Merge state: ${pr.mergeStateStatus || "unknown"}`, + `- Labels: ${(pr.labels || []).map((label) => label.name).join(", ") || "none"}`, + "", + "## PR Body", + "", + truncate(pr.body || "_No body._", 12_000), + "", + "## Files", + "", + summarizeFiles(pr.files), + "", + "## Checks", + "", + summarizeChecks(pr.statusCheckRollup), + "", + "## Existing Reviews", + "", + summarizeReviews(pr.reviews), + "", + "## Recent Comments", + "", + summarizeComments(pr.comments), + "", + "## Patch", + "", + "```diff", + truncate(patch, patchBytes), + "```", + "", + ].join("\n"); + + const prompt = [ + "You are reviewing a GitHub pull request using the MUW review contract.", + "", + "Read the context file listed below. Return only a GitHub-ready comment body.", + "Do not include Markdown fences around the whole answer.", + "", + "Required shape:", + "", + "VERDICT: PASS / FLAG / BLOCK", + "VERDICT_SUMMARY: three lines or fewer; what passed, what is risky, and the next action", + "EVIDENCE:", + "", + "Rules:", + "- Do not mark PASS from author summary alone.", + "- Ground claims in files, checks, comments, or patch evidence.", + "- Findings first, ordered by severity.", + "- For each finding include severity, file/path, evidence, why it matters, and fix guidance.", + "- If clean, include evidence checked and residual risk/test gap.", + "- Keep the answer concise and public-safe.", + "", + `Context file: ${contextPath}`, + "", + ].join("\n"); + + writeFileSync(contextPath, context); + writeFileSync(promptPath, prompt); + writeFileSync( + metadataPath, + `${JSON.stringify( + { + repo: options.repo, + pr: pr.number, + headRefOid: pr.headRefOid, + contextPath, + promptPath, + }, + null, + 2, + )}\n`, + ); + + process.stdout.write( + [ + `context=${contextPath}`, + `prompt=${promptPath}`, + `metadata=${metadataPath}`, + `head=${pr.headRefOid}`, + ].join("\n") + "\n", + ); +} + +function markerFor(options, headRefOid) { + return ``; +} + +function validateMuwBody(body) { + const missing = ["VERDICT:", "VERDICT_SUMMARY:", "EVIDENCE:"].filter( + (needle) => !body.includes(needle), + ); + if (missing.length) { + throw new Error(`Body is missing required MUW field(s): ${missing.join(", ")}`); + } +} + +function existingMarkerComment(options, marker) { + const [owner, repo] = options.repo.split("/"); + const comments = ghJson([ + "api", + `/repos/${owner}/${repo}/issues/${options.pr}/comments?per_page=100`, + ]); + return comments.find((comment) => String(comment.body || "").includes(marker)); +} + +function post(options) { + if (!options.bodyFile) throw new Error("--body-file is required for post"); + const body = redact(readFileSync(resolve(options.bodyFile), "utf8")).trim(); + validateMuwBody(body); + + const pr = ghJson([ + "pr", + "view", + String(options.pr), + "--repo", + options.repo, + "--json", + "headRefOid,url", + ]); + const marker = markerFor(options, pr.headRefOid); + const existing = existingMarkerComment(options, marker); + if (existing && !options.force) { + process.stdout.write( + `skip=existing-comment\nurl=${existing.html_url}\nhead=${pr.headRefOid}\n`, + ); + return; + } + + const comment = `${marker}\n${body}\n`; + const url = gh( + ["pr", "comment", String(options.pr), "--repo", options.repo, "--body", comment], + { inheritStderr: true }, + ); + process.stdout.write(`posted=${url}\nhead=${pr.headRefOid}\n`); +} + +try { + const options = parseArgs(process.argv.slice(2)); + if (options.command === "collect") collect(options); + else if (options.command === "post") post(options); +} catch (error) { + process.stderr.write(`muw-review-lane: ${error.message}\n`); + process.exit(1); +} From 65509e2a585290c931cad314e54a78c88a81d4dd Mon Sep 17 00:00:00 2001 From: 0xVox <35294173+Fearvox@users.noreply.github.com> Date: Thu, 14 May 2026 20:01:31 -0400 Subject: [PATCH 12/24] docs: split fork docs gate repair Merge the clean three-file docs-gate repair split from the mega-run audit packet. Remote Docs checks passed before merge. Co-authored-by: Codex --- .github/CONTRIBUTING.md | 84 ++++++++++++++++++++++++++++++++++++++ .github/workflows/docs.yml | 57 ++++++++++++++++++++++++++ .markdownlint.json | 11 +++++ 3 files changed, 152 insertions(+) create mode 100644 .markdownlint.json diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d55ce87d..3fd3c9e7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -65,6 +65,90 @@ should include: Images should be hosted with GitHub user attachments or another external asset URL instead of committed to the repository. +## Fork-as-Lab Workflow + +`Fearvox/EverOS` is a development fork of `EverMind-AI/EverOS`. All experimental +work happens on the fork before selective promotion upstream. + +### Staying Current with Upstream + +The fork auto-rebases onto upstream `main` every 6 hours via +`sync-upstream.yml`. This replays fork-only commits (templates, workflows, docs) +on top of the latest upstream. If you're working on a feature branch: + +```bash +# Rebase your branch onto the latest fork main +git fetch origin +git rebase origin/main +``` + +If the auto-rebase encounters a conflict, it aborts and opens a tracking issue. +Manual resolution: + +```bash +git checkout main +git pull upstream main --rebase +# resolve conflicts, then: +git push origin main --force-with-lease +``` + +### Branch Strategy + +| Branch pattern | Purpose | Lifetime | +|---------------|---------|----------| +| `sleep-iter-*-*` | Automated overnight runs | Feature branch, merged or closed | +| `codex-watch-*` | Codex co-agent patrol | Isolated worktree, never touch | +| `feature/*` | Human-driven features | Feature branch -> PR to origin/main | +| `sleep-log` | Overnight run audit log | Persistent tracking branch | + +### Label Conventions + +| Label | Color | Use on | +|-------|-------|--------| +| `pr-mirror` | `#0E8A16` | Issues that mirror an upstream PR; triggers Linear sync | +| `tracking` | `#5319E7` | Long-lived tracking issues | +| `security` | `#B60205` | Security advisories or security-relevant PRs | +| `urgent` | `#D93F0B` | High-priority; escalates in Linear | +| `sync-failed` | `#D93F0B` | Auto-applied when Linear sync fails for an issue | + +### Issue Templates + +Use the template picker when opening an issue. The two fork-specific templates: + +- **PR Tracker** (`pr_tracker.yml`) tracks an upstream PR for Linear/Slack + visibility. Requires `pr_number`, `pr_url`, `author`, `area`, `scope`, and + `evidence`. Applies `pr-mirror` and `tracking` labels. +- **Security Tracker** (`security_tracker.yml`) tracks a security advisory. + Adds `security` and `urgent` labels on top of the PR tracker labelset. + +Both templates auto-trigger `linear-sync.yml`, which creates a corresponding +Linear issue in the `EverMind-Dash` project and comments back with the EVE +identifier. + +### Linear Sync + +Issues labeled `pr-mirror` are mirrored to Linear's `EverMind-Dash` project +automatically. The sync is one-way from GitHub to Linear. The bot comments back +with the matching EVE issue identifier on success. + +If the bot adds a `sync-failed` label, check the workflow run logs at +`https://github.com/Fearvox/EverOS/actions/workflows/linear-sync.yml`. + +### Promoting to Upstream + +When a fork change is ready for `EverMind-AI/EverOS`: + +```bash +gh pr create --repo EverMind-AI/EverOS \ + --base main \ + --head Fearvox:main \ + --title "feat: description" --body "..." +``` + +Templates and workflows committed to the fork are replayed on top of upstream +during every rebase cycle. They never conflict unless upstream adds same-named +files, which is handled by auto-rebase conflict detection. + ## Style Notes - Follow existing patterns before adding new abstractions. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index aa74ce5f..cb06530f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,6 +19,63 @@ permissions: contents: read jobs: + markdown-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Collect changed Markdown files + id: changed-markdown + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + BEFORE_SHA: ${{ github.event.before }} + AFTER_SHA: ${{ github.sha }} + run: | + python3 - <<'PY' >> "$GITHUB_OUTPUT" + from pathlib import Path + import os + import subprocess + + event_name = os.environ["EVENT_NAME"] + if event_name == "pull_request": + base_ref = os.environ["BASE_REF"] + subprocess.run(["git", "fetch", "origin", base_ref, "--depth=1"], check=True) + diff_range = f"origin/{base_ref}...HEAD" + else: + before = os.environ.get("BEFORE_SHA", "") + after = os.environ["AFTER_SHA"] + if before and set(before) != {"0"}: + diff_range = f"{before}..{after}" + else: + diff_range = f"{after}^..{after}" + + result = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMRT", diff_range], + check=True, + text=True, + stdout=subprocess.PIPE, + ) + files = [ + path + for path in result.stdout.splitlines() + if path.endswith(".md") and Path(path).is_file() + ] + + print("files< Date: Thu, 14 May 2026 20:05:24 -0400 Subject: [PATCH 13/24] docs(agents): refresh fork-side addendum Replay the fork-side agent guidance onto current main and soften the external-message ban so approved Linear/MUW routing remains possible. Remote Docs checks passed before merge. Co-authored-by: Claude Opus 4.7 Co-authored-by: Codex --- AGENTS.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d5b6b160..9e5620c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,3 +82,73 @@ uv run pyright # Type check, if pyright is installed linked issue, and available checks first. - Report actionable findings first, ordered by severity, with file/path, evidence, impact, and fix guidance. + +## Fork-Side Addendum + +This fork is a development lab for `EverMind-AI/EverOS`. Agents operating here +must preserve the fork boundary and keep source-truth routing explicit. + +### Fork Identity + +| Remote | Repo | Role | +|---|---|---| +| `origin` | `Fearvox/EverOS` | Writeable fork for experiments, mirrors, docs, and review packets. | +| `upstream` | `EverMind-AI/EverOS` | Read-only source project unless the human owner explicitly approves an upstream return. | + +### GitHub CLI Rule + +Always pass `--repo Fearvox/EverOS` to `gh` commands that mutate fork state. +Do not rely on GitHub CLI default-target heuristics when a command can write. + +### Branch Conventions + +| Prefix | Owner | Rule | +|---|---|---| +| `sleep-iter-*` | Overnight agent lanes | Draft PR first; merge, revise, or close after live checks. | +| `codex-watch-*` | Codex watch lanes | Treat as isolated unless explicitly assigned. | +| `sleep-log` | Overnight audit trail | Only the owning sleep lane may force-push. | +| `feature/*` | Human developers | Leave untouched unless the owner assigns it. | + +### Planning Directory + +`.planning/` is agent workspace material. Keep it out of public PRs unless the +owner explicitly wants a curated evidence packet in repo history. + +Expected sleep-lane artifacts include: + +- `SLEEP_LOG.md` for per-iteration notes. +- `HEARTBEAT.txt` for the current iteration marker. +- `baseline/` for pre-run snapshots. +- `CODEX_INTERVENTION.md` for co-agent interrupts. +- `WAKEUP_REPORT.md` for end-of-run summaries. + +### Labels Agents May Apply + +| Label | When | +|---|---| +| `tracking` | Opening a long-lived watch or catalog issue. | +| `sync-failed` | Workflow error handling, normally applied by automation. | +| `pr-mirror` | Issue-template or sync-created PR mirrors; do not add casually. | + +Agents must not remove labels applied by humans or CI. + +### Hard Bans + +1. Never push to `upstream`. +2. Never push to `origin/main` directly; use feature branch plus draft PR. +3. Never modify `.claude/` or `settings.json` unless explicitly assigned. +4. Never send Slack, Linear, Discord, or email updates without explicit owner + approval. +5. Never delete files larger than 1 KB without a backup tag or owner approval. +6. Never force-push outside `sleep-log` and your own feature branches. +7. Never touch `codex-watch-*` branches unless explicitly assigned. +8. Never exceed 60 GitHub API requests in a 60-second window. + +### CI/CD Awareness + +- `.github/workflows/sync-upstream.yml` syncs the fork with upstream. Agent + branches should refresh from `origin/main` after sync cycles. +- `.github/workflows/linear-sync.yml` handles `pr-mirror`-labeled issues. + Manual label changes can create Linear mirror noise. +- `.github/workflows/overnight-watch.yml` powers the sleep-lane patrol. Do not + edit it during an active sleep run unless the owner assigns that work. From 964a478d7de505070c33bb043402f9971c51968f Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:56:07 -0400 Subject: [PATCH 14/24] chore(everos-memory): add gitignore for runs + console target Excludes raven/.local-runs/ and raven-console/target/ from version control so dogfood runs and Rust build artifacts stay local. Co-Authored-By: Claude Opus 4.7 (1M context) --- use-cases/hermes-everos-memory/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 use-cases/hermes-everos-memory/.gitignore 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/ From 78deb15ff9ff674a000a16c8e5dea8a99fe2ac42 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:56:23 -0400 Subject: [PATCH 15/24] chore(everos): exclude .playwright-mcp + .goal traces from repo root Adds .playwright-mcp/ and .goal/ to root .gitignore so per-session agent traces (Playwright MCP captures, goalv3 state) stay local and do not pollute the repo working tree. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 7ea98cfbf10438b3d2a3fb08924d81b179906e35 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:56:41 -0400 Subject: [PATCH 16/24] =?UTF-8?q?docs(raven):=20rebrand=20Riven=E2=86=92Ra?= =?UTF-8?q?ven=20and=20refresh=20contract+ledger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames RIVEN_CONCEPT.md to RAVEN_CONCEPT.md and refreshes the command contract, README, research ledger, and doomsday fixture to reflect the v2 Raven framing. Adds NATIVE_FEEL_AUDIT.md and REFERENCE_NOTES.md as supporting research. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../raven/COMMAND_CONTRACT.md | 193 +++++++++++++++ .../raven/NATIVE_FEEL_AUDIT.md | 35 +++ .../raven/RAVEN_CONCEPT.md | 70 ++++++ .../raven/RAVEN_V2_RESEARCH_LEDGER.md | 224 ++++++++++++++++++ .../hermes-everos-memory/raven/README.md | 127 ++++++++++ .../raven/REFERENCE_NOTES.md | 53 +++++ .../raven/fixtures/doomsday-run.json | 148 ++++++++++++ 7 files changed, 850 insertions(+) create mode 100644 use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md create mode 100644 use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md create mode 100644 use-cases/hermes-everos-memory/raven/RAVEN_CONCEPT.md create mode 100644 use-cases/hermes-everos-memory/raven/RAVEN_V2_RESEARCH_LEDGER.md create mode 100644 use-cases/hermes-everos-memory/raven/README.md create mode 100644 use-cases/hermes-everos-memory/raven/REFERENCE_NOTES.md create mode 100644 use-cases/hermes-everos-memory/raven/fixtures/doomsday-run.json 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..0f1401f9 --- /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." + ] +} From f6241e4c6839f5567852e449a0bd1817ec31b84b Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:57:01 -0400 Subject: [PATCH 17/24] feat(raven): scaffold raven-console TUI + launcher Adds the raven-console Rust crate (Cargo.toml, Cargo.lock, src/) implementing a ratatui+crossterm terminal UI with adapters for hermes, memory, muw, packet, sc, and verify lanes. Also adds the bin/raven shell launcher that boots the console with the right context resolution. Co-Authored-By: Claude Opus 4.7 (1M context) --- use-cases/hermes-everos-memory/bin/raven | 16 + .../raven-console/Cargo.lock | 925 +++++++++++++++ .../raven-console/Cargo.toml | 18 + .../raven-console/src/adapters/hermes.rs | 364 ++++++ .../raven-console/src/adapters/memory.rs | 108 ++ .../raven-console/src/adapters/mod.rs | 6 + .../raven-console/src/adapters/muw.rs | 460 +++++++ .../raven-console/src/adapters/packet.rs | 101 ++ .../raven-console/src/adapters/sc.rs | 382 ++++++ .../raven-console/src/adapters/verify.rs | 125 ++ .../raven-console/src/audit.rs | 164 +++ .../raven-console/src/commands.rs | 640 ++++++++++ .../raven-console/src/constants.rs | 28 + .../raven-console/src/context.rs | 38 + .../raven-console/src/main.rs | 40 + .../raven-console/src/model.rs | 354 ++++++ .../raven-console/src/output.rs | 442 +++++++ .../raven-console/src/receipt.rs | 167 +++ .../raven-console/src/repl.rs | 50 + .../raven-console/src/research.rs | 440 +++++++ .../raven-console/src/sanitizer.rs | 213 ++++ .../raven-console/src/snapshot.rs | 223 ++++ .../raven-console/src/tui.rs | 1056 +++++++++++++++++ .../raven-console/src/util.rs | 32 + 24 files changed, 6392 insertions(+) create mode 100755 use-cases/hermes-everos-memory/bin/raven create mode 100644 use-cases/hermes-everos-memory/raven-console/Cargo.lock create mode 100644 use-cases/hermes-everos-memory/raven-console/Cargo.toml create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/memory.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/mod.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/muw.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/packet.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/sc.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/adapters/verify.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/audit.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/commands.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/constants.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/context.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/main.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/model.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/output.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/receipt.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/repl.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/research.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/snapshot.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/tui.rs create mode 100644 use-cases/hermes-everos-memory/raven-console/src/util.rs 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/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..ff11c4dc --- /dev/null +++ b/use-cases/hermes-everos-memory/raven-console/src/commands.rs @@ -0,0 +1,640 @@ +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::{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(), + }, + } +} + +#[allow(dead_code)] +fn relative_path_exists(root: &Path, relative: &str) -> bool { + root.join(relative).exists() +} 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('\\', "/") +} From 50a40daf238728eb047fe4c9db116027c44f6e80 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:57:09 -0400 Subject: [PATCH 18/24] chore(raven): add deepseek auth preflight script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/deepseek-auth-preflight.sh which validates that the EverCore env file is configured for OpenRouter→DeepSeek routing before deploys. Checks LLM_PROVIDER, LLM_MODEL, base URLs, and optionally enforces a non-placeholder OPENROUTER_API_KEY. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/deepseek-auth-preflight.sh | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 use-cases/hermes-everos-memory/scripts/deepseek-auth-preflight.sh 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)" From d19b834f71e6dd9f5d910f40afa302ed7ea54075 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:57:17 -0400 Subject: [PATCH 19/24] docs(everos): refresh dogfood audit + supervisor packets Updates COMPLETION_AUDIT.md, OWNER_PACKET.md, and SUPERVISOR_DISPATCH.md with the post-Raven-v2 dogfood findings: audit results, supervisor dispatch flow, and the operator-facing packet summary. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../hermes-everos-memory/COMPLETION_AUDIT.md | 79 ++++++++++ .../hermes-everos-memory/OWNER_PACKET.md | 135 ++++++++++++++++++ .../SUPERVISOR_DISPATCH.md | 134 +++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 use-cases/hermes-everos-memory/COMPLETION_AUDIT.md create mode 100644 use-cases/hermes-everos-memory/OWNER_PACKET.md create mode 100644 use-cases/hermes-everos-memory/SUPERVISOR_DISPATCH.md 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..7e497537 --- /dev/null +++ b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md @@ -0,0 +1,79 @@ +# 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. 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/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`. From 25778f1a83b58a884450bb67752d1ee7cdc865c0 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:57:26 -0400 Subject: [PATCH 20/24] chore(everos-memory): bump deploy manifests + algo-profile Refreshes deploy/nixos/ packet, README, and env example; bumps justfile and package.json. Bootstraps .algo-profile/ with the circular chat buffer decision note used in raven-console TUI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../.algo-profile/README.md | 6 + .../structures/circular-chat-buffer.md | 22 +++ .../deploy/nixos/DEPLOY_PACKET.md | 105 +++++++++++++ .../deploy/nixos/README.md | 141 ++++++++++++++++++ .../deploy/nixos/evercore.env.example | 89 +++++++++++ use-cases/hermes-everos-memory/justfile | 135 +++++++++++++++++ use-cases/hermes-everos-memory/package.json | 38 +++++ 7 files changed, 536 insertions(+) create mode 100644 use-cases/hermes-everos-memory/.algo-profile/README.md create mode 100644 use-cases/hermes-everos-memory/.algo-profile/structures/circular-chat-buffer.md create mode 100644 use-cases/hermes-everos-memory/deploy/nixos/DEPLOY_PACKET.md create mode 100644 use-cases/hermes-everos-memory/deploy/nixos/README.md create mode 100644 use-cases/hermes-everos-memory/deploy/nixos/evercore.env.example create mode 100644 use-cases/hermes-everos-memory/justfile create mode 100644 use-cases/hermes-everos-memory/package.json 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/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/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/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" + } +} From 3da3b170810e6c8e0bf27043eaf99750db12c931 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 10:58:40 -0400 Subject: [PATCH 21/24] docs(everos): record raven-v2-closure landing Appends the Raven v2 closure landing note to COMPLETION_AUDIT.md with PR URL for the docs_report gate. Closes out the goalv3-cc goal raven-v2-closure receipt trail. Co-Authored-By: Claude Opus 4.7 (1M context) --- use-cases/hermes-everos-memory/COMPLETION_AUDIT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md index 7e497537..45eb1bdf 100644 --- a/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md +++ b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md @@ -77,3 +77,9 @@ rg -n -i -f use-cases/hermes-everos-memo 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 https://github.com/Fearvox/EverOS/pull/31 +targeting `main`. From a7a8e00a5af21656ea1bea95962cfde54454329a Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 12:34:14 -0400 Subject: [PATCH 22/24] fix(raven): harden console chat boundaries Add a timeout around Hermes chat subprocesses, redact public IPv4 addresses from public-surface output, and remove an unused command helper. Co-authored-by: Codex --- .../raven-console/src/adapters/hermes.rs | 50 ++++++++++++++++--- .../raven-console/src/commands.rs | 7 +-- .../raven-console/src/sanitizer.rs | 16 +++--- 3 files changed, 51 insertions(+), 22 deletions(-) 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 index 45c51e28..56b3d831 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/adapters/hermes.rs @@ -6,12 +6,14 @@ use crate::RavenResult; use std::env; use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::Instant; +use std::process::{Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; const MAX_PROMPT_CHARS: usize = 4_000; const MAX_RESPONSE_CHARS: usize = 8_000; const MAX_EVIDENCE_CHARS: usize = 1_200; +const HERMES_TURN_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Default)] pub struct HermesOptions { @@ -81,7 +83,8 @@ pub fn ask_with_options( }; let start = Instant::now(); - let output = Command::new(binary) + let mut command = Command::new(binary); + command .arg("-z") .arg(build_raven_prompt( &bounded_prompt, @@ -91,11 +94,12 @@ pub fn ask_with_options( .current_dir(&cwd) .env("RAVEN_WORKSPACE_ROOT", &ctx.root) .env("RAVEN_OPERATOR_CWD", &cwd) - .env("RAVEN_HERMES_RUNTIME", &meta.runtime) - .output(); + .env("RAVEN_HERMES_RUNTIME", &meta.runtime); + + let output = output_with_timeout(&mut command, HERMES_TURN_TIMEOUT); match output { - Ok(output) => { + Ok(TurnOutput::Completed(output)) => { let exit_code = output.status.code().unwrap_or(1); Ok(turn_from_output( &bounded_prompt, @@ -106,6 +110,14 @@ pub fn ask_with_options( meta, )) } + Ok(TurnOutput::TimedOut(output)) => Ok(turn_from_output( + &bounded_prompt, + 124, + &String::from_utf8_lossy(&output.stdout), + "Hermes turn timed out after 30s.", + start.elapsed().as_millis(), + meta, + )), Err(err) => Ok(flag_turn( &bounded_prompt, meta, @@ -117,6 +129,32 @@ pub fn ask_with_options( } } +enum TurnOutput { + Completed(Output), + TimedOut(Output), +} + +fn output_with_timeout(command: &mut Command, timeout: Duration) -> std::io::Result { + let start = Instant::now(); + let mut child = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + loop { + if child.try_wait()?.is_some() { + return child.wait_with_output().map(TurnOutput::Completed); + } + + if start.elapsed() >= timeout { + let _ = child.kill(); + return child.wait_with_output().map(TurnOutput::TimedOut); + } + + thread::sleep(Duration::from_millis(25)); + } +} + fn build_raven_prompt(prompt: &str, workspace: &str, runtime: &str) -> String { format!( "You are Hermes inside Raven's local operator console.\n\ diff --git a/use-cases/hermes-everos-memory/raven-console/src/commands.rs b/use-cases/hermes-everos-memory/raven-console/src/commands.rs index ff11c4dc..a4e53b69 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/commands.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/commands.rs @@ -4,7 +4,7 @@ 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::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; #[derive(Parser)] @@ -633,8 +633,3 @@ fn command_check(program: &str, args: &[&str]) -> DoctorCheck { }, } } - -#[allow(dead_code)] -fn relative_path_exists(root: &Path, relative: &str) -> bool { - root.join(relative).exists() -} diff --git a/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs b/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs index 34a35da1..0dae743c 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/sanitizer.rs @@ -40,7 +40,7 @@ pub fn sanitize_text(input: &str) -> String { .get(0) .map(|item| item.as_str()) .unwrap_or_default(); - if is_private_or_public_ipv4(value) { + if is_ipv4_address(value) { "[redacted-ip]".to_string() } else { value.to_string() @@ -83,7 +83,7 @@ fn contains_sensitive_shape(text: &str) -> bool { || localhost_re().is_match(text) || ipv4_re() .find_iter(text) - .any(|match_| is_private_or_public_ipv4(match_.as_str())) + .any(|match_| is_ipv4_address(match_.as_str())) } fn signed_url_re() -> &'static Regex { @@ -139,7 +139,7 @@ fn product_name_re() -> &'static Regex { }) } -fn is_private_or_public_ipv4(value: &str) -> bool { +fn is_ipv4_address(value: &str) -> bool { let host = value.split(':').next().unwrap_or(value); let parts = host.split('.').collect::>(); if parts.len() != 4 { @@ -153,11 +153,7 @@ fn is_private_or_public_ipv4(value: &str) -> bool { 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) + true } #[cfg(test)] @@ -187,10 +183,10 @@ mod tests { #[test] fn redacts_private_ips_and_localhost() { - let text = "http://192.168.1.5:9000 and localhost:3000"; + let text = "http://192.168.1.5:9000 and localhost:3000 and 74.199.157.194"; assert_eq!( sanitize_text(text), - "http://[redacted-ip] and [redacted-host]" + "http://[redacted-ip] and [redacted-host] and [redacted-ip]" ); } From 879c836cc982ef3de8195a12601b1c64e6d1dd14 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 12:37:49 -0400 Subject: [PATCH 23/24] fix(raven): satisfy docs markdown lint Replace the stale bare PR URL with the clean replay draft PR reference and escape table pipes in Raven command contracts. Co-authored-by: Codex --- use-cases/hermes-everos-memory/COMPLETION_AUDIT.md | 5 ++--- use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md index 45eb1bdf..c0d1b1b6 100644 --- a/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md +++ b/use-cases/hermes-everos-memory/COMPLETION_AUDIT.md @@ -80,6 +80,5 @@ rg -n -i -f use-cases/hermes-everos-memo ## 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 https://github.com/Fearvox/EverOS/pull/31 -targeting `main`. +Closeout via goalv3-cc goal `raven-v2-closure`: clean replay branch landed as +draft PR [#32](https://github.com/Fearvox/EverOS/pull/32) targeting `main`. diff --git a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md index 0f1401f9..9525f3a9 100644 --- a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md +++ b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md @@ -42,7 +42,7 @@ It does not own: | `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 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` | @@ -51,8 +51,8 @@ It does not own: | `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 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` | From c011bac7a2ee55d56d286973c36794f5ccf1a2c9 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Fri, 15 May 2026 13:02:24 -0400 Subject: [PATCH 24/24] Add Raven agentic loop surface Expose the single-agent capture/plan/act/observe/verify/receipt loop through typed snapshot state, CLI/REPL handlers, and the TUI Loop panel. Keep loop receipts and remote deploy gates explicit so local chat evidence cannot green DAS-2666. Co-authored-by: Codex --- .../hermes-everos-memory/OWNER_PACKET.md | 4 + use-cases/hermes-everos-memory/justfile | 5 +- .../raven-console/src/audit.rs | 7 +- .../raven-console/src/commands.rs | 22 ++ .../raven-console/src/model.rs | 53 +++++ .../raven-console/src/output.rs | 39 +++- .../raven-console/src/snapshot.rs | 132 ++++++++++- .../raven-console/src/tui.rs | 219 +++++++++++++++++- .../raven/COMMAND_CONTRACT.md | 26 +++ .../raven/NATIVE_FEEL_AUDIT.md | 12 +- .../hermes-everos-memory/raven/README.md | 8 + 11 files changed, 506 insertions(+), 21 deletions(-) diff --git a/use-cases/hermes-everos-memory/OWNER_PACKET.md b/use-cases/hermes-everos-memory/OWNER_PACKET.md index a361c7de..17ee1f66 100644 --- a/use-cases/hermes-everos-memory/OWNER_PACKET.md +++ b/use-cases/hermes-everos-memory/OWNER_PACKET.md @@ -21,6 +21,8 @@ but EverCore is not yet proven active on the remote loopback service. - 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 single-agent loop surface: `raven loop`, `/loop`, and TUI `l` expose + capture, plan, act, observe, verify, and receipt phases above raw chat. - 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. @@ -51,6 +53,8 @@ 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-loop +cd use-cases/hermes-everos-memory && bin/raven loop --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 diff --git a/use-cases/hermes-everos-memory/justfile b/use-cases/hermes-everos-memory/justfile index ef1051a8..f1e4e06f 100644 --- a/use-cases/hermes-everos-memory/justfile +++ b/use-cases/hermes-everos-memory/justfile @@ -63,6 +63,9 @@ raven-status: raven-packet: bin/raven packet show +raven-loop: + bin/raven loop + raven-gates: bin/raven gates @@ -112,7 +115,7 @@ 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 + printf '/status\n/loop\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 diff --git a/use-cases/hermes-everos-memory/raven-console/src/audit.rs b/use-cases/hermes-everos-memory/raven-console/src/audit.rs index 16770eb1..5d173465 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/audit.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/audit.rs @@ -22,6 +22,7 @@ pub fn run(ctx: &Context) -> NativeAuditReport { "keybindings", if source.contains("KeyCode::Char('q')") && source.contains("KeyCode::Char('?')") + && source.contains("KeyCode::Char('l')") && source.contains("KeyCode::Char('h')") && source.contains("KeyCode::Char('i')") && source.contains("KeyCode::Char('o')") @@ -30,7 +31,7 @@ pub fn run(ctx: &Context) -> NativeAuditReport { } 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.", + "TUI exposes l loop, h/c chat, i prompt input, q, ?, :, /, s, p, m, a, g, r, o, d, n, Esc, and Ctrl-C paths.", true, ), item( @@ -88,13 +89,13 @@ pub fn run(ctx: &Context) -> NativeAuditReport { item( "typed IPC", Verdict::Pass, - "RavenSnapshot, RavenReceipt, HermesChatTurn, and ScReport are serde-typed JSON contracts.", + "RavenSnapshot, AgenticLoopState, 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.", + "remote hard gates, loop phases, local gates, runs, docs, and watchlist evidence are visible.", false, ), item( diff --git a/use-cases/hermes-everos-memory/raven-console/src/commands.rs b/use-cases/hermes-everos-memory/raven-console/src/commands.rs index a4e53b69..52e7c33a 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/commands.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/commands.rs @@ -41,6 +41,11 @@ pub enum Commands { #[command(subcommand)] command: MemoryCommand, }, + /// Show the single-agent goal/act/observe/verify loop. + Loop { + #[command(subcommand)] + command: Option, + }, /// Show agent lane status. Agents { #[command(subcommand)] @@ -116,6 +121,12 @@ pub enum AgentsCommand { List, } +#[derive(Subcommand)] +pub enum LoopCommand { + /// Show loop state. + Status, +} + #[derive(Subcommand)] pub enum RunsCommand { /// List saved receipts or configured verification commands. @@ -227,6 +238,15 @@ pub fn execute(cli: Cli, ctx: &Context) -> RavenResult<()> { } } }, + Commands::Loop { command: _ } => { + let snapshot = snapshot::build(ctx); + if cli.json { + output::json(&snapshot.loop_state) + } else { + output::agentic_loop(&snapshot.loop_state); + Ok(()) + } + } Commands::Agents { command: _ } => { let snapshot = snapshot::build(ctx); if cli.json { @@ -408,6 +428,7 @@ pub fn dispatch_repl(ctx: &Context, input: &str) -> RavenResult { println!("/help"); println!("/status"); println!("/packet"); + println!("/loop"); println!("/chat "); println!("/memory "); println!("/agents"); @@ -421,6 +442,7 @@ pub fn dispatch_repl(ctx: &Context, input: &str) -> RavenResult { } "/status" => output::status(&snapshot::build(ctx)), "/packet" => output::packet(&snapshot::build(ctx)), + "/loop" => output::agentic_loop(&snapshot::build(ctx).loop_state), "/agents" => output::agents(&snapshot::build(ctx)), "/gates" => output::gates(&snapshot::build(ctx)), "/research" => output::research_lanes(&research::list_lanes()), diff --git a/use-cases/hermes-everos-memory/raven-console/src/model.rs b/use-cases/hermes-everos-memory/raven-console/src/model.rs index 68399a44..a64991bb 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/model.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/model.rs @@ -41,6 +41,36 @@ impl fmt::Display for Verdict { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum AgenticLoopPhase { + Capture, + Plan, + Act, + Observe, + Verify, + Receipt, +} + +impl AgenticLoopPhase { + pub fn as_str(self) -> &'static str { + match self { + Self::Capture => "CAPTURE", + Self::Plan => "PLAN", + Self::Act => "ACT", + Self::Observe => "OBSERVE", + Self::Verify => "VERIFY", + Self::Receipt => "RECEIPT", + } + } +} + +impl fmt::Display for AgenticLoopPhase { + 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, @@ -190,6 +220,28 @@ pub struct RunView { pub receipt_path: Option, } +#[derive(Clone, Debug, Serialize)] +pub struct AgenticLoopStep { + pub phase: AgenticLoopPhase, + pub label: String, + pub verdict: Verdict, + pub evidence: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct AgenticLoopState { + pub verdict: Verdict, + pub objective: String, + pub active_phase: AgenticLoopPhase, + pub mode: String, + pub mutation_policy: String, + pub allowed_actions: Vec, + pub stop_conditions: Vec, + pub evidence_required: Vec, + pub output_contract: String, + pub steps: Vec, +} + #[derive(Clone, Debug, Serialize)] pub struct ScStatusView { pub verdict: Verdict, @@ -256,6 +308,7 @@ pub struct RavenSnapshot { pub memory: MemoryHealth, pub runs: Vec, pub sc: ScReport, + pub loop_state: AgenticLoopState, pub risks: Vec, pub next_actions: Vec, pub public_safety: PublicSafetyResult, diff --git a/use-cases/hermes-everos-memory/raven-console/src/output.rs b/use-cases/hermes-everos-memory/raven-console/src/output.rs index bcfd6f07..8d1b343a 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/output.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/output.rs @@ -1,7 +1,7 @@ use crate::model::{ - DoctorReport, HermesChatTurn, MemorySearchResult, NativeAuditReport, RavenReceipt, - RavenSnapshot, ResearchLane, ResearchPacket, ResearchSynthesis, ScProviderView, ScReport, - ScSessionView, ScStatusView, ScWorktreeView, Verdict, + AgenticLoopState, 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; @@ -29,6 +29,10 @@ pub fn status(snapshot: &RavenSnapshot) { "MEMORY: {} ({})", snapshot.memory.verdict, snapshot.memory.status )); + line(&format!( + "LOOP: {} (phase {})", + snapshot.loop_state.verdict, snapshot.loop_state.active_phase + )); line(""); line("WATCHLIST:"); for issue in &snapshot.watchlist_issues { @@ -70,6 +74,35 @@ pub fn packet(snapshot: &RavenSnapshot) { } } +pub fn agentic_loop(state: &AgenticLoopState) { + line("RAVEN_AGENTIC_LOOP"); + line(&format!("VERDICT: {}", state.verdict)); + line(&format!("OBJECTIVE: {}", state.objective)); + line(&format!("ACTIVE_PHASE: {}", state.active_phase)); + line(&format!("MODE: {}", state.mode)); + line(&format!("MUTATION_POLICY: {}", state.mutation_policy)); + line("ALLOWED_ACTIONS:"); + for action in &state.allowed_actions { + line(&format!("- {action}")); + } + line("STOP_CONDITIONS:"); + for condition in &state.stop_conditions { + line(&format!("- {condition}")); + } + line("EVIDENCE_REQUIRED:"); + for item in &state.evidence_required { + line(&format!("- {item}")); + } + line("STEPS:"); + for step in &state.steps { + line(&format!( + "- {}: {} {} evidence={}", + step.phase, step.verdict, step.label, step.evidence + )); + } + line(&format!("OUTPUT_CONTRACT: {}", state.output_contract)); +} + pub fn packet_export_markdown(snapshot: &RavenSnapshot) -> String { let mut output = Vec::new(); output.push(format!("# {}", snapshot.packet.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 index 2c466769..a0ceabf8 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs @@ -5,8 +5,9 @@ use crate::constants::{ }; use crate::context::Context; use crate::model::{ - AgentView, IssueView, LocalGateView, MemoryHealth, PacketSummary, PublicSafetyResult, - RavenSnapshot, RemoteGate, RunView, ScReport, Verdict, + AgentView, AgenticLoopPhase, AgenticLoopState, AgenticLoopStep, IssueView, LocalGateView, + MemoryHealth, PacketSummary, PublicSafetyResult, RavenSnapshot, RemoteGate, RunView, ScReport, + Verdict, }; struct SnapshotParts { @@ -86,6 +87,7 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { sc, } = parts; let verdict = overall_verdict(packet_verdict, &remote_gates); + let loop_state = agentic_loop_state(ctx, packet_verdict, &remote_gates, &memory, &runs); let mut next_actions = ctx.packet.next_actions.clone(); if remote_gates @@ -125,6 +127,7 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { memory, runs, sc, + loop_state, risks: vec![ "Remote deploy remains separate from local packet PASS.".to_string(), "DAS-2675 adapter repair cannot change DAS-2666 verdict.".to_string(), @@ -138,6 +141,131 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { } } +fn agentic_loop_state( + ctx: &Context, + packet_verdict: Verdict, + remote_gates: &[RemoteGate], + memory: &MemoryHealth, + runs: &[RunView], +) -> AgenticLoopState { + let remote_blocked = remote_gates + .iter() + .any(|gate| gate.hard_gate && gate.verdict == Verdict::Block); + let auth_repaired = remote_gates + .iter() + .any(|gate| gate.id == ISSUE_AUTH_BLOCKER && gate.verdict == Verdict::Pass); + let receipt_ready = runs.iter().any(|run| run.receipt_path.is_some()); + let verify_verdict = if remote_blocked { + Verdict::Flag + } else if packet_verdict == Verdict::Pass { + Verdict::Pass + } else { + packet_verdict + }; + + let steps = vec![ + AgenticLoopStep { + phase: AgenticLoopPhase::Capture, + label: "Goal captured".to_string(), + verdict: Verdict::Pass, + evidence: "Run packet, watchlist gates, and prompt surfaces are typed.".to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Plan, + label: "One bounded objective".to_string(), + verdict: Verdict::Pass, + evidence: "Plan stays inside Raven packet scope and visible stop conditions." + .to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Act, + label: "Hermes turn boundary".to_string(), + verdict: if memory.verdict == Verdict::Block { + Verdict::Block + } else { + Verdict::Flag + }, + evidence: "CLI, REPL, and TUI execute one sanitized Hermes turn at a time.".to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Observe, + label: "Evidence stays attached".to_string(), + verdict: Verdict::Pass, + evidence: "TUI evidence drawer, chat transcript, gates, and runs are stable panes." + .to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Verify, + label: "Gates decide closure".to_string(), + verdict: verify_verdict, + evidence: if remote_blocked { + "Local packet may pass; remote deploy remains blocked by hard gate evidence." + .to_string() + } else if auth_repaired { + "Auth repair is present; remaining verifier gates decide loop closure.".to_string() + } else { + "Verifier has no remote hard block in the current snapshot.".to_string() + }, + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Receipt, + label: "Receipt is explicit".to_string(), + verdict: if receipt_ready { + Verdict::Pass + } else { + Verdict::Flag + }, + evidence: "Use --receipt or --save to materialize sanitized run evidence.".to_string(), + }, + ]; + + let verdict = if steps.iter().any(|step| step.verdict == Verdict::Block) { + Verdict::Block + } else if steps.iter().any(|step| step.verdict == Verdict::Flag) { + Verdict::Flag + } else { + Verdict::Pass + }; + + AgenticLoopState { + verdict, + objective: ctx.packet.goal.clone(), + active_phase: if remote_blocked { + AgenticLoopPhase::Verify + } else { + AgenticLoopPhase::Act + }, + mode: "single-agent / human-in-the-loop".to_string(), + mutation_policy: "read-only by default; writes require explicit command scope or receipt target" + .to_string(), + allowed_actions: vec![ + "status/gates refresh".to_string(), + "memory search".to_string(), + "Hermes chat turn".to_string(), + "run verify".to_string(), + "receipt save/export".to_string(), + "Superconductor inspect".to_string(), + ], + stop_conditions: vec![ + "DAS-2666 BLOCK keeps remote deploy red".to_string(), + "Hermes failure or timeout returns FLAG, not console crash".to_string(), + "public-safety sanitizer failure blocks receipt publication".to_string(), + "operator approval required before remote deploy or external mutation".to_string(), + ], + evidence_required: vec![ + "operator prompt".to_string(), + "Hermes response or stderr excerpt".to_string(), + "gate effects".to_string(), + "public-safety verdict".to_string(), + "receipt path or explicit no-save state".to_string(), + ], + output_contract: + "RavenReceipt plus visible loop transcript; local loop evidence never greens remote deploy" + .to_string(), + steps, + } +} + fn fallback_watchlist() -> Vec { WATCHLIST_ISSUES .iter() diff --git a/use-cases/hermes-everos-memory/raven-console/src/tui.rs b/use-cases/hermes-everos-memory/raven-console/src/tui.rs index 77d1b1d6..c2bbc8a7 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/tui.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/tui.rs @@ -1,6 +1,8 @@ use crate::adapters::hermes; use crate::context::Context; -use crate::model::{HermesChatTranscriptLine, HermesChatTurn, RavenSnapshot, Verdict}; +use crate::model::{ + AgenticLoopPhase, HermesChatTranscriptLine, HermesChatTurn, RavenSnapshot, Verdict, +}; use crate::sanitizer::sanitize_text; use crate::snapshot; use crate::RavenResult; @@ -26,6 +28,7 @@ use std::time::Duration; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Panel { Status, + Loop, Packet, Chat, Memory, @@ -60,6 +63,7 @@ struct TuiState { input: String, evidence: String, chat: VecDeque, + loop_activity: VecDeque, chat_inflight: bool, } @@ -69,6 +73,12 @@ struct ChatLine { verdict: Option, } +struct LoopActivityLine { + phase: AgenticLoopPhase, + verdict: Verdict, + text: String, +} + enum BackgroundEvent { Snapshot(Box), Chat(HermesChatTurn), @@ -76,6 +86,7 @@ enum BackgroundEvent { const SURFACE_TITLE: &str = "RAVEN // DOOMSDAY-MAXXED-MOGGED"; const CHAT_HISTORY_LIMIT: usize = 24; +const LOOP_ACTIVITY_LIMIT: usize = 16; impl Default for TuiState { fn default() -> Self { @@ -86,6 +97,7 @@ impl Default for TuiState { evidence: "Remote gates stay red until live evidence proves every hard gate." .to_string(), chat: VecDeque::new(), + loop_activity: VecDeque::new(), chat_inflight: false, } } @@ -136,6 +148,26 @@ fn run_loop( BackgroundEvent::Chat(turn) => { state.chat_inflight = false; state.evidence = turn.evidence.clone(); + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Observe, + verdict: turn.verdict, + text: format!( + "Hermes observed in {}ms via {}.", + turn.duration_ms, turn.runtime + ), + }, + ); + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Verify, + verdict: Verdict::Flag, + text: "Gate state unchanged; remote hard gates still decide closure." + .to_string(), + }, + ); push_chat_line( &mut state, ChatLine { @@ -221,6 +253,13 @@ fn push_chat_line(state: &mut TuiState, line: ChatLine) { state.chat.push_back(line); } +fn push_loop_activity(state: &mut TuiState, line: LoopActivityLine) { + if state.loop_activity.len() == LOOP_ACTIVITY_LIMIT { + state.loop_activity.pop_front(); + } + state.loop_activity.push_back(line); +} + fn receive_background_event(rx: &Receiver) -> Option { match rx.try_recv() { Ok(event) => Some(event), @@ -244,8 +283,14 @@ fn handle_normal_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { KeyCode::Char('q') => return TuiAction::Quit, KeyCode::Char('u') => return TuiAction::Refresh, KeyCode::Char('?') => state.panel = Panel::Help, + KeyCode::Char('l') => state.panel = Panel::Loop, KeyCode::Char('h') | KeyCode::Char('c') => state.panel = Panel::Chat, - KeyCode::Char('i') | KeyCode::Enter if state.panel == Panel::Chat => { + KeyCode::Char('i') if matches!(state.panel, Panel::Chat | Panel::Loop) => { + state.mode = InputMode::Chat; + state.input.clear(); + state.evidence = "Hermes input mode. Enter sends; Esc cancels.".to_string(); + } + KeyCode::Enter if matches!(state.panel, Panel::Chat | Panel::Loop) => { state.mode = InputMode::Chat; state.input.clear(); state.evidence = "Hermes input mode. Enter sends; Esc cancels.".to_string(); @@ -301,7 +346,9 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { } else if state.chat_inflight { state.evidence = "Hermes turn already running.".to_string(); } else { - state.panel = Panel::Chat; + if state.panel != Panel::Loop { + state.panel = Panel::Chat; + } push_chat_line( state, ChatLine { @@ -318,6 +365,22 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { verdict: Some(Verdict::Flag), }, ); + push_loop_activity( + state, + LoopActivityLine { + phase: AgenticLoopPhase::Capture, + verdict: Verdict::Pass, + text: "Operator prompt captured and sanitized.".to_string(), + }, + ); + push_loop_activity( + state, + LoopActivityLine { + phase: AgenticLoopPhase::Act, + verdict: Verdict::Flag, + text: "Hermes turn queued; UI remains live.".to_string(), + }, + ); state.mode = InputMode::Normal; state.input.clear(); return TuiAction::SendChat(prompt); @@ -338,6 +401,7 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { fn apply_palette(input: &str, state: &mut TuiState) { match input.trim().to_ascii_lowercase().as_str() { "status" | "s" => state.panel = Panel::Status, + "loop" | "agentic" | "single" | "l" => state.panel = Panel::Loop, "packet" | "p" => state.panel = Panel::Packet, "chat" | "hermes" | "h" | "c" => state.panel = Panel::Chat, "memory" | "m" => state.panel = Panel::Memory, @@ -434,6 +498,7 @@ fn render_body(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, stat fn render_rail(frame: &mut Frame<'_>, area: Rect, active: Panel) { let items = [ ("s", "Status", "truth stack", Panel::Status), + ("l", "Loop", "single agent", Panel::Loop), ("p", "Packet", "owner view", Panel::Packet), ("h", "Hermes Chat", "dialogue", Panel::Chat), ("m", "Memory", "bridge health", Panel::Memory), @@ -475,6 +540,7 @@ fn render_rail(frame: &mut Frame<'_>, area: Rect, active: Panel) { 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::Loop => ("Agentic Loop", loop_lines(snapshot, state)), Panel::Packet => ("Packet", packet_lines(snapshot)), Panel::Chat => ("Hermes Chat", chat_lines(state)), Panel::Memory => ("Memory", memory_lines(snapshot)), @@ -514,6 +580,17 @@ fn render_evidence(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, ])); } lines.push(Line::from("")); + lines.push(section("AGENTIC LOOP")); + lines.push(Line::from(vec![ + chip("loop", snapshot.loop_state.verdict.to_string()), + Span::raw(" "), + chip("phase", snapshot.loop_state.active_phase.to_string()), + ])); + lines.push(Line::from(vec![Span::styled( + snapshot.loop_state.output_contract.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![ @@ -534,7 +611,7 @@ 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" + "keys: l loop | 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, ), @@ -629,6 +706,73 @@ fn packet_lines(snapshot: &RavenSnapshot) -> Vec> { lines } +fn loop_lines(snapshot: &RavenSnapshot, state: &TuiState) -> Vec> { + let loop_state = &snapshot.loop_state; + let mut lines = vec![ + section("SINGLE AGENTIC LOOP"), + Line::from(vec![ + chip("verdict", loop_state.verdict.to_string()), + Span::raw(" "), + chip("active", loop_state.active_phase.to_string()), + ]), + kv("mode", &loop_state.mode), + kv("objective", &loop_state.objective), + kv("mutation", &loop_state.mutation_policy), + Line::from(""), + section("ALLOWED ACTIONS"), + ]; + for action in &loop_state.allowed_actions { + lines.push(bullet(action)); + } + lines.push(Line::from("")); + lines.push(section("STOP CONDITIONS")); + for condition in &loop_state.stop_conditions { + lines.push(bullet(condition)); + } + lines.push(Line::from("")); + lines.push(section("PHASES")); + for step in &loop_state.steps { + lines.push(Line::from(vec![ + phase_span(step.phase), + Span::raw(" "), + verdict_span(step.verdict.to_string()), + Span::raw(" "), + Span::styled( + format!("{:<24}", step.label), + Style::default().fg(Color::White), + ), + Span::styled(step.evidence.clone(), Style::default().fg(Color::Gray)), + ])); + } + + lines.push(Line::from("")); + lines.push(section("LIVE TURN")); + if state.loop_activity.is_empty() { + lines.push(Line::from(vec![ + Span::styled("idle", Style::default().fg(Color::DarkGray)), + Span::raw(" "), + Span::styled( + "press i here or in Hermes Chat to run one bounded turn.", + Style::default().fg(Color::Gray), + ), + ])); + } else { + for item in &state.loop_activity { + lines.push(Line::from(vec![ + phase_span(item.phase), + Span::raw(" "), + verdict_span(item.verdict.to_string()), + Span::raw(" "), + Span::styled(item.text.clone(), Style::default().fg(Color::Gray)), + ])); + } + } + + lines.push(Line::from("")); + lines.push(kv("contract", &loop_state.output_contract)); + lines +} + fn memory_lines(snapshot: &RavenSnapshot) -> Vec> { vec![ section("MEMORY BRIDGE"), @@ -891,12 +1035,13 @@ fn help_lines() -> Vec> { kv("?", "help"), kv(":", "palette"), kv("/", "memory/search"), + kv("l", "single-agent loop panel"), kv("h/c", "Hermes chat panel"), - kv("i", "prompt input when Hermes panel is active"), + kv("i", "prompt input when Loop or Hermes panel is active"), kv("u", "refresh live Multica + memory data"), kv( "panels", - "s status | p packet | h chat | m memory | a agents", + "s status | l loop | p packet | h chat | m memory | a agents", ), kv("panels", "g gates | r runs | d doctor | n native audit"), kv("panels", "o superconductor"), @@ -937,6 +1082,7 @@ fn shell_block(title: &'static str, accent: Color) -> Block<'static> { fn panel_color(panel: Panel) -> Color { match panel { Panel::Status => Color::Cyan, + Panel::Loop => Color::LightGreen, Panel::Packet => Color::Magenta, Panel::Chat => Color::Magenta, Panel::Memory => Color::Green, @@ -995,6 +1141,22 @@ fn section(label: &'static str) -> Line<'static> { )]) } +fn phase_span(phase: AgenticLoopPhase) -> Span<'static> { + Span::styled( + format!("{:<8}", phase), + Style::default() + .fg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) +} + +fn bullet(value: &str) -> Line<'static> { + Line::from(vec![ + Span::styled("- ", Style::default().fg(Color::Yellow)), + Span::styled(value.to_string(), Style::default().fg(Color::Gray)), + ]) +} + fn kv(label: &'static str, value: &str) -> Line<'static> { Line::from(vec![ Span::styled(format!("{label:<10}"), Style::default().fg(Color::DarkGray)), @@ -1053,4 +1215,49 @@ mod tests { Some("turn-29") ); } + + #[test] + fn loop_activity_is_bounded_fifo() { + let mut state = TuiState::default(); + + for index in 0..20 { + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Act, + verdict: Verdict::Flag, + text: format!("loop-{index}"), + }, + ); + } + + assert_eq!(state.loop_activity.len(), LOOP_ACTIVITY_LIMIT); + assert_eq!( + state.loop_activity.front().map(|line| line.text.as_str()), + Some("loop-4") + ); + assert_eq!( + state.loop_activity.back().map(|line| line.text.as_str()), + Some("loop-19") + ); + } + + #[test] + fn loop_prompt_stays_on_loop_panel() { + let mut state = TuiState { + panel: Panel::Loop, + mode: InputMode::Chat, + input: "one bounded turn".to_string(), + ..TuiState::default() + }; + + let action = handle_input_key( + KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), + &mut state, + ); + + assert!(matches!(action, TuiAction::SendChat(prompt) if prompt == "one bounded turn")); + assert_eq!(state.panel, Panel::Loop); + assert_eq!(state.loop_activity.len(), 2); + } } diff --git a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md index 9525f3a9..6ff2de90 100644 --- a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md +++ b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md @@ -20,6 +20,7 @@ It owns: - run packet validation; - memory health/search before work starts; - lane and mutation-policy visibility; +- single-agent loop visibility; - gate visibility and conservative verdict calculation; - sanitized JSON snapshot and receipt output; - owner packet export; @@ -41,6 +42,7 @@ It does not own: | `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 loop [status] [--json]` | packet + gate + run state | `AgenticLoopState` or compact loop contract | loop closure is `FLAG` while remote hard gates or missing receipts remain | | `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 | @@ -79,6 +81,30 @@ State transitions: 5. `done` only when every blocking gate is `pass`. 6. `blocked` when a blocking gate needs human or external action. +## Single Agentic Loop Behavior + +Raven keeps a visible single-agent loop above the raw chat transcript: + +```text +CAPTURE -> PLAN -> ACT -> OBSERVE -> VERIFY -> RECEIPT +``` + +The loop is intentionally human-in-the-loop. It captures one bounded objective, +routes one Hermes turn, keeps observations attached to the evidence drawer, then +lets gates and receipts decide closure. This is the missing bridge between +`chat` and `runs`: a prompt can produce useful local evidence, but it cannot +silently mutate gate state. + +Loop invariants: + +- `raven loop` prints the typed `AgenticLoopState`; +- `/loop` in the REPL maps to the same handler; +- `l` in the TUI opens the loop panel, and `i` from that panel starts one + Hermes prompt; +- chat turns add live loop breadcrumbs for capture, act, observe, and verify; +- receipts are explicit through `--receipt` or `--save`; +- remote deploy remains red until `DAS-2666` hard evidence passes. + ## Memory Behavior Before execution Raven asks memory for: diff --git a/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md index 7e8b8d9d..3749b9ca 100644 --- a/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md +++ b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md @@ -12,22 +12,22 @@ tools without copying any external reference implementation. | 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 | +| Keybindings | A TUI operator can move without memorizing long commands. | `l` loop, `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`, `Loop`, `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 | +| Command grammar | CLI and REPL commands share the same operator vocabulary. | Slash commands map to status, packet, loop, chat, memory, agents, gates, runs, doctor, audit, and quit handlers. | PASS | +| Typed IPC | Machine output is typed and redacted. | `RavenSnapshot`, `AgenticLoopState`, `RavenReceipt`, `HermesChatTurn`, and `ScReport` are serialized through the sanitizer before JSON printing. | PASS | +| Evidence visibility | Hard gates, loop phases, and receipts are first-class. | DAS-2666, DAS-2669, capture/plan/act/observe/verify/receipt state, 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 keybindings for loop/chat/input/quit/help/palette/search/status/gates/runs/audit; - missing stable TUI panes; - unsafe interrupt behavior; - missing typed JSON snapshot or receipt contracts; diff --git a/use-cases/hermes-everos-memory/raven/README.md b/use-cases/hermes-everos-memory/raven/README.md index 45bcdeba..ff5767a7 100644 --- a/use-cases/hermes-everos-memory/raven/README.md +++ b/use-cases/hermes-everos-memory/raven/README.md @@ -37,6 +37,8 @@ bin/raven status bin/raven status --json bin/raven packet show bin/raven packet export --output - +bin/raven loop +bin/raven loop --json 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" @@ -63,6 +65,7 @@ Just targets: ```bash just raven-status just raven-packet +just raven-loop just raven-gates just raven-agents just raven-research-lanes @@ -122,6 +125,11 @@ 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. +The single-agent loop is also explicit: `raven loop`, `/loop`, and the TUI `l` +panel expose capture, plan, act, observe, verify, and receipt phases. Prompt +turns add live loop breadcrumbs, but gate closure still requires verifier and +remote hard-gate evidence. + 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.