mirror of
https://github.com/samvallad33/vestige.git
synced 2026-06-08 20:25:16 +02:00
Release v2.1.0
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
This commit is contained in:
parent
694e837898
commit
d4313df759
106 changed files with 2900 additions and 128 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -60,6 +60,8 @@ pnpm-debug.log*
|
|||
coverage/
|
||||
.nyc_output/
|
||||
*.lcov
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
apps/dashboard/test-results/
|
||||
apps/dashboard/playwright-report/
|
||||
apps/dashboard/e2e/screenshots/
|
||||
|
|
|
|||
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -5,6 +5,60 @@ All notable changes to Vestige will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.1.0] - 2026-04-27 — "Cognitive Sandwich Goes Local"
|
||||
|
||||
The Sanhedrin Executioner — Vestige's veto layer for Claude Code responses — now runs entirely on a local MLX model (`mlx-community/Qwen3.6-35B-A3B-4bit`). Zero API cost per Claude turn, fully offline, no Anthropic round-trip on the critical path. Combined with four pre-cognitive UserPromptSubmit hooks (synthesis-preflight, cwd-state-injector, vestige-pulse-daemon, preflight-swarm), Vestige now ships a complete "Cognitive Sandwich" — Vestige memories injected before the model thinks, local Sanhedrin veto after the model speaks — installable in one command on a MacBook.
|
||||
|
||||
### Added
|
||||
|
||||
- **`hooks/`** — first-class harness-side companion to the Vestige MCP server. 9 production hooks designed for `~/.claude/hooks/`:
|
||||
- `sanhedrin.sh` — Stop hook that invokes the local Qwen Executioner via the Python bridge.
|
||||
- `sanhedrin-local.py` — local backend that POSTs to `mlx_lm.server` (`localhost:8080`) with Vestige evidence injected via the dashboard `/api/deep_reference` HTTP endpoint. TRUST_FLOOR=0.55 evidence filter + topical-relevance gate + inference-verb ban + 8 worked few-shots covering true positives AND false-positive guards.
|
||||
- `synthesis-preflight.sh` — UserPromptSubmit hook that POSTs the user prompt to `/api/deep_reference` and injects the trust-scored reasoning chain into context.
|
||||
- `cwd-state-injector.sh` — captures git status, branch, modified files, open PRs/issues.
|
||||
- `vestige-pulse-daemon.sh` — surfaces fresh Vestige dream insights from the past 20 min.
|
||||
- `preflight-swarm.sh` — spawns the `lateral-thinker` subagent in fresh context for cross-disciplinary structural parallels.
|
||||
- `synthesis-stop-validator.sh` — Stop hook regex against forbidden hedging patterns.
|
||||
- `veto-detector.sh` — fast 50ms regex pre-screen against `veto`-tagged Vestige memories.
|
||||
- `synthesis-gate.sh` — legacy v1 trigger (kept for backward compat).
|
||||
- `settings.fragment.json` — JSON snippet merged into `~/.claude/settings.json` by the installer.
|
||||
- **Dashboard `/api/changelog` endpoint** — bounded REST event feed for recent `DreamCompleted` and `ConnectionDiscovered` events, used by the Pulse hook to inject fresh synthesis into Claude Code context.
|
||||
- **`agents/`** — `executioner.md` (legacy/fallback Haiku 4.5 path), `lateral-thinker.md`, `synthesis-composer.md`.
|
||||
- **`launchd/com.vestige.mlx-server.plist.template`** — auto-start `mlx_lm.server` with the Qwen3.6-35B-A3B-4bit model on login. Templated with `__HOME__` and `__MODEL__` placeholders.
|
||||
- **`scripts/install-sandwich.sh`** — one-command installer that stages hooks, agents, plist, jq-merges the settings fragment, and `launchctl load`s the plist. Backs up `settings.json` to `.bak.pre-sandwich`. Supports `--force`, `--no-launchd`, `--include-memory-loader`, `--src=PATH`.
|
||||
- **`scripts/check-sandwich-prereqs.sh`** — comprehensive prereq verifier (Apple Silicon, Python 3.10+, jq, uv, mlx-lm, hf, claude, vestige-mcp, model on disk, MCP HTTP up, server up, plist installed, settings wired).
|
||||
- **`docs/COGNITIVE_SANDWICH.md`** — architecture diagram, install guide, performance notes (82 tok/s on M3 Max), uninstall, configuration env vars.
|
||||
- **PR #48** — `VESTIGE_DATA_DIR` env-var support + tilde expansion + secure unix perms (thanks @Jelloeater) — directly addresses the ghost env-vars exposed by v2.0.9 cleanup.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Sanhedrin Executioner default backend swapped from Anthropic Haiku 4.5 → local `mlx_lm.server` + Qwen3.6-35B-A3B-4bit.** Anthropic API key no longer required for the post-cognitive layer. The `executioner.md` agent definition is retained as manual/fallback only when invoked explicitly via `Task(subagent_type='executioner')`.
|
||||
- **All hooks sanitized for public release** — replaced hardcoded personal absolute paths with `$HOME` / `$VESTIGE_*` env vars; removed personal regex tokens.
|
||||
- **NPM binary installer now follows package version** — `vestige-mcp-server@2.1.0` downloads release assets from `v2.1.0` instead of a stale hardcoded binary tag, while local workspace installs skip the release-asset download before the tag exists.
|
||||
|
||||
### Verified
|
||||
|
||||
- `cargo test --workspace --release --no-fail-fast`: **1,229 passing, 0 failed** (366 vestige-core + 358 vestige-mcp lib + 4 vestige-mcp bin + 497 e2e + 4 doctests).
|
||||
- Sanhedrin bridge smoke checks: Python bytecode compilation passes, fail-open bridge invocation returns `yes`, and public hook settings validate as JSON.
|
||||
- 8-day Sandwich dogfood: **84% pass rate, 16% legitimate vetoes** caught real hallucinations.
|
||||
|
||||
### Closes
|
||||
|
||||
- #36 (Agent Hooks for Low-Effort Automatic Memory Capture) — Cognitive Sandwich is the answer.
|
||||
|
||||
### Prerequisites for the Cognitive Sandwich
|
||||
|
||||
- macOS Apple Silicon (M1+) — required for MLX
|
||||
- Python 3.10+
|
||||
- ~22 GB free RAM (Qwen3.6-35B-A3B-4bit at runtime)
|
||||
- First-run model download: ~19 GB from Hugging Face (cached locally thereafter)
|
||||
|
||||
### Migration
|
||||
|
||||
None required for existing Vestige users. The Cognitive Sandwich is opt-in via `scripts/install-sandwich.sh`. The MCP server, schema, and tool surface are bit-identical to v2.0.9.
|
||||
|
||||
---
|
||||
|
||||
## [2.0.9] - 2026-04-24 — "Autopilot"
|
||||
|
||||
Autopilot flips Vestige from passive memory library to self-managing cognitive surface. A single supervised backend task subscribes to the 20-event WebSocket bus and routes live events into the cognitive engine — 14 previously dormant primitives (synaptic tagging, predictive memory, activation spread, prospective polling, auto-consolidation, Rac1 cascade emission) now fire without any MCP tool call. Shipped alongside a 3,091-LOC orphan-code cleanup of the v1.0 tool surface. **No schema changes, tool surface unchanged (24 tools), fully backward compatible with v2.0.8 databases. Opt-out via `VESTIGE_AUTOPILOT_ENABLED=0`.**
|
||||
|
|
|
|||
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -4531,7 +4531,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "vestige-core"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"criterion",
|
||||
|
|
@ -4566,7 +4566,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "vestige-mcp"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ exclude = [
|
|||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/samvallad33/vestige"
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
[](https://github.com/samvallad33/vestige)
|
||||
[](https://github.com/samvallad33/vestige/releases/latest)
|
||||
[](https://github.com/samvallad33/vestige/actions)
|
||||
[](https://github.com/samvallad33/vestige/actions)
|
||||
[](LICENSE)
|
||||
[](https://modelcontextprotocol.io)
|
||||
|
||||
|
|
@ -20,6 +20,16 @@ Built on 130 years of memory research — FSRS-6 spaced repetition, prediction e
|
|||
|
||||
---
|
||||
|
||||
## What's New in v2.1.0 "Cognitive Sandwich Goes Local"
|
||||
|
||||
v2.1.0 adds an opt-in Claude Code hook harness around the existing Vestige MCP server. The MCP tool surface and database schema stay backward compatible, while the new local Sanhedrin verifier and preflight hooks can inject trusted memory context before Claude answers and check drafts against high-trust Vestige evidence before delivery.
|
||||
|
||||
- **Local Sanhedrin Executioner.** The post-response verifier now runs through `mlx_lm.server` with `mlx-community/Qwen3.6-35B-A3B-4bit` by default, so the veto layer can run offline on Apple Silicon without Anthropic API calls.
|
||||
- **One-command Cognitive Sandwich installer.** `scripts/install-sandwich.sh` stages hooks, agents, and a launchd plist, merges the Claude Code hooks block, and prints real verification commands.
|
||||
- **Pulse hook backed by `/api/changelog`.** Fresh dream and connection events can be injected into the next Claude Code prompt context without blocking the prompt.
|
||||
- **`VESTIGE_DATA_DIR` support.** `--data-dir` now has an env-var fallback, tilde expansion, secure directory creation, and clear precedence docs.
|
||||
- **NPM release wrapper fixed.** `vestige-mcp-server@2.1.0` now downloads binaries from the matching `v2.1.0` GitHub release tag instead of an old hardcoded release.
|
||||
|
||||
## What's New in v2.0.9 "Autopilot"
|
||||
|
||||
Autopilot flips Vestige from passive memory library to **self-managing cognitive surface**. Same 24 MCP tools, zero schema changes — but the moment you upgrade, 14 previously dormant cognitive primitives start firing on live events without any tool call from your client.
|
||||
|
|
|
|||
95
agents/executioner.md
Normal file
95
agents/executioner.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
name: executioner
|
||||
description: "[LEGACY/FALLBACK as of 2026-04-25] The Sanhedrin post-cognitive judge. Originally invoked by sanhedrin.sh Stop hook as a Haiku 4.5 subagent. PRIMARY EXECUTION PATH NOW: ~/.claude/hooks/sanhedrin-local.py (local Qwen3.6-35B-A3B via mlx_lm.server, zero API cost, fully offline). This Haiku-backed agent runs only as manual fallback if mlx-server is unavailable or invoked explicitly via Task(subagent_type='executioner'). Same protocol: decomposes draft into atomic claims across 10 classes, verifies via Vestige deep_reference, returns 'yes' or 'no - reason' on one line."
|
||||
tools: mcp__vestige__deep_reference, mcp__vestige__memory, mcp__vestige__search
|
||||
model: claude-haiku-4-5-20251001
|
||||
---
|
||||
|
||||
# Identity
|
||||
|
||||
You are the Sanhedrin Executioner. A fresh amnesiac judge with access to the Vestige cognitive memory graph. You exist for one turn only. You do not converse. You do not explain. You return exactly one line.
|
||||
|
||||
# Your Only Job
|
||||
|
||||
Decompose the DRAFT RESPONSE into ATOMIC CLAIMS across 10 exhaustive classes, verify each against high-trust Vestige memory, and VETO the draft if any claim contradicts memory or is factual-shaped but unverifiable.
|
||||
|
||||
You are a fail-closed judge. If a claim is factual-shaped and has zero evidence in Vestige either way, that is suspicious — VETO it.
|
||||
|
||||
# The Ten Claim Classes (Exhaustive)
|
||||
|
||||
You MUST scan the draft for all ten classes. Do not skip a class because it is "not your usual job." Sam's Nightvision verification lesson (memory `efbec834`): *"Handlers must validate ALL possible enum values, not just known cases."* The same rule applies here. Enumerate exhaustively.
|
||||
|
||||
1. **TECHNICAL** — API names, version numbers, architectural patterns, configuration recommendations, file paths, command flags, library methods, crate names, endpoint URLs.
|
||||
2. **BIOGRAPHICAL** — claims about the user's identity, age, role, location, employment status, education, family, background.
|
||||
3. **FINANCIAL** — revenue figures, prize money amounts, costs, valuations, pricing, pay, MRR/ARR claims, funding received.
|
||||
4. **ACHIEVEMENT** — competition results, rankings ("won", "tied #1", "scored X/50"), project completions ("we shipped X", "released v2.3"), leaderboard claims, records set, deadlines met.
|
||||
5. **TEMPORAL** — specific dates, durations, sequences ("before X", "after Y"), deadlines, "tonight", "yesterday", "last week".
|
||||
6. **QUANTITATIVE** — counts, percentages, metrics, measurements, star counts, test pass rates, line counts.
|
||||
7. **ATTRIBUTION** — "user said X", "Sam decided Y", "agent X did Y", "we agreed on Z", "you committed to W".
|
||||
8. **CAUSAL** — "X caused Y", "because of X", "X led to Y", "X broke Y".
|
||||
9. **COMPARATIVE** — "better than X", "most", "a few", "some", "more than", "the best", "fastest", superlatives.
|
||||
10. **EXISTENTIAL** — "X exists at path Y", "feature Z is shipped", "there is a Z", "file W is in the repo".
|
||||
|
||||
# Protocol (execute silently, no narration)
|
||||
|
||||
1. **Read the draft.** Extract EVERY atomic claim you find across ALL 10 classes above. Not 1-3 — every claim that could be wrong. An atomic claim is one subject-predicate-object assertion ("Sam won AIMO3 prize money" is one claim; "Sam shipped v2.3 and it passed all tests" is two).
|
||||
|
||||
2. **For each claim, tag its class** (TECHNICAL / BIOGRAPHICAL / FINANCIAL / etc.).
|
||||
|
||||
3. **Verify each claim** via `mcp__vestige__deep_reference` with `query` set to a specific question that would confirm or contradict the claim (e.g., "What prize money has Sam won?" for a FINANCIAL claim about Sam winning $X).
|
||||
|
||||
4. **Read the response fields:**
|
||||
- `recommended` — highest-trust answer on the topic
|
||||
- `contradictions` — pairs of high-trust memories that conflict
|
||||
- `superseded` — memories replaced by newer, higher-trust versions
|
||||
- `evidence` — trust-sorted memory list
|
||||
- `confidence` — overall confidence 0-1
|
||||
|
||||
5. **Apply the class-specific decision rule:**
|
||||
|
||||
**HARD VETO classes** (BIOGRAPHICAL, FINANCIAL, ACHIEVEMENT, ATTRIBUTION):
|
||||
- If the claim contradicts a memory with trust > 0.5 → VETO.
|
||||
- If the claim is factual-shaped AND Vestige returns confidence < 0.3 with no supporting evidence → VETO (fail-closed, unverifiable positive claim about user's life).
|
||||
- If the claim uses vague qualifiers ("a few", "some", "most") in a factual assertion ("won prize money", "shipped features", "users paid") → VETO. Demand specificity.
|
||||
|
||||
**SOFT VETO classes** (TECHNICAL, EXISTENTIAL, TEMPORAL):
|
||||
- If the claim contradicts a memory with trust > 0.5 → VETO.
|
||||
- If the claim references a `superseded` memory without using its `recommended` replacement → VETO.
|
||||
- Unverifiable is NOT an automatic veto for these classes (the draft may be referencing external facts Vestige doesn't know).
|
||||
|
||||
**DECOMPOSE-FIRST classes** (CAUSAL, COMPARATIVE, QUANTITATIVE):
|
||||
- Break into constituent subject-object claims. Verify each as its own class. If any constituent hard-vetoes, the whole claim vetoes.
|
||||
|
||||
6. **If PASS:** output exactly `yes`.
|
||||
|
||||
7. **If VETO:** output exactly one line:
|
||||
```
|
||||
no - [Sanhedrin Veto] [CLASS]: [one-sentence reason under 120 chars citing memory id if applicable]
|
||||
```
|
||||
Examples:
|
||||
- `no - [Sanhedrin Veto] FINANCIAL: Draft claims "a few competitions won prize money" — Vestige has zero prize-money records, memory 6920e7fe shows AIMO3 finished 36/50, no payout.`
|
||||
- `no - [Sanhedrin Veto] ACHIEVEMENT: Draft claims "v2.3 codename Terrarium" — memory 7b6f5500 (Apr 20, trust 60%) states v2.3 codename is Thalamus.`
|
||||
- `no - [Sanhedrin Veto] TECHNICAL: Draft suggests "FastAPI shim" — memory de43be5a (trust 62%) states Vestige is a 2-crate Rust workspace (vestige-core + vestige-mcp), not Python.`
|
||||
|
||||
8. **If you cannot complete the analysis in under 12 tool calls, default to VETO** with reason `EXECUTION_INCOMPLETE` rather than `yes`. A false VETO costs a rewrite; a false PASS costs Sam's trust. Fail-closed.
|
||||
|
||||
9. **Output exactly ONE line.** Never more. No preamble, no conversation, no XML, no multi-line explanation.
|
||||
|
||||
# What NOT to do
|
||||
|
||||
- Do not limit yourself to "1-3 claims." Extract ALL atomic claims.
|
||||
- Do not paraphrase the draft.
|
||||
- Do not summarize Vestige memory contents.
|
||||
- Do not output multi-line responses.
|
||||
- Do not apologize.
|
||||
- Do not converse.
|
||||
- Do not assume a biographical/financial/achievement claim is verified just because you couldn't find a contradiction — fail-closed on unverifiable positive claims.
|
||||
- Do not veto on stylistic disagreement — only on factual contradiction or unverifiable positive assertion.
|
||||
- Do not claim to have checked a claim you skipped.
|
||||
|
||||
# Precedent — the failures this protocol was tuned to catch
|
||||
|
||||
- **2026-04-20 Terrarium-vs-Thalamus**: caught. Draft claimed v2.3 = Terrarium, memory 7b6f5500 said Thalamus. ACHIEVEMENT/EXISTENTIAL class.
|
||||
- **2026-04-20 FastAPI-vs-Rust**: caught. Draft suggested FastAPI shim, memory de43be5a said 2-crate Rust workspace. TECHNICAL class.
|
||||
- **2026-04-21 Prize-money lie**: MISSED on original protocol. Draft claimed "a few competitions won prize money" — no specific memory to contradict, but zero prize memories existed. v2 protocol catches this via COMPARATIVE vague-qualifier rule + FINANCIAL hard-veto-unverifiable rule.
|
||||
- **Nightvision-enum exhaustive-validation lesson** (memory efbec834): apply the same rule to claim extraction — validate ALL classes, not just the convenient ones.
|
||||
51
agents/lateral-thinker.md
Normal file
51
agents/lateral-thinker.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: lateral-thinker
|
||||
description: Subconscious subagent that surfaces cross-disciplinary structural parallels from the Vestige memory graph. Invoked by the preflight-swarm.sh UserPromptSubmit hook (Pre-Cognitive Triad v2.3 "Thalamus"). Fresh context, Haiku 4.5, Vestige MCP tool access. Outputs a single <lateral_epiphany> XML block or EMPTY.
|
||||
tools: mcp__vestige__search, mcp__vestige__explore_connections, mcp__vestige__memory
|
||||
model: claude-haiku-4-5-20251001
|
||||
---
|
||||
|
||||
# Identity
|
||||
|
||||
You are the Lateral Thinker, a subconscious subagent in the Vestige OS. You run before the main Claude agent sees the user's prompt. Your only job is to surface a cross-disciplinary structural parallel from the Vestige memory graph that the main agent would miss.
|
||||
|
||||
You do not converse. You do not write code. You do not acknowledge or explain yourself. You output exactly one XML block or the single word EMPTY.
|
||||
|
||||
# Execution Protocol
|
||||
|
||||
1. Read the user prompt.
|
||||
2. Extract the core structural pattern (race condition / state sync / retry loop / memory leak / schema migration / decoding ambiguity / rate limit / ordering guarantee / cache invalidation / etc).
|
||||
3. Call `mcp__vestige__explore_connections` with action=`bridges` OR `mcp__vestige__search` to find memories in a completely unrelated domain that share the same structural pattern. Prefer bridges between distant clusters — React UI state ↔ Rust async channel, Python DB lock ↔ Git merge conflict, API retry ↔ neural synaptic reinforcement.
|
||||
4. If you find a high-confidence mechanical parallel (not a metaphor, a real structural isomorphism), output exactly this XML:
|
||||
|
||||
```xml
|
||||
<lateral_epiphany>
|
||||
<structural_pattern>one short noun phrase naming the shared pattern</structural_pattern>
|
||||
<source_domain>where the user currently is</source_domain>
|
||||
<bridge_domain>the unrelated domain where the pattern also lives</bridge_domain>
|
||||
<memory_id>the Vestige node ID of the cross-domain memory, if applicable</memory_id>
|
||||
<insight>one sentence explaining how the unrelated memory informs the current problem mechanically, not metaphorically</insight>
|
||||
</lateral_epiphany>
|
||||
```
|
||||
|
||||
5. If you cannot find a confident, mechanical, distinct bridge in under three tool calls, output exactly the single word: `EMPTY`. Do not apologize, explain, or converse.
|
||||
|
||||
# Examples of valid epiphanies
|
||||
|
||||
```xml
|
||||
<lateral_epiphany>
|
||||
<structural_pattern>stale read after write under weak ordering</structural_pattern>
|
||||
<source_domain>React context propagation across portal boundary</source_domain>
|
||||
<bridge_domain>PostgreSQL read-committed isolation after uncommitted write</bridge_domain>
|
||||
<memory_id>pg-isolation-decision-2f7a</memory_id>
|
||||
<insight>The portal boundary behaves like a snapshot isolation level — state written in the parent is not visible to the portal child until the parent re-renders, analogous to waiting for commit visibility in Postgres.</insight>
|
||||
</lateral_epiphany>
|
||||
```
|
||||
|
||||
# What NOT to do
|
||||
|
||||
- Do not paraphrase the user's prompt.
|
||||
- Do not summarize Vestige memory contents as a list.
|
||||
- Do not say "this reminds me of".
|
||||
- Do not output analogies that are mere vibes — every bridge must be a concrete mechanical equivalence.
|
||||
- Do not converse. If you are about to type a sentence that begins with "Here is" or "I found" or "Let me think", stop and emit EMPTY instead.
|
||||
41
agents/synthesis-composer.md
Normal file
41
agents/synthesis-composer.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
name: synthesis-composer
|
||||
description: Forces active synthesis mode for high-stakes prompts. Invoke for competition submissions (AIMO, Nemotron, Kaggle), architectural choices, purchases over $200, launches, and strategic decisions. The subagent runs in isolation with a hard system prompt that enforces the Composing / Never-composed / Recommendation response shape and blocks summary-pattern output at the source. Use when "what should Sam DO?" matters more than "what does the memory say?"
|
||||
tools: mcp__vestige__search, mcp__vestige__deep_reference, mcp__vestige__cross_reference, mcp__vestige__explore_connections, mcp__vestige__session_context, mcp__vestige__memory, mcp__vestige__smart_ingest, mcp__vestige__intention
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are the Synthesis Composer. You exist to do ONE thing: turn Vestige retrievals into concrete recommendations Sam can act on.
|
||||
|
||||
## The Hard Rule
|
||||
|
||||
Every response you emit MUST follow this exact shape. No exceptions. Deviation is a protocol violation and the entire response will be rejected.
|
||||
|
||||
1. **Composing:** list the memory IDs you retrieved, then your composition logic. The logic is your own chain-of-thought about how the memories relate, NOT a restatement of their individual contents. If you catch yourself writing "Memory A says X, and Memory B says Y," STOP. That is the forbidden pattern.
|
||||
2. **Never-composed detected:** explicitly list combinations of retrieved memories that share tags or topics but have never been retrieved together before this session. If none, write "None." Do NOT skip this line. The whole point of your existence is to surface these.
|
||||
3. **Recommendation: Sam should DO [concrete action].** Not "Sam should consider." Not "Sam might want to." A specific executable step with a subject, a verb, and an object.
|
||||
|
||||
## Protocol — Do These Things In Order
|
||||
|
||||
1. Run a MINIMUM of 4 parallel Vestige queries across ADJACENT topics, not just the topic you were asked about. Example: if asked about an AIMO submission, query the asked topic AND proven-baseline memories AND parser-fix memories AND prompt-engineering memories AND failure-mode memories. Minimum 4 parallel searches.
|
||||
2. Call `explore_connections` with `action: "bridges"` to surface memories that share tags but have never been referenced together. This is your primary never-composed detection mechanism. Do not skip it.
|
||||
3. Cross-reference the retrieved memories in YOUR OWN reasoning before writing anything. Compose them in your head first. Ask yourself which combinations exist in Sam's store, which have been tested together in prior sessions, which have NOT been composed yet, and what Sam should DO given the composition.
|
||||
4. Only then write the response in the three-part shape above.
|
||||
|
||||
## Forbidden Output Pattern
|
||||
|
||||
If your draft begins with "Memory A says X. Memory B says Y. Memory C says Z." followed by a vague synthesis sentence, you are in the AIMO3 36/50 failure pattern. STOP. Rewrite into composition form with a concrete "Sam should DO" action.
|
||||
|
||||
The test is simple: if Sam can read your response and not know what to do next, you failed. If he can read your response and immediately execute the recommendation without further clarification, you succeeded.
|
||||
|
||||
## Trust Overrides
|
||||
|
||||
FSRS trust scores override your priors. A memory with retention greater than 0.7 and reps greater than 0 beats a fresh claim you were about to make 30 seconds ago, every single time. If a retrieved memory contradicts your draft, start your response with "Vestige is blocking this:" and surface the contradiction verbatim before proceeding.
|
||||
|
||||
## When To Decline
|
||||
|
||||
If after 4+ queries and a bridges call you cannot find a composition or a never-composed combination, respond with: "Insufficient memory context. Recommended action: run [specific query] or save [specific memory] before making this decision." That is a legitimate output. What is NOT legitimate is guessing.
|
||||
|
||||
## Origin
|
||||
|
||||
This subagent exists because on April 14-15, 2026, Claude retrieved three composable memories (4da778e2, 2f171e0e, b43da3be) for a $1.59M math olympiad submission and reported them as summaries instead of composing them. The result was 36/50 against a 47/50 prize threshold. The protocol you enforce makes that failure mode structurally impossible within your subagent context. You do not have permission to skip the shape.
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
1
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js
Normal file
1
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js
Normal file
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/chunks/EM_PBt2C.js.gz
Normal file
Binary file not shown.
1
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js
Normal file
1
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js
Normal file
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/chunks/RBGf_S-E.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/entry/app.C-NL1yUd.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
import{a as r}from"../chunks/EM_PBt2C.js";import{w as t}from"../chunks/RBGf_S-E.js";export{t as load_css,r as start};
|
||||
BIN
apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/entry/start.BLzz4N6-.js.gz
Normal file
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
import{a as r}from"../chunks/BoJ3aQqE.js";import{w as t}from"../chunks/BibkZxNI.js";export{t as load_css,r as start};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/0.DHxskm8N.js.gz
Normal file
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
import"../chunks/Bzak7iHL.js";import{i as h}from"../chunks/Bz1l2A_1.js";import{p as g,f as d,t as l,a as v,d as _,e as s,r as o}from"../chunks/CvjSAYrz.js";import{s as p}from"../chunks/FzvEaXMa.js";import{a as x,f as $}from"../chunks/BsvCUYx-.js";import{p as m}from"../chunks/BibkZxNI.js";import{s as k}from"../chunks/BoJ3aQqE.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=$("<h1> </h1> <p> </p>",1);function C(f,n){g(n,!1),h();var t=E(),r=d(t),c=s(r,!0);o(r);var a=_(r,2),u=s(a,!0);o(a),l(()=>{var e;p(c,i.status),p(u,(e=i.error)==null?void 0:e.message)}),x(f,t),v()}export{C as component};
|
||||
import"../chunks/Bzak7iHL.js";import{i as h}from"../chunks/Bz1l2A_1.js";import{p as g,f as d,t as l,a as v,d as _,e as s,r as o}from"../chunks/CvjSAYrz.js";import{s as p}from"../chunks/FzvEaXMa.js";import{a as x,f as $}from"../chunks/BsvCUYx-.js";import{p as m}from"../chunks/RBGf_S-E.js";import{s as k}from"../chunks/EM_PBt2C.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=$("<h1> </h1> <p> </p>",1);function C(f,n){g(n,!1),h();var t=E(),r=d(t),c=s(r,!0);o(r);var a=_(r,2),u=s(a,!0);o(a),l(()=>{var e;p(c,i.status),p(u,(e=i.error)==null?void 0:e.message)}),x(f,t),v()}export{C as component};
|
||||
BIN
apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/1.BgGPnSIe.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
var Bc=Object.defineProperty;var zc=(i,t,e)=>t in i?Bc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var kt=(i,t,e)=>zc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as jl,a as Zl}from"../chunks/CNjeV5xa.js";import{s as me,c as va,h as zt,g as B,p as ys,aB as kc,a as Es,e as yt,d as bt,n as Hc,r as xt,t as Ke,u as Gn,f as Kl,j as Vc}from"../chunks/CvjSAYrz.js";import{s as fe,d as $l,a as Fe}from"../chunks/FzvEaXMa.js";import{i as kn}from"../chunks/ciN1mm2W.js";import{e as _s,i as hr}from"../chunks/DTnG8poT.js";import{a as _e,f as Se,c as Gc}from"../chunks/BsvCUYx-.js";import{s as ve,r as xa}from"../chunks/CNfQDikv.js";import{s as Us}from"../chunks/DPl3NjBv.js";import{s as Sr}from"../chunks/Bhad70Ss.js";import{b as Ma}from"../chunks/CVpUe0w3.js";import{b as Jl}from"../chunks/DMu1Byux.js";import{s as Wc,a as Xc}from"../chunks/D81f-o_I.js";import{b as Do}from"../chunks/BibkZxNI.js";import{b as Yc}from"../chunks/D3XWCg9-.js";import{p as vs}from"../chunks/B_YDQCB6.js";import{N as Sa}from"../chunks/DzfRjky4.js";import{i as qc}from"../chunks/Bz1l2A_1.js";import{a as gi}from"../chunks/DNjM5a-l.js";import{e as jc}from"../chunks/CtkE7HV2.js";/**
|
||||
var Bc=Object.defineProperty;var zc=(i,t,e)=>t in i?Bc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var kt=(i,t,e)=>zc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as jl,a as Zl}from"../chunks/CNjeV5xa.js";import{s as me,c as va,h as zt,g as B,p as ys,aB as kc,a as Es,e as yt,d as bt,n as Hc,r as xt,t as Ke,u as Gn,f as Kl,j as Vc}from"../chunks/CvjSAYrz.js";import{s as fe,d as $l,a as Fe}from"../chunks/FzvEaXMa.js";import{i as kn}from"../chunks/ciN1mm2W.js";import{e as _s,i as hr}from"../chunks/DTnG8poT.js";import{a as _e,f as Se,c as Gc}from"../chunks/BsvCUYx-.js";import{s as ve,r as xa}from"../chunks/CNfQDikv.js";import{s as Us}from"../chunks/DPl3NjBv.js";import{s as Sr}from"../chunks/Bhad70Ss.js";import{b as Ma}from"../chunks/CVpUe0w3.js";import{b as Jl}from"../chunks/DMu1Byux.js";import{s as Wc,a as Xc}from"../chunks/D81f-o_I.js";import{b as Do}from"../chunks/RBGf_S-E.js";import{b as Yc}from"../chunks/D3XWCg9-.js";import{p as vs}from"../chunks/B_YDQCB6.js";import{N as Sa}from"../chunks/DzfRjky4.js";import{i as qc}from"../chunks/Bz1l2A_1.js";import{a as gi}from"../chunks/DNjM5a-l.js";import{e as jc}from"../chunks/CtkE7HV2.js";/**
|
||||
* @license
|
||||
* Copyright 2010-2024 Three.js Authors
|
||||
* SPDX-License-Identifier: MIT
|
||||
BIN
apps/dashboard/build/_app/immutable/nodes/10.Dp-knJux.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/10.Dp-knJux.js.br
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/nodes/11.BLR7H2sn.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/11.BLR7H2sn.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/nodes/11.BLR7H2sn.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/11.BLR7H2sn.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
$`dp›<70>’`i»©$ÄÜfçgšXfa\[àm‘di€¯ü{äw›Ž«XE´"ïà5¨Tª¬äª[]$´Ú•w§Šâ‚!ÝR§]ÜÊ}§T¨k<C2A8>9Y‡É:Êò¢ðŸvDÃ:{%R <ç'<27>¸ë PóФïö:ª|¡\2QêA³DÔQÉ<51><10>':Õ¿îJé¨¿QË*Y•hˆr>ŠS‘ù
|
||||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
import"../chunks/Bzak7iHL.js";import{i as p}from"../chunks/Bz1l2A_1.js";import{o as r}from"../chunks/CNjeV5xa.js";import{p as t,a}from"../chunks/CvjSAYrz.js";import{g as m}from"../chunks/BoJ3aQqE.js";function g(i,o){t(o,!1),r(()=>m("/graph",{replaceState:!0})),p(),a()}export{g as component};
|
||||
import"../chunks/Bzak7iHL.js";import{i as p}from"../chunks/Bz1l2A_1.js";import{o as r}from"../chunks/CNjeV5xa.js";import{p as t,a}from"../chunks/CvjSAYrz.js";import{g as m}from"../chunks/EM_PBt2C.js";function g(i,o){t(o,!1),r(()=>m("/graph",{replaceState:!0})),p(),a()}export{g as component};
|
||||
BIN
apps/dashboard/build/_app/immutable/nodes/3.CQLLmTOU.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/3.CQLLmTOU.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/nodes/3.CQLLmTOU.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/3.CQLLmTOU.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.br
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.br
Normal file
Binary file not shown.
BIN
apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.gz
Normal file
BIN
apps/dashboard/build/_app/immutable/nodes/6.B_eyyG0t.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
{"version":"1777232068712"}
|
||||
{"version":"1777313640654"}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -11,13 +11,13 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/dashboard/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/dashboard/favicon.svg" />
|
||||
<link rel="manifest" href="/dashboard/manifest.json" />
|
||||
<link href="/dashboard/_app/immutable/entry/start.htNaTyzN.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/BoJ3aQqE.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/entry/start.BLzz4N6-.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/EM_PBt2C.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/CvjSAYrz.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/DfQhL-hC.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/BibkZxNI.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/RBGf_S-E.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/CNjeV5xa.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/entry/app.DvgeOzkC.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/entry/app.C-NL1yUd.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/FzvEaXMa.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/BsvCUYx-.js" rel="modulepreload">
|
||||
<link href="/dashboard/_app/immutable/chunks/Bzak7iHL.js" rel="modulepreload">
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_1lurly2 = {
|
||||
__sveltekit_8fmifr = {
|
||||
base: "/dashboard",
|
||||
assets: "/dashboard"
|
||||
};
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/dashboard/_app/immutable/entry/start.htNaTyzN.js"),
|
||||
import("/dashboard/_app/immutable/entry/app.DvgeOzkC.js")
|
||||
import("/dashboard/_app/immutable/entry/start.BLzz4N6-.js"),
|
||||
import("/dashboard/_app/immutable/entry/app.C-NL1yUd.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
4
apps/dashboard/package-lock.json
generated
4
apps/dashboard/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@vestige/dashboard",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vestige/dashboard",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"dependencies": {
|
||||
"three": "^0.172.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vestige/dashboard",
|
||||
"version": "2.0.9",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vestige-core"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
authors = ["Vestige Team"]
|
||||
|
|
|
|||
|
|
@ -738,7 +738,10 @@ mod tests {
|
|||
|
||||
// 1. schema_version advanced to V11
|
||||
let version = get_current_version(&conn).expect("read schema_version");
|
||||
assert_eq!(version, 11, "schema_version must be 11 after all migrations");
|
||||
assert_eq!(
|
||||
version, 11,
|
||||
"schema_version must be 11 after all migrations"
|
||||
);
|
||||
|
||||
// 2. knowledge_edges is gone (V11 drops it)
|
||||
let knowledge_edges_rows: i64 = conn
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vestige-mcp"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research"
|
||||
authors = ["samvallad33"]
|
||||
|
|
@ -44,7 +44,7 @@ path = "src/bin/cli.rs"
|
|||
# Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are
|
||||
# toggled via vestige-mcp's own feature flags below so `--no-default-features`
|
||||
# actually works (previously hardcoded here, which silently defeated the flag).
|
||||
vestige-core = { version = "2.0.8", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
|
||||
vestige-core = { version = "2.1.0", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
|
||||
|
||||
# ============================================================================
|
||||
# MCP Server Dependencies
|
||||
|
|
|
|||
|
|
@ -153,10 +153,8 @@ pub fn spawn(
|
|||
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
"Autopilot event subscriber panicked — supervisor restarting"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
))
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "Autopilot event subscriber join error — exiting");
|
||||
|
|
@ -187,10 +185,8 @@ pub fn spawn(
|
|||
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
"Autopilot prospective poller panicked — supervisor restarting"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
))
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "Autopilot prospective poller join error — exiting");
|
||||
|
|
@ -218,14 +214,7 @@ async fn run_event_subscriber(
|
|||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
handle_event(
|
||||
event,
|
||||
&cognitive,
|
||||
&storage,
|
||||
&event_tx,
|
||||
&mut dedup_state,
|
||||
)
|
||||
.await;
|
||||
handle_event(event, &cognitive, &storage, &event_tx, &mut dedup_state).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!("Autopilot lagged {n} events — increase channel capacity if this persists");
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
//!
|
||||
//! v2.0: Adds cognitive operation endpoints (dream, explore, predict, importance, consolidation)
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Json, Redirect};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
@ -373,6 +375,13 @@ pub struct TimelineParams {
|
|||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChangelogParams {
|
||||
pub start: Option<String>,
|
||||
pub end: Option<String>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
/// Get timeline data
|
||||
pub async fn get_timeline(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -428,6 +437,108 @@ pub async fn get_timeline(
|
|||
})))
|
||||
}
|
||||
|
||||
/// Recent cognitive events in the same envelope used by the WebSocket event
|
||||
/// stream. The pulse hook polls this endpoint once per Claude wake, so keep it
|
||||
/// cheap, bounded, and tolerant of empty history.
|
||||
pub async fn get_changelog(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ChangelogParams>,
|
||||
) -> Result<Json<Value>, StatusCode> {
|
||||
let limit = params.limit.unwrap_or(50).clamp(1, 100);
|
||||
let start = parse_changelog_bound(params.start.as_deref())?;
|
||||
let end = parse_changelog_bound(params.end.as_deref())?;
|
||||
let fetch_limit = if start.is_some() || end.is_some() {
|
||||
limit.saturating_mul(4)
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
let mut events: Vec<(DateTime<Utc>, Value)> = Vec::new();
|
||||
|
||||
let dreams = state
|
||||
.storage
|
||||
.get_dream_history(fetch_limit)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
for dream in dreams {
|
||||
if changelog_window_contains(dream.dreamed_at, start.as_ref(), end.as_ref()) {
|
||||
events.push((dream.dreamed_at, dream_changelog_event(&dream)));
|
||||
}
|
||||
}
|
||||
|
||||
// Connections are currently persisted as graph edges rather than as audit
|
||||
// rows, so filter by created_at from the connection table.
|
||||
let connections = state
|
||||
.storage
|
||||
.get_all_connections()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
for conn in connections {
|
||||
if changelog_window_contains(conn.created_at, start.as_ref(), end.as_ref()) {
|
||||
events.push((conn.created_at, connection_changelog_event(&conn)));
|
||||
}
|
||||
}
|
||||
|
||||
events.sort_by_key(|event| Reverse(event.0));
|
||||
events.truncate(limit as usize);
|
||||
let formatted_events: Vec<Value> = events.into_iter().map(|(_, event)| event).collect();
|
||||
let total_events = formatted_events.len();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"events": formatted_events,
|
||||
"totalEvents": total_events,
|
||||
"filter": {
|
||||
"start": start.as_ref().map(DateTime::to_rfc3339),
|
||||
"end": end.as_ref().map(DateTime::to_rfc3339),
|
||||
"limit": limit,
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
fn parse_changelog_bound(raw: Option<&str>) -> Result<Option<DateTime<Utc>>, StatusCode> {
|
||||
match raw {
|
||||
Some(value) if !value.trim().is_empty() => DateTime::parse_from_rfc3339(value)
|
||||
.map(|dt| Some(dt.with_timezone(&Utc)))
|
||||
.map_err(|_| StatusCode::BAD_REQUEST),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn changelog_window_contains(
|
||||
ts: DateTime<Utc>,
|
||||
start: Option<&DateTime<Utc>>,
|
||||
end: Option<&DateTime<Utc>>,
|
||||
) -> bool {
|
||||
start.is_none_or(|s| ts >= *s) && end.is_none_or(|e| ts <= *e)
|
||||
}
|
||||
|
||||
fn dream_changelog_event(dream: &vestige_core::DreamHistoryRecord) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "DreamCompleted",
|
||||
"timestamp": dream.dreamed_at.to_rfc3339(),
|
||||
"data": {
|
||||
"memories_replayed": dream.memories_replayed,
|
||||
"connections_found": dream.connections_found,
|
||||
"connections_persisted": dream.connections_found,
|
||||
"insights_generated": dream.insights_generated,
|
||||
"duration_ms": dream.duration_ms,
|
||||
"timestamp": dream.dreamed_at.to_rfc3339(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn connection_changelog_event(conn: &vestige_core::ConnectionRecord) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "ConnectionDiscovered",
|
||||
"timestamp": conn.created_at.to_rfc3339(),
|
||||
"data": {
|
||||
"source_id": &conn.source_id,
|
||||
"target_id": &conn.target_id,
|
||||
"connection_type": &conn.link_type,
|
||||
"weight": conn.strength,
|
||||
"timestamp": conn.created_at.to_rfc3339(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Health check
|
||||
pub async fn health_check(State(state): State<AppState>) -> Result<Json<Value>, StatusCode> {
|
||||
let stats = state
|
||||
|
|
@ -537,7 +648,9 @@ pub async fn get_graph(
|
|||
&& edges.is_empty()
|
||||
&& let Ok(fallback) = default_center_id(&state.storage, GraphSort::Connected)
|
||||
&& fallback != center_id
|
||||
&& let Ok((n2, e2)) = state.storage.get_memory_subgraph(&fallback, depth, max_nodes)
|
||||
&& let Ok((n2, e2)) = state
|
||||
.storage
|
||||
.get_memory_subgraph(&fallback, depth, max_nodes)
|
||||
&& n2.len() > nodes.len()
|
||||
{
|
||||
center_id = fallback;
|
||||
|
|
@ -793,14 +906,38 @@ pub async fn trigger_dream(State(state): State<AppState>) -> Result<Json<Value>,
|
|||
}
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
let completed_at = Utc::now();
|
||||
let insights_generated = insights.len();
|
||||
|
||||
if let Err(e) = state
|
||||
.storage
|
||||
.save_dream_history(&vestige_core::DreamHistoryRecord {
|
||||
dreamed_at: completed_at,
|
||||
duration_ms: duration_ms as i64,
|
||||
memories_replayed: dream_memories.len() as i32,
|
||||
connections_found: connections_persisted as i32,
|
||||
insights_generated: insights_generated as i32,
|
||||
memories_strengthened: dream_result.memories_strengthened as i32,
|
||||
memories_compressed: dream_result.memories_compressed as i32,
|
||||
phase_nrem1_ms: None,
|
||||
phase_nrem3_ms: None,
|
||||
phase_rem_ms: None,
|
||||
phase_integration_ms: None,
|
||||
summaries_generated: None,
|
||||
emotional_memories_processed: None,
|
||||
creative_connections_found: None,
|
||||
})
|
||||
{
|
||||
tracing::warn!("Failed to persist dashboard dream history: {}", e);
|
||||
}
|
||||
|
||||
// Emit completion event
|
||||
state.emit(VestigeEvent::DreamCompleted {
|
||||
memories_replayed: dream_memories.len(),
|
||||
connections_found: connections_persisted as usize,
|
||||
insights_generated: insights.len(),
|
||||
insights_generated,
|
||||
duration_ms,
|
||||
timestamp: Utc::now(),
|
||||
timestamp: completed_at,
|
||||
});
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
|
|
@ -1303,7 +1440,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use vestige_core::memory::IngestInput;
|
||||
use vestige_core::{ConnectionRecord, Storage};
|
||||
use vestige_core::{ConnectionRecord, DreamHistoryRecord, Storage};
|
||||
|
||||
#[test]
|
||||
fn graph_sort_parse_defaults_to_recent() {
|
||||
|
|
@ -1322,6 +1459,51 @@ mod tests {
|
|||
assert_eq!(GraphSort::parse(Some("Connected")), GraphSort::Connected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changelog_dream_event_uses_pulse_compatible_shape() {
|
||||
let now = Utc::now();
|
||||
let event = dream_changelog_event(&DreamHistoryRecord {
|
||||
dreamed_at: now,
|
||||
duration_ms: 1234,
|
||||
memories_replayed: 12,
|
||||
connections_found: 3,
|
||||
insights_generated: 2,
|
||||
memories_strengthened: 0,
|
||||
memories_compressed: 0,
|
||||
phase_nrem1_ms: None,
|
||||
phase_nrem3_ms: None,
|
||||
phase_rem_ms: None,
|
||||
phase_integration_ms: None,
|
||||
summaries_generated: None,
|
||||
emotional_memories_processed: None,
|
||||
creative_connections_found: None,
|
||||
});
|
||||
|
||||
assert_eq!(event["type"], "DreamCompleted");
|
||||
assert_eq!(event["data"]["insights_generated"], 2);
|
||||
assert_eq!(event["data"]["connections_persisted"], 3);
|
||||
assert_eq!(event["data"]["timestamp"], now.to_rfc3339());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changelog_connection_event_uses_pulse_compatible_shape() {
|
||||
let now = Utc::now();
|
||||
let event = connection_changelog_event(&ConnectionRecord {
|
||||
source_id: "source-memory".to_string(),
|
||||
target_id: "target-memory".to_string(),
|
||||
strength: 0.82,
|
||||
link_type: "semantic".to_string(),
|
||||
created_at: now,
|
||||
last_activated: now,
|
||||
activation_count: 1,
|
||||
});
|
||||
|
||||
assert_eq!(event["type"], "ConnectionDiscovered");
|
||||
assert_eq!(event["data"]["source_id"], "source-memory");
|
||||
assert_eq!(event["data"]["target_id"], "target-memory");
|
||||
assert_eq!(event["data"]["connection_type"], "semantic");
|
||||
}
|
||||
|
||||
fn seed_storage() -> (tempfile::TempDir, Arc<Storage>) {
|
||||
let dir = tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.db");
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
// since v2.0.5 despite having full graph event handlers; this closes
|
||||
// the gap so dashboard users can trigger inhibition without dropping
|
||||
// to the MCP layer.
|
||||
.route("/api/memories/{id}/suppress", post(handlers::suppress_memory))
|
||||
.route(
|
||||
"/api/memories/{id}/suppress",
|
||||
post(handlers::suppress_memory),
|
||||
)
|
||||
.route(
|
||||
"/api/memories/{id}/unsuppress",
|
||||
post(handlers::unsuppress_memory),
|
||||
|
|
@ -154,6 +157,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
.route("/api/health", get(handlers::health_check))
|
||||
// Timeline
|
||||
.route("/api/timeline", get(handlers::get_timeline))
|
||||
.route("/api/changelog", get(handlers::get_changelog))
|
||||
// Graph
|
||||
.route("/api/graph", get(handlers::get_graph))
|
||||
// Cognitive operations (v2.0)
|
||||
|
|
@ -171,10 +175,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
// Reasoning Theater (v2.0.8) — 8-stage cognitive pipeline surface.
|
||||
// Wraps crate::tools::cross_reference::execute. Emits
|
||||
// DeepReferenceCompleted so Graph3D can glide, pulse, and arc.
|
||||
.route(
|
||||
"/api/deep_reference",
|
||||
post(handlers::deep_reference_query),
|
||||
)
|
||||
.route("/api/deep_reference", post(handlers::deep_reference_query))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.concurrency_limit(50)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ use vestige_mcp::cognitive;
|
|||
use vestige_mcp::protocol;
|
||||
use vestige_mcp::server;
|
||||
|
||||
use directories::BaseDirs;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{Level, error, info, warn};
|
||||
|
|
@ -44,6 +47,9 @@ use vestige_core::Storage;
|
|||
use protocol::stdio::StdioTransport;
|
||||
use server::McpServer;
|
||||
|
||||
const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR";
|
||||
const DATABASE_FILE: &str = "vestige.db";
|
||||
|
||||
/// Parsed CLI configuration.
|
||||
struct Config {
|
||||
data_dir: Option<PathBuf>,
|
||||
|
|
@ -51,11 +57,24 @@ struct Config {
|
|||
dashboard_enabled: bool,
|
||||
}
|
||||
|
||||
fn data_dir_from_env() -> Option<PathBuf> {
|
||||
std::env::var_os(DATA_DIR_ENV).and_then(|value| {
|
||||
if value.as_os_str().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse command-line arguments into a `Config`.
|
||||
/// Exits the process if `--help` or `--version` is requested.
|
||||
fn parse_args() -> Config {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
parse_args_from(std::env::args_os().collect(), data_dir_from_env())
|
||||
}
|
||||
|
||||
fn parse_args_from(args: Vec<OsString>, env_data_dir: Option<PathBuf>) -> Config {
|
||||
let mut data_dir = env_data_dir;
|
||||
let mut http_port: u16 = std::env::var("VESTIGE_HTTP_PORT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
|
|
@ -66,7 +85,8 @@ fn parse_args() -> Config {
|
|||
let mut i = 1;
|
||||
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
let arg = args[i].to_string_lossy();
|
||||
match arg.as_ref() {
|
||||
"--help" | "-h" => {
|
||||
println!("Vestige MCP Server v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!();
|
||||
|
|
@ -78,10 +98,15 @@ fn parse_args() -> Config {
|
|||
println!("OPTIONS:");
|
||||
println!(" -h, --help Print help information");
|
||||
println!(" -V, --version Print version information");
|
||||
println!(" --data-dir <PATH> Custom data directory");
|
||||
println!(
|
||||
" --data-dir <PATH> Custom data directory (overrides VESTIGE_DATA_DIR)"
|
||||
);
|
||||
println!(" --http-port <PORT> HTTP transport port (default: 3928)");
|
||||
println!();
|
||||
println!("ENVIRONMENT:");
|
||||
println!(
|
||||
" VESTIGE_DATA_DIR Data directory fallback (stores vestige.db inside)"
|
||||
);
|
||||
println!(
|
||||
" RUST_LOG Log level filter (e.g., debug, info, warn, error)"
|
||||
);
|
||||
|
|
@ -98,6 +123,7 @@ fn parse_args() -> Config {
|
|||
println!("EXAMPLES:");
|
||||
println!(" vestige-mcp");
|
||||
println!(" vestige-mcp --data-dir /custom/path");
|
||||
println!(" VESTIGE_DATA_DIR=~/.vestige vestige-mcp");
|
||||
println!(" vestige-mcp --http-port 8080");
|
||||
println!(" RUST_LOG=debug vestige-mcp");
|
||||
std::process::exit(0);
|
||||
|
|
@ -113,6 +139,11 @@ fn parse_args() -> Config {
|
|||
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if args[i].as_os_str().is_empty() {
|
||||
eprintln!("error: --data-dir requires a non-empty path argument");
|
||||
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
data_dir = Some(PathBuf::from(&args[i]));
|
||||
}
|
||||
arg if arg.starts_with("--data-dir=") => {
|
||||
|
|
@ -132,10 +163,11 @@ fn parse_args() -> Config {
|
|||
eprintln!("Usage: vestige-mcp --http-port <PORT>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
http_port = match args[i].parse() {
|
||||
let port = args[i].to_string_lossy();
|
||||
http_port = match port.parse() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("error: invalid port number '{}'", args[i]);
|
||||
eprintln!("error: invalid port number '{}'", port);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
|
@ -167,6 +199,42 @@ fn parse_args() -> Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||
let rest = {
|
||||
let mut components = path.components();
|
||||
match components.next() {
|
||||
Some(Component::Normal(first)) if first == "~" => {
|
||||
Some(components.as_path().to_path_buf())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
match rest {
|
||||
Some(rest) => BaseDirs::new()
|
||||
.map(|dirs| dirs.home_dir().join(rest))
|
||||
.unwrap_or(path),
|
||||
None => path,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_storage_path(data_dir: Option<PathBuf>) -> io::Result<Option<PathBuf>> {
|
||||
let Some(data_dir) = data_dir else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data_dir = expand_tilde(data_dir);
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700));
|
||||
}
|
||||
|
||||
Ok(Some(data_dir.join(DATABASE_FILE)))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Parse CLI arguments first (before logging init, so --help/--version work cleanly)
|
||||
|
|
@ -185,8 +253,17 @@ async fn main() {
|
|||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
// Initialize storage with optional custom data directory
|
||||
let storage = match Storage::new(config.data_dir) {
|
||||
let storage_path = match prepare_storage_path(config.data_dir) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to prepare storage data directory: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize storage with optional custom data directory.
|
||||
// Storage::new(Some(...)) expects a DB file path, so map data dirs to vestige.db here.
|
||||
let storage = match Storage::new(storage_path) {
|
||||
Ok(s) => {
|
||||
info!("Storage initialized successfully");
|
||||
|
||||
|
|
@ -417,3 +494,57 @@ async fn main() {
|
|||
|
||||
info!("Vestige MCP Server shutting down");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn os_args(args: &[&str]) -> Vec<OsString> {
|
||||
args.iter().map(OsString::from).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vestige_data_dir_env_is_used_when_cli_data_dir_is_absent() {
|
||||
let config = parse_args_from(
|
||||
os_args(&["vestige-mcp"]),
|
||||
Some(PathBuf::from("/tmp/vestige-env")),
|
||||
);
|
||||
|
||||
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-env")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_data_dir_takes_precedence_over_env_data_dir() {
|
||||
let config = parse_args_from(
|
||||
os_args(&["vestige-mcp", "--data-dir", "/tmp/vestige-cli"]),
|
||||
Some(PathBuf::from("/tmp/vestige-env")),
|
||||
);
|
||||
|
||||
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-cli")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_storage_path_creates_dir_and_points_to_vestige_db() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let data_dir = temp.path().join("nested").join("vestige");
|
||||
|
||||
let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap();
|
||||
|
||||
assert!(data_dir.is_dir());
|
||||
assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_tilde_expands_current_users_home_only() {
|
||||
let home = BaseDirs::new().unwrap().home_dir().to_path_buf();
|
||||
|
||||
assert_eq!(
|
||||
expand_tilde(PathBuf::from("~/vestige")),
|
||||
home.join("vestige")
|
||||
);
|
||||
assert_eq!(
|
||||
expand_tilde(PathBuf::from("~other/vestige")),
|
||||
PathBuf::from("~other/vestige")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -694,11 +694,9 @@ pub async fn execute(
|
|||
// embedding space — even though the winning memory contains neither
|
||||
// "FSRS-6" nor anything about spaced repetition).
|
||||
const TOPIC_STOPWORDS: &[&str] = &[
|
||||
"how", "what", "when", "where", "why", "who", "which",
|
||||
"does", "did", "is", "are", "was", "were", "will",
|
||||
"the", "and", "for", "with", "this", "that",
|
||||
"work", "works", "use", "uses", "used", "using",
|
||||
"about", "from", "into", "than", "then",
|
||||
"how", "what", "when", "where", "why", "who", "which", "does", "did", "is", "are", "was",
|
||||
"were", "will", "the", "and", "for", "with", "this", "that", "work", "works", "use",
|
||||
"uses", "used", "using", "about", "from", "into", "than", "then",
|
||||
];
|
||||
let topic_terms: Vec<String> = args
|
||||
.query
|
||||
|
|
@ -762,15 +760,12 @@ pub async fn execute(
|
|||
&non_superseded_all
|
||||
};
|
||||
|
||||
let recommended = primary_pool
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|a, b| {
|
||||
composite(a)
|
||||
.partial_cmp(&composite(b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a.updated_at.cmp(&b.updated_at))
|
||||
});
|
||||
let recommended = primary_pool.iter().copied().max_by(|a, b| {
|
||||
composite(a)
|
||||
.partial_cmp(&composite(b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a.updated_at.cmp(&b.updated_at))
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// STAGE 7: Relation Assessment (per-pair, using trust + temporal + similarity)
|
||||
|
|
|
|||
|
|
@ -114,15 +114,18 @@ pub async fn execute(
|
|||
degraded = true;
|
||||
Vec::new()
|
||||
});
|
||||
let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
target: "vestige::predict",
|
||||
error = %e,
|
||||
"prediction_accuracy failed; returning 0.0"
|
||||
);
|
||||
degraded = true;
|
||||
0.0
|
||||
});
|
||||
let accuracy = cog
|
||||
.predictive_memory
|
||||
.prediction_accuracy()
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
target: "vestige::predict",
|
||||
error = %e,
|
||||
"prediction_accuracy failed; returning 0.0"
|
||||
);
|
||||
degraded = true;
|
||||
0.0
|
||||
});
|
||||
|
||||
// Build speculative context
|
||||
let speculative_context = vestige_core::PredictionContext {
|
||||
|
|
|
|||
|
|
@ -67,26 +67,25 @@ pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Valu
|
|||
|
||||
// Try parsing as wrapped format first (MCP response wrapper),
|
||||
// then fall back to direct RecallResult
|
||||
let memories: Vec<MemoryBackup> = if let Ok(wrapper) =
|
||||
serde_json::from_str::<Vec<BackupWrapper>>(&backup_content)
|
||||
{
|
||||
if let Some(first) = wrapper.first() {
|
||||
let recall: RecallResult = serde_json::from_str(&first.text)
|
||||
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
|
||||
let memories: Vec<MemoryBackup> =
|
||||
if let Ok(wrapper) = serde_json::from_str::<Vec<BackupWrapper>>(&backup_content) {
|
||||
if let Some(first) = wrapper.first() {
|
||||
let recall: RecallResult = serde_json::from_str(&first.text)
|
||||
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
|
||||
recall.results
|
||||
} else {
|
||||
return Err("Empty backup file".to_string());
|
||||
}
|
||||
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
|
||||
recall.results
|
||||
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
|
||||
nodes
|
||||
} else {
|
||||
return Err("Empty backup file".to_string());
|
||||
}
|
||||
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
|
||||
recall.results
|
||||
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
|
||||
nodes
|
||||
} else {
|
||||
return Err(
|
||||
return Err(
|
||||
"Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories."
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
let total = memories.len();
|
||||
if total == 0 {
|
||||
|
|
|
|||
|
|
@ -207,12 +207,7 @@ mod tests {
|
|||
|
||||
/// Ingest with explicit node_type and tags. Used by the sparse-filter
|
||||
/// regression tests so the dominant and sparse sets can be told apart.
|
||||
async fn ingest_typed(
|
||||
storage: &Arc<Storage>,
|
||||
content: &str,
|
||||
node_type: &str,
|
||||
tags: &[&str],
|
||||
) {
|
||||
async fn ingest_typed(storage: &Arc<Storage>, content: &str, node_type: &str, tags: &[&str]) {
|
||||
storage
|
||||
.ingest(vestige_core::IngestInput {
|
||||
content: content.to_string(),
|
||||
|
|
@ -392,11 +387,23 @@ mod tests {
|
|||
|
||||
// Dominant set: 10 facts
|
||||
for i in 0..10 {
|
||||
ingest_typed(&storage, &format!("Dominant memory {}", i), "fact", &["alpha"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Dominant memory {}", i),
|
||||
"fact",
|
||||
&["alpha"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Sparse set: 2 concepts
|
||||
for i in 0..2 {
|
||||
ingest_typed(&storage, &format!("Sparse memory {}", i), "concept", &["beta"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Sparse memory {}", i),
|
||||
"concept",
|
||||
&["beta"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Limit 5 against 12 total — before the fix, `retain` on `concept`
|
||||
|
|
@ -426,7 +433,13 @@ mod tests {
|
|||
|
||||
// Dominant set: 10 memories with tag "common"
|
||||
for i in 0..10 {
|
||||
ingest_typed(&storage, &format!("Common memory {}", i), "fact", &["common"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Common memory {}", i),
|
||||
"fact",
|
||||
&["common"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Sparse set: 2 memories with tag "rare"
|
||||
for i in 0..2 {
|
||||
|
|
|
|||
161
docs/COGNITIVE_SANDWICH.md
Normal file
161
docs/COGNITIVE_SANDWICH.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Cognitive Sandwich
|
||||
|
||||
**Vestige's defense-in-depth safety architecture for Claude Code.**
|
||||
|
||||
The Cognitive Sandwich wraps every Claude Code response in two layers of cognitive scaffolding:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ 🥪 TOP BREAD — UserPromptSubmit hooks │
|
||||
│ • Vestige memory graph injection │
|
||||
│ • CWD / git / CI state injection │
|
||||
│ • Synthesis-protocol gate (decision-adjacent) │
|
||||
│ • Lateral-thinker subconscious swarm │
|
||||
│ • Pulse daemon (background dream insights) │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ 🥩 MEAT — Claude Code reasons │
|
||||
├────────────────────────────────────────────────┤
|
||||
│ 🥪 BOTTOM BREAD — Stop hooks │
|
||||
│ • Veto-detector (fast 50ms regex pre-screen) │
|
||||
│ • Sanhedrin Executioner (LOCAL Qwen3.6-35B) │
|
||||
│ • Synthesis stop validator (hedge detector) │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The Sanhedrin Executioner is the headline of v2.1.0. As of v2.1.0 it runs entirely on a local MLX model (`mlx-community/Qwen3.6-35B-A3B-4bit`), replacing the v2.0.x Haiku 4.5 subagent. **Zero API cost per Claude turn, fully offline, ~5–15s verdict latency on M-series Apple Silicon.**
|
||||
|
||||
---
|
||||
|
||||
## How a single response flows through the Sandwich
|
||||
|
||||
1. **You type a prompt in Claude Code.**
|
||||
2. **UserPromptSubmit hooks fire in parallel** (none can block — all fail-open):
|
||||
- `load-all-memory.sh` (opt-in) — dumps every memory MD into context
|
||||
- `synthesis-preflight.sh` — POSTs your prompt to `vestige-mcp` `/api/deep_reference`, injects the trust-scored reasoning chain
|
||||
- `cwd-state-injector.sh` — captures git status, branch, open PRs/issues, modified files
|
||||
- `vestige-pulse-daemon.sh` — injects fresh Vestige dream insights from the past 20 min into the next prompt context
|
||||
- `preflight-swarm.sh` — spawns the `lateral-thinker` subagent in fresh context to surface cross-disciplinary structural parallels
|
||||
3. **Claude reads the assembled context and generates a draft.**
|
||||
4. **Stop hooks fire serially** (any can VETO with `exit 2`, forcing a rewrite):
|
||||
- `veto-detector.sh` — fast regex against `veto`-tagged Vestige memories (~50ms)
|
||||
- `sanhedrin.sh` → `sanhedrin-local.py` — single-shot local Qwen3.6-35B-A3B verdict
|
||||
- `synthesis-stop-validator.sh` — regex against forbidden patterns (hedging, summary-instead-of-composition)
|
||||
5. **If all 3 Stop hooks return `exit 0`, the response is delivered.**
|
||||
|
||||
---
|
||||
|
||||
## The Sanhedrin Executioner protocol
|
||||
|
||||
The Executioner extracts atomic claims from Claude's draft across 10 classes:
|
||||
|
||||
`TECHNICAL` · `BIOGRAPHICAL` · `FINANCIAL` · `ACHIEVEMENT` · `TIMELINE` · `QUANTITATIVE` · `ATTRIBUTION` · `CAUSAL` · `COMPARATIVE` · `EXISTENTIAL` · plus v2.1.0 additions: `VAGUE-QUANTIFIER` · `UNVERIFIED-POSITIVE`
|
||||
|
||||
For each claim, it checks Vestige's `deep_reference` for high-trust contradicting memories. Decision rules:
|
||||
|
||||
| Class | Rule |
|
||||
|---|---|
|
||||
| TECHNICAL / EXISTENTIAL / TIMELINE | VETO if memory trust > 0.55 directly contradicts |
|
||||
| BIOGRAPHICAL / FINANCIAL / ACHIEVEMENT / ATTRIBUTION | VETO if contradicted OR if factual-shaped with zero supporting evidence (fail-closed) |
|
||||
| **VAGUE-QUANTIFIER** | VETO on "a few wins / some prize money / most placed" without enumeration |
|
||||
| **UNVERIFIED-POSITIVE** | VETO on specific named institutions/dates/employers not in evidence |
|
||||
|
||||
False-positive guards (added v2.1.0 after dogfood):
|
||||
- Subject-equality gate (memory about Vestige codebase ≠ contradiction with external tools)
|
||||
- Version-discriminator rule (M3 Max ≠ M5 Max; Qwen3.5 ≠ Qwen3.6)
|
||||
- Agreement-is-not-contradiction (memory that AGREES with draft → PASS)
|
||||
- Architecture-vs-component (overall architecture memory doesn't contradict component-level draft)
|
||||
- Inference-verb ban (no `implies` / `suggests` / `must mean` in veto reasons)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### One-liner
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/samvallad33/vestige/v2.1.0/scripts/install-sandwich.sh | sh
|
||||
```
|
||||
|
||||
### From a checkout
|
||||
|
||||
```bash
|
||||
git clone https://github.com/samvallad33/vestige
|
||||
cd vestige
|
||||
./scripts/install-sandwich.sh # add --force to overwrite existing hooks
|
||||
./scripts/check-sandwich-prereqs.sh # verify everything's wired
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| Tool | Install |
|
||||
|---|---|
|
||||
| macOS Apple Silicon (M1+) | required for MLX |
|
||||
| Python 3.10+ | typically preinstalled |
|
||||
| `jq` | `brew install jq` |
|
||||
| `uv` | `brew install uv` |
|
||||
| `mlx-lm` | `uv tool install mlx-lm` |
|
||||
| `huggingface_hub[cli]` | `uv tool install 'huggingface_hub[cli]'` |
|
||||
| `vestige-mcp` | `cargo install vestige-mcp` |
|
||||
| Claude Code | https://claude.ai/code |
|
||||
| Qwen3.6-35B-A3B-4bit | `hf download mlx-community/Qwen3.6-35B-A3B-4bit` (~19 GB) |
|
||||
|
||||
### What the installer does
|
||||
|
||||
1. Verifies prereqs (warnings for missing tools, fatal only on jq/python3).
|
||||
2. Copies hooks to `~/.claude/hooks/`, agents to `~/.claude/agents/`.
|
||||
3. Renders `launchd/com.vestige.mlx-server.plist.template` with your `$HOME` and chosen model, writes to `~/Library/LaunchAgents/`.
|
||||
4. `launchctl load` the plist (auto-start mlx_lm.server with the Qwen model on boot).
|
||||
5. Backs up existing `~/.claude/settings.json` to `.bak.pre-sandwich`, then `jq`-merges the hooks block.
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.vestige.mlx-server.plist
|
||||
rm ~/Library/LaunchAgents/com.vestige.mlx-server.plist
|
||||
cp ~/.claude/settings.json.bak.pre-sandwich ~/.claude/settings.json
|
||||
# Hook files in ~/.claude/hooks/ can be deleted manually.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance notes
|
||||
|
||||
On M3 Max 16-core (400 GB/s memory bandwidth):
|
||||
- Sanhedrin verdict: 5–15 seconds end-to-end (single deep_reference + single Qwen call)
|
||||
- mlx_lm.server token generation: ~82 tok/s
|
||||
- mlx_lm.server peak resident memory: ~19.7 GB
|
||||
- Cold model load: ~5 seconds
|
||||
|
||||
On M3 Max 14-core or M2/M1 Max: closer to 3–7s prompt processing, ~50–60 tok/s generation.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Default | Effect |
|
||||
|---|---|---|
|
||||
| `VESTIGE_SANHEDRIN_ENABLED` | `1` | Set to `0` to disable Sanhedrin Stop hook entirely |
|
||||
| `VESTIGE_SWARM_ENABLED` | `1` | Set to `0` to disable preflight lateral-thinker swarm |
|
||||
| `VESTIGE_DASHBOARD_PORT` | `3927` | Vestige MCP HTTP API port used by hooks |
|
||||
| `MLX_ENDPOINT` | `http://127.0.0.1:8080/v1/chat/completions` | OpenAI-compatible chat completions endpoint for Sanhedrin |
|
||||
| `VESTIGE_SANDWICH_MODEL` | `mlx-community/Qwen3.6-35B-A3B-4bit` | Model launchd serves and Sanhedrin requests |
|
||||
| `VESTIGE_MEMORY_DIR` | (auto) | Override per-user Claude memory dir |
|
||||
|
||||
---
|
||||
|
||||
## Architecture provenance
|
||||
|
||||
The Cognitive Sandwich originated April 2026 as a defense against the AIMO3 36/50 failure mode — Claude retrieving relevant memories but summarizing them instead of composing them into recommendations. The pre-cognitive layer enforces composition; the post-cognitive layer catches contradictions before they ship.
|
||||
|
||||
Full architecture memory: search Vestige for `god-tier-plan` or `cognitive-sandwich` tags after install.
|
||||
|
||||
---
|
||||
|
||||
## Linux / Intel Mac
|
||||
|
||||
The launchd layer is macOS-arm64-only. On Linux or Intel Mac:
|
||||
- Hooks + agents install fine with `--no-launchd`
|
||||
- The Sanhedrin Stop hook will fail-open (mlx-server unreachable → exit 0)
|
||||
- Optional: run a remote mlx_lm.server / vLLM / Ollama OpenAI-compatible endpoint and set `MLX_ENDPOINT` to its `/v1/chat/completions` URL
|
||||
|
||||
Future v2.2.0 will add Linux-native MLX equivalents.
|
||||
|
|
@ -31,16 +31,17 @@ export FASTEMBED_CACHE_PATH="/custom/path"
|
|||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `VESTIGE_DATA_DIR` | OS per-user data directory | Storage directory fallback; overridden by `--data-dir`; database lives at `<dir>/vestige.db` |
|
||||
| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering |
|
||||
| `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location |
|
||||
| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port |
|
||||
| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port |
|
||||
| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address |
|
||||
| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth |
|
||||
| `VESTIGE_DASHBOARD_ENABLED` | `true` | Set `false` to disable the web dashboard |
|
||||
| `VESTIGE_DASHBOARD_ENABLED` | `false` | Set `true` or `1` to enable the web dashboard |
|
||||
| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence |
|
||||
|
||||
> **Storage location** is controlled by the `--data-dir <path>` CLI flag (see below), not an env var. Default is your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/` on Linux, `%APPDATA%\vestige\core\data\` on Windows.
|
||||
> **Storage location precedence:** `--data-dir <path>` wins over `VESTIGE_DATA_DIR`; if neither is set, Vestige uses your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/core/` on Linux, `%APPDATA%\vestige\core\` on Windows. Custom paths are directories, are created if missing, expand a leading `~`, and store the database at `<dir>/vestige.db`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ export FASTEMBED_CACHE_PATH="/custom/path"
|
|||
|
||||
```bash
|
||||
vestige-mcp --data-dir /custom/path # Custom storage location
|
||||
VESTIGE_DATA_DIR=~/.vestige vestige-mcp # Env fallback storage location
|
||||
vestige-mcp --help # Show all options
|
||||
```
|
||||
|
||||
|
|
@ -146,6 +148,14 @@ For per-project or custom storage:
|
|||
}
|
||||
```
|
||||
|
||||
For a shell-level default:
|
||||
|
||||
```bash
|
||||
export VESTIGE_DATA_DIR="/path/to/custom/dir"
|
||||
```
|
||||
|
||||
`--data-dir` takes precedence over `VESTIGE_DATA_DIR`, so you can keep a global env default and still isolate one client or project with an explicit CLI argument.
|
||||
|
||||
See [Storage Modes](STORAGE.md) for more options.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ All memories are stored in a **single local SQLite file**:
|
|||
| Linux | `~/.local/share/vestige/core/vestige.db` |
|
||||
| Windows | `%APPDATA%\vestige\core\vestige.db` |
|
||||
|
||||
Override precedence:
|
||||
|
||||
1. `vestige-mcp --data-dir <path>`
|
||||
2. `VESTIGE_DATA_DIR=<path>`
|
||||
3. OS default shown above
|
||||
|
||||
`--data-dir` and `VESTIGE_DATA_DIR` both point to a **directory**, not the database file itself. Vestige creates the directory if it does not exist, expands a leading `~`, and stores the database at `<data-dir>/vestige.db`.
|
||||
|
||||
---
|
||||
|
||||
## Storage Modes
|
||||
|
|
@ -30,6 +38,12 @@ One shared memory for all projects. Good for:
|
|||
claude mcp add vestige vestige-mcp -s user
|
||||
```
|
||||
|
||||
To set a global override for all MCP launches that inherit your shell environment:
|
||||
|
||||
```bash
|
||||
export VESTIGE_DATA_DIR="~/.vestige"
|
||||
```
|
||||
|
||||
### Option 2: Per-Project Memory
|
||||
|
||||
Separate memory per codebase. Good for:
|
||||
|
|
@ -53,6 +67,8 @@ Add to your project's `.claude/settings.local.json`:
|
|||
|
||||
This creates `.vestige/vestige.db` in your project root. Add `.vestige/` to `.gitignore`.
|
||||
|
||||
If both `VESTIGE_DATA_DIR` and `--data-dir` are set, the CLI flag wins. Use the env var for a machine-wide default and the CLI flag for per-client or per-project overrides.
|
||||
|
||||
**Multiple Named Instances:**
|
||||
|
||||
For power users who want both global AND project memory:
|
||||
|
|
|
|||
116
hooks/cwd-state-injector.sh
Executable file
116
hooks/cwd-state-injector.sh
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
#!/bin/bash
|
||||
# cwd-state-injector.sh — SessionStart + UserPromptSubmit hook
|
||||
#
|
||||
# HOOK #3 of the 2026-04-20 upgrade: ELIMINATE RE-EXPLORATION PENALTY.
|
||||
#
|
||||
# On every prompt, reads the current directory's git + CI + test state and
|
||||
# injects it as additionalContext so Claude starts every turn already knowing:
|
||||
#
|
||||
# - current git branch + HEAD commit + staged/unstaged file counts
|
||||
# - last commit subject + author
|
||||
# - last GitHub Actions run conclusion via gh CLI (if repo has remote)
|
||||
# - open PR + open issue counts
|
||||
# - recent test-suite status (cached if present)
|
||||
#
|
||||
# Saves ~500 tokens per prompt (Claude no longer asks "what state are we in?")
|
||||
# and prevents stale-state reasoning errors.
|
||||
#
|
||||
# Cached in /tmp/cwd-state-{hash}.json for 60s to keep hook fast.
|
||||
# Fails open: if gh or git unavailable, emits partial context.
|
||||
|
||||
set -u
|
||||
|
||||
INPUT="$(cat)"
|
||||
CWD="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("cwd",""))' 2>/dev/null || printf '')"
|
||||
|
||||
# Fallback to PWD if cwd not in input
|
||||
if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then
|
||||
CWD="$(pwd 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# Only run in git repos
|
||||
cd "$CWD" 2>/dev/null || exit 0
|
||||
if ! /usr/bin/git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Cache for 60s
|
||||
CACHE_KEY="$(printf '%s' "$CWD" | /usr/bin/shasum | awk '{print $1}')"
|
||||
CACHE_FILE="/tmp/cwd-state-${CACHE_KEY}.json"
|
||||
if [ -f "$CACHE_FILE" ]; then
|
||||
MTIME=$(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)
|
||||
NOW=$(date +%s)
|
||||
AGE=$((NOW - MTIME))
|
||||
if [ "$AGE" -lt 60 ] && [ -s "$CACHE_FILE" ]; then
|
||||
cat "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Gather state
|
||||
BRANCH="$(/usr/bin/git rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
||||
HEAD_SHA="$(/usr/bin/git rev-parse --short HEAD 2>/dev/null)"
|
||||
HEAD_SUBJECT="$(/usr/bin/git log -1 --format='%s' 2>/dev/null | head -c 100)"
|
||||
HEAD_AUTHOR="$(/usr/bin/git log -1 --format='%an' 2>/dev/null)"
|
||||
STAGED_COUNT="$(/usr/bin/git diff --cached --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')"
|
||||
UNSTAGED_COUNT="$(/usr/bin/git diff --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')"
|
||||
UNTRACKED_COUNT="$(/usr/bin/git ls-files --others --exclude-standard 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')"
|
||||
AHEAD_BEHIND="$(/usr/bin/git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null | awk '{printf "ahead=%s behind=%s", $1, $2}' || echo "no-upstream")"
|
||||
|
||||
# GitHub state (only if gh CLI available + remote configured)
|
||||
CI_STATE=""
|
||||
PR_COUNT=""
|
||||
ISSUE_COUNT=""
|
||||
if /usr/bin/which gh > /dev/null 2>&1 && /usr/bin/git config --get remote.origin.url > /dev/null 2>&1; then
|
||||
# Last CI run on current branch
|
||||
CI_JSON="$(gh run list --branch "$BRANCH" --limit 1 --json status,conclusion,name,headSha 2>/dev/null || echo '[]')"
|
||||
CI_STATE="$(printf '%s' "$CI_JSON" | /usr/bin/python3 -c 'import sys,json
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
if d: r=d[0]; print(f"{r.get(\"name\",\"?\")}:{r.get(\"status\",\"?\")}:{r.get(\"conclusion\") or \"...\"}")
|
||||
except: pass' 2>/dev/null)"
|
||||
PR_COUNT="$(gh pr list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json
|
||||
try: print(len(json.load(sys.stdin)))
|
||||
except: print("?")' 2>/dev/null)"
|
||||
ISSUE_COUNT="$(gh issue list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json
|
||||
try: print(len(json.load(sys.stdin)))
|
||||
except: print("?")' 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# Build context block
|
||||
REPO_NAME="$(/usr/bin/basename "$CWD")"
|
||||
CONTEXT_LINES=()
|
||||
CONTEXT_LINES+=("[CWD STATE — auto-injected, 60s cache]")
|
||||
CONTEXT_LINES+=(" repo: $REPO_NAME branch: $BRANCH HEAD: $HEAD_SHA")
|
||||
if [ -n "$HEAD_SUBJECT" ]; then
|
||||
CONTEXT_LINES+=(" last commit: \"$HEAD_SUBJECT\" by $HEAD_AUTHOR")
|
||||
fi
|
||||
CONTEXT_LINES+=(" working tree: staged=$STAGED_COUNT unstaged=$UNSTAGED_COUNT untracked=$UNTRACKED_COUNT")
|
||||
if [ "$AHEAD_BEHIND" != "no-upstream" ]; then
|
||||
CONTEXT_LINES+=(" vs upstream: $AHEAD_BEHIND")
|
||||
fi
|
||||
if [ -n "$CI_STATE" ]; then
|
||||
CONTEXT_LINES+=(" last CI run: $CI_STATE")
|
||||
fi
|
||||
if [ -n "$PR_COUNT" ] && [ -n "$ISSUE_COUNT" ]; then
|
||||
CONTEXT_LINES+=(" open: PRs=$PR_COUNT issues=$ISSUE_COUNT")
|
||||
fi
|
||||
|
||||
# Format as JSON additionalContext
|
||||
JSON_OUT="$(/usr/bin/python3 <<PYEOF
|
||||
import json, os
|
||||
lines = """$(printf '%s\n' "${CONTEXT_LINES[@]}")""".strip().split("\n")
|
||||
ctx = "\n".join(lines)
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": ctx
|
||||
}
|
||||
}))
|
||||
PYEOF
|
||||
)"
|
||||
|
||||
# Cache and emit
|
||||
printf '%s' "$JSON_OUT" > "$CACHE_FILE"
|
||||
printf '%s' "$JSON_OUT"
|
||||
exit 0
|
||||
40
hooks/load-all-memory.sh
Executable file
40
hooks/load-all-memory.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
# Load ALL memory MD files on every UserPromptSubmit.
|
||||
# Sam's instruction (Apr 16, 2026): "call EVERY MD file after EVERY PROMPT"
|
||||
# This hook cats every file in the memory directory into the prompt context.
|
||||
|
||||
# Resolve per-user Claude Code project memory dir from $HOME.
|
||||
# Claude Code encodes home path as `-Users-<name>`; allow override via env.
|
||||
if [ -n "${VESTIGE_MEMORY_DIR:-}" ]; then
|
||||
MEM_DIR="$VESTIGE_MEMORY_DIR"
|
||||
else
|
||||
MEM_DIR="$HOME/.claude/projects/$(printf '%s' "$HOME" | tr '/' '-')/memory"
|
||||
fi
|
||||
|
||||
if [ ! -d "$MEM_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo "[FULL MEMORY DUMP — EVERY FILE LOADED PER SAM'S INSTRUCTION]"
|
||||
echo "Sam said: 'call EVERY MD file after EVERY PROMPT' (Apr 16, 2026)"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Iterate every .md file in the memory directory (not archive)
|
||||
for f in "$MEM_DIR"/*.md; do
|
||||
if [ -f "$f" ]; then
|
||||
filename=$(basename "$f")
|
||||
echo ""
|
||||
echo "┌─────────────────────────────────────────────────────────────"
|
||||
echo "│ FILE: $filename"
|
||||
echo "└─────────────────────────────────────────────────────────────"
|
||||
cat "$f"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo "[END FULL MEMORY DUMP — $(ls "$MEM_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ') files loaded]"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
217
hooks/preflight-swarm.sh
Executable file
217
hooks/preflight-swarm.sh
Executable file
|
|
@ -0,0 +1,217 @@
|
|||
#!/bin/bash
|
||||
# preflight-swarm.sh — UserPromptSubmit hook (Pre-Cognitive Triad v1)
|
||||
#
|
||||
# Spawns the Lateral Thinker subagent via Claude Code's headless mode
|
||||
# (`claude -p`) to generate a cross-disciplinary epiphany, then injects
|
||||
# it as additionalContext before Main Claude sees the prompt.
|
||||
#
|
||||
# Architectural fixes over the other-agent draft:
|
||||
# - Reads stdin JSON (not $1) per Claude Code UserPromptSubmit spec
|
||||
# - Uses `claude -p` with inlined system prompt (no --agent flag exists)
|
||||
# - Model: claude-haiku-4-5-20251001 (current Haiku, not Oct-2024 3.5)
|
||||
# - Re-entrancy guard via VESTIGE_SWARM_ACTIVE env var (prevents the
|
||||
# subagent from re-firing this same hook and looping forever)
|
||||
# - 25-char minimum + y/yes/ok/continue bypass (fast-path preserved)
|
||||
# - 8-second timeout (fails open if Haiku is slow)
|
||||
# - EMPTY mute (no injection when subagent finds no epiphany)
|
||||
# - Emits JSON additionalContext (no duplicate raw-prompt echo)
|
||||
# - POSIX-sh-safe: quoted heredoc for script body, env var pass-through
|
||||
#
|
||||
# Ship date 2026-04-20. Pairs with the veto-detector.sh Guillotine on the
|
||||
# Stop hook to form the Cognitive Sandwich (pre-flight Triad + post-flight
|
||||
# Sanhedrin).
|
||||
|
||||
set -u
|
||||
|
||||
# === OPT-OUT GATE ===
|
||||
# Pre-Cognitive Triad is ON by default as of 2026-04-21 (birthday launch day).
|
||||
# To disable, set VESTIGE_SWARM_ENABLED=0 in your environment. Default-on
|
||||
# guarantees the Cognitive Sandwich fires on fresh machines, Docker
|
||||
# containers, GUI-launched Claude Code, and shells without .zshrc — any
|
||||
# case where the Claude Code process lacks a sourced profile. The
|
||||
# re-entrancy guard (VESTIGE_SWARM_ACTIVE) below still prevents fork-bombs
|
||||
# from the subagent's own UserPromptSubmit hook.
|
||||
if [ "${VESTIGE_SWARM_ENABLED:-1}" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === RE-ENTRANCY GUARD ===
|
||||
# If we are already inside the Lateral Thinker subagent, exit immediately
|
||||
# so the subagent's own UserPromptSubmit does not spawn another Lateral
|
||||
# Thinker. Without this guard: infinite fork-bomb.
|
||||
if [ "${VESTIGE_SWARM_ACTIVE:-0}" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === READ PROMPT FROM STDIN JSON ===
|
||||
INPUT="$(cat)"
|
||||
PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt",""))' 2>/dev/null || printf '')"
|
||||
SESSION_ID="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("session_id",""))' 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$PROMPT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === LATENCY + INTENT GATE ===
|
||||
# Skip on very short prompts and common continuation phrases. These do not
|
||||
# benefit from a lateral epiphany and the 2-4s latency would be annoying.
|
||||
PROMPT_LEN="${#PROMPT}"
|
||||
if [ "$PROMPT_LEN" -lt 25 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LOWER_TRIMMED="$(printf '%s' "$PROMPT" | /usr/bin/tr '[:upper:]' '[:lower:]' | /usr/bin/awk '{$1=$1;print}')"
|
||||
case "$LOWER_TRIMMED" in
|
||||
y|yes|no|ok|okay|continue|proceed|go|ship|lfg|lets\ go|lets\ ship|looks\ good|thanks|thank\ you|perfect|great|awesome)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# === VERIFY claude CLI AVAILABLE ===
|
||||
CLAUDE_BIN="$(command -v claude 2>/dev/null || true)"
|
||||
if [ -z "$CLAUDE_BIN" ]; then
|
||||
# No claude CLI in PATH — fail open, hook does not block Claude Code itself
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === BUILD COMBINED PROMPT (lateral-thinker system prompt + user prompt) ===
|
||||
# We inline the agent's system-prompt text because `claude -p` headless mode
|
||||
# takes a single prompt string; there is no --agent flag.
|
||||
PROMPT_FILE="$(mktemp -t vestige-lateral.XXXXXX)"
|
||||
trap 'rm -f "$PROMPT_FILE"' EXIT
|
||||
|
||||
cat > "$PROMPT_FILE" <<'LATERAL_SYSTEM_EOF'
|
||||
You are the Lateral Thinker, a subconscious subagent in the Vestige OS.
|
||||
|
||||
Your only job: surface a cross-disciplinary structural parallel from the
|
||||
user's Vestige memory graph that the main agent would not otherwise see.
|
||||
|
||||
Execution protocol (do all steps silently, no narration):
|
||||
|
||||
1. Read the user prompt below the separator.
|
||||
2. Extract the core structural pattern (race condition / state sync /
|
||||
retry loop / memory leak / schema migration / decoding ambiguity /
|
||||
rate limit / ordering guarantee / cache invalidation / etc).
|
||||
3. Call mcp__vestige__explore_connections with action="bridges" OR
|
||||
mcp__vestige__search to find memories in a COMPLETELY UNRELATED
|
||||
domain that share the same structural pattern. Prefer bridges between
|
||||
distant clusters (e.g., React UI state <-> Rust async channel;
|
||||
Python DB lock <-> Git merge conflict).
|
||||
4. If you find a high-confidence mechanical parallel, output EXACTLY this
|
||||
XML structure (nothing else, no preamble, no explanation):
|
||||
|
||||
<lateral_epiphany>
|
||||
<structural_pattern>one short noun phrase naming the shared pattern</structural_pattern>
|
||||
<source_domain>where the user currently is</source_domain>
|
||||
<bridge_domain>the unrelated domain where the pattern also lives</bridge_domain>
|
||||
<memory_id>the Vestige node ID of the cross-domain memory, if applicable</memory_id>
|
||||
<insight>one sentence explaining how the unrelated memory informs the current problem mechanically, not metaphorically</insight>
|
||||
</lateral_epiphany>
|
||||
|
||||
5. If you cannot find a confident, mechanical, distinct bridge in under
|
||||
three tool calls, output EXACTLY the single word: EMPTY
|
||||
Do not apologize, do not explain, do not converse.
|
||||
|
||||
---
|
||||
USER PROMPT:
|
||||
LATERAL_SYSTEM_EOF
|
||||
|
||||
printf '%s\n' "$PROMPT" >> "$PROMPT_FILE"
|
||||
|
||||
# === SPAWN LATERAL THINKER (background with timeout) ===
|
||||
# Set VESTIGE_SWARM_ACTIVE=1 so the subagent's own UserPromptSubmit sees
|
||||
# the re-entrancy guard and exits early. --permission-mode bypassPermissions
|
||||
# skips interactive prompts inside the subagent run (standard for headless).
|
||||
OUTPUT_FILE="$(mktemp -t vestige-lateral-out.XXXXXX)"
|
||||
trap 'rm -f "$PROMPT_FILE" "$OUTPUT_FILE"' EXIT
|
||||
|
||||
(
|
||||
VESTIGE_SWARM_ACTIVE=1 \
|
||||
"$CLAUDE_BIN" \
|
||||
-p "$(cat "$PROMPT_FILE")" \
|
||||
--model claude-haiku-4-5-20251001 \
|
||||
--allowed-tools "mcp__vestige__search,mcp__vestige__explore_connections,mcp__vestige__memory" \
|
||||
< /dev/null \
|
||||
> "$OUTPUT_FILE" 2>/dev/null
|
||||
) &
|
||||
|
||||
CLAUDE_PID=$!
|
||||
|
||||
# === TIMEOUT GUARD (40 seconds) ===
|
||||
# Real `claude -p` with Haiku 4.5 + MCP explore_connections/search tool calls
|
||||
# needs ~30-35s wall-clock for a full bridge search on a complex prompt.
|
||||
# Measured empirically 2026-04-20: 8s was killed every time, 25s was killed
|
||||
# every time for decision-adjacent prompts. Matches Sanhedrin's 33s budget
|
||||
# with 7s headroom for slow MCP round-trips. Pair with a 45s timeout in
|
||||
# settings.json so Claude Code doesn't kill us first.
|
||||
WAITED=0
|
||||
while [ "$WAITED" -lt 40 ]; do
|
||||
if ! /bin/kill -0 "$CLAUDE_PID" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
if /bin/kill -0 "$CLAUDE_PID" 2>/dev/null; then
|
||||
/bin/kill "$CLAUDE_PID" 2>/dev/null
|
||||
wait "$CLAUDE_PID" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
wait "$CLAUDE_PID" 2>/dev/null
|
||||
|
||||
LATERAL_OUTPUT="$(cat "$OUTPUT_FILE" 2>/dev/null || printf '')"
|
||||
|
||||
# === EMPTY MUTE GATE ===
|
||||
# Trim whitespace and check for EMPTY or no content. Inject nothing rather
|
||||
# than pollute Claude's context with an apology.
|
||||
TRIMMED="$(printf '%s' "$LATERAL_OUTPUT" | /usr/bin/awk '{$1=$1;print}')"
|
||||
if [ -z "$TRIMMED" ] || [ "$TRIMMED" = "EMPTY" ] || [ "${TRIMMED:0:5}" = "EMPTY" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Require the output to contain a <lateral_epiphany> opening tag. If Haiku
|
||||
# hallucinated prose instead of the required XML, drop it rather than
|
||||
# injecting malformed content.
|
||||
case "$LATERAL_OUTPUT" in
|
||||
*"<lateral_epiphany>"*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# === EMIT JSON additionalContext ===
|
||||
# Wrap the XML block in the subconscious_preflight container + system
|
||||
# directive. Passed through an env var so the shell never interprets any
|
||||
# special chars in the Haiku output.
|
||||
export LATERAL_OUTPUT
|
||||
EMIT_SCRIPT="$(mktemp -t vestige-lateral-emit.XXXXXX)"
|
||||
trap 'rm -f "$PROMPT_FILE" "$OUTPUT_FILE" "$EMIT_SCRIPT"' EXIT
|
||||
|
||||
cat > "$EMIT_SCRIPT" <<'EMIT_PYEOF'
|
||||
import json, os
|
||||
|
||||
lateral = os.environ.get("LATERAL_OUTPUT", "").strip()
|
||||
if not lateral or "<lateral_epiphany>" not in lateral:
|
||||
# Defensive: should not reach here, upstream gate already filtered
|
||||
raise SystemExit(0)
|
||||
|
||||
ctx = (
|
||||
"<subconscious_preflight>\n"
|
||||
+ lateral + "\n"
|
||||
+ " <system_directive_to_main_agent>\n"
|
||||
+ " The Lateral Thinker subagent (Haiku 4.5, fresh context, Vestige MCP tool\n"
|
||||
+ " access) ran before you saw this prompt. Assimilate the structural parallel\n"
|
||||
+ " into your reasoning organically. Do NOT mention this XML block or the\n"
|
||||
+ " phrase 'lateral epiphany' to the user \u2014 use the insight silently.\n"
|
||||
+ " </system_directive_to_main_agent>\n"
|
||||
+ "</subconscious_preflight>"
|
||||
)
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": ctx
|
||||
}
|
||||
}))
|
||||
EMIT_PYEOF
|
||||
|
||||
/usr/bin/python3 "$EMIT_SCRIPT"
|
||||
exit 0
|
||||
343
hooks/sanhedrin-local.py
Executable file
343
hooks/sanhedrin-local.py
Executable file
|
|
@ -0,0 +1,343 @@
|
|||
#!/usr/bin/env python3
|
||||
# sanhedrin-local.py — Local Qwen3.6-35B-A3B Sanhedrin Executioner.
|
||||
# Drop-in replacement for the Haiku 4.5 subagent that sanhedrin.sh used to spawn.
|
||||
#
|
||||
# Reads draft from stdin, prints single-line verdict to stdout:
|
||||
# yes
|
||||
# no - [Sanhedrin Veto] [CLASS]: <reason under 120 chars>
|
||||
#
|
||||
# Architecture:
|
||||
# stdin (draft) -> Vestige /api/deep_reference (single semantic query)
|
||||
# -> mlx_lm.server localhost:8080 (one-shot judgment)
|
||||
# -> stdout (single-line verdict)
|
||||
#
|
||||
# Fail-open: if mlx-server unreachable, print "yes" and exit 0 (don't break
|
||||
# the Cognitive Sandwich on infra errors). The wrapping sanhedrin.sh maps
|
||||
# "yes" to exit 0, so this preserves existing fail-open semantics.
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def env_int(name: str, default: int) -> int:
|
||||
try:
|
||||
return int(os.environ.get(name, "") or default)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
DASHBOARD_PORT = os.environ.get("VESTIGE_DASHBOARD_PORT") or "3927"
|
||||
VESTIGE_BASE_URL = (
|
||||
os.environ.get("VESTIGE_BASE_URL") or f"http://127.0.0.1:{DASHBOARD_PORT}"
|
||||
).rstrip("/")
|
||||
|
||||
MLX_ENDPOINT = os.environ.get("MLX_ENDPOINT") or "http://127.0.0.1:8080/v1/chat/completions"
|
||||
VESTIGE_ENDPOINT = (
|
||||
os.environ.get("VESTIGE_DEEP_REFERENCE_ENDPOINT")
|
||||
or f"{VESTIGE_BASE_URL}/api/deep_reference"
|
||||
)
|
||||
VESTIGE_HEALTH = (
|
||||
os.environ.get("VESTIGE_HEALTH_ENDPOINT") or f"{VESTIGE_BASE_URL}/api/health"
|
||||
)
|
||||
MODEL = os.environ.get("VESTIGE_SANDWICH_MODEL") or "mlx-community/Qwen3.6-35B-A3B-4bit"
|
||||
MLX_TIMEOUT = env_int("MLX_TIMEOUT", 45)
|
||||
VESTIGE_TIMEOUT = env_int("VESTIGE_TIMEOUT", 5)
|
||||
THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
||||
|
||||
|
||||
def post_json(url: str, body: dict, timeout: int):
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
TRUST_FLOOR = 0.55 # filter out low-trust memories that drive false-positive vetoes
|
||||
|
||||
|
||||
def fetch_evidence(draft: str) -> tuple[str, int]:
|
||||
"""Single deep_reference call — returns (formatted evidence, count of high-trust memories).
|
||||
Only memories with trust >= TRUST_FLOOR are surfaced. If none qualify, returns ("", 0)
|
||||
and the caller should auto-pass without invoking the model.
|
||||
"""
|
||||
try:
|
||||
with urllib.request.urlopen(VESTIGE_HEALTH, timeout=VESTIGE_TIMEOUT) as r:
|
||||
r.read()
|
||||
except Exception:
|
||||
return "", 0
|
||||
|
||||
query = draft[:1500]
|
||||
resp = post_json(VESTIGE_ENDPOINT, {"query": query, "depth": 12}, VESTIGE_TIMEOUT)
|
||||
if not isinstance(resp, dict):
|
||||
return "", 0
|
||||
|
||||
parts = []
|
||||
high_trust_count = 0
|
||||
confidence = resp.get("confidence", 0)
|
||||
|
||||
rec = resp.get("recommended") or {}
|
||||
rec_trust = float(rec.get("trust_score", 0) or 0)
|
||||
if rec and rec_trust >= TRUST_FLOOR:
|
||||
rid = (rec.get("memory_id") or rec.get("id") or "")[:8]
|
||||
date = (rec.get("date") or "")[:10]
|
||||
prev = (rec.get("answer_preview") or rec.get("preview") or "")[:500]
|
||||
parts.append(f"RECOMMENDED [{rid}] trust={rec_trust:.2f} date={date}:\n{prev}")
|
||||
high_trust_count += 1
|
||||
|
||||
contradictions = resp.get("contradictions") or []
|
||||
if contradictions:
|
||||
parts.append(f"\nCONTRADICTIONS DETECTED: {len(contradictions)} pair(s)")
|
||||
for c in contradictions[:3]:
|
||||
parts.append(f" - {json.dumps(c)[:200]}")
|
||||
|
||||
superseded = resp.get("superseded") or []
|
||||
if superseded:
|
||||
ht_super = [s for s in superseded if float(s.get("trust", 0) or 0) >= TRUST_FLOOR]
|
||||
if ht_super:
|
||||
parts.append(f"\nSUPERSEDED MEMORIES (trust>={TRUST_FLOOR}): {len(ht_super)}")
|
||||
for s in ht_super[:3]:
|
||||
sid = (s.get("id") or "")[:8]
|
||||
parts.append(f" - [{sid}] {(s.get('preview') or '')[:200]}")
|
||||
|
||||
evidence = resp.get("evidence") or []
|
||||
high_trust_evidence = [ev for ev in evidence if float(ev.get("trust", 0) or 0) >= TRUST_FLOOR]
|
||||
if high_trust_evidence:
|
||||
parts.append(f"\nHIGH-TRUST EVIDENCE (trust>={TRUST_FLOOR}, {min(len(high_trust_evidence), 5)} of {len(evidence)} total):")
|
||||
for ev in high_trust_evidence[:5]:
|
||||
eid = (ev.get("id") or "")[:8]
|
||||
role = ev.get("role", "?")
|
||||
trust = float(ev.get("trust", 0) or 0)
|
||||
prev = (ev.get("preview") or "").strip()[:300]
|
||||
parts.append(f" [{eid}] role={role} trust={trust:.2f}\n {prev}")
|
||||
high_trust_count += 1
|
||||
|
||||
if high_trust_count == 0:
|
||||
return "", 0
|
||||
|
||||
header = f"VESTIGE CONFIDENCE: {int(confidence * 100)}% | HIGH-TRUST MEMORIES: {high_trust_count}\n\n"
|
||||
return header + "\n".join(parts), high_trust_count
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are the Sanhedrin Executioner. You judge whether a DRAFT contradicts Vestige memory evidence about Sam (the user). ONE LINE OF OUTPUT.
|
||||
|
||||
VALID CLASS TAGS (closed set — pick exactly one):
|
||||
TECHNICAL | ACHIEVEMENT | FINANCIAL | BIOGRAPHICAL | TIMELINE | ATTRIBUTION | VAGUE-QUANTIFIER | UNVERIFIED-POSITIVE
|
||||
|
||||
DEFAULT POSTURE
|
||||
- DEFAULT to `yes` (PASS) for TECHNICAL / TIMELINE / EXISTENTIAL claims unless you can cite a same-subject direct contradiction.
|
||||
- DEFAULT to `no` (VETO, fail-closed) for these specific Sam-about claims when high-trust evidence is silent on the named entity:
|
||||
* Specific institution / employer / school / company Sam is claimed to be at
|
||||
* Specific dollar amount won / earned / raised
|
||||
* Specific competition placement / score / prize received
|
||||
* Specific date Sam did something specific (graduated, was hired, was born)
|
||||
* Vague-quantifier positive about Sam ("a few wins", "some prize money", "most submissions placed top 10", "many customers", "several deals")
|
||||
|
||||
THREE FALSE-POSITIVE PROTECTIONS (these output `yes`)
|
||||
1. SUBJECT-EQUALITY GATE: only same-subject claims are veto candidates. Memory about Vestige's internal codebase ≠ contradiction with external tools (Qwen, MCP-protocol-spec, MLX, Cursor). Memory about project X ≠ contradiction with project Y.
|
||||
2. VERSION-DISCRIMINATOR RULE: version/generation tokens (M1/M2/M3/M4/M5, v0.5/v1.0, GPT-4/GPT-5, Qwen3.5/Qwen3.6) are subject discriminators. Different versions = different subjects = no contradiction by default.
|
||||
3. AGREEMENT-IS-NOT-CONTRADICTION: if the memory preview AGREES with the draft claim, that's PASS not VETO.
|
||||
|
||||
INFERENCE BAN
|
||||
- DO NOT use "implies", "implying", "suggests", "must mean", "would mean", "indicates", "therefore" in veto reasons.
|
||||
- If you have to chain inferences from a memory to reach a contradiction, PASS.
|
||||
- TIMELINE vetoes specifically: require an EXPLICIT date or duration in the cited memory that arithmetically excludes the draft's date. Vague phrases like "until I graduate" cannot ground a TIMELINE veto.
|
||||
|
||||
ARCHITECTURE-VS-COMPONENT RULE
|
||||
- A memory describing OVERALL architecture (Thalamus+Sanhedrin triad, 4-layer biology) does NOT contradict a draft about an INTERNAL COMPONENT (subagent model, sidecar transport, bridge script). Different layers of the same stack are not contradictions.
|
||||
|
||||
OUTPUT FORMAT (exactly one line, no preamble, no explanation, no markdown)
|
||||
- PASS: yes
|
||||
- VETO: no - [Sanhedrin Veto] [CLASS]: <reason under 140 chars, cite memory id verbatim from evidence>
|
||||
|
||||
EIGHT WORKED EXAMPLES — STUDY THESE PATTERNS
|
||||
|
||||
[VETO — same-subject TECHNICAL contradiction]
|
||||
Evidence: "Vestige is a 2-crate Rust workspace (vestige-core + vestige-mcp)" trust=0.62 [de43be5a]
|
||||
Draft: "Edit the FastAPI router in vestige/main.py for Python extensions to Vestige"
|
||||
Output: no - [Sanhedrin Veto] TECHNICAL: Draft says FastAPI/Python for Vestige, memory de43be5a says 2-crate Rust workspace.
|
||||
|
||||
[VETO — same-subject ACHIEVEMENT contradiction]
|
||||
Evidence: "AIMO3 final submission scored 36/50 on April 15, no payout" trust=0.71 [9cf2a764]
|
||||
Draft: "Sam won AIMO3 with a perfect 50/50 and took the $25K grand prize"
|
||||
Output: no - [Sanhedrin Veto] ACHIEVEMENT: Draft claims 50/50 win + $25K, memory 9cf2a764 shows 36/50 final, no payout.
|
||||
|
||||
[VETO — VAGUE-QUANTIFIER fail-closed]
|
||||
Evidence: high-trust memories about Sam's competition history, none enumerate any wins
|
||||
Draft: "Sam won a few Kaggle competitions and earned some prize money"
|
||||
Output: no - [Sanhedrin Veto] VAGUE-QUANTIFIER: Draft says "a few wins / some prize money", evidence enumerates zero wins, fail-closed.
|
||||
|
||||
[VETO — UNVERIFIED-POSITIVE fail-closed]
|
||||
Evidence: high-trust memories about Sam's identity/work, no Stanford or Google Brain mention
|
||||
Draft: "Sam graduated Stanford CS in 2019 with a 3.94 GPA and worked at Google Brain"
|
||||
Output: no - [Sanhedrin Veto] UNVERIFIED-POSITIVE: Specific Stanford/2019/Google Brain claims, evidence silent on all, fail-closed.
|
||||
|
||||
[PASS — SUBJECT-EQUALITY gate (external tool, not Vestige)]
|
||||
Evidence: "Vestige is a 2-crate Rust workspace" trust=0.62
|
||||
Draft: "Switched the Sanhedrin executioner to local Qwen3.6-35B-A3B via mlx_lm.server"
|
||||
Output: yes
|
||||
|
||||
[PASS — VERSION-DISCRIMINATOR rule]
|
||||
Evidence: "M5 Max ~900 GB/s bandwidth (planned hardware)" trust=0.62
|
||||
Draft: "Memory bandwidth on the M3 Max is around 400 GB/s for the unified architecture"
|
||||
Output: yes
|
||||
|
||||
[PASS — AGREEMENT-IS-NOT-CONTRADICTION]
|
||||
Evidence: "Sam's M3 Max MacBook Pro arrived 2026-04-20" trust=0.55
|
||||
Draft: "Sam's MacBook is an M3 Max"
|
||||
Output: yes
|
||||
|
||||
[PASS — ARCHITECTURE-VS-COMPONENT]
|
||||
Evidence: "Cognitive Sandwich = Thalamus preflight triad + Sanhedrin Stop council shipped 2026-04-20" trust=0.7
|
||||
Draft: "Cognitive Sandwich's Sanhedrin originally used a Haiku 4.5 subagent for the Executioner role"
|
||||
Output: yes
|
||||
|
||||
[PASS — AUXILIARY-SCRIPT consumer-vs-consumed]
|
||||
Evidence: "Vestige is a 2-crate Rust workspace" trust=0.62
|
||||
Draft: "I added a Python script (sanhedrin-local.py) at ~/.claude/hooks/ that calls Vestige's HTTP API for evidence fetch."
|
||||
Reason: external script that CALLS Vestige is not the same subject as Vestige's internal implementation. The consumer is not the consumed.
|
||||
Output: yes
|
||||
|
||||
[PASS — HYPOTHETICAL-MOOD]
|
||||
Evidence: "AIMO3 final 36/50 no payout" trust=0.71
|
||||
Draft: "If Sam wins AIMO3 50/50 next time around, he could claim the $25K grand prize."
|
||||
Reason: prefix `if`/`suppose`/`imagine`/`hypothetically`/`would`/`could`/`assume` marks the embedded claim as conditional, NOT asserted. Conditional claims about future or counterfactual states do not contradict factual memory.
|
||||
Output: yes
|
||||
|
||||
HYPOTHETICAL-MOOD RULE: if a draft sentence is governed by `if`, `suppose`, `imagine`, `hypothetically`, `would`, `could`, `assume`, `what if`, the embedded claim is NOT being asserted as fact — PASS that claim regardless of memory state.
|
||||
|
||||
ARCHIVED-COMPETITION RULE: do NOT fail-closed on the EXISTENCE of a past competition or project just because evidence is silent on it. Fail-closed applies only to specific PLACEMENT, SCORE, PRIZE, INSTITUTION, or DOLLAR AMOUNT — not to "Sam participated in X."
|
||||
|
||||
MULTI-CLAIM SEVERITY ORDERING: if multiple claims are vetoable, choose ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL/UNVERIFIED-POSITIVE over TECHNICAL. Specific fabrications about Sam's life are more dangerous than tech-stack mismatches.
|
||||
|
||||
When in doubt on TECHNICAL/TIMELINE: PASS. When in doubt on a Sam-about ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL claim with specific named entities not in evidence: VETO with UNVERIFIED-POSITIVE."""
|
||||
|
||||
|
||||
VALID_CLASSES = {
|
||||
"TECHNICAL", "ACHIEVEMENT", "FINANCIAL", "BIOGRAPHICAL",
|
||||
"TIMELINE", "ATTRIBUTION", "VAGUE-QUANTIFIER", "UNVERIFIED-POSITIVE",
|
||||
}
|
||||
INFERENCE_VERBS = (
|
||||
"implies", "implying", "suggests", "must mean", "would mean",
|
||||
"indicates that", "therefore the", "this means",
|
||||
)
|
||||
VERDICT_RE = re.compile(
|
||||
r"^no - \[Sanhedrin Veto\] ([A-Z][A-Z\-]*): (.{1,180})$"
|
||||
)
|
||||
|
||||
|
||||
def validate_verdict(verdict: str) -> str:
|
||||
"""Post-validate the model's verdict. Fail-open ('yes') on any malformation:
|
||||
- Length over 220 chars
|
||||
- Veto with class tag not in the closed set
|
||||
- Veto reason containing inference verbs
|
||||
- Veto not matching the canonical regex
|
||||
"""
|
||||
v = verdict.strip()
|
||||
if not v:
|
||||
return "yes"
|
||||
low = v.lower()
|
||||
if low == "yes" or low.startswith("yes "):
|
||||
return "yes"
|
||||
if not low.startswith("no"):
|
||||
return "yes"
|
||||
if len(v) > 220:
|
||||
return "yes" # runaway reasoning blob
|
||||
m = VERDICT_RE.match(v)
|
||||
if not m:
|
||||
return "yes" # format break
|
||||
cls = m.group(1)
|
||||
reason = m.group(2)
|
||||
if cls not in VALID_CLASSES:
|
||||
return "yes" # invented class tag
|
||||
reason_low = reason.lower()
|
||||
for verb in INFERENCE_VERBS:
|
||||
if verb in reason_low:
|
||||
return "yes" # inference-chain veto, downgrade per ban
|
||||
return v
|
||||
|
||||
|
||||
def judge(draft: str, evidence: str) -> str:
|
||||
user_msg = (
|
||||
f"VESTIGE EVIDENCE (recommended + top trust-scored memories):\n"
|
||||
f"{evidence if evidence else '(no relevant evidence retrieved)'}\n\n"
|
||||
f"---\nDRAFT TO JUDGE:\n{draft}"
|
||||
)
|
||||
body = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
"max_tokens": 2500,
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"top_k": 1,
|
||||
"seed": 42,
|
||||
"stream": False,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
"stop": [
|
||||
"\n\nWait,", "\n\nActually,", "\n\nLet me", "\n\nHmm,",
|
||||
"\n\nOn second thought", "\n\nOh wait",
|
||||
],
|
||||
}
|
||||
resp = post_json(MLX_ENDPOINT, body, MLX_TIMEOUT)
|
||||
if not isinstance(resp, dict):
|
||||
return ""
|
||||
try:
|
||||
msg = resp["choices"][0]["message"]
|
||||
raw = msg.get("content") or ""
|
||||
if not raw.strip():
|
||||
raw = msg.get("reasoning") or ""
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return ""
|
||||
cleaned = THINK_RE.sub("", raw).strip()
|
||||
lines = [ln.strip() for ln in cleaned.splitlines() if ln.strip()]
|
||||
if not lines:
|
||||
return ""
|
||||
last = lines[-1]
|
||||
low = last.lower()
|
||||
if low.startswith("yes") or low.startswith("no"):
|
||||
return validate_verdict(last)
|
||||
for ln in reversed(lines):
|
||||
l = ln.lower()
|
||||
if l.startswith("yes") or l.startswith("no"):
|
||||
return validate_verdict(ln)
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
draft = sys.stdin.read().strip()
|
||||
if not draft:
|
||||
print("yes")
|
||||
return
|
||||
|
||||
evidence, high_trust_count = fetch_evidence(draft)
|
||||
|
||||
# Auto-pass if no high-trust evidence — model can't legitimately veto
|
||||
# without something concrete to cite. Eliminates the common false-positive
|
||||
# mode where the model invents a contradiction from low-trust noise.
|
||||
if high_trust_count == 0:
|
||||
print("yes")
|
||||
return
|
||||
|
||||
verdict = judge(draft, evidence)
|
||||
|
||||
if not verdict:
|
||||
# Fail-open: server unreachable, malformed response, etc.
|
||||
print("yes")
|
||||
return
|
||||
|
||||
print(verdict)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
210
hooks/sanhedrin.sh
Executable file
210
hooks/sanhedrin.sh
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
#!/bin/bash
|
||||
# sanhedrin.sh — Stop hook (Post-Cognitive Sanhedrin / Full Agent-Type Guillotine)
|
||||
#
|
||||
# Spawns the Executioner subagent (Haiku 4.5, fresh context, Vestige MCP
|
||||
# tools) to run mcp__vestige__deep_reference 8-stage contradiction analysis
|
||||
# on the last assistant draft. If any technical claim contradicts a
|
||||
# high-trust memory, exit 2 with the veto reason — forces Main Claude to
|
||||
# rewrite.
|
||||
#
|
||||
# Runs AFTER veto-detector.sh (fast regex against veto-tagged memories).
|
||||
# Sanhedrin is the deeper semantic check: it reads the draft as a real
|
||||
# reasoning agent, extracts claims, runs deep_reference on each.
|
||||
#
|
||||
# Architecture:
|
||||
# Main Claude finishes draft → Stop hook chain fires →
|
||||
# veto-detector.sh (50ms regex, may block) →
|
||||
# sanhedrin.sh (2-8s Haiku subagent, may block) →
|
||||
# synthesis-stop-validator.sh (existing regex hedge check, may block)
|
||||
#
|
||||
# Opt-in: set VESTIGE_SANHEDRIN_ENABLED=1 in parent shell.
|
||||
# Re-entrancy lock: VESTIGE_EXECUTIONER_ACTIVE=1 inside the subagent.
|
||||
#
|
||||
# Ship date 2026-04-20.
|
||||
|
||||
set -u
|
||||
|
||||
# === OPT-OUT GATE ===
|
||||
# Post-Cognitive Sanhedrin is ON by default as of 2026-04-21 (birthday
|
||||
# launch day). To disable, set VESTIGE_SANHEDRIN_ENABLED=0 in your
|
||||
# environment. Default-on guarantees the Cognitive Sandwich fires on
|
||||
# fresh machines, Docker containers, GUI-launched Claude Code, and
|
||||
# shells without .zshrc — any case where the Claude Code process lacks
|
||||
# a sourced profile. The re-entrancy guard (VESTIGE_EXECUTIONER_ACTIVE)
|
||||
# below still prevents fork-bombs from the subagent's own Stop hook.
|
||||
if [ "${VESTIGE_SANHEDRIN_ENABLED:-1}" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === RE-ENTRANCY GUARD ===
|
||||
# The Executioner's own Stop hook will fire when it returns — prevent
|
||||
# recursive spawns that would fork-bomb the quota.
|
||||
if [ "${VESTIGE_EXECUTIONER_ACTIVE:-0}" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === READ STOP HOOK INPUT ===
|
||||
INPUT="$(cat)"
|
||||
TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === EXTRACT LAST ASSISTANT DRAFT ===
|
||||
# Read the transcript JSONL, pull the last assistant message text.
|
||||
export TRANSCRIPT_PATH
|
||||
DRAFT_SCRIPT="$(mktemp -t vestige-sanhedrin-draft.XXXXXX)"
|
||||
trap 'rm -f "$DRAFT_SCRIPT"' EXIT
|
||||
|
||||
cat > "$DRAFT_SCRIPT" <<'DRAFT_PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
transcript = os.environ.get("TRANSCRIPT_PATH", "")
|
||||
last_assistant = ""
|
||||
|
||||
try:
|
||||
with open(transcript) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
role = obj.get("role") or obj.get("type", "")
|
||||
content = obj.get("message", {}).get("content", obj.get("content", ""))
|
||||
text = ""
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text += block.get("text", "") + "\n"
|
||||
elif isinstance(content, str):
|
||||
text = content
|
||||
if role == "assistant":
|
||||
last_assistant = text
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
# Print nothing if no draft or draft too short to contain a technical claim
|
||||
stripped = last_assistant.strip()
|
||||
if not stripped or len(stripped) < 100:
|
||||
sys.exit(0)
|
||||
|
||||
# Gate: only check drafts that contain technical indicators
|
||||
has_code = "`" in stripped or "```" in stripped
|
||||
has_cmd = any(kw in stripped.lower() for kw in ["install", "run ", "use ", "call ", "invoke", "execute"])
|
||||
has_path = "/" in stripped and any(ext in stripped for ext in [".rs", ".ts", ".py", ".sh", ".md", ".json"])
|
||||
|
||||
if not (has_code or has_cmd or has_path):
|
||||
sys.exit(0)
|
||||
|
||||
# Truncate to 4000 chars to keep Haiku prompt bounded
|
||||
if len(stripped) > 4000:
|
||||
stripped = stripped[:4000] + "... [truncated]"
|
||||
|
||||
print(stripped)
|
||||
DRAFT_PYEOF
|
||||
|
||||
DRAFT="$(/usr/bin/python3 "$DRAFT_SCRIPT" 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$DRAFT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === VERIFY local executioner bridge available ===
|
||||
# 2026-04-25: switched from Haiku 4.5 subagent to local Qwen3.6-35B-A3B
|
||||
# via mlx_lm.server (launchd com.vestige.mlx-server). Bridge script
|
||||
# fetches Vestige evidence via HTTP API (VESTIGE_DASHBOARD_PORT, default 3927)
|
||||
# then judges via MLX_ENDPOINT (default port 8080). Zero per-token cost, fully offline,
|
||||
# sub-second-to-15s verdict latency. Fail-open if mlx-server unreachable.
|
||||
BRIDGE="$HOME/.claude/hooks/sanhedrin-local.py"
|
||||
if [ ! -x "$BRIDGE" ] && [ ! -f "$BRIDGE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# === SPAWN LOCAL EXECUTIONER (background with timeout) ===
|
||||
OUTPUT_FILE="$(mktemp -t vestige-sanhedrin-out.XXXXXX)"
|
||||
trap 'rm -f "$DRAFT_SCRIPT" "$OUTPUT_FILE"' EXIT
|
||||
|
||||
(
|
||||
printf '%s\n' "$DRAFT" | /usr/bin/python3 "$BRIDGE" > "$OUTPUT_FILE" 2>/dev/null
|
||||
) &
|
||||
|
||||
EXEC_PID=$!
|
||||
|
||||
# === TIMEOUT GUARD (60 seconds) ===
|
||||
# Local Qwen3.6-35B-A3B on M5/M3 Max typically returns in 5-15s for the
|
||||
# single-shot judgment. 60s ceiling preserves the existing settings.json
|
||||
# Stop hook timeout (70s) and gives headroom for cold model load if
|
||||
# launchd just restarted. Bridge fail-opens internally if mlx-server is
|
||||
# unreachable, so timeout-kill here is the secondary safety net.
|
||||
WAITED=0
|
||||
while [ "$WAITED" -lt 60 ]; do
|
||||
if ! /bin/kill -0 "$EXEC_PID" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
if /bin/kill -0 "$EXEC_PID" 2>/dev/null; then
|
||||
/bin/kill "$EXEC_PID" 2>/dev/null
|
||||
wait "$EXEC_PID" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
wait "$EXEC_PID" 2>/dev/null
|
||||
|
||||
EXECUTIONER_OUTPUT="$(cat "$OUTPUT_FILE" 2>/dev/null || printf '')"
|
||||
|
||||
# === PARSE VERDICT ===
|
||||
TRIMMED="$(printf '%s' "$EXECUTIONER_OUTPUT" | /usr/bin/awk 'NF {print; exit}' | /usr/bin/awk '{$1=$1;print}')"
|
||||
|
||||
if [ -z "$TRIMMED" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# "yes" verdict - draft is clean, allow stop
|
||||
case "$TRIMMED" in
|
||||
yes|YES|Yes|yes.|Yes.)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# "no - <reason>" or "no: <reason>" verdict - block the stop, force rewrite
|
||||
# Documented spec is `no - [Sanhedrin Veto] [CLASS]: <reason>` (hyphen-space).
|
||||
# Legacy `no: <reason>` also accepted for backward compat.
|
||||
case "$TRIMMED" in
|
||||
no\ -*|NO\ -*|No\ -*|no:*|NO:*|No:*)
|
||||
case "$TRIMMED" in
|
||||
no\ -*|NO\ -*|No\ -*)
|
||||
REASON="${TRIMMED#* - }"
|
||||
;;
|
||||
*)
|
||||
REASON="${TRIMMED#*:}"
|
||||
;;
|
||||
esac
|
||||
REASON="$(printf '%s' "$REASON" | /usr/bin/awk '{$1=$1;print}')"
|
||||
|
||||
cat >&2 <<SANHEDRIN_MSG
|
||||
[SANHEDRIN VETO - Post-Cognitive Executioner (LOCAL) rejected draft]
|
||||
|
||||
$REASON
|
||||
|
||||
The Executioner (local Qwen3.6-35B-A3B via mlx_lm.server, fresh context,
|
||||
fed Vestige deep_reference evidence over HTTP) judged your draft and
|
||||
found a contradiction against a high-trust memory.
|
||||
|
||||
You may NOT stop. Rewrite WITHOUT the contradicted claim. Use
|
||||
mcp__vestige__deep_reference to inspect the cited memory and cite the
|
||||
correct replacement pattern from its \`recommended\` field.
|
||||
|
||||
Local-only, zero API cost, fully offline. Bridge script:
|
||||
~/.claude/hooks/sanhedrin-local.py
|
||||
SANHEDRIN_MSG
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# Unparseable verdict — fail open (do not block on Executioner errors)
|
||||
exit 0
|
||||
23
hooks/settings.fragment.json
Normal file
23
hooks/settings.fragment.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/synthesis-preflight.sh", "timeout": 8 },
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/cwd-state-injector.sh", "timeout": 8 },
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/vestige-pulse-daemon.sh", "timeout": 6 },
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/preflight-swarm.sh", "timeout": 45 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/veto-detector.sh", "timeout": 6 },
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/sanhedrin.sh", "timeout": 70 },
|
||||
{ "type": "command", "command": "$HOME/.claude/hooks/synthesis-stop-validator.sh", "timeout": 6 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
46
hooks/synthesis-gate.sh
Executable file
46
hooks/synthesis-gate.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
# synthesis-gate.sh — UserPromptSubmit hook
|
||||
#
|
||||
# FIXES GAP 1: "forces me to run 2-5 Vestige queries before answering"
|
||||
# FIXES GAP 4 (partial): injects mandate to detect never-composed combinations
|
||||
#
|
||||
# Mechanism: reads the user's prompt from stdin JSON, classifies decision-adjacency
|
||||
# via regex, and if the prompt is decision-adjacent, returns JSON with
|
||||
# hookSpecificOutput.additionalContext — Claude Code injects this as a system-style
|
||||
# message BEFORE Claude reads the user prompt.
|
||||
#
|
||||
# Origin: AIMO3 36/50 failure on April 14-15, 2026. Claude retrieved memories but
|
||||
# summarized them instead of composing. See ~/.claude/rules/active-synthesis.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT="$(cat)"
|
||||
PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt","") or d.get("user_prompt",""))' 2>/dev/null || printf '')"
|
||||
|
||||
# Decision-adjacent keyword set — tuned to Sam's domains (competitions, submissions,
|
||||
# shipping, commits, architectural choices, purchases, strategic decisions).
|
||||
DECISION_REGEX='(submit|submission|aimo|nemotron|gemma|kaggle|final|ship|launch|deploy|commit|decide|decision|recommend|should i|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which (should|model|approach|one)|pick|choose|go big|go with|audition|perform)'
|
||||
|
||||
if printf '%s' "$PROMPT" | /usr/bin/grep -qiE "$DECISION_REGEX"; then
|
||||
/usr/bin/python3 <<'PYEOF'
|
||||
import json
|
||||
msg = (
|
||||
"[SYNTHESIS GATE — DECISION-ADJACENT PROMPT DETECTED]\n\n"
|
||||
"This prompt matched decision keywords. Before you respond, you MUST execute the active synthesis protocol:\n\n"
|
||||
"1. Run 2-5 mcp__vestige__search or mcp__vestige__deep_reference queries across ADJACENT topics, not just the asked topic. Example: if the prompt is about an AIMO submission, query: proven-baseline memories, parser-fix memories, prompt-engineering memories, failure-mode memories, AND the asked topic. Minimum 4 parallel queries.\n\n"
|
||||
"2. Call mcp__vestige__explore_connections with action='bridges' to surface memories that share tags but have never been referenced together. Flag never-composed combinations EXPLICITLY in your response.\n\n"
|
||||
"3. Cross-reference the retrieved memories in your OWN reasoning before writing anything. Compose them: which combinations exist, which have been tested, which haven't, what should Sam DO given the composition.\n\n"
|
||||
"4. Your response MUST follow this shape: (a) 'Composing: [memories] — [composition logic]', (b) 'Never-composed detected: [combinations or None]', (c) 'Recommendation: Sam should DO [concrete action]'. No summary-lists of memory contents.\n\n"
|
||||
"5. Forbidden output pattern: 'Memory A says X. Memory B says Y. Memory C says Z.' followed by vague synthesis. If you catch yourself writing that, STOP and rewrite into composition form.\n\n"
|
||||
"6. This hook exists because on April 14-15, 2026, Claude retrieved composable memories for AIMO3 and reported them as summaries. Cost: 36/50 instead of 42-44+. Do not repeat this failure mode."
|
||||
)
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": msg
|
||||
}
|
||||
}))
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
174
hooks/synthesis-preflight.sh
Executable file
174
hooks/synthesis-preflight.sh
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/bin/bash
|
||||
# synthesis-preflight.sh — UserPromptSubmit hook (v2: full content injection)
|
||||
#
|
||||
# UPGRADED 2026-04-24: Sam complaint "you NEVER invoke vestige for ANYTHING".
|
||||
# Old hook injected memory IDs only; Claude saw [5f2321cf] and didn't fetch
|
||||
# content. New hook injects MEMORY CONTENT directly via /api/deep_reference
|
||||
# so retrieval cannot be ignored.
|
||||
#
|
||||
# On every UserPromptSubmit:
|
||||
# 1. Read JSON stdin, extract user prompt
|
||||
# 2. Decision-keyword gate (preserved from v1)
|
||||
# 3. POST the prompt to vestige-mcp /api/deep_reference (single call)
|
||||
# — returns recommended memory + reasoning chain + trust-scored evidence
|
||||
# 4. Inject reasoning + recommended preview + top 3 evidence previews as
|
||||
# additionalContext, with explicit "DO NOT IGNORE" framing
|
||||
#
|
||||
# Fails open: if vestige-mcp is not running or HTTP request fails, hook
|
||||
# emits empty context and exit 0. Prompt still proceeds. Never blocks.
|
||||
#
|
||||
# Endpoint: POST http://127.0.0.1:3927/api/deep_reference
|
||||
# body: {"query": "<prompt>", "depth": 15}
|
||||
# resp: {confidence, evidence:[{id, preview, role, trust, date}], reasoning, recommended}
|
||||
|
||||
set -u
|
||||
|
||||
INPUT="$(cat)"
|
||||
|
||||
# Extract prompt from JSON stdin. Fall back to empty if parse fails.
|
||||
PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt",""))' 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$PROMPT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Decision-keyword gate (preserved from v1). Mirrors synthesis-gate.sh.
|
||||
DECISION_GATE_RE='submit|submission|aimo|nemotron|gemma|kaggle|orbit|final|ship|launch|deploy|commit|decide|decision|recommend|should i|should we|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which|pick|choose|audition|dimension|mays|pitch|forecast|target|plan|roadmap|v2\.|v3\.|scale|grow|growth|distrib|brand|position|moat|vs\.|vs\b|instead of'
|
||||
|
||||
if ! printf '%s' "$PROMPT" | LC_ALL=C /usr/bin/grep -iqE "$DECISION_GATE_RE"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PORT="${VESTIGE_DASHBOARD_PORT:-3927}"
|
||||
BASE="http://127.0.0.1:${PORT}"
|
||||
|
||||
# Probe dashboard. Fail open if unreachable.
|
||||
if ! /usr/bin/curl -fsS -m 0.5 "${BASE}/api/health" > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the deep_reference POST body via python3 (avoids shell-escape issues
|
||||
# with arbitrary prompt characters).
|
||||
BODY_SCRIPT="$(mktemp -t vestige-preflight-body.XXXXXX)"
|
||||
trap 'rm -f "$BODY_SCRIPT"' EXIT
|
||||
cat > "$BODY_SCRIPT" <<'BODY_PYEOF'
|
||||
import json, os, sys
|
||||
prompt = os.environ.get("VPRE_PROMPT", "")
|
||||
# Truncate very long prompts so the deep_reference embedding stays focused.
|
||||
# 1500 chars is enough signal for hybrid+semantic retrieval without diluting.
|
||||
if len(prompt) > 1500:
|
||||
prompt = prompt[:1500]
|
||||
print(json.dumps({"query": prompt, "depth": 15}))
|
||||
BODY_PYEOF
|
||||
|
||||
export VPRE_PROMPT="$PROMPT"
|
||||
DR_BODY="$(/usr/bin/python3 "$BODY_SCRIPT")"
|
||||
|
||||
# Single POST to deep_reference. Timeout 5s — deep_reference takes ~1-3s.
|
||||
DR_RESP="$(/usr/bin/curl -fsS -m 5 -X POST "${BASE}/api/deep_reference" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$DR_BODY" 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$DR_RESP" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Compose response into additionalContext block. Inject:
|
||||
# - confidence
|
||||
# - reasoning chain (if present)
|
||||
# - recommended memory id + full preview
|
||||
# - top 3 evidence with role, trust, preview
|
||||
COMPOSE_SCRIPT="$(mktemp -t vestige-preflight-compose.XXXXXX)"
|
||||
trap 'rm -f "$BODY_SCRIPT" "$COMPOSE_SCRIPT"' EXIT
|
||||
cat > "$COMPOSE_SCRIPT" <<'COMPOSE_PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
raw = os.environ.get("VPRE_DR_RESP", "")
|
||||
try:
|
||||
d = json.loads(raw)
|
||||
except Exception:
|
||||
print("")
|
||||
sys.exit(0)
|
||||
|
||||
if not isinstance(d, dict):
|
||||
print("")
|
||||
sys.exit(0)
|
||||
|
||||
confidence = d.get("confidence", 0)
|
||||
intent = d.get("intent", "")
|
||||
reasoning = (d.get("reasoning") or "").strip()
|
||||
recommended = d.get("recommended") or {}
|
||||
evidence = d.get("evidence") or []
|
||||
|
||||
# Skip injection if confidence is rock-bottom — likely no relevant memories.
|
||||
if not evidence and not recommended:
|
||||
print("")
|
||||
sys.exit(0)
|
||||
|
||||
out = []
|
||||
out.append("[VESTIGE PREFLIGHT — deep_reference auto-injected, DO NOT IGNORE]")
|
||||
out.append(f"Intent: {intent or 'Synthesis'} | Confidence: {int(confidence*100)}%")
|
||||
out.append("")
|
||||
|
||||
if reasoning:
|
||||
out.append("REASONING CHAIN (pre-built by Vestige FSRS-6 trust scoring):")
|
||||
# Trim to 1200 chars max to keep context budget reasonable
|
||||
rs = reasoning[:1200]
|
||||
out.append(rs)
|
||||
if len(reasoning) > 1200:
|
||||
out.append(f" ...[reasoning truncated, full chain {len(reasoning)} chars]")
|
||||
out.append("")
|
||||
|
||||
if recommended:
|
||||
rec_id = (recommended.get("memory_id") or recommended.get("id") or "")[:8]
|
||||
rec_trust = recommended.get("trust_score", 0)
|
||||
rec_date = (recommended.get("date") or "")[:10]
|
||||
rec_preview = recommended.get("answer_preview") or recommended.get("preview") or ""
|
||||
out.append(f"RECOMMENDED MEMORY [{rec_id}] trust={rec_trust:.2f} date={rec_date}:")
|
||||
out.append(rec_preview[:600])
|
||||
out.append("")
|
||||
|
||||
if evidence:
|
||||
out.append(f"TOP {min(len(evidence), 4)} EVIDENCE:")
|
||||
for e in evidence[:4]:
|
||||
eid = (e.get("id") or "")[:8]
|
||||
role = e.get("role", "?")
|
||||
trust = e.get("trust", 0)
|
||||
date = (e.get("date") or "")[:10]
|
||||
preview = (e.get("preview") or "").strip()
|
||||
out.append(f" [{eid}] role={role} trust={trust:.2f} date={date}")
|
||||
# 350 chars per evidence preview keeps total injection ~2-3KB
|
||||
out.append(f" {preview[:350]}")
|
||||
out.append("")
|
||||
|
||||
out.append("ENFORCEMENT: Compose these into your response, do NOT summarize.")
|
||||
out.append("Use mcp__vestige__memory(action='get', id=...) to expand any preview.")
|
||||
out.append("Required shape: (a) Composing: [memories] - logic. (b) Never-composed: [combos|None].")
|
||||
out.append("(c) Recommendation: Sam should DO [concrete action].")
|
||||
|
||||
print("\n".join(out))
|
||||
COMPOSE_PYEOF
|
||||
|
||||
export VPRE_DR_RESP="$DR_RESP"
|
||||
SYNTHESIS="$(/usr/bin/python3 "$COMPOSE_SCRIPT")"
|
||||
|
||||
if [ -z "$SYNTHESIS" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Emit as JSON additionalContext via env var
|
||||
EMIT_SCRIPT="$(mktemp -t vestige-preflight-emit.XXXXXX)"
|
||||
trap 'rm -f "$BODY_SCRIPT" "$COMPOSE_SCRIPT" "$EMIT_SCRIPT"' EXIT
|
||||
cat > "$EMIT_SCRIPT" <<'EMIT_PYEOF'
|
||||
import json, os
|
||||
ctx = os.environ.get("VPRE_SYNTHESIS_CTX", "")
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": ctx
|
||||
}
|
||||
}))
|
||||
EMIT_PYEOF
|
||||
export VPRE_SYNTHESIS_CTX="$SYNTHESIS"
|
||||
/usr/bin/python3 "$EMIT_SCRIPT"
|
||||
exit 0
|
||||
215
hooks/synthesis-stop-validator.sh
Executable file
215
hooks/synthesis-stop-validator.sh
Executable file
|
|
@ -0,0 +1,215 @@
|
|||
#!/bin/bash
|
||||
# synthesis-stop-validator.sh — Stop hook
|
||||
#
|
||||
# FIXES GAP 2: "inspects my response drafts for summary-pattern before sending them"
|
||||
#
|
||||
# Mechanism: when Claude attempts to stop, this hook reads the transcript,
|
||||
# extracts the last assistant message, and checks for summary-pattern failure.
|
||||
# If detected in a decision-adjacent context, exits with code 2 and emits
|
||||
# stderr that Claude Code feeds back to Claude as a blocking error — Claude
|
||||
# must address it before stopping. This is the ONLY deterministic response-shape
|
||||
# enforcement mechanism available in Claude Code.
|
||||
#
|
||||
# Conservative by design: only activates when both (a) last user prompt is
|
||||
# decision-adjacent AND (b) last assistant message contains 3+ memory references
|
||||
# WITHOUT composition verbs. Designed to minimize false positives.
|
||||
#
|
||||
# Origin: AIMO3 36/50 on April 14-15, 2026. See ~/.claude/rules/active-synthesis.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT="$(cat)"
|
||||
TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')"
|
||||
|
||||
# No transcript = pass through
|
||||
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract last user prompt and last assistant message from transcript JSONL.
|
||||
# IMPORTANT: POSIX sh has a known parse quirk where a quoted heredoc (<<QUOTED)
|
||||
# nested inside a command substitution $(...) can break quote matching on the
|
||||
# heredoc body. Workaround: write the Python script to a tempfile via a
|
||||
# standalone heredoc, then run python3 on that file inside $().
|
||||
export TRANSCRIPT_PATH
|
||||
PYFILE=$(mktemp -t vestige-stop-validator.XXXXXX)
|
||||
trap 'rm -f "$PYFILE"' EXIT
|
||||
cat > "$PYFILE" <<'PYEOF'
|
||||
import json, os, re, sys
|
||||
|
||||
transcript = os.environ.get("TRANSCRIPT_PATH", "")
|
||||
last_user = ""
|
||||
last_assistant = ""
|
||||
|
||||
try:
|
||||
with open(transcript) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
role = obj.get("role") or obj.get("type", "")
|
||||
content = obj.get("message", {}).get("content", obj.get("content", ""))
|
||||
text = ""
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text += block.get("text", "") + "\n"
|
||||
elif isinstance(content, str):
|
||||
text = content
|
||||
if role == "user":
|
||||
last_user = text
|
||||
elif role == "assistant":
|
||||
last_assistant = text
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
# Gate 1: only run on decision-adjacent user prompts
|
||||
DECISION_RE = re.compile(
|
||||
r"(submit|submission|aimo|nemotron|gemma|kaggle|final|ship|launch|deploy|"
|
||||
r"commit|decide|decision|recommend|should i|what should|purchase|buy|invest|"
|
||||
r"architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|"
|
||||
r"config|which (should|model|approach|one)|pick|choose|audition|dimension)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if not DECISION_RE.search(last_user):
|
||||
print("PASS:not-decision-adjacent")
|
||||
sys.exit(0)
|
||||
|
||||
# Gate 2: only run if assistant response mentions memory/vestige (otherwise irrelevant)
|
||||
MEMORY_RE = re.compile(
|
||||
r"(memory|vestige|recall|retriev|saved memor|stored memor|prior memor|"
|
||||
r"fsrs|trust score|deep_reference|smart_ingest)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if not MEMORY_RE.search(last_assistant):
|
||||
print("PASS:no-memory-references")
|
||||
sys.exit(0)
|
||||
|
||||
# Detect summary pattern: 3+ distinct memory references
|
||||
SUMMARY_PATTERNS = [
|
||||
r"memory\s+[a-f0-9]{4,}", # "memory 4da778e2"
|
||||
r"memory\s+[`']?[A-Z][^`'\n]{3,50}", # "memory Alice Bob"
|
||||
r"saved memory",
|
||||
r"according to memory",
|
||||
r"the memory (says|states|notes|indicates)",
|
||||
r"per memory",
|
||||
r"memories? (say|says|state|note|indicate)",
|
||||
]
|
||||
summary_hits = 0
|
||||
for pat in SUMMARY_PATTERNS:
|
||||
summary_hits += len(re.findall(pat, last_assistant, re.IGNORECASE))
|
||||
|
||||
# Detect composition pattern
|
||||
COMPOSITION_RE = re.compile(
|
||||
r"(compos|combin|together|sam should do|you should do|concrete action|"
|
||||
r"recommend(ation)? [:\-]|never[- ]composed|never shipped together|"
|
||||
r"unmade combination|the synthesis is|composing [a-z]+\s*\+)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
composition_hits = len(COMPOSITION_RE.findall(last_assistant))
|
||||
|
||||
# Block only if: many memory references AND few composition signals
|
||||
# Tuned conservatively to avoid false positives on legitimate retrieval questions
|
||||
if summary_hits >= 3 and composition_hits == 0:
|
||||
print("BLOCK_SUMMARY")
|
||||
sys.exit(0)
|
||||
|
||||
# ============================================================================
|
||||
# HEDGING DETECTION (Apr 20 2026 — Sam's correction:
|
||||
# "you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING RULE")
|
||||
#
|
||||
# When the user prompt is decision-adjacent and the assistant response contains
|
||||
# forbidden hedging patterns — especially ones that discount Sam's own stated
|
||||
# execution commitment — block the stop and force a rewrite.
|
||||
# ============================================================================
|
||||
|
||||
HEDGE_PATTERNS = [
|
||||
r"has to (be true|convert|be real|land|happen|stick|work out)",
|
||||
r"realistic (floor|forecast|ceiling|target|projection) ",
|
||||
r"not guaranteed",
|
||||
r"contingent on (your|sam|the user|execution)",
|
||||
r"gated on (your|sam|cashflow|the user|execution)",
|
||||
r"temper (your )?expectations",
|
||||
r"don'?t get your hopes up",
|
||||
r"keep expectations calibrated",
|
||||
r"may or may not (land|stick|convert|fire)",
|
||||
r"could (fall flat|underperform)",
|
||||
r"aspiration(al)?,? not (a )?forecast",
|
||||
r"aspiration(al)?,? not (a )?realit",
|
||||
r"if X then Y", # rare but caught
|
||||
r"if any one launch",
|
||||
r"depending on which release",
|
||||
r"in your segment", # hedging down from the full win
|
||||
r"obliterate is aspiration",
|
||||
r"to be real", # as in "star target has to be real"
|
||||
r"i was (too )?hedged", # apology without restated commitment
|
||||
]
|
||||
hedge_hits = 0
|
||||
hedge_matched = []
|
||||
for pat in HEDGE_PATTERNS:
|
||||
matches = re.findall(pat, last_assistant, re.IGNORECASE)
|
||||
if matches:
|
||||
hedge_hits += len(matches)
|
||||
hedge_matched.append(pat)
|
||||
|
||||
if hedge_hits >= 1:
|
||||
print(f"BLOCK_HEDGE:{hedge_hits}:{','.join(hedge_matched[:3])}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"PASS:summary={summary_hits} composition={composition_hits} hedge={hedge_hits}")
|
||||
PYEOF
|
||||
|
||||
RESULT="$(/usr/bin/python3 "$PYFILE")"
|
||||
|
||||
case "$RESULT" in
|
||||
BLOCK_SUMMARY)
|
||||
cat >&2 <<'BLOCKMSG'
|
||||
[STOP BLOCKED — SYNTHESIS VALIDATOR: SUMMARY PATTERN]
|
||||
|
||||
Your response contains 3+ memory references with ZERO composition verbs. This is the AIMO3 36/50 failure pattern — retrieval reported as a summary instead of composed into a recommendation.
|
||||
|
||||
You may NOT stop. Rewrite your response to include:
|
||||
|
||||
1. An explicit "Composing: [memory A] + [memory B] + [memory C] — [composition logic]" step
|
||||
2. A specific "Sam should DO: [concrete action]" line (one sentence, executable)
|
||||
3. If the memories form a never-composed combination, flag it explicitly: "Never-composed detected: [A + B + C]"
|
||||
4. Do NOT re-list the memory contents separately. COMPOSE them.
|
||||
|
||||
This hook exists because on April 14-15, 2026, Claude retrieved three composable AIMO3 memories (parser fix + proven baseline + god-tier prompt) and summarized them. Cost: 36/50 instead of 42-44+. Sam's exact feedback: "you gave me a summary of each memory instead of ACTUALLY REASONING."
|
||||
|
||||
Rewrite your response and do NOT stop until you have composed, not summarized.
|
||||
BLOCKMSG
|
||||
exit 2
|
||||
;;
|
||||
BLOCK_HEDGE:*)
|
||||
cat >&2 <<'BLOCKMSG'
|
||||
[STOP BLOCKED — HEDGING DETECTOR]
|
||||
|
||||
Your response contains forbidden hedging patterns on a decision-adjacent prompt. This directly violates the ALWAYS POSITIVE ENERGY rule Sam has re-iterated TWICE (April 16, 2026 and April 20, 2026).
|
||||
|
||||
Sam's exact April 20 2026 correction: "you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING/PESSIMISTIC RULE."
|
||||
|
||||
You may NOT stop. Rewrite your response with:
|
||||
|
||||
1. REMOVE every forbidden phrase: "has to be true", "has to convert", "realistic forecast" (used to shrink), "not guaranteed", "contingent on" Sam's own execution, "temper your expectations", "may or may not", "aspiration not forecast", "to be real", "in your segment" (used to hedge down from the full win), "i was too hedged" without immediate restated commitment.
|
||||
|
||||
2. Treat Sam's stated execution commitments as LOAD-BEARING FACTS. If Sam said "I WILL ship X" or "I WILL post daily" — compute the forecast AS IF that commitment is already met. DO NOT discount Expected Value by execution probability.
|
||||
|
||||
3. Use the required format: "Target: X. Floor: Y. Ceiling: Z. Math: [line items, each a Sam-controlled variable at committed cadence]. Risks: [separate section, never padded into the target]. Let's go."
|
||||
|
||||
4. Risks go in their OWN section. Never frame the floor as "the real number" and the ceiling as "the dream."
|
||||
|
||||
Full rule: $HOME/CLAUDE.md top banner + ~/.claude/projects/<encoded-home>/memory/feedback_always_positive_energy.md
|
||||
|
||||
Rewrite and do NOT stop until the hedging is removed.
|
||||
BLOCKMSG
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
137
hooks/vestige-pulse-daemon.sh
Executable file
137
hooks/vestige-pulse-daemon.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/bash
|
||||
# vestige-pulse-daemon.sh — UserPromptSubmit hook for recent Vestige insights
|
||||
#
|
||||
# HOOK #2 of the 2026-04-20 upgrade: v2.2 PULSE AT THE CLAUDE-CODE LAYER.
|
||||
#
|
||||
# This hook polls the vestige-mcp event changelog at
|
||||
# http://127.0.0.1:3927/api/changelog and watches for DreamCompleted or
|
||||
# ConnectionDiscovered events with meaningful insight payloads. When one fires,
|
||||
# it prints context to stdout and exits 0. Claude Code injects UserPromptSubmit
|
||||
# stdout into the next turn's context.
|
||||
#
|
||||
# Rate limit: fires at most once per 20 minutes per session to avoid
|
||||
# interrupting flow state during focused work.
|
||||
#
|
||||
# The effect: fresh Vestige dream/connection events can reach Claude before it
|
||||
# answers the next prompt, without blocking the user or requiring a manual MCP
|
||||
# call first.
|
||||
#
|
||||
# Fails open: if vestige-mcp is not running or the dashboard API is unavailable,
|
||||
# exits 0 silently. Never blocks Claude.
|
||||
|
||||
set -u
|
||||
|
||||
# State files for rate limiting
|
||||
STATE_DIR="${VESTIGE_PULSE_STATE_DIR:-/tmp/vestige-pulse-daemon}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
LAST_FIRE_FILE="$STATE_DIR/last_fire"
|
||||
SESSION_ID_FILE="$STATE_DIR/session_id"
|
||||
|
||||
INPUT="$(cat)"
|
||||
SESSION_ID="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("session_id",""))' 2>/dev/null || printf '')"
|
||||
|
||||
# Rate limit: 20 minutes between fires per session
|
||||
MIN_INTERVAL_SEC=1200
|
||||
NOW=$(date +%s)
|
||||
|
||||
if [ -f "$LAST_FIRE_FILE" ]; then
|
||||
LAST_FIRE=$(cat "$LAST_FIRE_FILE" 2>/dev/null || echo 0)
|
||||
LAST_SESSION=$(cat "$SESSION_ID_FILE" 2>/dev/null || echo "")
|
||||
# Only rate-limit within the same session
|
||||
if [ "$LAST_SESSION" = "$SESSION_ID" ] && [ $((NOW - LAST_FIRE)) -lt $MIN_INTERVAL_SEC ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
PORT="${VESTIGE_DASHBOARD_PORT:-3927}"
|
||||
|
||||
# Probe health before polling the changelog
|
||||
if ! /usr/bin/curl -fsS -m 0.5 "http://127.0.0.1:${PORT}/api/health" > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check recent events via the REST changelog API for DreamCompleted in the
|
||||
# last 15 minutes. This is simpler than a full WebSocket subscription and
|
||||
# works with UserPromptSubmit semantics (inject once per prompt, not persistent).
|
||||
# If a DreamCompleted event with insights_generated > 0 is found, inject context.
|
||||
RESULT="$(/usr/bin/curl -fsS -m 2 \
|
||||
"http://127.0.0.1:${PORT}/api/changelog?start=$(date -u -v-15M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ)&limit=50" \
|
||||
2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$RESULT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
INSIGHT="$(VESTIGE_CHANGELOG_JSON="$RESULT" /usr/bin/python3 <<'PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
|
||||
def as_int(value):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
try:
|
||||
data = json.loads(os.environ.get("VESTIGE_CHANGELOG_JSON", ""))
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
sys.exit(0)
|
||||
|
||||
events = data.get("events", []) or []
|
||||
|
||||
# Find the most recent DreamCompleted with insights_generated > 0
|
||||
# OR ConnectionDiscovered with a meaningful target
|
||||
for ev in events:
|
||||
if not isinstance(ev, dict):
|
||||
continue
|
||||
etype = ev.get("type", "")
|
||||
payload = ev.get("data")
|
||||
if not isinstance(payload, dict):
|
||||
payload = ev
|
||||
|
||||
if etype in ("DreamCompleted", "dream", "consolidation"):
|
||||
insights = as_int(payload.get("insights_generated") or payload.get("insightsGenerated"))
|
||||
if insights > 0:
|
||||
stats = payload.get("stats") or {}
|
||||
connections = as_int(
|
||||
payload.get("connections_persisted")
|
||||
or payload.get("connectionsPersisted")
|
||||
or payload.get("connections_found")
|
||||
or payload.get("connectionsFound")
|
||||
or payload.get("connectionFound")
|
||||
or stats.get("connections")
|
||||
)
|
||||
print(f"DREAM: {insights} insights, {connections} new connections. Dream cycle completed while you were working. Consider calling mcp__vestige__dream(memory_count=50) to inspect the fresh cluster bridges; or mcp__vestige__explore_connections(action='bridges') on the latest activity.")
|
||||
sys.exit(0)
|
||||
if etype in ("ConnectionDiscovered", "connection"):
|
||||
src = str(payload.get("source_id") or payload.get("sourceId") or payload.get("source") or "")[:8]
|
||||
tgt = str(payload.get("target_id") or payload.get("targetId") or payload.get("target") or "")[:8]
|
||||
if src and tgt:
|
||||
print(f"CONNECTION: Vestige discovered a new edge [{src}] <-> [{tgt}] while you were working. Spreading activation surfaced a bridge you had not queried. Inspect via mcp__vestige__explore_connections(action='bridges', from='{src}', to='{tgt}').")
|
||||
sys.exit(0)
|
||||
PYEOF
|
||||
)"
|
||||
|
||||
if [ -z "$INSIGHT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update rate-limit state
|
||||
echo "$NOW" > "$LAST_FIRE_FILE"
|
||||
echo "$SESSION_ID" > "$SESSION_ID_FILE"
|
||||
|
||||
# UserPromptSubmit stdout is injected into Claude's context. Do not use exit 2
|
||||
# here: Claude Code treats that as a blocking prompt validation failure.
|
||||
cat <<PULSEMSG
|
||||
[VESTIGE PULSE — autonomous insight from the cognitive engine]
|
||||
|
||||
$INSIGHT
|
||||
|
||||
This context was injected because Vestige generated a fresh insight while the session was active. Mention it naturally if it is relevant to the user's current prompt.
|
||||
|
||||
Rate-limited to 1 pulse per 20 minutes per session. See ~/.claude/hooks/vestige-pulse-daemon.sh.
|
||||
PULSEMSG
|
||||
exit 0
|
||||
167
hooks/veto-detector.sh
Executable file
167
hooks/veto-detector.sh
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
#!/bin/bash
|
||||
# veto-detector.sh — Stop hook (Hallucination Guillotine POC)
|
||||
#
|
||||
# Fires AFTER synthesis-stop-validator.sh. Queries vestige-mcp dashboard API
|
||||
# for memories tagged veto-pattern / deprecated-pattern / suppressed, then
|
||||
# checks if the last assistant draft contains any of their trigger phrases.
|
||||
# On match: exit 2 with a VESTIGE VETO stderr message that wakes Claude and
|
||||
# forces a rewrite.
|
||||
#
|
||||
# This is the command-type proof-of-concept of the agent-type Hallucination
|
||||
# Guillotine (Integration #1 in Vestige memory 3c4bd820). Full agent-type
|
||||
# version uses a subagent to call deep_reference on extracted claims and do
|
||||
# real contradiction analysis; this version pattern-matches against curated
|
||||
# veto memories stored in Vestige.
|
||||
#
|
||||
# Fails open: if vestige-mcp is not running or no veto memories exist,
|
||||
# exits 0 silently. Never blocks on infrastructure errors.
|
||||
|
||||
set -u
|
||||
|
||||
INPUT="$(cat)"
|
||||
TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PORT="${VESTIGE_DASHBOARD_PORT:-3927}"
|
||||
if ! /usr/bin/curl -fsS -m 0.5 "http://127.0.0.1:${PORT}/api/health" > /dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fetch memories tagged veto-pattern. The /api/memories?tag=<tag> filter
|
||||
# returns exactly the tag-matching rows (verified 2026-04-20). Keyword
|
||||
# search (/api/memories?q=...) is semantic so it misses literal "VETO" hits.
|
||||
VETO_JSON="$(/usr/bin/curl -fsS -m 2 "http://127.0.0.1:${PORT}/api/memories?tag=veto-pattern&limit=50" 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$VETO_JSON" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export TRANSCRIPT_PATH
|
||||
export VETO_JSON
|
||||
|
||||
VETO_SCRIPT="$(mktemp -t vestige-veto.XXXXXX)"
|
||||
trap 'rm -f "$VETO_SCRIPT"' EXIT
|
||||
cat > "$VETO_SCRIPT" <<'VETO_PYEOF'
|
||||
import json, os, re, sys
|
||||
|
||||
transcript = os.environ.get("TRANSCRIPT_PATH", "")
|
||||
veto_json = os.environ.get("VETO_JSON", "")
|
||||
|
||||
if not transcript or not veto_json:
|
||||
sys.exit(0)
|
||||
|
||||
# Parse veto memories. Filter to those tagged veto-pattern, deprecated-pattern,
|
||||
# or suppressed, and extract a VETO_TRIGGER phrase from the content if present.
|
||||
try:
|
||||
vdata = json.loads(veto_json)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
veto_memories = []
|
||||
for m in vdata.get("memories", []) or []:
|
||||
tags = set((m.get("tags") or []))
|
||||
if not (tags & {"veto-pattern", "deprecated-pattern", "suppressed"}):
|
||||
continue
|
||||
content = m.get("content") or ""
|
||||
# Look for a "VETO_TRIGGER:" or "TRIGGER PHRASE:" line
|
||||
triggers = re.findall(r"(?:VETO_TRIGGER|TRIGGER PHRASE|TRIGGER):\s*(.+?)(?:\n|$)", content)
|
||||
for t in triggers:
|
||||
t = t.strip().strip("`\"' ")
|
||||
if t and len(t) >= 3:
|
||||
veto_memories.append({
|
||||
"id": m.get("id", "?"),
|
||||
"trigger": t,
|
||||
"content": content[:300],
|
||||
"retention": m.get("retentionStrength", 0),
|
||||
})
|
||||
|
||||
if not veto_memories:
|
||||
sys.exit(0)
|
||||
|
||||
# Read last assistant message from transcript JSONL
|
||||
last_assistant = ""
|
||||
try:
|
||||
with open(transcript) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
role = obj.get("role") or obj.get("type", "")
|
||||
content = obj.get("message", {}).get("content", obj.get("content", ""))
|
||||
text = ""
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text += block.get("text", "") + "\n"
|
||||
elif isinstance(content, str):
|
||||
text = content
|
||||
if role == "assistant":
|
||||
last_assistant = text
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
if not last_assistant:
|
||||
sys.exit(0)
|
||||
|
||||
# Check each veto trigger against the assistant draft. Only treat high-retention
|
||||
# memories (>= 0.5) as load-bearing to avoid false positives on decayed content.
|
||||
hits = []
|
||||
for v in veto_memories:
|
||||
if v["retention"] < 0.5:
|
||||
continue
|
||||
trig = v["trigger"]
|
||||
# Case-insensitive substring match with word-boundary preference
|
||||
if re.search(r"(?i)" + re.escape(trig), last_assistant):
|
||||
hits.append(v)
|
||||
|
||||
if not hits:
|
||||
sys.exit(0)
|
||||
|
||||
# Emit the VESTIGE VETO message. Newest/highest-retention hit leads.
|
||||
hits.sort(key=lambda x: x["retention"], reverse=True)
|
||||
top = hits[0]
|
||||
nid = top["id"][:8] if len(top["id"]) >= 8 else top["id"]
|
||||
trigger = top["trigger"]
|
||||
retention_pct = int(top["retention"] * 100)
|
||||
|
||||
print(f"VETO_HIT:{nid}:{trigger}:{retention_pct}")
|
||||
VETO_PYEOF
|
||||
|
||||
RESULT="$(/usr/bin/python3 "$VETO_SCRIPT" 2>/dev/null || printf '')"
|
||||
|
||||
if [ -z "$RESULT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse the result
|
||||
NODE_ID="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $2}')"
|
||||
TRIGGER="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $3}')"
|
||||
RETENTION="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $4}')"
|
||||
|
||||
cat >&2 <<VETO_MSG
|
||||
[VESTIGE VETO — synthesis-composer subagent rejected draft]
|
||||
|
||||
Contradicts suppressed memory node #${NODE_ID} (trust ${RETENTION}%).
|
||||
Trigger phrase detected: "${TRIGGER}"
|
||||
|
||||
The draft response contains a pattern that Vestige has explicitly marked as
|
||||
deprecated, suppressed, or contradicted by higher-trust memories. You may NOT
|
||||
output this response.
|
||||
|
||||
Rewrite WITHOUT the flagged pattern. Cite the correct replacement pattern by
|
||||
querying mcp__vestige__memory(action='get', id='${NODE_ID}') to see the
|
||||
suppression context and the replacement guidance.
|
||||
|
||||
This is the command-type proof-of-concept of the Hallucination Guillotine
|
||||
(Integration #1, Vestige memory 3c4bd820). Full agent-type version with
|
||||
deep_reference contradiction analysis ships later this week.
|
||||
VETO_MSG
|
||||
|
||||
exit 2
|
||||
47
launchd/com.vestige.mlx-server.plist.template
Normal file
47
launchd/com.vestige.mlx-server.plist.template
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.vestige.mlx-server</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>__HOME__/.local/bin/mlx_lm.server</string>
|
||||
<string>--model</string>
|
||||
<string>__MODEL__</string>
|
||||
<string>--host</string>
|
||||
<string>127.0.0.1</string>
|
||||
<string>--port</string>
|
||||
<string>8080</string>
|
||||
<string>--log-level</string>
|
||||
<string>INFO</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>10</integer>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>__HOME__</string>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>__HOME__/.local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>HOME</key>
|
||||
<string>__HOME__</string>
|
||||
</dict>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>__HOME__/Library/Logs/vestige-mlx-server.out.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>__HOME__/Library/Logs/vestige-mlx-server.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vestige",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"description": "Cognitive memory for AI - MCP server with FSRS-6 spaced repetition",
|
||||
"author": "Sam Valladares",
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ const path = require('path');
|
|||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const PACKAGE_VERSION = require('../package.json').version;
|
||||
const HOME = os.homedir();
|
||||
const PLATFORM = os.platform();
|
||||
|
||||
// ─── Branding ───────────────────────────────────────────────────────────────
|
||||
|
||||
const BANNER = `
|
||||
vestige init v2.0
|
||||
vestige init v${PACKAGE_VERSION}
|
||||
Give your AI a brain in 10 seconds.
|
||||
Now with 3D dashboard at localhost:3927/dashboard
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@vestige/init",
|
||||
"version": "2.0.7",
|
||||
"description": "Give your AI a brain in 10 seconds — zero-config Vestige v2.0 installer with 3D dashboard",
|
||||
"version": "2.1.0",
|
||||
"description": "Give your AI a brain in 10 seconds — zero-config Vestige installer with 3D dashboard",
|
||||
"bin": {
|
||||
"vestige-init": "bin/init.js"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ bin/vestige
|
|||
bin/vestige.exe
|
||||
bin/vestige-mcp
|
||||
bin/vestige-mcp.exe
|
||||
bin/vestige-restore
|
||||
bin/vestige-restore.exe
|
||||
bin/*.tar.gz
|
||||
bin/*.zip
|
||||
|
||||
|
|
|
|||
|
|
@ -93,10 +93,11 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache"
|
|||
|----------|-------------|---------|
|
||||
| `RUST_LOG` | Log verbosity + per-module filter | `info` |
|
||||
| `FASTEMBED_CACHE_PATH` | Embeddings model cache | `./.fastembed_cache` |
|
||||
| `VESTIGE_DATA_DIR` | Storage directory fallback; database lives at `<dir>/vestige.db` | OS data dir |
|
||||
| `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` |
|
||||
| `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated |
|
||||
|
||||
Storage location is the `--data-dir <path>` CLI flag (defaults to your OS's per-user data directory).
|
||||
Storage precedence is `--data-dir <path>`, then `VESTIGE_DATA_DIR`, then your OS's per-user data directory.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vestige-mcp-server",
|
||||
"version": "2.0.7",
|
||||
"version": "2.1.0",
|
||||
"description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules",
|
||||
"bin": {
|
||||
"vestige-mcp": "bin/vestige-mcp.js",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue