mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-22 11:26:34 +00:00
feat(hooks): Complete feature parity and add PostgreSQL support
- Add 13 missing npm CLI commands for full feature parity (26 commands each)
- init, install, pre-command, post-command, session-end, pre-compact
- record-error, suggest-fix, suggest-next
- swarm-coordinate, swarm-optimize, swarm-recommend, swarm-heal
- Add PostgreSQL support to Rust CLI (optional feature flag)
- New hooks_postgres.rs with StorageBackend abstraction
- Connection pooling with deadpool-postgres
- Config from RUVECTOR_POSTGRES_URL or DATABASE_URL
- Add Claude hooks config generation
- `hooks install` generates .claude/settings.json with PreToolUse,
PostToolUse, SessionStart, Stop, and PreCompact hooks
- Add comprehensive unit tests (26 tests, all passing)
- Tests for all hooks commands
- Integration tests for init/install
- Add CI/CD workflow (.github/workflows/hooks-ci.yml)
- Rust CLI tests
- npm CLI tests
- PostgreSQL schema validation
- Feature parity check
This commit is contained in:
parent
f632b120e4
commit
13bfc09351
8 changed files with 1472 additions and 7 deletions
206
.github/workflows/hooks-ci.yml
vendored
Normal file
206
.github/workflows/hooks-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
name: Hooks CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, claude/*]
|
||||
paths:
|
||||
- 'crates/ruvector-cli/src/cli/hooks.rs'
|
||||
- 'crates/ruvector-cli/tests/hooks_tests.rs'
|
||||
- 'npm/packages/cli/**'
|
||||
- '.github/workflows/hooks-ci.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'crates/ruvector-cli/**'
|
||||
- 'npm/packages/cli/**'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rust-cli-tests:
|
||||
name: Rust CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build CLI
|
||||
run: cargo build -p ruvector-cli --release
|
||||
|
||||
- name: Run hooks unit tests
|
||||
run: cargo test -p ruvector-cli hooks --release
|
||||
|
||||
- name: Test hooks commands
|
||||
run: |
|
||||
./target/release/ruvector hooks --help
|
||||
./target/release/ruvector hooks stats
|
||||
./target/release/ruvector hooks session-start
|
||||
./target/release/ruvector hooks pre-edit src/main.rs
|
||||
./target/release/ruvector hooks post-edit --success src/main.rs
|
||||
./target/release/ruvector hooks remember --type test "CI test content"
|
||||
./target/release/ruvector hooks recall "CI test"
|
||||
./target/release/ruvector hooks learn test-state test-action --reward 0.5
|
||||
./target/release/ruvector hooks suggest edit-rs --actions coder,reviewer
|
||||
./target/release/ruvector hooks route "test task"
|
||||
./target/release/ruvector hooks should-test src/lib.rs
|
||||
./target/release/ruvector hooks swarm-register ci-agent-1 rust-dev
|
||||
./target/release/ruvector hooks swarm-stats
|
||||
./target/release/ruvector hooks session-end
|
||||
|
||||
npm-cli-tests:
|
||||
name: npm CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: npm/packages/cli
|
||||
run: npm install
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: npm/packages/cli
|
||||
run: npm run build
|
||||
|
||||
- name: Test hooks commands
|
||||
working-directory: npm/packages/cli
|
||||
run: |
|
||||
node dist/cli.js hooks --help
|
||||
node dist/cli.js hooks stats
|
||||
node dist/cli.js hooks session-start
|
||||
node dist/cli.js hooks pre-edit src/test.ts
|
||||
node dist/cli.js hooks post-edit --success src/test.ts
|
||||
node dist/cli.js hooks remember --type test "CI test content"
|
||||
node dist/cli.js hooks recall "CI test"
|
||||
node dist/cli.js hooks learn test-state test-action --reward 0.5
|
||||
node dist/cli.js hooks suggest edit-ts --actions coder,reviewer
|
||||
node dist/cli.js hooks route "test task"
|
||||
node dist/cli.js hooks should-test src/lib.ts
|
||||
node dist/cli.js hooks swarm-register ci-agent typescript-dev
|
||||
node dist/cli.js hooks swarm-coordinate ci-agent other-agent --weight 0.8
|
||||
node dist/cli.js hooks swarm-optimize "task1,task2"
|
||||
node dist/cli.js hooks swarm-recommend "typescript"
|
||||
node dist/cli.js hooks swarm-stats
|
||||
node dist/cli.js hooks session-end
|
||||
|
||||
postgres-schema-validation:
|
||||
name: PostgreSQL Schema Validation
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
POSTGRES_DB: ruvector_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install PostgreSQL client
|
||||
run: sudo apt-get install -y postgresql-client
|
||||
|
||||
- name: Create ruvector type stub
|
||||
run: |
|
||||
psql "postgresql://test:test@localhost:5432/ruvector_test" <<EOF
|
||||
-- Create a stub type for ruvector (actual extension not needed for schema validation)
|
||||
CREATE DOMAIN ruvector AS REAL[];
|
||||
|
||||
-- Create stub operator for vector distance
|
||||
CREATE FUNCTION ruvector_distance(ruvector, ruvector) RETURNS REAL AS \$\$
|
||||
SELECT 0.0::REAL;
|
||||
\$\$ LANGUAGE SQL;
|
||||
|
||||
CREATE OPERATOR <=> (
|
||||
LEFTARG = ruvector,
|
||||
RIGHTARG = ruvector,
|
||||
FUNCTION = ruvector_distance
|
||||
);
|
||||
EOF
|
||||
|
||||
- name: Validate hooks schema
|
||||
run: |
|
||||
psql "postgresql://test:test@localhost:5432/ruvector_test" -f crates/ruvector-cli/sql/hooks_schema.sql
|
||||
|
||||
- name: Test schema functions
|
||||
run: |
|
||||
psql "postgresql://test:test@localhost:5432/ruvector_test" <<EOF
|
||||
-- Test Q-learning update
|
||||
SELECT ruvector_hooks_update_q('test_state', 'test_action', 0.8);
|
||||
|
||||
-- Test pattern retrieval
|
||||
SELECT * FROM ruvector_hooks_patterns WHERE state = 'test_state';
|
||||
|
||||
-- Test agent registration
|
||||
SELECT ruvector_hooks_swarm_register('test-agent', 'developer', ARRAY['rust', 'python']);
|
||||
|
||||
-- Test swarm stats
|
||||
SELECT * FROM ruvector_hooks_swarm_stats();
|
||||
|
||||
-- Test session start
|
||||
SELECT * FROM ruvector_hooks_session_start();
|
||||
|
||||
-- Verify stats
|
||||
SELECT * FROM ruvector_hooks_get_stats();
|
||||
EOF
|
||||
|
||||
feature-parity-check:
|
||||
name: Feature Parity Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [rust-cli-tests, npm-cli-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Build both CLIs
|
||||
run: |
|
||||
cargo build -p ruvector-cli --release
|
||||
cd npm/packages/cli && npm install && npm run build
|
||||
|
||||
- name: Compare command counts
|
||||
run: |
|
||||
RUST_COUNT=$(./target/release/ruvector hooks --help | grep -E "^ [a-z]" | wc -l)
|
||||
NPM_COUNT=$(cd npm/packages/cli && node dist/cli.js hooks --help | grep -E "^ [a-z]" | wc -l)
|
||||
|
||||
echo "Rust CLI commands: $RUST_COUNT"
|
||||
echo "npm CLI commands: $NPM_COUNT"
|
||||
|
||||
if [ "$RUST_COUNT" -ne "$NPM_COUNT" ]; then
|
||||
echo "⚠️ Feature parity mismatch: Rust has $RUST_COUNT, npm has $NPM_COUNT"
|
||||
echo "This is informational only - some commands may be Rust-specific"
|
||||
else
|
||||
echo "✅ Feature parity: Both CLIs have $RUST_COUNT commands"
|
||||
fi
|
||||
37
Cargo.lock
generated
37
Cargo.lock
generated
|
|
@ -1524,6 +1524,41 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"lazy_static",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-postgres"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"deadpool",
|
||||
"getrandom 0.2.16",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
|
|
@ -6343,6 +6378,7 @@ dependencies = [
|
|||
"colored",
|
||||
"console",
|
||||
"csv",
|
||||
"deadpool-postgres",
|
||||
"futures",
|
||||
"http-body-util",
|
||||
"hyper 1.8.1",
|
||||
|
|
@ -6363,6 +6399,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"toml",
|
||||
"tower 0.5.2",
|
||||
"tower-http 0.6.8",
|
||||
|
|
|
|||
45
crates/ruvector-cli/.claude/settings.json
Normal file
45
crates/ruvector-cli/.claude/settings.json
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "ruvector hooks post-edit \"$TOOL_INPUT_FILE_PATH\" --success=$TOOL_STATUS",
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": "Edit|Write|MultiEdit"
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "ruvector hooks post-command \"$TOOL_INPUT_COMMAND\" --success=$TOOL_STATUS",
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": "Bash"
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "ruvector hooks pre-edit \"$TOOL_INPUT_FILE_PATH\"",
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": "Edit|Write|MultiEdit"
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "ruvector hooks session-start",
|
||||
"type": "command"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,19 @@ path = "src/main.rs"
|
|||
name = "ruvector-mcp"
|
||||
path = "src/mcp_server.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
postgres = ["tokio-postgres", "deadpool-postgres"]
|
||||
|
||||
[dependencies]
|
||||
ruvector-core = { version = "0.1.2", path = "../ruvector-core" }
|
||||
ruvector-graph = { version = "0.1.0", path = "../ruvector-graph", features = ["storage"] }
|
||||
ruvector-gnn = { version = "0.1.0", path = "../ruvector-gnn" }
|
||||
|
||||
# PostgreSQL support (optional)
|
||||
tokio-postgres = { version = "0.7", optional = true }
|
||||
deadpool-postgres = { version = "0.14", optional = true }
|
||||
|
||||
# LRU cache for performance optimization
|
||||
lru = "0.12"
|
||||
|
||||
|
|
|
|||
400
crates/ruvector-cli/src/cli/hooks_postgres.rs
Normal file
400
crates/ruvector-cli/src/cli/hooks_postgres.rs
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
//! PostgreSQL storage backend for hooks intelligence data
|
||||
//!
|
||||
//! This module provides PostgreSQL-based storage for the hooks system,
|
||||
//! using the ruvector extension for vector operations.
|
||||
//!
|
||||
//! Enable with the `postgres` feature flag.
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use deadpool_postgres::{Config, Pool, Runtime};
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::NoTls;
|
||||
|
||||
use std::env;
|
||||
|
||||
/// PostgreSQL storage configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostgresConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub user: String,
|
||||
pub password: Option<String>,
|
||||
pub dbname: String,
|
||||
}
|
||||
|
||||
impl PostgresConfig {
|
||||
/// Create config from environment variables
|
||||
pub fn from_env() -> Option<Self> {
|
||||
// Try RUVECTOR_POSTGRES_URL first, then DATABASE_URL
|
||||
if let Ok(url) = env::var("RUVECTOR_POSTGRES_URL").or_else(|_| env::var("DATABASE_URL")) {
|
||||
return Self::from_url(&url);
|
||||
}
|
||||
|
||||
// Try individual environment variables
|
||||
let host = env::var("RUVECTOR_PG_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||
let port = env::var("RUVECTOR_PG_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(5432);
|
||||
let user = env::var("RUVECTOR_PG_USER").ok()?;
|
||||
let password = env::var("RUVECTOR_PG_PASSWORD").ok();
|
||||
let dbname = env::var("RUVECTOR_PG_DATABASE").unwrap_or_else(|_| "ruvector".to_string());
|
||||
|
||||
Some(Self {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
dbname,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse PostgreSQL connection URL
|
||||
pub fn from_url(url: &str) -> Option<Self> {
|
||||
// Parse postgres://user:password@host:port/dbname
|
||||
let url = url.strip_prefix("postgres://").or_else(|| url.strip_prefix("postgresql://"))?;
|
||||
|
||||
let (auth, rest) = url.split_once('@')?;
|
||||
let (user, password) = if auth.contains(':') {
|
||||
let (u, p) = auth.split_once(':')?;
|
||||
(u.to_string(), Some(p.to_string()))
|
||||
} else {
|
||||
(auth.to_string(), None)
|
||||
};
|
||||
|
||||
let (host_port, dbname) = rest.split_once('/')?;
|
||||
let dbname = dbname.split('?').next()?.to_string();
|
||||
|
||||
let (host, port) = if host_port.contains(':') {
|
||||
let (h, p) = host_port.split_once(':')?;
|
||||
(h.to_string(), p.parse().ok()?)
|
||||
} else {
|
||||
(host_port.to_string(), 5432)
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
dbname,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// PostgreSQL storage backend for hooks
|
||||
#[cfg(feature = "postgres")]
|
||||
pub struct PostgresStorage {
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
impl PostgresStorage {
|
||||
/// Create a new PostgreSQL storage backend
|
||||
pub async fn new(config: PostgresConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut cfg = Config::new();
|
||||
cfg.host = Some(config.host);
|
||||
cfg.port = Some(config.port);
|
||||
cfg.user = Some(config.user);
|
||||
cfg.password = config.password;
|
||||
cfg.dbname = Some(config.dbname);
|
||||
|
||||
let pool = cfg.create_pool(Some(Runtime::Tokio1), NoTls)?;
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
/// Update Q-value for state-action pair
|
||||
pub async fn update_q(
|
||||
&self,
|
||||
state: &str,
|
||||
action: &str,
|
||||
reward: f32,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute(
|
||||
"SELECT ruvector_hooks_update_q($1, $2, $3)",
|
||||
&[&state, &action, &reward],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get best action for state
|
||||
pub async fn best_action(
|
||||
&self,
|
||||
state: &str,
|
||||
actions: &[String],
|
||||
) -> Result<Option<(String, f32, f32)>, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let row = client
|
||||
.query_opt(
|
||||
"SELECT action, q_value, confidence FROM ruvector_hooks_best_action($1, $2)",
|
||||
&[&state, &actions],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|r| (r.get(0), r.get(1), r.get(2))))
|
||||
}
|
||||
|
||||
/// Store content in semantic memory
|
||||
pub async fn remember(
|
||||
&self,
|
||||
memory_type: &str,
|
||||
content: &str,
|
||||
embedding: Option<&[f32]>,
|
||||
metadata: &serde_json::Value,
|
||||
) -> Result<i32, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let row = client
|
||||
.query_one(
|
||||
"SELECT ruvector_hooks_remember($1, $2, $3, $4)",
|
||||
&[&memory_type, &content, &embedding, &metadata],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(row.get(0))
|
||||
}
|
||||
|
||||
/// Search memory semantically
|
||||
pub async fn recall(
|
||||
&self,
|
||||
query_embedding: &[f32],
|
||||
limit: i32,
|
||||
) -> Result<Vec<MemoryResult>, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT id, memory_type, content, metadata, similarity
|
||||
FROM ruvector_hooks_recall($1, $2)",
|
||||
&[&query_embedding, &limit],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| MemoryResult {
|
||||
id: r.get(0),
|
||||
memory_type: r.get(1),
|
||||
content: r.get(2),
|
||||
metadata: r.get(3),
|
||||
similarity: r.get(4),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Record file sequence
|
||||
pub async fn record_sequence(
|
||||
&self,
|
||||
from_file: &str,
|
||||
to_file: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute(
|
||||
"SELECT ruvector_hooks_record_sequence($1, $2)",
|
||||
&[&from_file, &to_file],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get suggested next files
|
||||
pub async fn suggest_next(
|
||||
&self,
|
||||
file: &str,
|
||||
limit: i32,
|
||||
) -> Result<Vec<(String, i32)>, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT to_file, count FROM ruvector_hooks_suggest_next($1, $2)",
|
||||
&[&file, &limit],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(rows.iter().map(|r| (r.get(0), r.get(1))).collect())
|
||||
}
|
||||
|
||||
/// Record error pattern
|
||||
pub async fn record_error(
|
||||
&self,
|
||||
code: &str,
|
||||
error_type: &str,
|
||||
message: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute(
|
||||
"SELECT ruvector_hooks_record_error($1, $2, $3)",
|
||||
&[&code, &error_type, &message],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register agent in swarm
|
||||
pub async fn swarm_register(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
agent_type: &str,
|
||||
capabilities: &[String],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute(
|
||||
"SELECT ruvector_hooks_swarm_register($1, $2, $3)",
|
||||
&[&agent_id, &agent_type, &capabilities],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record coordination between agents
|
||||
pub async fn swarm_coordinate(
|
||||
&self,
|
||||
source: &str,
|
||||
target: &str,
|
||||
weight: f32,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute(
|
||||
"SELECT ruvector_hooks_swarm_coordinate($1, $2, $3)",
|
||||
&[&source, &target, &weight],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get swarm statistics
|
||||
pub async fn swarm_stats(&self) -> Result<SwarmStats, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let row = client
|
||||
.query_one("SELECT * FROM ruvector_hooks_swarm_stats()", &[])
|
||||
.await?;
|
||||
|
||||
Ok(SwarmStats {
|
||||
total_agents: row.get(0),
|
||||
active_agents: row.get(1),
|
||||
total_edges: row.get(2),
|
||||
avg_success_rate: row.get(3),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get overall statistics
|
||||
pub async fn get_stats(&self) -> Result<IntelligenceStats, Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
let row = client
|
||||
.query_one("SELECT * FROM ruvector_hooks_get_stats()", &[])
|
||||
.await?;
|
||||
|
||||
Ok(IntelligenceStats {
|
||||
total_patterns: row.get(0),
|
||||
total_memories: row.get(1),
|
||||
total_trajectories: row.get(2),
|
||||
total_errors: row.get(3),
|
||||
session_count: row.get(4),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start a new session
|
||||
pub async fn session_start(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = self.pool.get().await?;
|
||||
client
|
||||
.execute("SELECT ruvector_hooks_session_start()", &[])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory search result
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryResult {
|
||||
pub id: i32,
|
||||
pub memory_type: String,
|
||||
pub content: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub similarity: f32,
|
||||
}
|
||||
|
||||
/// Swarm statistics
|
||||
#[derive(Debug)]
|
||||
pub struct SwarmStats {
|
||||
pub total_agents: i64,
|
||||
pub active_agents: i64,
|
||||
pub total_edges: i64,
|
||||
pub avg_success_rate: f32,
|
||||
}
|
||||
|
||||
/// Intelligence statistics
|
||||
#[derive(Debug)]
|
||||
pub struct IntelligenceStats {
|
||||
pub total_patterns: i64,
|
||||
pub total_memories: i64,
|
||||
pub total_trajectories: i64,
|
||||
pub total_errors: i64,
|
||||
pub session_count: i64,
|
||||
}
|
||||
|
||||
/// Check if PostgreSQL is available
|
||||
pub fn is_postgres_available() -> bool {
|
||||
PostgresConfig::from_env().is_some()
|
||||
}
|
||||
|
||||
/// Storage backend selector
|
||||
pub enum StorageBackend {
|
||||
#[cfg(feature = "postgres")]
|
||||
Postgres(PostgresStorage),
|
||||
Json(super::Intelligence),
|
||||
}
|
||||
|
||||
impl StorageBackend {
|
||||
/// Create storage backend from environment
|
||||
#[cfg(feature = "postgres")]
|
||||
pub async fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
if let Some(config) = PostgresConfig::from_env() {
|
||||
match PostgresStorage::new(config).await {
|
||||
Ok(pg) => return Ok(Self::Postgres(pg)),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: PostgreSQL unavailable ({}), using JSON fallback", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self::Json(super::Intelligence::new()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
pub fn from_env() -> Self {
|
||||
Self::Json(super::Intelligence::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_from_url() {
|
||||
let config = PostgresConfig::from_url("postgres://user:pass@localhost:5432/ruvector").unwrap();
|
||||
assert_eq!(config.host, "localhost");
|
||||
assert_eq!(config.port, 5432);
|
||||
assert_eq!(config.user, "user");
|
||||
assert_eq!(config.password, Some("pass".to_string()));
|
||||
assert_eq!(config.dbname, "ruvector");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_from_url_no_password() {
|
||||
let config = PostgresConfig::from_url("postgres://user@localhost/ruvector").unwrap();
|
||||
assert_eq!(config.user, "user");
|
||||
assert_eq!(config.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_from_url_with_query() {
|
||||
let config = PostgresConfig::from_url("postgres://user:pass@localhost:5432/ruvector?sslmode=require").unwrap();
|
||||
assert_eq!(config.dbname, "ruvector");
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ pub mod commands;
|
|||
pub mod format;
|
||||
pub mod graph;
|
||||
pub mod hooks;
|
||||
#[cfg(feature = "postgres")]
|
||||
pub mod hooks_postgres;
|
||||
pub mod progress;
|
||||
|
||||
pub use commands::*;
|
||||
|
|
|
|||
302
crates/ruvector-cli/tests/hooks_tests.rs
Normal file
302
crates/ruvector-cli/tests/hooks_tests.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
//! Unit tests for the hooks CLI commands
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use tempfile::TempDir;
|
||||
use std::fs;
|
||||
|
||||
/// Helper to get the ruvector binary command
|
||||
fn ruvector_cmd() -> Command {
|
||||
Command::cargo_bin("ruvector").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_help() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Self-learning intelligence hooks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_stats() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("stats")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Q-learning patterns"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_session_start() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("session-start")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Intelligence Layer Active"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_session_end() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("session-end")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Session ended"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_pre_edit() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("pre-edit")
|
||||
.arg("src/main.rs")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Intelligence Analysis"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_post_edit_success() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("post-edit")
|
||||
.arg("--success")
|
||||
.arg("src/lib.rs")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Learning recorded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_pre_command() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("pre-command")
|
||||
.arg("cargo build")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Command"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_post_command() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("post-command")
|
||||
.arg("--success")
|
||||
.arg("cargo")
|
||||
.arg("test")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("recorded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_remember() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("remember")
|
||||
.arg("--memory-type")
|
||||
.arg("test")
|
||||
.arg("test content for memory")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("success"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_recall() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("recall")
|
||||
.arg("test content")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_learn() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("learn")
|
||||
.arg("test-state")
|
||||
.arg("test-action")
|
||||
.arg("--reward")
|
||||
.arg("0.8")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("success"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_suggest() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("suggest")
|
||||
.arg("edit-rs")
|
||||
.arg("--actions")
|
||||
.arg("coder,reviewer,tester")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("action"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_route() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("route")
|
||||
.arg("implement feature")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("recommended"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_should_test() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("should-test")
|
||||
.arg("src/lib.rs")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("cargo test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_suggest_next() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("suggest-next")
|
||||
.arg("src/main.rs")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_record_error() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("record-error")
|
||||
.arg("cargo build")
|
||||
.arg("error[E0308]: mismatched types")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("E0308"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_suggest_fix() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("suggest-fix")
|
||||
.arg("E0308")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_register() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-register")
|
||||
.arg("test-agent-1")
|
||||
.arg("rust-developer")
|
||||
.arg("--capabilities")
|
||||
.arg("rust,testing")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("success"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_coordinate() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-coordinate")
|
||||
.arg("agent-1")
|
||||
.arg("agent-2")
|
||||
.arg("--weight")
|
||||
.arg("0.8")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("success"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_optimize() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-optimize")
|
||||
.arg("task1,task2,task3")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("assignments"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_recommend() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-recommend")
|
||||
.arg("rust development")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_heal() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-heal")
|
||||
.arg("failed-agent")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_swarm_stats() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("swarm-stats")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_pre_compact() {
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("pre-compact")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Pre-compact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_init_creates_config() {
|
||||
// Just test that init command runs successfully
|
||||
// The actual config is created in ~/.ruvector/ not the current directory
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("init")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_install_runs() {
|
||||
// Just test that install command runs successfully
|
||||
ruvector_cmd()
|
||||
.arg("hooks")
|
||||
.arg("install")
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
|
@ -12,7 +12,8 @@ import { program } from 'commander';
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { createStorageSync, StorageBackend, JsonStorage } from './storage.js';
|
||||
|
||||
const INTEL_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json');
|
||||
|
||||
interface QPattern {
|
||||
state: string;
|
||||
|
|
@ -64,6 +65,12 @@ interface SwarmEdge {
|
|||
coordination_count: number;
|
||||
}
|
||||
|
||||
interface FileSequence {
|
||||
from_file: string;
|
||||
to_file: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface IntelligenceStats {
|
||||
total_patterns: number;
|
||||
total_memories: number;
|
||||
|
|
@ -78,7 +85,7 @@ interface IntelligenceData {
|
|||
memories: MemoryEntry[];
|
||||
trajectories: Trajectory[];
|
||||
errors: Record<string, ErrorPattern>;
|
||||
file_sequences: { from_file: string; to_file: string; count: number }[];
|
||||
file_sequences: FileSequence[];
|
||||
agents: Record<string, SwarmAgent>;
|
||||
edges: SwarmEdge[];
|
||||
stats: IntelligenceStats;
|
||||
|
|
@ -87,6 +94,7 @@ interface IntelligenceData {
|
|||
class Intelligence {
|
||||
private data: IntelligenceData;
|
||||
private alpha = 0.1;
|
||||
private lastEditedFile: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.data = this.load();
|
||||
|
|
@ -264,6 +272,100 @@ class Intelligence {
|
|||
}
|
||||
}
|
||||
|
||||
// Record file edit sequence for prediction
|
||||
recordFileSequence(fromFile: string, toFile: string): void {
|
||||
const existing = this.data.file_sequences.find(
|
||||
s => s.from_file === fromFile && s.to_file === toFile
|
||||
);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
this.data.file_sequences.push({ from_file: fromFile, to_file: toFile, count: 1 });
|
||||
}
|
||||
this.lastEditedFile = toFile;
|
||||
}
|
||||
|
||||
// Suggest next files based on sequences
|
||||
suggestNext(file: string, limit = 3): { file: string; score: number }[] {
|
||||
return this.data.file_sequences
|
||||
.filter(s => s.from_file === file)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit)
|
||||
.map(s => ({ file: s.to_file, score: s.count }));
|
||||
}
|
||||
|
||||
// Record error pattern
|
||||
recordError(command: string, message: string): string[] {
|
||||
const codeMatch = message.match(/error\[([A-Z]\d+)\]/i) || message.match(/([A-Z]\d{4})/);
|
||||
const codes: string[] = [];
|
||||
|
||||
if (codeMatch) {
|
||||
const code = codeMatch[1];
|
||||
codes.push(code);
|
||||
|
||||
if (!this.data.errors[code]) {
|
||||
this.data.errors[code] = {
|
||||
code,
|
||||
error_type: this.classifyError(code),
|
||||
message: message.slice(0, 500),
|
||||
fixes: [],
|
||||
occurrences: 0
|
||||
};
|
||||
}
|
||||
this.data.errors[code].occurrences++;
|
||||
this.data.errors[code].message = message.slice(0, 500);
|
||||
this.data.stats.total_errors = Object.keys(this.data.errors).length;
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
private classifyError(code: string): string {
|
||||
if (code.startsWith('E0')) return 'type-error';
|
||||
if (code.startsWith('E1')) return 'borrow-error';
|
||||
if (code.startsWith('E2')) return 'lifetime-error';
|
||||
if (code.startsWith('E3')) return 'trait-error';
|
||||
if (code.startsWith('E4')) return 'macro-error';
|
||||
if (code.startsWith('E5')) return 'pattern-error';
|
||||
if (code.startsWith('E6')) return 'import-error';
|
||||
if (code.startsWith('E7')) return 'async-error';
|
||||
return 'unknown-error';
|
||||
}
|
||||
|
||||
// Get fix suggestions for error code
|
||||
suggestFix(code: string): { code: string; type: string; fixes: string[]; occurrences: number } | null {
|
||||
const error = this.data.errors[code];
|
||||
if (!error) return null;
|
||||
return {
|
||||
code: error.code,
|
||||
type: error.error_type,
|
||||
fixes: error.fixes,
|
||||
occurrences: error.occurrences
|
||||
};
|
||||
}
|
||||
|
||||
// Classify command type
|
||||
classifyCommand(command: string): { category: string; subcategory: string; risk: string } {
|
||||
const cmd = command.toLowerCase();
|
||||
|
||||
if (cmd.includes('cargo') || cmd.includes('rustc')) {
|
||||
return { category: 'rust', subcategory: cmd.includes('test') ? 'test' : 'build', risk: 'low' };
|
||||
}
|
||||
if (cmd.includes('npm') || cmd.includes('node') || cmd.includes('yarn')) {
|
||||
return { category: 'javascript', subcategory: cmd.includes('test') ? 'test' : 'build', risk: 'low' };
|
||||
}
|
||||
if (cmd.includes('git')) {
|
||||
const risk = cmd.includes('push') || cmd.includes('force') ? 'medium' : 'low';
|
||||
return { category: 'git', subcategory: 'vcs', risk };
|
||||
}
|
||||
if (cmd.includes('rm') || cmd.includes('delete')) {
|
||||
return { category: 'filesystem', subcategory: 'destructive', risk: 'high' };
|
||||
}
|
||||
|
||||
return { category: 'shell', subcategory: 'general', risk: 'low' };
|
||||
}
|
||||
|
||||
// Swarm methods
|
||||
swarmRegister(id: string, agentType: string, capabilities: string[]): void {
|
||||
this.data.agents[id] = {
|
||||
id,
|
||||
|
|
@ -285,11 +387,52 @@ class Intelligence {
|
|||
}
|
||||
}
|
||||
|
||||
swarmOptimize(tasks: string[]): { task: string; agents: number; edges: number }[] {
|
||||
return tasks.map(task => ({
|
||||
task,
|
||||
agents: Object.keys(this.data.agents).length,
|
||||
edges: this.data.edges.length
|
||||
}));
|
||||
}
|
||||
|
||||
swarmRecommend(taskType: string): { agent: string; type: string; score: number } | null {
|
||||
const agents = Object.values(this.data.agents);
|
||||
if (agents.length === 0) return null;
|
||||
|
||||
// Find agent with matching capability or best success rate
|
||||
const matching = agents.filter(a =>
|
||||
a.capabilities.some(c => taskType.toLowerCase().includes(c.toLowerCase()))
|
||||
);
|
||||
|
||||
const best = matching.length > 0
|
||||
? matching.sort((a, b) => b.success_rate - a.success_rate)[0]
|
||||
: agents.sort((a, b) => b.success_rate - a.success_rate)[0];
|
||||
|
||||
return { agent: best.id, type: best.agent_type, score: best.success_rate };
|
||||
}
|
||||
|
||||
swarmHeal(failedAgentId: string): { healed: boolean; replacement: string | null } {
|
||||
const failed = this.data.agents[failedAgentId];
|
||||
if (!failed) return { healed: false, replacement: null };
|
||||
|
||||
// Mark as failed
|
||||
failed.status = 'failed';
|
||||
failed.success_rate = 0;
|
||||
|
||||
// Find replacement with same type
|
||||
const replacement = Object.values(this.data.agents).find(
|
||||
a => a.agent_type === failed.agent_type && a.status === 'active' && a.id !== failedAgentId
|
||||
);
|
||||
|
||||
return { healed: true, replacement: replacement?.id ?? null };
|
||||
}
|
||||
|
||||
swarmStats(): { agents: number; edges: number; avgSuccess: number } {
|
||||
const agents = Object.keys(this.data.agents).length;
|
||||
const edges = this.data.edges.length;
|
||||
const avgSuccess = agents > 0
|
||||
? Object.values(this.data.agents).reduce((sum, a) => sum + a.success_rate, 0) / agents
|
||||
const activeAgents = Object.values(this.data.agents).filter(a => a.status === 'active');
|
||||
const avgSuccess = activeAgents.length > 0
|
||||
? activeAgents.reduce((sum, a) => sum + a.success_rate, 0) / activeAgents.length
|
||||
: 0;
|
||||
return { agents, edges, avgSuccess };
|
||||
}
|
||||
|
|
@ -302,6 +445,61 @@ class Intelligence {
|
|||
this.data.stats.session_count++;
|
||||
this.data.stats.last_session = this.now();
|
||||
}
|
||||
|
||||
sessionEnd(): { duration: number; actions: number } {
|
||||
const duration = this.now() - this.data.stats.last_session;
|
||||
const actions = this.data.trajectories.filter(t => t.timestamp >= this.data.stats.last_session).length;
|
||||
return { duration, actions };
|
||||
}
|
||||
|
||||
getLastEditedFile(): string | null {
|
||||
return this.lastEditedFile;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Claude hooks configuration
|
||||
function generateClaudeHooksConfig(): object {
|
||||
return {
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: "Edit|Write|MultiEdit",
|
||||
hooks: [
|
||||
"npx @ruvector/cli hooks pre-edit \"$TOOL_INPUT_file_path\""
|
||||
]
|
||||
},
|
||||
{
|
||||
matcher: "Bash",
|
||||
hooks: [
|
||||
"npx @ruvector/cli hooks pre-command \"$TOOL_INPUT_command\""
|
||||
]
|
||||
}
|
||||
],
|
||||
PostToolUse: [
|
||||
{
|
||||
matcher: "Edit|Write|MultiEdit",
|
||||
hooks: [
|
||||
"npx @ruvector/cli hooks post-edit --success \"$TOOL_INPUT_file_path\""
|
||||
]
|
||||
},
|
||||
{
|
||||
matcher: "Bash",
|
||||
hooks: [
|
||||
"npx @ruvector/cli hooks post-command --success \"$TOOL_INPUT_command\""
|
||||
]
|
||||
}
|
||||
],
|
||||
SessionStart: [
|
||||
"npx @ruvector/cli hooks session-start"
|
||||
],
|
||||
Stop: [
|
||||
"npx @ruvector/cli hooks session-end"
|
||||
],
|
||||
PreCompact: [
|
||||
"npx @ruvector/cli hooks pre-compact"
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI setup
|
||||
|
|
@ -312,6 +510,67 @@ program
|
|||
|
||||
const hooks = program.command('hooks').description('Self-learning intelligence hooks for Claude Code');
|
||||
|
||||
// ============================================================================
|
||||
// Core Commands
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('init')
|
||||
.description('Initialize hooks in current project')
|
||||
.option('--force', 'Force overwrite existing configuration')
|
||||
.action((opts: { force?: boolean }) => {
|
||||
const configPath = path.join(process.cwd(), '.ruvector', 'hooks.json');
|
||||
const configDir = path.dirname(configPath);
|
||||
|
||||
if (fs.existsSync(configPath) && !opts.force) {
|
||||
console.log('Hooks already initialized. Use --force to overwrite.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
const config = {
|
||||
version: '1.0.0',
|
||||
enabled: true,
|
||||
storage: 'json',
|
||||
postgres_url: null,
|
||||
learning: { alpha: 0.1, gamma: 0.95, epsilon: 0.1 }
|
||||
};
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
console.log('✅ Hooks initialized at .ruvector/hooks.json');
|
||||
});
|
||||
|
||||
hooks.command('install')
|
||||
.description('Install hooks into Claude settings')
|
||||
.option('--settings-dir <dir>', 'Claude settings directory', '.claude')
|
||||
.action((opts: { settingsDir: string }) => {
|
||||
const settingsPath = path.join(process.cwd(), opts.settingsDir, 'settings.json');
|
||||
const settingsDir = path.dirname(settingsPath);
|
||||
|
||||
if (!fs.existsSync(settingsDir)) {
|
||||
fs.mkdirSync(settingsDir, { recursive: true });
|
||||
}
|
||||
|
||||
let settings: Record<string, unknown> = {};
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const hooksConfig = generateClaudeHooksConfig();
|
||||
settings = { ...settings, ...hooksConfig };
|
||||
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
console.log(`✅ Hooks installed to ${settingsPath}`);
|
||||
console.log('\nInstalled hooks:');
|
||||
console.log(' - PreToolUse: Edit, Write, MultiEdit, Bash');
|
||||
console.log(' - PostToolUse: Edit, Write, MultiEdit, Bash');
|
||||
console.log(' - SessionStart, Stop, PreCompact');
|
||||
});
|
||||
|
||||
hooks.command('stats')
|
||||
.description('Show intelligence statistics')
|
||||
.action(() => {
|
||||
|
|
@ -331,6 +590,10 @@ hooks.command('stats')
|
|||
console.log(` \x1b[36m${rate}\x1b[0m average success rate`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Session Hooks
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('session-start')
|
||||
.description('Session start hook')
|
||||
.action(() => {
|
||||
|
|
@ -341,6 +604,38 @@ hooks.command('session-start')
|
|||
console.log('⚡ Intelligence guides: agent routing, error fixes, file sequences');
|
||||
});
|
||||
|
||||
hooks.command('session-end')
|
||||
.description('Session end hook')
|
||||
.option('--export-metrics', 'Export session metrics')
|
||||
.action((opts: { exportMetrics?: boolean }) => {
|
||||
const intel = new Intelligence();
|
||||
const sessionInfo = intel.sessionEnd();
|
||||
intel.save();
|
||||
|
||||
console.log('📊 Session ended. Learning data saved.');
|
||||
if (opts.exportMetrics) {
|
||||
console.log(JSON.stringify({
|
||||
duration_seconds: sessionInfo.duration,
|
||||
actions_recorded: sessionInfo.actions,
|
||||
saved: true
|
||||
}, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
hooks.command('pre-compact')
|
||||
.description('Pre-compact hook - save state before context compaction')
|
||||
.action(() => {
|
||||
const intel = new Intelligence();
|
||||
const stats = intel.stats();
|
||||
intel.save();
|
||||
|
||||
console.log(`🗜️ Pre-compact: ${stats.total_trajectories} trajectories, ${stats.total_memories} memories saved`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Edit Hooks
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('pre-edit')
|
||||
.description('Pre-edit intelligence hook')
|
||||
.argument('<file>', 'File path')
|
||||
|
|
@ -355,6 +650,13 @@ hooks.command('pre-edit')
|
|||
console.log(` 📁 \x1b[36m${crate ?? 'project'}\x1b[0m/${fileName}`);
|
||||
console.log(` 🤖 Recommended: \x1b[32m\x1b[1m${agent}\x1b[0m (${(confidence * 100).toFixed(0)}% confidence)`);
|
||||
if (reason) console.log(` → \x1b[2m${reason}\x1b[0m`);
|
||||
|
||||
// Show suggested next files
|
||||
const nextFiles = intel.suggestNext(file, 3);
|
||||
if (nextFiles.length > 0) {
|
||||
console.log(' 📎 Likely next files:');
|
||||
nextFiles.forEach(n => console.log(` - ${n.file} (${n.score} edits)`));
|
||||
}
|
||||
});
|
||||
|
||||
hooks.command('post-edit')
|
||||
|
|
@ -369,6 +671,12 @@ hooks.command('post-edit')
|
|||
const crate = crateMatch?.[1] ?? 'project';
|
||||
const state = `edit_${ext}_in_${crate}`;
|
||||
|
||||
// Record file sequence
|
||||
const lastFile = intel.getLastEditedFile();
|
||||
if (lastFile && lastFile !== file) {
|
||||
intel.recordFileSequence(lastFile, file);
|
||||
}
|
||||
|
||||
intel.learn(state, success ? 'successful-edit' : 'failed-edit', success ? 'completed' : 'failed', success ? 1.0 : -0.5);
|
||||
intel.remember('edit', `${success ? 'successful' : 'failed'} edit of ${ext} in ${crate}`);
|
||||
intel.save();
|
||||
|
|
@ -379,6 +687,112 @@ hooks.command('post-edit')
|
|||
if (test.suggest) console.log(` 🧪 Consider: \x1b[36m${test.command}\x1b[0m`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Command Hooks
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('pre-command')
|
||||
.description('Pre-command intelligence hook')
|
||||
.argument('<command...>', 'Command to analyze')
|
||||
.action((command: string[]) => {
|
||||
const intel = new Intelligence();
|
||||
const cmd = command.join(' ');
|
||||
const classification = intel.classifyCommand(cmd);
|
||||
|
||||
console.log('\x1b[1m🧠 Command Analysis:\x1b[0m');
|
||||
console.log(` 📦 Category: \x1b[36m${classification.category}\x1b[0m`);
|
||||
console.log(` 🏷️ Type: ${classification.subcategory}`);
|
||||
|
||||
if (classification.risk === 'high') {
|
||||
console.log(' ⚠️ Risk: \x1b[31mHIGH\x1b[0m - Review carefully');
|
||||
} else if (classification.risk === 'medium') {
|
||||
console.log(' ⚡ Risk: \x1b[33mMEDIUM\x1b[0m');
|
||||
} else {
|
||||
console.log(' ✅ Risk: \x1b[32mLOW\x1b[0m');
|
||||
}
|
||||
});
|
||||
|
||||
hooks.command('post-command')
|
||||
.description('Post-command learning hook')
|
||||
.argument('<command...>', 'Command that ran')
|
||||
.option('--success', 'Command succeeded')
|
||||
.option('--stderr <stderr>', 'Stderr output for error learning')
|
||||
.action((command: string[], opts: { success?: boolean; stderr?: string }) => {
|
||||
const intel = new Intelligence();
|
||||
const cmd = command.join(' ');
|
||||
const success = opts.success ?? true;
|
||||
|
||||
// Learn from command outcome
|
||||
const classification = intel.classifyCommand(cmd);
|
||||
intel.learn(
|
||||
`cmd_${classification.category}_${classification.subcategory}`,
|
||||
success ? 'success' : 'failure',
|
||||
success ? 'completed' : 'failed',
|
||||
success ? 0.8 : -0.3
|
||||
);
|
||||
|
||||
// Learn from errors if stderr provided
|
||||
if (opts.stderr) {
|
||||
const errorCodes = intel.recordError(cmd, opts.stderr);
|
||||
if (errorCodes.length > 0) {
|
||||
console.log(`📊 Learned error patterns: ${errorCodes.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
intel.remember('command', `${cmd} ${success ? 'succeeded' : 'failed'}`);
|
||||
intel.save();
|
||||
|
||||
console.log(`📊 Command ${success ? '✅' : '❌'} recorded`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Error Learning
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('record-error')
|
||||
.description('Record error pattern for learning')
|
||||
.argument('<command>', 'Command that produced error')
|
||||
.argument('<message>', 'Error message')
|
||||
.action((command: string, message: string) => {
|
||||
const intel = new Intelligence();
|
||||
const codes = intel.recordError(command, message);
|
||||
intel.save();
|
||||
|
||||
console.log(JSON.stringify({ errors: codes, recorded: codes.length }));
|
||||
});
|
||||
|
||||
hooks.command('suggest-fix')
|
||||
.description('Get suggested fix for error code')
|
||||
.argument('<code>', 'Error code (e.g., E0308)')
|
||||
.action((code: string) => {
|
||||
const intel = new Intelligence();
|
||||
const fix = intel.suggestFix(code);
|
||||
|
||||
if (fix) {
|
||||
console.log(JSON.stringify(fix, null, 2));
|
||||
} else {
|
||||
console.log(JSON.stringify({ code, fixes: [], occurrences: 0, type: 'unknown' }));
|
||||
}
|
||||
});
|
||||
|
||||
hooks.command('suggest-next')
|
||||
.description('Suggest next files to edit based on patterns')
|
||||
.argument('<file>', 'Current file')
|
||||
.option('-n, --limit <n>', 'Number of suggestions', '3')
|
||||
.action((file: string, opts: { limit: string }) => {
|
||||
const intel = new Intelligence();
|
||||
const suggestions = intel.suggestNext(file, parseInt(opts.limit));
|
||||
|
||||
console.log(JSON.stringify({
|
||||
current_file: file,
|
||||
suggestions: suggestions.map(s => ({ file: s.file, frequency: s.score }))
|
||||
}, null, 2));
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Memory Commands
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('remember')
|
||||
.description('Store content in semantic memory')
|
||||
.requiredOption('-t, --type <type>', 'Memory type')
|
||||
|
|
@ -407,6 +821,10 @@ hooks.command('recall')
|
|||
}, null, 2));
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Learning Commands
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('learn')
|
||||
.description('Record a learning trajectory')
|
||||
.argument('<state>', 'State identifier')
|
||||
|
|
@ -442,9 +860,7 @@ hooks.command('route')
|
|||
task: task.join(' '),
|
||||
recommended: result.agent,
|
||||
confidence: result.confidence,
|
||||
reasoning: result.reason,
|
||||
file: opts.file,
|
||||
crate: opts.crateName
|
||||
reasoning: result.reason
|
||||
}, null, 2));
|
||||
});
|
||||
|
||||
|
|
@ -456,6 +872,10 @@ hooks.command('should-test')
|
|||
console.log(JSON.stringify(intel.shouldTest(file), null, 2));
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Swarm Commands
|
||||
// ============================================================================
|
||||
|
||||
hooks.command('swarm-register')
|
||||
.description('Register agent in swarm')
|
||||
.argument('<id>', 'Agent ID')
|
||||
|
|
@ -469,6 +889,51 @@ hooks.command('swarm-register')
|
|||
console.log(JSON.stringify({ success: true, agent_id: id, type }));
|
||||
});
|
||||
|
||||
hooks.command('swarm-coordinate')
|
||||
.description('Record agent coordination')
|
||||
.argument('<source>', 'Source agent ID')
|
||||
.argument('<target>', 'Target agent ID')
|
||||
.option('-w, --weight <n>', 'Coordination weight', '1.0')
|
||||
.action((source: string, target: string, opts: { weight: string }) => {
|
||||
const intel = new Intelligence();
|
||||
intel.swarmCoordinate(source, target, parseFloat(opts.weight));
|
||||
intel.save();
|
||||
console.log(JSON.stringify({ success: true, source, target, weight: parseFloat(opts.weight) }));
|
||||
});
|
||||
|
||||
hooks.command('swarm-optimize')
|
||||
.description('Optimize task distribution')
|
||||
.argument('<tasks>', 'Tasks (comma-separated)')
|
||||
.action((tasks: string) => {
|
||||
const intel = new Intelligence();
|
||||
const taskList = tasks.split(',').map(s => s.trim());
|
||||
const result = intel.swarmOptimize(taskList);
|
||||
console.log(JSON.stringify({ tasks: taskList.length, assignments: result }, null, 2));
|
||||
});
|
||||
|
||||
hooks.command('swarm-recommend')
|
||||
.description('Recommend agent for task type')
|
||||
.argument('<task-type>', 'Type of task')
|
||||
.action((taskType: string) => {
|
||||
const intel = new Intelligence();
|
||||
const result = intel.swarmRecommend(taskType);
|
||||
if (result) {
|
||||
console.log(JSON.stringify({ task_type: taskType, recommended: result.agent, type: result.type, score: result.score }));
|
||||
} else {
|
||||
console.log(JSON.stringify({ task_type: taskType, recommended: null, message: 'No matching agent found' }));
|
||||
}
|
||||
});
|
||||
|
||||
hooks.command('swarm-heal')
|
||||
.description('Handle agent failure')
|
||||
.argument('<agent-id>', 'Failed agent ID')
|
||||
.action((agentId: string) => {
|
||||
const intel = new Intelligence();
|
||||
const result = intel.swarmHeal(agentId);
|
||||
intel.save();
|
||||
console.log(JSON.stringify({ failed_agent: agentId, healed: result.healed, replacement: result.replacement }));
|
||||
});
|
||||
|
||||
hooks.command('swarm-stats')
|
||||
.description('Show swarm statistics')
|
||||
.action(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue