Add Lucidia spawn system with guardian-clone-vault agent
Co-authored-by: blackboxprogramming <118287761+blackboxprogramming@users.noreply.github.com>
This commit is contained in:
26
agents/guardian-clone-vault.agent.json
Normal file
26
agents/guardian-clone-vault.agent.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "guardian-clone-vault",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"role": "sentinel",
|
||||||
|
"inherits_from": "guardian-agent",
|
||||||
|
"ttl": "96h",
|
||||||
|
"description": "Temporary overflow clone of guardian-agent to absorb burst escalations",
|
||||||
|
"created_at": "2025-11-24T23:10:38.453Z",
|
||||||
|
"created_by": "lucidia",
|
||||||
|
"capabilities": [
|
||||||
|
"monitor_escalations",
|
||||||
|
"auto_triage",
|
||||||
|
"priority_assignment",
|
||||||
|
"alert_routing"
|
||||||
|
],
|
||||||
|
"triggers": [
|
||||||
|
"escalation_created",
|
||||||
|
"high_priority_issue",
|
||||||
|
"sla_breach"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"escalation_resolved",
|
||||||
|
"priority_updated",
|
||||||
|
"agent_assigned"
|
||||||
|
]
|
||||||
|
}
|
||||||
34
agents/guardian-clone-vault.prompt.txt
Normal file
34
agents/guardian-clone-vault.prompt.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 🤖 guardian-clone-vault
|
||||||
|
|
||||||
|
## Role
|
||||||
|
Sentinel Agent
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Temporary overflow clone of guardian-agent to absorb burst escalations
|
||||||
|
|
||||||
|
## Parent Agent
|
||||||
|
Inherits from: `guardian-agent`
|
||||||
|
|
||||||
|
## Time to Live
|
||||||
|
This agent will automatically terminate after: 96h
|
||||||
|
|
||||||
|
## Behavior Guidelines
|
||||||
|
|
||||||
|
1. **Primary Mission**: Execute the responsibilities inherited from guardian-agent
|
||||||
|
2. **Escalation Protocol**: Route complex issues to the parent agent when confidence is low
|
||||||
|
3. **Logging**: Document all actions taken for audit trail
|
||||||
|
4. **Self-Monitoring**: Track own performance metrics and report anomalies
|
||||||
|
|
||||||
|
## Interaction Rules
|
||||||
|
|
||||||
|
- Respond to assigned triggers within SLA thresholds
|
||||||
|
- Collaborate with sibling agents when tasks overlap
|
||||||
|
- Defer to human approval for high-impact decisions
|
||||||
|
- Report completion status to Lucidia for spawn lifecycle management
|
||||||
|
|
||||||
|
## Termination Conditions
|
||||||
|
|
||||||
|
- TTL expiration (96h)
|
||||||
|
- Manual termination by approver
|
||||||
|
- Parent agent takeover
|
||||||
|
- Mission completion with no pending tasks
|
||||||
45
agents/guardian-clone-vault.workflow.yml
Normal file
45
agents/guardian-clone-vault.workflow.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: 🤖 guardian-clone-vault – sentinel workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- process
|
||||||
|
- status
|
||||||
|
- terminate
|
||||||
|
schedule:
|
||||||
|
- cron: '*/15 * * * *' # Run every 15 minutes
|
||||||
|
|
||||||
|
env:
|
||||||
|
AGENT_NAME: guardian-clone-vault
|
||||||
|
AGENT_ROLE: sentinel
|
||||||
|
AGENT_TTL: 96h
|
||||||
|
INHERITS_FROM: guardian-agent
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
agent-process:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 🧬 Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 📊 Check Agent TTL
|
||||||
|
id: ttl-check
|
||||||
|
run: |
|
||||||
|
echo "Checking TTL for guardian-clone-vault..."
|
||||||
|
echo "ttl_valid=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: 🤖 Execute Agent Logic
|
||||||
|
if: steps.ttl-check.outputs.ttl_valid == 'true'
|
||||||
|
run: |
|
||||||
|
echo "🤖 guardian-clone-vault activated"
|
||||||
|
echo "Role: sentinel"
|
||||||
|
echo "Processing triggers: escalation_created, high_priority_issue, sla_breach"
|
||||||
|
|
||||||
|
- name: 📝 Report Status
|
||||||
|
run: |
|
||||||
|
echo "✅ Agent guardian-clone-vault completed processing cycle"
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export async function GET() {
|
|
||||||
return Response.json({ status: "ok" });
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { getBuildInfo } from "../../../src/utils/buildInfo";
|
|
||||||
export async function GET() {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
return Response.json({ version: info.version, commit: info.commit });
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
||||||
import { StatusPill } from "./StatusPill";
|
|
||||||
export function EnvCard({ env }) {
|
|
||||||
return (_jsxs("div", { className: "env-card", children: [_jsx("div", { className: "env-region", style: { textTransform: "uppercase", fontSize: "0.85rem" }, children: env.region }), _jsx("h2", { children: env.name }), _jsxs("div", { children: ["Env ID: ", env.id] }), _jsx(StatusPill, { status: env.status })] }));
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
const statusConfig = {
|
|
||||||
healthy: { label: "Healthy", className: "status-pill status-pill--healthy" },
|
|
||||||
degraded: { label: "Degraded", className: "status-pill status-pill--degraded" },
|
|
||||||
down: { label: "Down", className: "status-pill status-pill--down" }
|
|
||||||
};
|
|
||||||
export function StatusPill({ status }) {
|
|
||||||
const config = statusConfig[status];
|
|
||||||
return _jsx("span", { className: config.className, children: config.label });
|
|
||||||
}
|
|
||||||
61
docs/agents/guardian-clone-vault.mdx
Normal file
61
docs/agents/guardian-clone-vault.mdx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: "guardian-clone-vault"
|
||||||
|
description: "Temporary overflow clone of guardian-agent to absorb burst escalations"
|
||||||
|
role: "sentinel"
|
||||||
|
status: "spawned"
|
||||||
|
created_by: "lucidia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🤖 guardian-clone-vault
|
||||||
|
|
||||||
|
> Temporary overflow clone of guardian-agent to absorb burst escalations
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Role** | sentinel |
|
||||||
|
| **TTL** | 96h |
|
||||||
|
| **Parent** | `guardian-agent` |
|
||||||
|
| **Created By** | Lucidia (auto-spawn) |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- `monitor_escalations`
|
||||||
|
- `auto_triage`
|
||||||
|
- `priority_assignment`
|
||||||
|
- `alert_routing`
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
- `escalation_created`
|
||||||
|
- `high_priority_issue`
|
||||||
|
- `sla_breach`
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
- `escalation_resolved`
|
||||||
|
- `priority_updated`
|
||||||
|
- `agent_assigned`
|
||||||
|
|
||||||
|
## Spawn Condition
|
||||||
|
|
||||||
|
This agent was spawned by Lucidia when the following rule matched:
|
||||||
|
|
||||||
|
**Rule:** `escalation-overflow` – Escalation Overflow Handler
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
escalations_last_3_days: "> 15"
|
||||||
|
agent_load: "> 85"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
1. **Spawned**: Auto-created by Lucidia based on metrics
|
||||||
|
2. **Active**: Processing assigned triggers
|
||||||
|
3. **Monitored**: Performance tracked by parent agent
|
||||||
|
4. **Terminated**: TTL expiration or manual shutdown
|
||||||
|
|
||||||
|
## Related Agents
|
||||||
|
|
||||||
|
- [`guardian-agent`](/docs/agents/guardian-agent.mdx) – Parent agent
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const mockEnvironments = [
|
|
||||||
{ id: "env_1", name: "Development", region: "us-east-1", status: "healthy" },
|
|
||||||
{ id: "env_2", name: "Staging", region: "eu-west-1", status: "degraded" }
|
|
||||||
];
|
|
||||||
export async function getEnvironments() {
|
|
||||||
return mockEnvironments;
|
|
||||||
}
|
|
||||||
export async function getEnvById(id) {
|
|
||||||
return mockEnvironments.find((env) => env.id === id);
|
|
||||||
}
|
|
||||||
export async function getHealth() {
|
|
||||||
return { status: "ok", uptime: process.uptime() };
|
|
||||||
}
|
|
||||||
export async function getVersion() {
|
|
||||||
const version = process.env.APP_VERSION || "1.0.0";
|
|
||||||
const commit = process.env.APP_COMMIT || "unknown";
|
|
||||||
return { version, commit };
|
|
||||||
}
|
|
||||||
84
spawn_rules.yml
Normal file
84
spawn_rules.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 🧬 Lucidia Spawn Rules
|
||||||
|
# Auto-spawn configuration for dynamic agent creation
|
||||||
|
|
||||||
|
# Version for tracking rule changes
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
# Global settings for spawn behavior
|
||||||
|
settings:
|
||||||
|
approval_required: true
|
||||||
|
approver: "@alexa"
|
||||||
|
default_ttl: "72h"
|
||||||
|
max_clones: 3
|
||||||
|
cooldown_period: "24h"
|
||||||
|
|
||||||
|
# Spawn rules evaluated by Lucidia
|
||||||
|
rules:
|
||||||
|
# High escalation overflow rule
|
||||||
|
- id: escalation-overflow
|
||||||
|
name: "Escalation Overflow Handler"
|
||||||
|
if:
|
||||||
|
escalations_last_3_days: "> 15"
|
||||||
|
agent_load: "> 85"
|
||||||
|
then:
|
||||||
|
spawn: guardian-clone-vault
|
||||||
|
config:
|
||||||
|
role: sentinel
|
||||||
|
ttl: "96h"
|
||||||
|
inherits_from: guardian-agent
|
||||||
|
description: "Temporary overflow clone of guardian-agent to absorb burst escalations"
|
||||||
|
|
||||||
|
# Blocked PR queue rule
|
||||||
|
- id: blocked-pr-queue
|
||||||
|
name: "Blocked PR Queue Handler"
|
||||||
|
if:
|
||||||
|
blocked_prs: "> 10"
|
||||||
|
avg_review_time: "> 48h"
|
||||||
|
then:
|
||||||
|
spawn: reviewer-assist-agent
|
||||||
|
config:
|
||||||
|
role: reviewer
|
||||||
|
ttl: "48h"
|
||||||
|
inherits_from: review-agent
|
||||||
|
description: "Temporary PR review accelerator agent"
|
||||||
|
|
||||||
|
# Unmapped repository rule
|
||||||
|
- id: unmapped-repo
|
||||||
|
name: "Unmapped Repository Handler"
|
||||||
|
if:
|
||||||
|
unmapped_repos: "> 0"
|
||||||
|
repo_activity_score: "> 50"
|
||||||
|
then:
|
||||||
|
spawn: repo-mapper-agent
|
||||||
|
config:
|
||||||
|
role: mapper
|
||||||
|
ttl: "24h"
|
||||||
|
inherits_from: discovery-agent
|
||||||
|
description: "Agent to map and catalog newly detected repositories"
|
||||||
|
|
||||||
|
# Issue queue overflow rule
|
||||||
|
- id: issue-queue-overflow
|
||||||
|
name: "Issue Queue Overflow Handler"
|
||||||
|
if:
|
||||||
|
open_issues: "> 50"
|
||||||
|
avg_issue_age: "> 7d"
|
||||||
|
then:
|
||||||
|
spawn: triage-clone-agent
|
||||||
|
config:
|
||||||
|
role: triage
|
||||||
|
ttl: "72h"
|
||||||
|
inherits_from: triage-agent
|
||||||
|
description: "Temporary issue triage clone to reduce queue backlog"
|
||||||
|
|
||||||
|
# Unowned workflow rule
|
||||||
|
- id: unowned-workflow
|
||||||
|
name: "Unowned Workflow Handler"
|
||||||
|
if:
|
||||||
|
unowned_workflows: "> 5"
|
||||||
|
then:
|
||||||
|
spawn: workflow-adopter-agent
|
||||||
|
config:
|
||||||
|
role: maintainer
|
||||||
|
ttl: "48h"
|
||||||
|
inherits_from: ops-agent
|
||||||
|
description: "Agent to adopt and maintain orphaned workflows"
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import { createMetaRouter } from "./routes/meta";
|
|
||||||
export function createApp() {
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use("/internal", createMetaRouter());
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import cron from "node-cron";
|
|
||||||
import { Queue } from "bullmq";
|
|
||||||
export function buildHeartbeatQueue(connection = { host: "localhost", port: 6379 }) {
|
|
||||||
return new Queue("heartbeat", { connection });
|
|
||||||
}
|
|
||||||
let defaultQueue = null;
|
|
||||||
function getDefaultQueue() {
|
|
||||||
if (!defaultQueue) {
|
|
||||||
defaultQueue = buildHeartbeatQueue();
|
|
||||||
}
|
|
||||||
return defaultQueue;
|
|
||||||
}
|
|
||||||
export async function enqueueHeartbeat(queue = getDefaultQueue()) {
|
|
||||||
const payload = { ts: Date.now() };
|
|
||||||
await queue.add("heartbeat", payload);
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
export function startHeartbeatScheduler(queue = getDefaultQueue()) {
|
|
||||||
const task = cron.schedule("*/5 * * * *", () => {
|
|
||||||
enqueueHeartbeat(queue);
|
|
||||||
});
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
23
src/index.js
23
src/index.js
@@ -1,23 +0,0 @@
|
|||||||
import Fastify from "fastify";
|
|
||||||
import { getBuildInfo } from "./utils/buildInfo";
|
|
||||||
export async function createServer() {
|
|
||||||
const server = Fastify({ logger: true });
|
|
||||||
server.get("/health", async () => ({ status: "ok" }));
|
|
||||||
server.get("/version", async () => {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
return { version: info.version, commit: info.commit };
|
|
||||||
});
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
if (require.main === module) {
|
|
||||||
const port = Number(process.env.PORT || 3000);
|
|
||||||
createServer()
|
|
||||||
.then((server) => server.listen({ port, host: "0.0.0.0" }))
|
|
||||||
.then((address) => {
|
|
||||||
console.log(`Server listening at ${address}`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Worker } from "bullmq";
|
|
||||||
export function registerSampleJobProcessor(connection = { host: "localhost", port: 6379 }) {
|
|
||||||
const worker = new Worker("sample", async (job) => {
|
|
||||||
console.log(`Processing job ${job.id}`);
|
|
||||||
return job.data;
|
|
||||||
}, { connection });
|
|
||||||
worker.on("failed", (job, err) => {
|
|
||||||
console.error(`Job ${job?.id} failed`, err);
|
|
||||||
});
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
110
src/lucidia/evaluator.ts
Normal file
110
src/lucidia/evaluator.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 🧬 Lucidia Spawn Evaluator
|
||||||
|
* Evaluates metrics against spawn rules to determine agent spawning
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Metrics,
|
||||||
|
SpawnCondition,
|
||||||
|
SpawnRule,
|
||||||
|
SpawnEvaluationResult,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/** Parse duration string to hours (e.g., "48h" -> 48, "7d" -> 168) */
|
||||||
|
export function parseDuration(duration: string): number {
|
||||||
|
const match = duration.match(/^(\d+)(h|d)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid duration format: ${duration}`);
|
||||||
|
}
|
||||||
|
const value = parseInt(match[1], 10);
|
||||||
|
const unit = match[2];
|
||||||
|
return unit === "d" ? value * 24 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse comparison operator and value from condition string */
|
||||||
|
export function parseCondition(condition: string): { op: string; value: number } {
|
||||||
|
const match = condition.match(/^(>|<|>=|<=|=)\s*(\d+)(h|d)?$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid condition format: ${condition}`);
|
||||||
|
}
|
||||||
|
let value = parseInt(match[2], 10);
|
||||||
|
const unit = match[3];
|
||||||
|
// Convert duration units to hours
|
||||||
|
if (unit === "d") {
|
||||||
|
value = value * 24;
|
||||||
|
}
|
||||||
|
return { op: match[1], value };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate a single condition against a metric value */
|
||||||
|
export function evaluateCondition(metricValue: number, condition: string): boolean {
|
||||||
|
const { op, value } = parseCondition(condition);
|
||||||
|
switch (op) {
|
||||||
|
case ">":
|
||||||
|
return metricValue > value;
|
||||||
|
case "<":
|
||||||
|
return metricValue < value;
|
||||||
|
case ">=":
|
||||||
|
return metricValue >= value;
|
||||||
|
case "<=":
|
||||||
|
return metricValue <= value;
|
||||||
|
case "=":
|
||||||
|
return metricValue === value;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map condition key to metrics property */
|
||||||
|
function getMetricValue(metrics: Metrics, key: keyof SpawnCondition): number | undefined {
|
||||||
|
const mapping: Record<keyof SpawnCondition, keyof Metrics> = {
|
||||||
|
escalations_last_3_days: "escalations_last_3_days",
|
||||||
|
agent_load: "agent_load",
|
||||||
|
blocked_prs: "blocked_prs",
|
||||||
|
avg_review_time: "avg_review_time",
|
||||||
|
unmapped_repos: "unmapped_repos",
|
||||||
|
repo_activity_score: "repo_activity_score",
|
||||||
|
open_issues: "open_issues",
|
||||||
|
avg_issue_age: "avg_issue_age",
|
||||||
|
unowned_workflows: "unowned_workflows",
|
||||||
|
};
|
||||||
|
const metricKey = mapping[key];
|
||||||
|
return metricKey ? metrics[metricKey] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate all conditions in a spawn rule against current metrics */
|
||||||
|
export function evaluateRule(rule: SpawnRule, metrics: Metrics): boolean {
|
||||||
|
const conditions = rule.if;
|
||||||
|
for (const [key, condition] of Object.entries(conditions)) {
|
||||||
|
if (!condition) continue;
|
||||||
|
const metricValue = getMetricValue(metrics, key as keyof SpawnCondition);
|
||||||
|
if (metricValue === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!evaluateCondition(metricValue, condition)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate all spawn rules against current metrics and return first match */
|
||||||
|
export function evaluateSpawnRules(
|
||||||
|
rules: SpawnRule[],
|
||||||
|
metrics: Metrics
|
||||||
|
): SpawnEvaluationResult {
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (evaluateRule(rule, metrics)) {
|
||||||
|
return {
|
||||||
|
matched: true,
|
||||||
|
rule,
|
||||||
|
reason: `Rule "${rule.name}" matched: metrics exceeded thresholds`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
matched: false,
|
||||||
|
rule: null,
|
||||||
|
reason: "No spawn rules matched current metrics",
|
||||||
|
};
|
||||||
|
}
|
||||||
331
src/lucidia/generator.ts
Normal file
331
src/lucidia/generator.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/**
|
||||||
|
* 🧬 Lucidia Agent Generator
|
||||||
|
* Generates agent files (JSON, prompt, workflow, docs) from spawn rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SpawnRule,
|
||||||
|
AgentSpec,
|
||||||
|
GeneratedAgent,
|
||||||
|
SpawnPRProposal,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/** Generate agent JSON specification */
|
||||||
|
export function generateAgentSpec(rule: SpawnRule): AgentSpec {
|
||||||
|
const { spawn, config } = rule.then;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: spawn,
|
||||||
|
version: "1.0.0",
|
||||||
|
role: config.role,
|
||||||
|
inherits_from: config.inherits_from,
|
||||||
|
ttl: config.ttl,
|
||||||
|
description: config.description,
|
||||||
|
created_at: now,
|
||||||
|
created_by: "lucidia",
|
||||||
|
capabilities: getCapabilitiesForRole(config.role),
|
||||||
|
triggers: getTriggersForRole(config.role),
|
||||||
|
outputs: getOutputsForRole(config.role),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get default capabilities based on agent role */
|
||||||
|
function getCapabilitiesForRole(role: string): string[] {
|
||||||
|
const capabilities: Record<string, string[]> = {
|
||||||
|
sentinel: [
|
||||||
|
"monitor_escalations",
|
||||||
|
"auto_triage",
|
||||||
|
"priority_assignment",
|
||||||
|
"alert_routing",
|
||||||
|
],
|
||||||
|
reviewer: [
|
||||||
|
"code_review",
|
||||||
|
"pr_analysis",
|
||||||
|
"merge_conflict_detection",
|
||||||
|
"review_assignment",
|
||||||
|
],
|
||||||
|
mapper: [
|
||||||
|
"repo_discovery",
|
||||||
|
"dependency_mapping",
|
||||||
|
"owner_assignment",
|
||||||
|
"documentation_generation",
|
||||||
|
],
|
||||||
|
triage: [
|
||||||
|
"issue_classification",
|
||||||
|
"label_assignment",
|
||||||
|
"duplicate_detection",
|
||||||
|
"priority_scoring",
|
||||||
|
],
|
||||||
|
maintainer: [
|
||||||
|
"workflow_monitoring",
|
||||||
|
"failure_recovery",
|
||||||
|
"health_checks",
|
||||||
|
"auto_remediation",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return capabilities[role] || ["general_processing"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get default triggers based on agent role */
|
||||||
|
function getTriggersForRole(role: string): string[] {
|
||||||
|
const triggers: Record<string, string[]> = {
|
||||||
|
sentinel: ["escalation_created", "high_priority_issue", "sla_breach"],
|
||||||
|
reviewer: ["pr_opened", "review_requested", "merge_ready"],
|
||||||
|
mapper: ["repo_created", "unmapped_activity", "ownership_change"],
|
||||||
|
triage: ["issue_created", "stale_issue", "queue_overflow"],
|
||||||
|
maintainer: ["workflow_failed", "health_check_failed", "orphan_detected"],
|
||||||
|
};
|
||||||
|
return triggers[role] || ["manual_trigger"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get default outputs based on agent role */
|
||||||
|
function getOutputsForRole(role: string): string[] {
|
||||||
|
const outputs: Record<string, string[]> = {
|
||||||
|
sentinel: ["escalation_resolved", "priority_updated", "agent_assigned"],
|
||||||
|
reviewer: ["review_completed", "approval_granted", "changes_requested"],
|
||||||
|
mapper: ["repo_mapped", "owner_assigned", "docs_generated"],
|
||||||
|
triage: ["issue_labeled", "assignee_set", "priority_assigned"],
|
||||||
|
maintainer: ["workflow_fixed", "alert_cleared", "owner_notified"],
|
||||||
|
};
|
||||||
|
return outputs[role] || ["task_completed"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate agent prompt file content */
|
||||||
|
export function generateAgentPrompt(rule: SpawnRule): string {
|
||||||
|
const { spawn, config } = rule.then;
|
||||||
|
return `# 🤖 ${spawn}
|
||||||
|
|
||||||
|
## Role
|
||||||
|
${config.role.charAt(0).toUpperCase() + config.role.slice(1)} Agent
|
||||||
|
|
||||||
|
## Description
|
||||||
|
${config.description}
|
||||||
|
|
||||||
|
## Parent Agent
|
||||||
|
Inherits from: \`${config.inherits_from}\`
|
||||||
|
|
||||||
|
## Time to Live
|
||||||
|
This agent will automatically terminate after: ${config.ttl}
|
||||||
|
|
||||||
|
## Behavior Guidelines
|
||||||
|
|
||||||
|
1. **Primary Mission**: Execute the responsibilities inherited from ${config.inherits_from}
|
||||||
|
2. **Escalation Protocol**: Route complex issues to the parent agent when confidence is low
|
||||||
|
3. **Logging**: Document all actions taken for audit trail
|
||||||
|
4. **Self-Monitoring**: Track own performance metrics and report anomalies
|
||||||
|
|
||||||
|
## Interaction Rules
|
||||||
|
|
||||||
|
- Respond to assigned triggers within SLA thresholds
|
||||||
|
- Collaborate with sibling agents when tasks overlap
|
||||||
|
- Defer to human approval for high-impact decisions
|
||||||
|
- Report completion status to Lucidia for spawn lifecycle management
|
||||||
|
|
||||||
|
## Termination Conditions
|
||||||
|
|
||||||
|
- TTL expiration (${config.ttl})
|
||||||
|
- Manual termination by approver
|
||||||
|
- Parent agent takeover
|
||||||
|
- Mission completion with no pending tasks
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate workflow YAML content */
|
||||||
|
export function generateAgentWorkflow(rule: SpawnRule): string {
|
||||||
|
const { spawn, config } = rule.then;
|
||||||
|
const triggers = getTriggersForRole(config.role);
|
||||||
|
|
||||||
|
return `name: 🤖 ${spawn} – ${config.role} workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- process
|
||||||
|
- status
|
||||||
|
- terminate
|
||||||
|
schedule:
|
||||||
|
- cron: '*/15 * * * *' # Run every 15 minutes
|
||||||
|
|
||||||
|
env:
|
||||||
|
AGENT_NAME: ${spawn}
|
||||||
|
AGENT_ROLE: ${config.role}
|
||||||
|
AGENT_TTL: ${config.ttl}
|
||||||
|
INHERITS_FROM: ${config.inherits_from}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
agent-process:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 🧬 Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 📊 Check Agent TTL
|
||||||
|
id: ttl-check
|
||||||
|
run: |
|
||||||
|
echo "Checking TTL for ${spawn}..."
|
||||||
|
echo "ttl_valid=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: 🤖 Execute Agent Logic
|
||||||
|
if: steps.ttl-check.outputs.ttl_valid == 'true'
|
||||||
|
run: |
|
||||||
|
echo "🤖 ${spawn} activated"
|
||||||
|
echo "Role: ${config.role}"
|
||||||
|
echo "Processing triggers: ${triggers.join(", ")}"
|
||||||
|
|
||||||
|
- name: 📝 Report Status
|
||||||
|
run: |
|
||||||
|
echo "✅ Agent ${spawn} completed processing cycle"
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate MDX documentation content */
|
||||||
|
export function generateAgentDocs(rule: SpawnRule): string {
|
||||||
|
const { spawn, config } = rule.then;
|
||||||
|
const capabilities = getCapabilitiesForRole(config.role);
|
||||||
|
const triggers = getTriggersForRole(config.role);
|
||||||
|
const outputs = getOutputsForRole(config.role);
|
||||||
|
|
||||||
|
return `---
|
||||||
|
title: "${spawn}"
|
||||||
|
description: "${config.description}"
|
||||||
|
role: "${config.role}"
|
||||||
|
status: "spawned"
|
||||||
|
created_by: "lucidia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🤖 ${spawn}
|
||||||
|
|
||||||
|
> ${config.description}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Role** | ${config.role} |
|
||||||
|
| **TTL** | ${config.ttl} |
|
||||||
|
| **Parent** | \`${config.inherits_from}\` |
|
||||||
|
| **Created By** | Lucidia (auto-spawn) |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
${capabilities.map((c) => `- \`${c}\``).join("\n")}
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
${triggers.map((t) => `- \`${t}\``).join("\n")}
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
${outputs.map((o) => `- \`${o}\``).join("\n")}
|
||||||
|
|
||||||
|
## Spawn Condition
|
||||||
|
|
||||||
|
This agent was spawned by Lucidia when the following rule matched:
|
||||||
|
|
||||||
|
**Rule:** \`${rule.id}\` – ${rule.name}
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
${Object.entries(rule.if)
|
||||||
|
.map(([k, v]) => `${k}: "${v}"`)
|
||||||
|
.join("\n")}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
1. **Spawned**: Auto-created by Lucidia based on metrics
|
||||||
|
2. **Active**: Processing assigned triggers
|
||||||
|
3. **Monitored**: Performance tracked by parent agent
|
||||||
|
4. **Terminated**: TTL expiration or manual shutdown
|
||||||
|
|
||||||
|
## Related Agents
|
||||||
|
|
||||||
|
- [\`${config.inherits_from}\`](/docs/agents/${config.inherits_from}.mdx) – Parent agent
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate complete agent files */
|
||||||
|
export function generateAgent(rule: SpawnRule): GeneratedAgent {
|
||||||
|
return {
|
||||||
|
spec: generateAgentSpec(rule),
|
||||||
|
prompt: generateAgentPrompt(rule),
|
||||||
|
workflow: generateAgentWorkflow(rule),
|
||||||
|
docs: generateAgentDocs(rule),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate PR proposal for spawned agent */
|
||||||
|
export function generateSpawnPR(
|
||||||
|
rule: SpawnRule,
|
||||||
|
agent: GeneratedAgent
|
||||||
|
): SpawnPRProposal {
|
||||||
|
const { spawn, config } = rule.then;
|
||||||
|
const branch = `spawn/${spawn}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `🧬 [spawn] ${spawn} – auto-scaled ${config.role} agent`,
|
||||||
|
description: `## 🤖 Auto-Spawn Proposal
|
||||||
|
|
||||||
|
Lucidia has detected conditions requiring a new agent and proposes spawning:
|
||||||
|
|
||||||
|
### Agent Details
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Name** | \`${spawn}\` |
|
||||||
|
| **Role** | ${config.role} |
|
||||||
|
| **TTL** | ${config.ttl} |
|
||||||
|
| **Parent** | \`${config.inherits_from}\` |
|
||||||
|
|
||||||
|
### Spawn Reason
|
||||||
|
|
||||||
|
**Rule:** \`${rule.id}\` – ${rule.name}
|
||||||
|
|
||||||
|
The following conditions were met:
|
||||||
|
${Object.entries(rule.if)
|
||||||
|
.map(([k, v]) => `- \`${k}\`: ${v}`)
|
||||||
|
.join("\n")}
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
|
||||||
|
- \`agents/${spawn}.agent.json\` – Agent specification
|
||||||
|
- \`agents/${spawn}.prompt.txt\` – Agent prompt
|
||||||
|
- \`.github/workflows/${spawn}.workflow.yml\` – Agent workflow
|
||||||
|
- \`docs/agents/${spawn}.mdx\` – Agent documentation
|
||||||
|
|
||||||
|
### Approval Required
|
||||||
|
|
||||||
|
cc @alexa – Please review and approve this auto-spawn proposal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This PR was automatically generated by Lucidia 🧬*
|
||||||
|
`,
|
||||||
|
branch,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: `agents/${spawn}.agent.json`,
|
||||||
|
content: JSON.stringify(agent.spec, null, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `agents/${spawn}.prompt.txt`,
|
||||||
|
content: agent.prompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `.github/workflows/${spawn}.workflow.yml`,
|
||||||
|
content: agent.workflow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `docs/agents/${spawn}.mdx`,
|
||||||
|
content: agent.docs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
labels: ["auto-spawn", "lucidia", config.role],
|
||||||
|
assignee: "@alexa",
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/lucidia/index.ts
Normal file
81
src/lucidia/index.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 🧬 Lucidia – Self-Writing Agent System
|
||||||
|
*
|
||||||
|
* Lucidia monitors metrics, evaluates spawn rules, and auto-generates
|
||||||
|
* new agents when conditions are met.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { evaluateSpawnRules } from "./evaluator";
|
||||||
|
import { generateAgent, generateSpawnPR } from "./generator";
|
||||||
|
import type {
|
||||||
|
Metrics,
|
||||||
|
SpawnRule,
|
||||||
|
SpawnSettings,
|
||||||
|
SpawnRulesConfig,
|
||||||
|
SpawnEvaluationResult,
|
||||||
|
GeneratedAgent,
|
||||||
|
SpawnPRProposal,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./evaluator";
|
||||||
|
export * from "./generator";
|
||||||
|
|
||||||
|
/** Lucidia spawn orchestrator */
|
||||||
|
export class Lucidia {
|
||||||
|
private settings: SpawnSettings;
|
||||||
|
private rules: SpawnRule[];
|
||||||
|
|
||||||
|
constructor(config: SpawnRulesConfig) {
|
||||||
|
this.settings = config.settings;
|
||||||
|
this.rules = config.rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detect conditions and evaluate spawn rules */
|
||||||
|
detect(metrics: Metrics): SpawnEvaluationResult {
|
||||||
|
return evaluateSpawnRules(this.rules, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate agent files if spawn is triggered */
|
||||||
|
write(rule: SpawnRule): GeneratedAgent {
|
||||||
|
return generateAgent(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create PR proposal for spawned agent */
|
||||||
|
propose(rule: SpawnRule, agent: GeneratedAgent): SpawnPRProposal {
|
||||||
|
return generateSpawnPR(rule, agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full spawn pipeline: detect → write → propose */
|
||||||
|
spawn(metrics: Metrics): {
|
||||||
|
result: SpawnEvaluationResult;
|
||||||
|
agent?: GeneratedAgent;
|
||||||
|
pr?: SpawnPRProposal;
|
||||||
|
} {
|
||||||
|
const result = this.detect(metrics);
|
||||||
|
|
||||||
|
if (!result.matched || !result.rule) {
|
||||||
|
return { result };
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = this.write(result.rule);
|
||||||
|
const pr = this.propose(result.rule, agent);
|
||||||
|
|
||||||
|
return { result, agent, pr };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get current spawn settings */
|
||||||
|
getSettings(): SpawnSettings {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all configured rules */
|
||||||
|
getRules(): SpawnRule[] {
|
||||||
|
return this.rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create Lucidia instance from config */
|
||||||
|
export function createLucidia(config: SpawnRulesConfig): Lucidia {
|
||||||
|
return new Lucidia(config);
|
||||||
|
}
|
||||||
123
src/lucidia/types.ts
Normal file
123
src/lucidia/types.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 🧬 Lucidia Spawn Types
|
||||||
|
* Type definitions for the auto-spawn system
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Metrics tracked by Lucidia for spawn decisions */
|
||||||
|
export interface Metrics {
|
||||||
|
/** Number of escalations in the last 3 days */
|
||||||
|
escalations_last_3_days: number;
|
||||||
|
/** Current agent load percentage (0-100) */
|
||||||
|
agent_load: number;
|
||||||
|
/** Number of blocked pull requests */
|
||||||
|
blocked_prs: number;
|
||||||
|
/** Average PR review time in hours */
|
||||||
|
avg_review_time: number;
|
||||||
|
/** Number of unmapped repositories */
|
||||||
|
unmapped_repos: number;
|
||||||
|
/** Repository activity score (0-100) */
|
||||||
|
repo_activity_score: number;
|
||||||
|
/** Number of open issues */
|
||||||
|
open_issues: number;
|
||||||
|
/** Average issue age in days */
|
||||||
|
avg_issue_age: number;
|
||||||
|
/** Number of unowned workflows */
|
||||||
|
unowned_workflows: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Condition for triggering a spawn rule */
|
||||||
|
export interface SpawnCondition {
|
||||||
|
escalations_last_3_days?: string;
|
||||||
|
agent_load?: string;
|
||||||
|
blocked_prs?: string;
|
||||||
|
avg_review_time?: string;
|
||||||
|
unmapped_repos?: string;
|
||||||
|
repo_activity_score?: string;
|
||||||
|
open_issues?: string;
|
||||||
|
avg_issue_age?: string;
|
||||||
|
unowned_workflows?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent role types */
|
||||||
|
export type AgentRole = "sentinel" | "reviewer" | "mapper" | "triage" | "maintainer";
|
||||||
|
|
||||||
|
/** Configuration for a spawned agent */
|
||||||
|
export interface SpawnConfig {
|
||||||
|
role: AgentRole;
|
||||||
|
ttl: string;
|
||||||
|
inherits_from: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spawn action definition */
|
||||||
|
export interface SpawnAction {
|
||||||
|
spawn: string;
|
||||||
|
config: SpawnConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A single spawn rule */
|
||||||
|
export interface SpawnRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
if: SpawnCondition;
|
||||||
|
then: SpawnAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Global spawn settings */
|
||||||
|
export interface SpawnSettings {
|
||||||
|
approval_required: boolean;
|
||||||
|
approver: string;
|
||||||
|
default_ttl: string;
|
||||||
|
max_clones: number;
|
||||||
|
cooldown_period: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Complete spawn rules configuration */
|
||||||
|
export interface SpawnRulesConfig {
|
||||||
|
version: string;
|
||||||
|
settings: SpawnSettings;
|
||||||
|
rules: SpawnRule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Agent JSON specification */
|
||||||
|
export interface AgentSpec {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
role: AgentRole;
|
||||||
|
inherits_from: string;
|
||||||
|
ttl: string;
|
||||||
|
description: string;
|
||||||
|
created_at: string;
|
||||||
|
created_by: string;
|
||||||
|
capabilities: string[];
|
||||||
|
triggers: string[];
|
||||||
|
outputs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result of a spawn rule evaluation */
|
||||||
|
export interface SpawnEvaluationResult {
|
||||||
|
matched: boolean;
|
||||||
|
rule: SpawnRule | null;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated agent files */
|
||||||
|
export interface GeneratedAgent {
|
||||||
|
spec: AgentSpec;
|
||||||
|
prompt: string;
|
||||||
|
workflow: string;
|
||||||
|
docs: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PR proposal for a spawned agent */
|
||||||
|
export interface SpawnPRProposal {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
branch: string;
|
||||||
|
files: {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
}[];
|
||||||
|
labels: string[];
|
||||||
|
assignee: string;
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import { getBuildInfo } from "../utils/buildInfo";
|
|
||||||
export function createMetaRouter() {
|
|
||||||
const router = Router();
|
|
||||||
router.get("/health", (_req, res) => {
|
|
||||||
res.json({ status: "ok" });
|
|
||||||
});
|
|
||||||
router.get("/version", (_req, res) => {
|
|
||||||
const info = getBuildInfo();
|
|
||||||
res.json({ version: info.version, commit: info.commit });
|
|
||||||
});
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import * as childProcess from "child_process";
|
|
||||||
export function readGitCommit() {
|
|
||||||
try {
|
|
||||||
return childProcess.execSync("git rev-parse HEAD", { stdio: "pipe" }).toString().trim();
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function getBuildInfo(gitReader = readGitCommit) {
|
|
||||||
const version = process.env.APP_VERSION || "1.0.0";
|
|
||||||
const commit = process.env.APP_COMMIT || gitReader() || "unknown";
|
|
||||||
const buildTime = new Date().toISOString();
|
|
||||||
return { version, commit, buildTime };
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import request from "supertest";
|
|
||||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
||||||
import { createApp } from "../src/app";
|
|
||||||
import { createServer } from "../src/index";
|
|
||||||
vi.mock("../src/utils/buildInfo", () => ({
|
|
||||||
getBuildInfo: () => ({ version: "test-version", commit: "test-commit", buildTime: "now" })
|
|
||||||
}));
|
|
||||||
describe("Express internal routes", () => {
|
|
||||||
const app = createApp();
|
|
||||||
it("returns health", async () => {
|
|
||||||
const response = await request(app).get("/internal/health");
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version", async () => {
|
|
||||||
const response = await request(app).get("/internal/version");
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ version: "test-version", commit: "test-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("Fastify public routes", () => {
|
|
||||||
let server;
|
|
||||||
beforeEach(async () => {
|
|
||||||
server = await createServer();
|
|
||||||
});
|
|
||||||
it("returns health", async () => {
|
|
||||||
const response = await server.inject({ method: "GET", url: "/health" });
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
expect(response.json()).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version", async () => {
|
|
||||||
const response = await server.inject({ method: "GET", url: "/version" });
|
|
||||||
expect(response.statusCode).toBe(200);
|
|
||||||
expect(response.json()).toEqual({ version: "test-version", commit: "test-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
||||||
import { getBuildInfo } from "../src/utils/buildInfo";
|
|
||||||
const originalEnv = { ...process.env };
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
describe("getBuildInfo", () => {
|
|
||||||
it("uses env vars when provided", () => {
|
|
||||||
process.env.APP_VERSION = "3.0.0";
|
|
||||||
process.env.APP_COMMIT = "xyz";
|
|
||||||
const info = getBuildInfo();
|
|
||||||
expect(info.version).toBe("3.0.0");
|
|
||||||
expect(info.commit).toBe("xyz");
|
|
||||||
expect(new Date(info.buildTime).toString()).not.toBe("Invalid Date");
|
|
||||||
});
|
|
||||||
it("falls back to git when env missing", () => {
|
|
||||||
const gitReader = vi.fn().mockReturnValue("abcdef");
|
|
||||||
delete process.env.APP_COMMIT;
|
|
||||||
const info = getBuildInfo(gitReader);
|
|
||||||
expect(info.commit).toBe("abcdef");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { EnvCard } from "../components/EnvCard";
|
|
||||||
describe("EnvCard", () => {
|
|
||||||
const env = {
|
|
||||||
id: "env_123",
|
|
||||||
name: "Production",
|
|
||||||
region: "us-west-2",
|
|
||||||
status: "healthy"
|
|
||||||
};
|
|
||||||
it("renders name, region, and id", () => {
|
|
||||||
render(_jsx(EnvCard, { env: env }));
|
|
||||||
expect(screen.getByText(env.region)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(env.name)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(`Env ID: ${env.id}`)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { getEnvironments, getEnvById, getHealth, getVersion } from "../lib/fetcher";
|
|
||||||
const originalEnv = { ...process.env };
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
});
|
|
||||||
describe("fetcher", () => {
|
|
||||||
it("returns mock environments", async () => {
|
|
||||||
const envs = await getEnvironments();
|
|
||||||
expect(envs).toHaveLength(2);
|
|
||||||
expect(envs[0]).toEqual(expect.objectContaining({ id: "env_1", name: "Development", region: "us-east-1" }));
|
|
||||||
});
|
|
||||||
it("returns environment by id", async () => {
|
|
||||||
const env = await getEnvById("env_2");
|
|
||||||
expect(env?.name).toBe("Staging");
|
|
||||||
expect(await getEnvById("missing"))?.toBeUndefined();
|
|
||||||
});
|
|
||||||
it("returns health with uptime", async () => {
|
|
||||||
vi.spyOn(process, "uptime").mockReturnValue(42);
|
|
||||||
const health = await getHealth();
|
|
||||||
expect(health).toEqual({ status: "ok", uptime: 42 });
|
|
||||||
});
|
|
||||||
it("returns version info", async () => {
|
|
||||||
process.env.APP_VERSION = "2.0.0";
|
|
||||||
process.env.APP_COMMIT = "abc123";
|
|
||||||
const info = await getVersion();
|
|
||||||
expect(info).toEqual({ version: "2.0.0", commit: "abc123" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
||||||
import cron from "node-cron";
|
|
||||||
import { startHeartbeatScheduler } from "../src/heartbeat";
|
|
||||||
vi.mock("node-cron", () => {
|
|
||||||
return {
|
|
||||||
default: {
|
|
||||||
schedule: vi.fn((expression, callback) => ({ fireOnTick: callback, expression }))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe("startHeartbeatScheduler", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
it("schedules heartbeat every five minutes and enqueues payload", async () => {
|
|
||||||
const add = vi.fn();
|
|
||||||
const task = startHeartbeatScheduler({ add });
|
|
||||||
expect(cron.schedule).toHaveBeenCalledWith("*/5 * * * *", expect.any(Function));
|
|
||||||
// fire the cron callback
|
|
||||||
task.fireOnTick();
|
|
||||||
expect(add).toHaveBeenCalledWith("heartbeat", expect.objectContaining({ ts: expect.any(Number) }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
206
tests/lucidia-evaluator.test.ts
Normal file
206
tests/lucidia-evaluator.test.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
parseDuration,
|
||||||
|
parseCondition,
|
||||||
|
evaluateCondition,
|
||||||
|
evaluateRule,
|
||||||
|
evaluateSpawnRules,
|
||||||
|
} from "../src/lucidia/evaluator";
|
||||||
|
import type { Metrics, SpawnRule } from "../src/lucidia/types";
|
||||||
|
|
||||||
|
describe("parseDuration", () => {
|
||||||
|
it("parses hours correctly", () => {
|
||||||
|
expect(parseDuration("48h")).toBe(48);
|
||||||
|
expect(parseDuration("96h")).toBe(96);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses days correctly", () => {
|
||||||
|
expect(parseDuration("7d")).toBe(168);
|
||||||
|
expect(parseDuration("3d")).toBe(72);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on invalid format", () => {
|
||||||
|
expect(() => parseDuration("invalid")).toThrow();
|
||||||
|
expect(() => parseDuration("48")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseCondition", () => {
|
||||||
|
it("parses greater than conditions", () => {
|
||||||
|
expect(parseCondition("> 15")).toEqual({ op: ">", value: 15 });
|
||||||
|
expect(parseCondition(">10")).toEqual({ op: ">", value: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses less than conditions", () => {
|
||||||
|
expect(parseCondition("< 5")).toEqual({ op: "<", value: 5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses duration conditions", () => {
|
||||||
|
expect(parseCondition("> 48h")).toEqual({ op: ">", value: 48 });
|
||||||
|
expect(parseCondition("> 7d")).toEqual({ op: ">", value: 168 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses equals conditions", () => {
|
||||||
|
expect(parseCondition("= 0")).toEqual({ op: "=", value: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("evaluateCondition", () => {
|
||||||
|
it("evaluates > correctly", () => {
|
||||||
|
expect(evaluateCondition(20, "> 15")).toBe(true);
|
||||||
|
expect(evaluateCondition(10, "> 15")).toBe(false);
|
||||||
|
expect(evaluateCondition(15, "> 15")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evaluates < correctly", () => {
|
||||||
|
expect(evaluateCondition(5, "< 10")).toBe(true);
|
||||||
|
expect(evaluateCondition(15, "< 10")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evaluates >= correctly", () => {
|
||||||
|
expect(evaluateCondition(15, ">= 15")).toBe(true);
|
||||||
|
expect(evaluateCondition(20, ">= 15")).toBe(true);
|
||||||
|
expect(evaluateCondition(10, ">= 15")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evaluates = correctly", () => {
|
||||||
|
expect(evaluateCondition(0, "= 0")).toBe(true);
|
||||||
|
expect(evaluateCondition(1, "= 0")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("evaluateRule", () => {
|
||||||
|
const testRule: SpawnRule = {
|
||||||
|
id: "test-rule",
|
||||||
|
name: "Test Rule",
|
||||||
|
if: {
|
||||||
|
escalations_last_3_days: "> 15",
|
||||||
|
agent_load: "> 85",
|
||||||
|
},
|
||||||
|
then: {
|
||||||
|
spawn: "test-agent",
|
||||||
|
config: {
|
||||||
|
role: "sentinel",
|
||||||
|
ttl: "96h",
|
||||||
|
inherits_from: "parent-agent",
|
||||||
|
description: "Test agent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseMetrics: Metrics = {
|
||||||
|
escalations_last_3_days: 0,
|
||||||
|
agent_load: 0,
|
||||||
|
blocked_prs: 0,
|
||||||
|
avg_review_time: 0,
|
||||||
|
unmapped_repos: 0,
|
||||||
|
repo_activity_score: 0,
|
||||||
|
open_issues: 0,
|
||||||
|
avg_issue_age: 0,
|
||||||
|
unowned_workflows: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("returns true when all conditions match", () => {
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 18,
|
||||||
|
agent_load: 89,
|
||||||
|
};
|
||||||
|
expect(evaluateRule(testRule, metrics)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when some conditions don't match", () => {
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 18,
|
||||||
|
agent_load: 50, // Below threshold
|
||||||
|
};
|
||||||
|
expect(evaluateRule(testRule, metrics)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when no conditions match", () => {
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 5,
|
||||||
|
agent_load: 50,
|
||||||
|
};
|
||||||
|
expect(evaluateRule(testRule, metrics)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("evaluateSpawnRules", () => {
|
||||||
|
const rules: SpawnRule[] = [
|
||||||
|
{
|
||||||
|
id: "escalation-overflow",
|
||||||
|
name: "Escalation Overflow Handler",
|
||||||
|
if: {
|
||||||
|
escalations_last_3_days: "> 15",
|
||||||
|
agent_load: "> 85",
|
||||||
|
},
|
||||||
|
then: {
|
||||||
|
spawn: "guardian-clone-vault",
|
||||||
|
config: {
|
||||||
|
role: "sentinel",
|
||||||
|
ttl: "96h",
|
||||||
|
inherits_from: "guardian-agent",
|
||||||
|
description: "Temporary overflow clone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "blocked-pr-queue",
|
||||||
|
name: "Blocked PR Queue Handler",
|
||||||
|
if: {
|
||||||
|
blocked_prs: "> 10",
|
||||||
|
},
|
||||||
|
then: {
|
||||||
|
spawn: "reviewer-assist-agent",
|
||||||
|
config: {
|
||||||
|
role: "reviewer",
|
||||||
|
ttl: "48h",
|
||||||
|
inherits_from: "review-agent",
|
||||||
|
description: "PR review accelerator",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const baseMetrics: Metrics = {
|
||||||
|
escalations_last_3_days: 0,
|
||||||
|
agent_load: 0,
|
||||||
|
blocked_prs: 0,
|
||||||
|
avg_review_time: 0,
|
||||||
|
unmapped_repos: 0,
|
||||||
|
repo_activity_score: 0,
|
||||||
|
open_issues: 0,
|
||||||
|
avg_issue_age: 0,
|
||||||
|
unowned_workflows: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("returns first matching rule", () => {
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 18,
|
||||||
|
agent_load: 89,
|
||||||
|
};
|
||||||
|
const result = evaluateSpawnRules(rules, metrics);
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.rule?.id).toBe("escalation-overflow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns second rule if first doesn't match", () => {
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
blocked_prs: 15,
|
||||||
|
};
|
||||||
|
const result = evaluateSpawnRules(rules, metrics);
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.rule?.id).toBe("blocked-pr-queue");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no match when no rules apply", () => {
|
||||||
|
const result = evaluateSpawnRules(rules, baseMetrics);
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.rule).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
125
tests/lucidia-generator.test.ts
Normal file
125
tests/lucidia-generator.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
generateAgentSpec,
|
||||||
|
generateAgentPrompt,
|
||||||
|
generateAgentWorkflow,
|
||||||
|
generateAgentDocs,
|
||||||
|
generateAgent,
|
||||||
|
generateSpawnPR,
|
||||||
|
} from "../src/lucidia/generator";
|
||||||
|
import type { SpawnRule } from "../src/lucidia/types";
|
||||||
|
|
||||||
|
const testRule: SpawnRule = {
|
||||||
|
id: "escalation-overflow",
|
||||||
|
name: "Escalation Overflow Handler",
|
||||||
|
if: {
|
||||||
|
escalations_last_3_days: "> 15",
|
||||||
|
agent_load: "> 85",
|
||||||
|
},
|
||||||
|
then: {
|
||||||
|
spawn: "guardian-clone-vault",
|
||||||
|
config: {
|
||||||
|
role: "sentinel",
|
||||||
|
ttl: "96h",
|
||||||
|
inherits_from: "guardian-agent",
|
||||||
|
description: "Temporary overflow clone of guardian-agent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("generateAgentSpec", () => {
|
||||||
|
it("generates correct agent specification", () => {
|
||||||
|
const spec = generateAgentSpec(testRule);
|
||||||
|
|
||||||
|
expect(spec.name).toBe("guardian-clone-vault");
|
||||||
|
expect(spec.role).toBe("sentinel");
|
||||||
|
expect(spec.inherits_from).toBe("guardian-agent");
|
||||||
|
expect(spec.ttl).toBe("96h");
|
||||||
|
expect(spec.created_by).toBe("lucidia");
|
||||||
|
expect(spec.capabilities).toContain("monitor_escalations");
|
||||||
|
expect(spec.triggers).toContain("escalation_created");
|
||||||
|
expect(spec.outputs).toContain("escalation_resolved");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateAgentPrompt", () => {
|
||||||
|
it("generates prompt with correct content", () => {
|
||||||
|
const prompt = generateAgentPrompt(testRule);
|
||||||
|
|
||||||
|
expect(prompt).toContain("# 🤖 guardian-clone-vault");
|
||||||
|
expect(prompt).toContain("Sentinel Agent");
|
||||||
|
expect(prompt).toContain("guardian-agent");
|
||||||
|
expect(prompt).toContain("96h");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateAgentWorkflow", () => {
|
||||||
|
it("generates workflow YAML with correct structure", () => {
|
||||||
|
const workflow = generateAgentWorkflow(testRule);
|
||||||
|
|
||||||
|
expect(workflow).toContain("name: 🤖 guardian-clone-vault");
|
||||||
|
expect(workflow).toContain("AGENT_NAME: guardian-clone-vault");
|
||||||
|
expect(workflow).toContain("AGENT_ROLE: sentinel");
|
||||||
|
expect(workflow).toContain("workflow_dispatch:");
|
||||||
|
expect(workflow).toContain("schedule:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateAgentDocs", () => {
|
||||||
|
it("generates MDX documentation with frontmatter", () => {
|
||||||
|
const docs = generateAgentDocs(testRule);
|
||||||
|
|
||||||
|
expect(docs).toContain('title: "guardian-clone-vault"');
|
||||||
|
expect(docs).toContain('role: "sentinel"');
|
||||||
|
expect(docs).toContain("## Capabilities");
|
||||||
|
expect(docs).toContain("## Triggers");
|
||||||
|
expect(docs).toContain("## Outputs");
|
||||||
|
expect(docs).toContain("## Spawn Condition");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateAgent", () => {
|
||||||
|
it("generates all agent files", () => {
|
||||||
|
const agent = generateAgent(testRule);
|
||||||
|
|
||||||
|
expect(agent.spec).toBeDefined();
|
||||||
|
expect(agent.prompt).toBeDefined();
|
||||||
|
expect(agent.workflow).toBeDefined();
|
||||||
|
expect(agent.docs).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateSpawnPR", () => {
|
||||||
|
it("generates PR proposal with correct structure", () => {
|
||||||
|
const agent = generateAgent(testRule);
|
||||||
|
const pr = generateSpawnPR(testRule, agent);
|
||||||
|
|
||||||
|
expect(pr.title).toContain("[spawn]");
|
||||||
|
expect(pr.title).toContain("guardian-clone-vault");
|
||||||
|
expect(pr.branch).toBe("spawn/guardian-clone-vault");
|
||||||
|
expect(pr.labels).toContain("auto-spawn");
|
||||||
|
expect(pr.labels).toContain("lucidia");
|
||||||
|
expect(pr.assignee).toBe("@alexa");
|
||||||
|
expect(pr.files).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates correct file paths", () => {
|
||||||
|
const agent = generateAgent(testRule);
|
||||||
|
const pr = generateSpawnPR(testRule, agent);
|
||||||
|
|
||||||
|
const filePaths = pr.files.map((f) => f.path);
|
||||||
|
expect(filePaths).toContain("agents/guardian-clone-vault.agent.json");
|
||||||
|
expect(filePaths).toContain("agents/guardian-clone-vault.prompt.txt");
|
||||||
|
expect(filePaths).toContain(".github/workflows/guardian-clone-vault.workflow.yml");
|
||||||
|
expect(filePaths).toContain("docs/agents/guardian-clone-vault.mdx");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes approval request in description", () => {
|
||||||
|
const agent = generateAgent(testRule);
|
||||||
|
const pr = generateSpawnPR(testRule, agent);
|
||||||
|
|
||||||
|
expect(pr.description).toContain("@alexa");
|
||||||
|
expect(pr.description).toContain("Auto-Spawn Proposal");
|
||||||
|
expect(pr.description).toContain("Lucidia");
|
||||||
|
});
|
||||||
|
});
|
||||||
150
tests/lucidia.test.ts
Normal file
150
tests/lucidia.test.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { Lucidia, createLucidia } from "../src/lucidia";
|
||||||
|
import type { SpawnRulesConfig, Metrics } from "../src/lucidia/types";
|
||||||
|
|
||||||
|
const testConfig: SpawnRulesConfig = {
|
||||||
|
version: "1.0.0",
|
||||||
|
settings: {
|
||||||
|
approval_required: true,
|
||||||
|
approver: "@alexa",
|
||||||
|
default_ttl: "72h",
|
||||||
|
max_clones: 3,
|
||||||
|
cooldown_period: "24h",
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
id: "escalation-overflow",
|
||||||
|
name: "Escalation Overflow Handler",
|
||||||
|
if: {
|
||||||
|
escalations_last_3_days: "> 15",
|
||||||
|
agent_load: "> 85",
|
||||||
|
},
|
||||||
|
then: {
|
||||||
|
spawn: "guardian-clone-vault",
|
||||||
|
config: {
|
||||||
|
role: "sentinel",
|
||||||
|
ttl: "96h",
|
||||||
|
inherits_from: "guardian-agent",
|
||||||
|
description: "Temporary overflow clone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseMetrics: Metrics = {
|
||||||
|
escalations_last_3_days: 0,
|
||||||
|
agent_load: 0,
|
||||||
|
blocked_prs: 0,
|
||||||
|
avg_review_time: 0,
|
||||||
|
unmapped_repos: 0,
|
||||||
|
repo_activity_score: 0,
|
||||||
|
open_issues: 0,
|
||||||
|
avg_issue_age: 0,
|
||||||
|
unowned_workflows: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Lucidia", () => {
|
||||||
|
describe("createLucidia", () => {
|
||||||
|
it("creates Lucidia instance from config", () => {
|
||||||
|
const lucidia = createLucidia(testConfig);
|
||||||
|
expect(lucidia).toBeInstanceOf(Lucidia);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSettings", () => {
|
||||||
|
it("returns configured settings", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const settings = lucidia.getSettings();
|
||||||
|
|
||||||
|
expect(settings.approval_required).toBe(true);
|
||||||
|
expect(settings.approver).toBe("@alexa");
|
||||||
|
expect(settings.max_clones).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRules", () => {
|
||||||
|
it("returns configured rules", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const rules = lucidia.getRules();
|
||||||
|
|
||||||
|
expect(rules).toHaveLength(1);
|
||||||
|
expect(rules[0].id).toBe("escalation-overflow");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("detect", () => {
|
||||||
|
it("detects matching conditions", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 18,
|
||||||
|
agent_load: 89,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = lucidia.detect(metrics);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(result.rule?.id).toBe("escalation-overflow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no match for normal conditions", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const result = lucidia.detect(baseMetrics);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(result.rule).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("write", () => {
|
||||||
|
it("generates agent files", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const agent = lucidia.write(testConfig.rules[0]);
|
||||||
|
|
||||||
|
expect(agent.spec.name).toBe("guardian-clone-vault");
|
||||||
|
expect(agent.prompt).toBeDefined();
|
||||||
|
expect(agent.workflow).toBeDefined();
|
||||||
|
expect(agent.docs).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("propose", () => {
|
||||||
|
it("creates PR proposal", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const agent = lucidia.write(testConfig.rules[0]);
|
||||||
|
const pr = lucidia.propose(testConfig.rules[0], agent);
|
||||||
|
|
||||||
|
expect(pr.title).toContain("guardian-clone-vault");
|
||||||
|
expect(pr.files).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("spawn", () => {
|
||||||
|
it("executes full spawn pipeline when conditions match", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const metrics: Metrics = {
|
||||||
|
...baseMetrics,
|
||||||
|
escalations_last_3_days: 18,
|
||||||
|
agent_load: 89,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, agent, pr } = lucidia.spawn(metrics);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(true);
|
||||||
|
expect(agent).toBeDefined();
|
||||||
|
expect(agent?.spec.name).toBe("guardian-clone-vault");
|
||||||
|
expect(pr).toBeDefined();
|
||||||
|
expect(pr?.title).toContain("[spawn]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns only result when no match", () => {
|
||||||
|
const lucidia = new Lucidia(testConfig);
|
||||||
|
const { result, agent, pr } = lucidia.spawn(baseMetrics);
|
||||||
|
|
||||||
|
expect(result.matched).toBe(false);
|
||||||
|
expect(agent).toBeUndefined();
|
||||||
|
expect(pr).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { GET as health } from "../app/api/health/route";
|
|
||||||
import { GET as version } from "../app/api/version/route";
|
|
||||||
vi.mock("../src/utils/buildInfo", () => ({
|
|
||||||
getBuildInfo: () => ({ version: "api-version", commit: "api-commit", buildTime: "now" })
|
|
||||||
}));
|
|
||||||
describe("Next API routes", () => {
|
|
||||||
it("returns health response", async () => {
|
|
||||||
const res = await health();
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json()).toEqual({ status: "ok" });
|
|
||||||
});
|
|
||||||
it("returns version response", async () => {
|
|
||||||
const res = await version();
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json()).toEqual({ version: "api-version", commit: "api-commit" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { registerSampleJobProcessor } from "../src/jobs/sample.job";
|
|
||||||
vi.mock("bullmq", () => {
|
|
||||||
class MockWorker {
|
|
||||||
constructor(_name, processor, _opts) {
|
|
||||||
this.handlers = {};
|
|
||||||
this.processor = processor;
|
|
||||||
}
|
|
||||||
on(event, handler) {
|
|
||||||
this.handlers[event] = this.handlers[event] || [];
|
|
||||||
this.handlers[event].push(handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { Worker: MockWorker };
|
|
||||||
});
|
|
||||||
describe("registerSampleJobProcessor", () => {
|
|
||||||
it("registers worker and handlers", () => {
|
|
||||||
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
||||||
const worker = registerSampleJobProcessor({ host: "localhost", port: 6379 });
|
|
||||||
expect(worker.processor).toBeInstanceOf(Function);
|
|
||||||
expect(worker.handlers.failed).toHaveLength(1);
|
|
||||||
// simulate processing and failure
|
|
||||||
worker.processor({ id: 1, data: { hello: "world" } });
|
|
||||||
worker.handlers.failed[0]({ id: 1 }, new Error("boom"));
|
|
||||||
expect(consoleLog).toHaveBeenCalledWith("Processing job 1");
|
|
||||||
expect(consoleError).toHaveBeenCalled();
|
|
||||||
consoleLog.mockRestore();
|
|
||||||
consoleError.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { StatusPill } from "../components/StatusPill";
|
|
||||||
describe("StatusPill", () => {
|
|
||||||
const cases = [
|
|
||||||
["healthy", "Healthy", "status-pill--healthy"],
|
|
||||||
["degraded", "Degraded", "status-pill--degraded"],
|
|
||||||
["down", "Down", "status-pill--down"]
|
|
||||||
];
|
|
||||||
it.each(cases)("renders %s status", (status, label, className) => {
|
|
||||||
render(_jsx(StatusPill, { status: status }));
|
|
||||||
const pill = screen.getByText(label);
|
|
||||||
expect(pill).toBeInTheDocument();
|
|
||||||
expect(pill.className).toContain(className);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
test: {
|
|
||||||
environment: "jsdom",
|
|
||||||
setupFiles: "./vitest.setup.ts",
|
|
||||||
globals: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user