2870 lines
114 KiB
JavaScript
2870 lines
114 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { Command } from "commander";
|
|
import { MetaAgent } from "./tier3-compounding/meta-agent.js";
|
|
import { loadEnv } from "./upgrades/env-loader.js";
|
|
import type { SkillDefinition, LoopIteration } from "./core/types.js";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
// Load .env before any command handlers run
|
|
const envResult = loadEnv();
|
|
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
);
|
|
|
|
const program = new Command();
|
|
|
|
program
|
|
.name("fable-agent")
|
|
.description("Harness-agnostic, model-agnostic self-improving agent system — loops, dynamic workflows, routines")
|
|
.version(packageJson.version);
|
|
|
|
// ── Run ─────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("run <task>")
|
|
.description("Run a task through the self-improving agent system")
|
|
.option("-l, --loop <n>", "Number of feedback loop iterations", parseInt)
|
|
.option("-w, --workflow <id>", "Workflow definition ID to execute")
|
|
.option("-s, --skill <id>", "Skill ID to execute")
|
|
.option("-c, --compound <n>", "Number of compound runs", parseInt)
|
|
.action(async (task: string, opts: Record<string, unknown>) => {
|
|
const agent = new MetaAgent();
|
|
|
|
console.log(`\n Fable Agent v${packageJson.version}`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: ${task}`);
|
|
console.log(` `);
|
|
|
|
try {
|
|
const compound = opts.compound ? Number(opts.compound) : 1;
|
|
|
|
if (compound > 1) {
|
|
const results = await agent.compound(task, compound);
|
|
console.log(`\n ✓ Compound run complete (${compound} iterations)`);
|
|
for (const r of results) {
|
|
console.log(` Session ${r.sessionId.slice(0, 8)}… — ${r.loopIterations} loops, converged=${r.converged}`);
|
|
}
|
|
} else {
|
|
const result = await agent.run(task, {
|
|
skillId: opts.skill as string | undefined,
|
|
workflowId: opts.workflow as string | undefined,
|
|
loopIterations: opts.loop ? Number(opts.loop) : undefined,
|
|
});
|
|
console.log(`\n ✓ Run complete`);
|
|
console.log(` Session: ${result.sessionId}`);
|
|
console.log(` Iterations: ${result.loopIterations}`);
|
|
console.log(` Converged: ${result.converged} (${result.convergenceReason ?? "n/a"})`);
|
|
console.log(` Context: ${result.contextUsage.pct}% of ${result.contextUsage.budget} budget`);
|
|
console.log(` Knowledge: ${result.knowledgeEntryCount} entries`);
|
|
}
|
|
|
|
const status = agent.getStatus();
|
|
console.log(`\n System State:`);
|
|
console.log(` Skills: ${status.skills.total} (${status.skills.withHistory} with history)`);
|
|
console.log(` Knowledge: ${status.knowledge.entries} entries, ${status.knowledge.tags.length} tags`);
|
|
console.log(` `);
|
|
} catch (err) {
|
|
console.error(`\n ✗ Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// ── Session ─────────────────────────────────────────────────
|
|
|
|
const session = program
|
|
.command("session")
|
|
.description("Manage autonomous agent sessions");
|
|
|
|
session
|
|
.command("start")
|
|
.description("Start a new autonomous session")
|
|
.argument("<task>", "Task description")
|
|
.action(async (task: string) => {
|
|
const agent = new MetaAgent();
|
|
const sess = agent.session.start(task);
|
|
console.log(`\n Session started:`);
|
|
console.log(` ID: ${sess.id}`);
|
|
console.log(` Task: ${sess.task}`);
|
|
console.log(` Run \`fable-agent session resume ${sess.id}\` to resume if interrupted`);
|
|
console.log(` `);
|
|
});
|
|
|
|
session
|
|
.command("resume <id>")
|
|
.description("Resume a checkpointed session")
|
|
.action(async (id: string) => {
|
|
const agent = new MetaAgent();
|
|
const sess = agent.session.resume(id);
|
|
if (!sess) {
|
|
console.error(` ✗ Session not found: ${id}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(` ✓ Session resumed: ${id}`);
|
|
console.log(` Task: ${sess.task}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
session
|
|
.command("list")
|
|
.description("List all sessions")
|
|
.action(() => {
|
|
const agent = new MetaAgent();
|
|
const sessions = agent.session.list();
|
|
if (sessions.length === 0) {
|
|
console.log(" No sessions found.");
|
|
return;
|
|
}
|
|
console.log(` Sessions (${sessions.length}):`);
|
|
for (const s of sessions) {
|
|
const age = Math.round(
|
|
(Date.now() - new Date(s.createdAt).getTime()) / 1000 / 60
|
|
);
|
|
console.log(` ${s.id.slice(0, 8)}… ${s.status.padEnd(14)} ${age}m ago "${s.task.slice(0, 50)}"`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
session
|
|
.command("inspect <id>")
|
|
.description("View session details")
|
|
.action(async (id: string) => {
|
|
const agent = new MetaAgent();
|
|
const s = agent.session.get(id);
|
|
if (!s) {
|
|
console.error(` ✗ Session not found: ${id}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\n Session: ${s.id}`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Status: ${s.status}`);
|
|
console.log(` Task: ${s.task}`);
|
|
console.log(` Created: ${s.createdAt}`);
|
|
console.log(` Updated: ${s.updatedAt}`);
|
|
console.log(` Iterations: ${s.metadata.totalIterations ?? "?"}`);
|
|
console.log(` Config:`);
|
|
console.log(` Max iterations: ${s.config.maxIterations}`);
|
|
console.log(` Convergence thresh: ${s.config.convergenceThreshold}`);
|
|
console.log(` Context budget: ${s.config.contextTokenBudget}`);
|
|
console.log(` Heartbeat interval: ${s.config.heartbeatIntervalMs}ms`);
|
|
if (agent.session.isStalled(id)) {
|
|
console.log(` ⚠ Session may be stalled (no recent heartbeat)`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── Skills ──────────────────────────────────────────────────
|
|
|
|
const skills = program
|
|
.command("skills")
|
|
.description("Manage skills (routines)");
|
|
|
|
skills
|
|
.command("list")
|
|
.description("List all registered skills")
|
|
.option("-t, --tag <tag>", "Filter by tag")
|
|
.action((opts: { tag?: string }) => {
|
|
const agent = new MetaAgent();
|
|
let skillsList: SkillDefinition[];
|
|
|
|
if (opts.tag) {
|
|
skillsList = agent.skillRegistry.findByTag(opts.tag);
|
|
} else {
|
|
skillsList = agent.skillRegistry.list();
|
|
}
|
|
|
|
if (skillsList.length === 0) {
|
|
console.log(" No skills registered.");
|
|
return;
|
|
}
|
|
|
|
console.log(`\n Skills (${skillsList.length}):`);
|
|
for (const sk of skillsList) {
|
|
console.log(
|
|
` ${sk.id.padEnd(24)} v${sk.version.padEnd(6)} ${String(sk.metrics.totalExecutions).padStart(3)} runs ${sk.name}`
|
|
);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
skills
|
|
.command("create <name>")
|
|
.description("Create a new skill from a template")
|
|
.requiredOption("-d, --description <desc>", "Skill description")
|
|
.option("-s, --steps <steps>", "Comma-separated step labels")
|
|
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
.action((name: string, opts: { description: string; steps?: string; tags?: string }) => {
|
|
const agent = new MetaAgent();
|
|
const stepLabels = opts.steps?.split(",").map((s) => s.trim()) ?? [
|
|
"Analyze requirements",
|
|
"Plan approach",
|
|
"Execute",
|
|
"Verify results",
|
|
"Reflect",
|
|
];
|
|
|
|
const skill = agent.skillRegistry.createFromTemplate({
|
|
name,
|
|
description: opts.description,
|
|
steps: stepLabels.map((label) => ({
|
|
label,
|
|
instruction: `Execute step: ${label}`,
|
|
expectedOutcome: `Completed: ${label}`,
|
|
})),
|
|
tags: opts.tags?.split(",").map((t) => t.trim()) ?? ["general"],
|
|
});
|
|
|
|
console.log(`\n ✓ Skill created: ${skill.id} v${skill.version}`);
|
|
console.log(` Steps: ${skill.steps.length}`);
|
|
console.log(` Tags: ${skill.tags.join(", ")}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
skills
|
|
.command("inspect <id>")
|
|
.description("View skill details and execution history")
|
|
.action((id: string) => {
|
|
const agent = new MetaAgent();
|
|
const skill = agent.skillRegistry.get(id);
|
|
if (!skill) {
|
|
console.error(` ✗ Skill not found: ${id}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n Skill: ${skill.name} (${skill.id})`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Version: ${skill.version}`);
|
|
console.log(` Description: ${skill.description}`);
|
|
console.log(` Tags: ${skill.tags.join(", ")}`);
|
|
console.log(` `);
|
|
console.log(` Steps:`);
|
|
for (const step of skill.steps) {
|
|
console.log(` ${step.id}: ${step.label}`);
|
|
if (step.validationCriteria.length > 0) {
|
|
console.log(` Validates: ${step.validationCriteria.join(", ")}`);
|
|
}
|
|
}
|
|
console.log(` `);
|
|
console.log(` Metrics:`);
|
|
console.log(` Executions: ${skill.metrics.totalExecutions}`);
|
|
console.log(` Avg duration: ${Math.round(skill.metrics.avgDurationMs / 1000)}s`);
|
|
console.log(` Avg quality: ${skill.metrics.avgQualityScore.toFixed(2)}`);
|
|
console.log(` Success rate: ${(skill.metrics.successRate * 100).toFixed(0)}%`);
|
|
console.log(` Evolutions: ${skill.metrics.evolutionCount}`);
|
|
if (skill.history.length > 0) {
|
|
console.log(` `);
|
|
console.log(` Recent executions:`);
|
|
for (const h of skill.history.slice(-5).reverse()) {
|
|
const date = new Date(h.completedAt).toISOString().slice(0, 16);
|
|
console.log(
|
|
` ${date} ${h.success ? "✓" : "✗"} q=${h.qualityScore.toFixed(2)} ${h.issues.length} issues`
|
|
);
|
|
}
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
skills
|
|
.command("evolve <id>")
|
|
.description("Analyze and auto-improve a skill")
|
|
.action((id: string) => {
|
|
const agent = new MetaAgent();
|
|
const { evolved, changes, report } = agent.routineEvolution.autoEvolve(id);
|
|
|
|
console.log(`\n Evolution Report for "${id}":`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Analyzable: ${report.analyzable}`);
|
|
console.log(` Score: ${report.score}/100`);
|
|
if (report.trend) console.log(` Trend: ${report.trend}`);
|
|
|
|
if (report.findings.length > 0) {
|
|
console.log(` `);
|
|
console.log(` Findings:`);
|
|
for (const f of report.findings) console.log(` • ${f}`);
|
|
}
|
|
|
|
if (evolved) {
|
|
console.log(` `);
|
|
console.log(` ✓ Applied ${changes.length} change(s):`);
|
|
for (const c of changes) {
|
|
console.log(` • ${c.type}: ${c.description}`);
|
|
}
|
|
} else {
|
|
console.log(` No changes applied: ${report.reason ?? "skill is healthy"}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
skills
|
|
.command("sharpen")
|
|
.description("Run meta-review on all skills with execution history")
|
|
.action(() => {
|
|
const agent = new MetaAgent();
|
|
const reviews = agent.skillSharpener.reviewAll();
|
|
|
|
if (reviews.length === 0) {
|
|
console.log(" No skills with execution history to review.");
|
|
return;
|
|
}
|
|
|
|
console.log(`\n Skill Sharpener Review (${reviews.length} skills):`);
|
|
console.log(` ─────────────────────────────`);
|
|
for (const review of reviews) {
|
|
const skill = agent.skillRegistry.get(review.skillId);
|
|
console.log(` ${skill?.name ?? review.skillId}`);
|
|
console.log(` Score: ${review.score}/100`);
|
|
console.log(` Findings: ${review.findings.length}`);
|
|
for (const f of review.findings) console.log(` • ${f}`);
|
|
if (review.suggestedChanges.length > 0) {
|
|
console.log(` Suggestions: ${review.suggestedChanges.length}`);
|
|
for (const c of review.suggestedChanges.slice(0, 2)) {
|
|
console.log(` → ${c.type}: ${c.description.slice(0, 80)}`);
|
|
}
|
|
}
|
|
console.log(` `);
|
|
}
|
|
});
|
|
|
|
// ── State / Knowledge Base ──────────────────────────────────
|
|
|
|
program
|
|
.command("state")
|
|
.description("View accumulated state knowledge base")
|
|
.option("-t, --tag <tag>", "Filter by tag")
|
|
.option("--stats", "Show statistics only")
|
|
.action((opts: { tag?: string; stats?: boolean }) => {
|
|
const agent = new MetaAgent();
|
|
|
|
if (opts.stats) {
|
|
const stats = agent.stateRepository.getStats();
|
|
console.log(`\n Knowledge Base Stats:`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Total entries: ${stats.totalEntries}`);
|
|
console.log(` By type:`);
|
|
for (const [type, count] of Object.entries(stats.byType)) {
|
|
console.log(` ${type}: ${count}`);
|
|
}
|
|
console.log(` Top tags: ${stats.topTags.join(", ")}`);
|
|
console.log(` Avg score: ${stats.avgScore.toFixed(2)}`);
|
|
} else if (opts.tag) {
|
|
const entries = agent.stateRepository.findByTag(opts.tag, 10);
|
|
if (entries.length === 0) {
|
|
console.log(` No entries with tag "${opts.tag}".`);
|
|
return;
|
|
}
|
|
console.log(`\n Entries tagged "${opts.tag}" (${entries.length}):`);
|
|
for (const e of entries) {
|
|
console.log(` [${e.type}] ${e.content.slice(0, 100)}`);
|
|
}
|
|
} else {
|
|
const stats = agent.stateRepository.getStats();
|
|
console.log(`\n Knowledge Base: ${stats.totalEntries} entries`);
|
|
console.log(` Tags: ${stats.topTags.join(", ")}`);
|
|
console.log(` Run with --tag <tag> to view entries, --stats for details`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── PAI Integration ─────────────────────────────────────────
|
|
|
|
const pai = program
|
|
.command("pai")
|
|
.description("PAI integration commands");
|
|
|
|
pai
|
|
.command("status")
|
|
.description("Check PAI integration health")
|
|
.action(async () => {
|
|
const { PaiConnector } = await import("./pai/pai-connector.js");
|
|
const { ProxyClient } = await import("./pai/proxy-client.js");
|
|
const { TelosBridge } = await import("./pai/telos-bridge.js");
|
|
|
|
const connector = new PaiConnector();
|
|
const proxy = new ProxyClient();
|
|
const telos = new TelosBridge();
|
|
|
|
console.log(`\n PAI Integration Status`);
|
|
console.log(` ─────────────────────────────`);
|
|
|
|
const ctx = connector.loadAll();
|
|
console.log(` TELOS files: ${ctx.telos.length}`);
|
|
console.log(` PAI skills: ${ctx.skills.length}`);
|
|
console.log(` ISA sessions: ${ctx.isa ? 1 : 0}`);
|
|
|
|
const proxyOk = await proxy.health();
|
|
console.log(` Proxy (:18901): ${proxyOk ? "✓ online" : "✗ offline"}`);
|
|
|
|
if (proxyOk) {
|
|
try {
|
|
const models = await proxy.listModels();
|
|
console.log(` Available models: ${models.length}`);
|
|
for (const m of models.slice(0, 5)) {
|
|
console.log(` ${m}`);
|
|
}
|
|
if (models.length > 5) console.log(` ... and ${models.length - 5} more`);
|
|
} catch {
|
|
console.log(` Models: unable to list`);
|
|
}
|
|
}
|
|
|
|
const telosSections = telos.getAllSectionNames();
|
|
console.log(` TELOS sections: ${telosSections.length > 0 ? telosSections.join(", ") : "none"}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
pai
|
|
.command("telos")
|
|
.description("Load PAI TELOS context")
|
|
.option("-s, --section <name>", "Specific section to load")
|
|
.action(async (opts: { section?: string }) => {
|
|
const { PaiConnector } = await import("./pai/pai-connector.js");
|
|
const connector = new PaiConnector();
|
|
|
|
if (opts.section) {
|
|
const content = connector.readFile(`USER/TELOS/${opts.section}.md`);
|
|
if (!content) {
|
|
console.error(` ✗ TELOS section not found: ${opts.section}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\n TELOS: ${opts.section}`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(content.slice(0, 2000));
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
|
|
const ctx = connector.loadAll();
|
|
console.log(`\n PAI Context loaded:`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` TELOS documents (${ctx.telos.length}):`);
|
|
for (const t of ctx.telos) {
|
|
const preview = t.content.replace(/\n/g, " ").slice(0, 80);
|
|
console.log(` ${t.name}: ${preview}...`);
|
|
}
|
|
console.log(` PAI skills (${ctx.skills.length}):`);
|
|
for (const s of ctx.skills) {
|
|
console.log(` ${s.name}: ${s.description.slice(0, 60)}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
pai
|
|
.command("proxy")
|
|
.description("Test proxy model completion")
|
|
.argument("<prompt>", "Prompt to send")
|
|
.option("-m, --model <id>", "Model ID (default: fi-groq)")
|
|
.option("-t, --temperature <n>", "Temperature", parseFloat)
|
|
.option("-s, --system <text>", "System prompt")
|
|
.action(async (prompt: string, opts: { model?: string; temperature?: number; system?: string }) => {
|
|
const { ProxyClient } = await import("./pai/proxy-client.js");
|
|
const proxy = new ProxyClient();
|
|
|
|
const ok = await proxy.health();
|
|
if (!ok) {
|
|
console.error(" ✗ Proxy is not running on :18901. Start it first.");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n Proxy completion:`);
|
|
console.log(` Model: ${opts.model ?? "fi-groq"}`);
|
|
console.log(` Prompt: ${prompt.slice(0, 100)}${prompt.length > 100 ? "…" : ""}`);
|
|
console.log(` ─────────────────────────────`);
|
|
|
|
try {
|
|
const result = await proxy.complete(prompt, opts.model, {
|
|
temperature: opts.temperature,
|
|
systemPrompt: opts.system,
|
|
});
|
|
console.log(` Response:`);
|
|
console.log(` ${result.content}`);
|
|
console.log(` `);
|
|
console.log(` Tokens: ${result.usage.totalTokens} (${result.usage.promptTokens} prompt + ${result.usage.completionTokens} completion)`);
|
|
console.log(` Model: ${result.model}`);
|
|
console.log(` `);
|
|
} catch (err) {
|
|
console.error(` ✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
pai
|
|
.command("sync")
|
|
.description("Sync PAI skills into skill registry")
|
|
.action(async () => {
|
|
const { SkillSync } = await import("./pai/skill-sync.js");
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
|
|
const syncer = new SkillSync();
|
|
const agent = new MetaAgent();
|
|
const paiSkills = syncer.findAllPaiSkills();
|
|
|
|
if (paiSkills.length === 0) {
|
|
console.log(" No PAI skills found to sync.");
|
|
return;
|
|
}
|
|
|
|
const definitions = syncer.toSkillDefinitions(paiSkills);
|
|
let imported = 0;
|
|
|
|
for (const def of definitions) {
|
|
const existing = agent.skillRegistry.get(def.id);
|
|
if (existing) {
|
|
console.log(` ↻ Already synced: ${def.name} (${def.id})`);
|
|
continue;
|
|
}
|
|
agent.skillRegistry.register(def);
|
|
imported++;
|
|
}
|
|
|
|
console.log(`\n ✓ Synced ${imported}/${definitions.length} PAI skills into skill registry`);
|
|
console.log(` `);
|
|
});
|
|
|
|
pai
|
|
.command("dream")
|
|
.description("Run dream cycle and write insights to TELOS")
|
|
.option("-c, --cycle <n>", "Cycle number", parseInt)
|
|
.option("--dry-run", "Show what would be written without writing")
|
|
.action(async (opts: { cycle?: number; dryRun?: boolean }) => {
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
const { DreamingSystem } = await import("./upgrades/dreaming-system.js");
|
|
const { TelosBridge } = await import("./pai/telos-bridge.js");
|
|
|
|
const agent = new MetaAgent();
|
|
const state = agent.stateRepository;
|
|
const allKnowledge = state.query({ limit: 100 });
|
|
|
|
if (allKnowledge.length < 3) {
|
|
console.error(` ✗ Need at least 3 knowledge entries to dream (have ${allKnowledge.length})`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const dreaming = new DreamingSystem(agent.skillRegistry, agent.store);
|
|
const telos = new TelosBridge();
|
|
|
|
const dummyIterations: import("./core/types.js").LoopIteration[] = allKnowledge.slice(-5).map((k, i) => ({
|
|
number: i + 1,
|
|
phase: "reflect" as const,
|
|
reflection: k.content,
|
|
metrics: {
|
|
qualityScore: k.score,
|
|
successRate: k.score > 0.5 ? 0.8 : 0.3,
|
|
improvementDelta: k.score - 0.5,
|
|
durationMs: 1000,
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
|
|
const cycleNumber = opts.cycle ?? 1;
|
|
|
|
if (opts.dryRun) {
|
|
console.log(`\n Dry run — dream cycle #${cycleNumber}`);
|
|
console.log(` Would create dream from ${dummyIterations.length} iterations`);
|
|
console.log(` Would write to TELOS/_dreams/`);
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const dream = dreaming.dream("pai-session", dummyIterations, cycleNumber);
|
|
const path = telos.writeDreamInsights(dream);
|
|
console.log(`\n ✓ Dream cycle #${cycleNumber} complete`);
|
|
console.log(` Patterns: ${dream.patterns.length}`);
|
|
console.log(` Insights: ${dream.distillations.length}`);
|
|
console.log(` Dreams: ${dream.dreams.length}`);
|
|
console.log(` Written to: ${path}`);
|
|
console.log(` `);
|
|
} catch (err) {
|
|
console.error(` ✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
pai
|
|
.command("cert")
|
|
.description("Run certification: ISA → run → ISA → verify cycle")
|
|
.option("-t, --task <text>", "Task description for the run")
|
|
.option("-l, --loop <n>", "Loop iterations", parseInt)
|
|
.option("--skip-proxy-check", "Skip proxy health check")
|
|
.action(async (opts: { task?: string; loop?: number; skipProxyCheck?: boolean }) => {
|
|
const { PaiConnector } = await import("./pai/pai-connector.js");
|
|
const { ProxyClient } = await import("./pai/proxy-client.js");
|
|
const { IsaWriter } = await import("./pai/isa-writer.js");
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
|
|
if (!opts.skipProxyCheck) {
|
|
const proxy = new ProxyClient();
|
|
const ok = await proxy.health();
|
|
if (!ok) {
|
|
console.error(" ✗ Proxy not running. Use --skip-proxy-check to bypass.");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const task = opts.task ?? "Run PAI certification cycle — build, test, verify, reflect";
|
|
const agent = new MetaAgent();
|
|
const connector = new PaiConnector();
|
|
const isaWriter = new IsaWriter();
|
|
|
|
const ctx = connector.loadAll();
|
|
const sessionDir = isaWriter.createSessionDir("pai-cert-cycle");
|
|
|
|
isaWriter.writeIsa(sessionDir, {
|
|
title: `PAI Certification — ${new Date().toISOString().slice(0, 16)}`,
|
|
sections: [
|
|
{ title: "Problem", content: task },
|
|
{ title: "Vision", content: "A self-certifying agent system that validates its own outputs" },
|
|
{ title: "Context", content: `TELOS docs: ${ctx.telos.length}, PAI skills: ${ctx.skills.length}` },
|
|
{ title: "Execution", content: "Pending run" },
|
|
],
|
|
});
|
|
|
|
console.log(`\n PAI Certification Cycle`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: ${task}`);
|
|
console.log(` ISA: ${sessionDir}`);
|
|
console.log(` `);
|
|
|
|
const result = await agent.run(task, {
|
|
loopIterations: opts.loop ?? 3,
|
|
});
|
|
|
|
isaWriter.updateSection(sessionDir, "Execution",
|
|
`Completed ${result.loopIterations} iterations, converged=${result.converged}\n` +
|
|
`Knowledge entries: ${result.knowledgeEntryCount}`
|
|
);
|
|
|
|
console.log(` ✓ Certification run complete`);
|
|
console.log(` Iterations: ${result.loopIterations}`);
|
|
console.log(` Converged: ${result.converged}`);
|
|
console.log(` ISA updated: ${sessionDir}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
pai
|
|
.command("bridge")
|
|
.description("Sync PAI TELOS → FA rubric engine")
|
|
.action(async () => {
|
|
const { TELOSBridge } = await import("./pai/pai-fa-bridge.js");
|
|
const bridge = new TELOSBridge();
|
|
const entries = bridge.scan();
|
|
const rubric = bridge.toRubric();
|
|
console.log(`\n PAI → FA TELOS Bridge:`);
|
|
console.log(` Entries found: ${entries.length}`);
|
|
console.log(` Rubric criteria: ${rubric.criteria.length}`);
|
|
console.log(` Exit conditions: ${rubric.exitConditions.length}\n`);
|
|
});
|
|
|
|
pai
|
|
.command("memory")
|
|
.description("Sync PAI memory → FA persistent memory")
|
|
.action(async () => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { PAIMemoryBridge } = await import("./pai/pai-fa-bridge.js");
|
|
const agent = new EnhancedMetaAgent();
|
|
const bridge = new PAIMemoryBridge(agent);
|
|
const result = bridge.importMemory();
|
|
console.log(`\n PAI → FA Memory Sync:`);
|
|
console.log(` Episodic: ${result.episodic}`);
|
|
console.log(` Semantic: ${result.semantic}\n`);
|
|
});
|
|
|
|
pai
|
|
.command("packs")
|
|
.description("Import PAI skill packs → FA skill registry")
|
|
.action(async () => {
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
const { PAISkillBridge } = await import("./pai/pai-fa-bridge.js");
|
|
const agent = new MetaAgent();
|
|
const bridge = new PAISkillBridge(agent.skillRegistry);
|
|
const result = bridge.importPacks();
|
|
console.log(`\n PAI → FA Skill Pack Import:`);
|
|
console.log(` Created: ${result.created}`);
|
|
if (result.errors.length > 0) console.log(` Errors: ${result.errors.length}\n`);
|
|
});
|
|
|
|
pai
|
|
.command("report")
|
|
.description("Show PAI+FA integration report")
|
|
.action(async () => {
|
|
const { paiFAReport } = await import("./pai/pai-fa-bridge.js");
|
|
console.log(`\n${paiFAReport()}\n`);
|
|
});
|
|
|
|
pai
|
|
.command("run <goal>")
|
|
.description("Run a PAI goal through FA's engine")
|
|
.action(async (goal: string) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { TELOSBridge, PAIMemoryBridge } = await import("./pai/pai-fa-bridge.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const telos = new TELOSBridge();
|
|
|
|
// Load PAI TELOS context
|
|
const entries = telos.scan();
|
|
const matching = entries.filter((e) =>
|
|
goal.toLowerCase().includes(e.description.slice(0, 20).toLowerCase())
|
|
);
|
|
if (matching.length > 0) {
|
|
agent.context.add({
|
|
source: "pai-telos",
|
|
content: `Related PAI ${matching[0].type}: ${matching[0].description}`,
|
|
priority: "high",
|
|
tokenCount: matching[0].description.length,
|
|
});
|
|
}
|
|
|
|
// Import PAI memory for context
|
|
const memBridge = new PAIMemoryBridge(agent);
|
|
memBridge.importMemory();
|
|
|
|
// Run through FA engine
|
|
const result = await agent.run(goal, { loopIterations: 8 });
|
|
|
|
console.log(`\n PAI Goal → FA Engine:`);
|
|
console.log(` Goal: ${goal}`);
|
|
console.log(` Iterations: ${result.iterations}`);
|
|
console.log(` Converged: ${result.converged}`);
|
|
if (result.rubricScore) console.log(` Rubric: ${result.rubricScore}/100`);
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Status ──────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("status")
|
|
.description("Show system status overview")
|
|
.action(() => {
|
|
const agent = new MetaAgent();
|
|
const status = agent.getStatus();
|
|
|
|
console.log(`\n Fable Agent System Status`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Active session: ${status.activeSession ?? "none"}`);
|
|
console.log(` Skills: ${status.skills.total} total, ${status.skills.withHistory} with execution history`);
|
|
console.log(` Knowledge base: ${status.knowledge.entries} entries, ${status.knowledge.tags.length} tags`);
|
|
console.log(` Context: ${status.context.pct}% of ${status.context.budget.toLocaleString()} token budget`);
|
|
console.log(` Uptime: ${Math.round(status.uptime)}s`);
|
|
console.log(` Compound runs: ${status.compoundIterations}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── Workflow ────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("workflow")
|
|
.description("Workflow management")
|
|
.argument("<file>", "Path to workflow JSON definition")
|
|
.action(async (file: string) => {
|
|
const agent = new MetaAgent();
|
|
try {
|
|
const data = fs.readFileSync(file, "utf-8");
|
|
const definition = JSON.parse(data);
|
|
agent.workflowGraph.register(definition);
|
|
console.log(`\n ✓ Workflow registered: ${definition.name} (${definition.id})`);
|
|
console.log(` Steps: ${definition.steps.length}`);
|
|
console.log(` `);
|
|
} catch (err) {
|
|
console.error(` ✗ Failed to load workflow: ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// ── Benchmark ────────────────────────────────────────────────
|
|
|
|
const benchmark = program
|
|
.command("benchmark")
|
|
.description("Run benchmarks and measure compounding");
|
|
|
|
benchmark
|
|
.command("run")
|
|
.description("Run all benchmarks and show report")
|
|
.option("-b, --benchmark <id>", "Run a specific benchmark by ID")
|
|
.action(async (opts: { benchmark?: string }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { BenchmarkRunner } = await import("./upgrades/benchmark-runner.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const runner = new BenchmarkRunner(agent);
|
|
|
|
if (opts.benchmark) {
|
|
await runner.runOne(opts.benchmark);
|
|
} else {
|
|
await runner.runAll();
|
|
}
|
|
|
|
console.log(`\n${runner.report(opts.benchmark)}\n`);
|
|
});
|
|
|
|
benchmark
|
|
.command("trend")
|
|
.description("Show compounding trend over time")
|
|
.option("-b, --benchmark <id>", "Filter by benchmark ID")
|
|
.action(async (opts: { benchmark?: string }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { BenchmarkRunner } = await import("./upgrades/benchmark-runner.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const runner = new BenchmarkRunner(agent);
|
|
const trend = runner.getTrend(opts.benchmark);
|
|
|
|
const label = opts.benchmark ?? "overall";
|
|
console.log(`\n Trend for "${label}":`);
|
|
console.log(` Runs: ${trend.runs}`);
|
|
console.log(` Rubric score: ${trend.avgRubricScore.first.toFixed(1)} → ${trend.avgRubricScore.last.toFixed(1)} (${trend.avgRubricScore.delta > 0 ? "+" : ""}${trend.avgRubricScore.delta.toFixed(1)})`);
|
|
console.log(` Iterations: ${trend.avgIterations.first.toFixed(1)} → ${trend.avgIterations.last.toFixed(1)} (${trend.avgIterations.delta < 0 ? "faster" : "slower"})`);
|
|
console.log(` Convergence: ${(trend.convergenceRate.first * 100).toFixed(0)}% → ${(trend.convergenceRate.last * 100).toFixed(0)}%\n`);
|
|
});
|
|
|
|
benchmark
|
|
.command("list")
|
|
.description("List available benchmarks")
|
|
.action(async () => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { BenchmarkRunner } = await import("./upgrades/benchmark-runner.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const runner = new BenchmarkRunner(agent);
|
|
|
|
console.log(`\n Available benchmarks:`);
|
|
for (const bm of runner.listBenchmarks()) {
|
|
console.log(` ${bm.id.padEnd(15)} ${bm.name.padEnd(20)} ${bm.tasks.length} tasks`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Cost ─────────────────────────────────────────────────────
|
|
|
|
const cost = program
|
|
.command("cost")
|
|
.description("Track model usage costs");
|
|
|
|
cost
|
|
.command("report")
|
|
.description("Show cost summary")
|
|
.option("-h, --hours <n>", "Hours to look back", parseInt)
|
|
.action(async (opts: { hours?: number }) => {
|
|
const { StateStore } = await import("./core/state-store.js");
|
|
const { CostTracker } = await import("./upgrades/cost-tracker.js");
|
|
|
|
const store = new StateStore();
|
|
const tracker = new CostTracker(store);
|
|
console.log(`\n${tracker.getReport(opts.hours ?? 24)}\n`);
|
|
});
|
|
|
|
// ── Exa Search ───────────────────────────────────────────────
|
|
|
|
const exa = program
|
|
.command("exa")
|
|
.description("Exa AI web search and company research");
|
|
|
|
function requireExaApiKey(): string {
|
|
const apiKey = process.env.EXA_API_KEY;
|
|
if (!apiKey) {
|
|
console.error(" ✗ EXA_API_KEY is required. Set it in your environment or .env file.");
|
|
process.exit(1);
|
|
}
|
|
return apiKey;
|
|
}
|
|
|
|
exa
|
|
.command("search <query>")
|
|
.description("Search the web via Exa AI")
|
|
.option("-n, --num-results <n>", "Number of results", parseInt)
|
|
.option("-t, --type <type>", "Search type: keyword, neural, or auto")
|
|
.option("--include-domains <domains>", "Comma-separated domains to include")
|
|
.action(async (query: string, opts: { numResults?: number; type?: string; includeDomains?: string }) => {
|
|
const { StateStore } = await import("./core/state-store.js");
|
|
const { ExaSearch } = await import("./upgrades/exa-search.js");
|
|
|
|
const apiKey = requireExaApiKey();
|
|
const exa = new ExaSearch(apiKey, new StateStore());
|
|
|
|
console.log(`\n Searching: ${query}\n`);
|
|
const result = await exa.search({
|
|
query,
|
|
numResults: opts.numResults ?? 5,
|
|
type: (opts.type as "keyword" | "neural" | "auto") ?? "auto",
|
|
includeDomains: opts.includeDomains?.split(",").map((d) => d.trim()),
|
|
});
|
|
console.log(exa.formatResults(result));
|
|
console.log(`\n Credits used: ${result.costCredits}\n`);
|
|
});
|
|
|
|
exa
|
|
.command("company <name>")
|
|
.description("Research a company via Exa AI")
|
|
.action(async (name: string) => {
|
|
const { ExaSearch } = await import("./upgrades/exa-search.js");
|
|
|
|
const apiKey = requireExaApiKey();
|
|
const exa = new ExaSearch(apiKey);
|
|
|
|
console.log(`\n Researching: ${name}\n`);
|
|
const result = await exa.companyResearch({ name });
|
|
console.log(exa.formatCompany(result));
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Skills Sync ──────────────────────────────────────────────
|
|
|
|
skills
|
|
.command("sync")
|
|
.description("Sync SKILLS/ markdown files with the TS registry")
|
|
.action(async () => {
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
const { SkillSync } = await import("./upgrades/skill-sync.js");
|
|
|
|
const agent = new MetaAgent();
|
|
const syncer = new SkillSync(agent.skillRegistry);
|
|
const result = syncer.sync();
|
|
|
|
console.log(`\n Skill sync complete:`);
|
|
console.log(` Imported: ${result.imported.created} created, ${result.imported.updated} updated`);
|
|
console.log(` Exported: ${result.exported} written to SKILLS/`);
|
|
if (result.errors.length > 0) {
|
|
console.log(` Errors: ${result.errors.length}`);
|
|
for (const err of result.errors.slice(0, 3)) {
|
|
console.log(` ${err}`);
|
|
}
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Demo ─────────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("demo")
|
|
.description("Run the full compounding demonstration (search → learn → sharpen → report)")
|
|
.option("-q, --query <q>", "Search query for the demo")
|
|
.action(async (opts: { query?: string }) => {
|
|
const { ExaSearch } = await import("./upgrades/exa-search.js");
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { SkillSync } = await import("./upgrades/skill-sync.js");
|
|
|
|
const apiKey = requireExaApiKey();
|
|
const exa = new ExaSearch(apiKey);
|
|
const agent = new EnhancedMetaAgent();
|
|
|
|
const query = opts.query ?? "self-improving AI agent systems loop engineering 2026";
|
|
console.log(`\n Demo: Research → Learn → Sharpen → Compound\n`);
|
|
|
|
// Phase 1: Research
|
|
console.log(` [1/5] Researching: "${query}"`);
|
|
const searchResults = await exa.search({ query, numResults: 3 });
|
|
for (const r of searchResults.results) {
|
|
console.log(` ✓ ${r.title}`);
|
|
}
|
|
|
|
// Phase 2: Register
|
|
console.log(` [2/5] Registering skill from research...`);
|
|
try {
|
|
agent.skillRegistry.createFromTemplate({
|
|
name: "Research Skill",
|
|
description: `Knowledge from: ${query}`,
|
|
steps: [
|
|
{ label: "Research", instruction: "Search for patterns", expectedOutcome: "Research complete" },
|
|
{ label: "Extract", instruction: "Extract key patterns", expectedOutcome: "Patterns extracted" },
|
|
{ label: "Apply", instruction: "Apply patterns", expectedOutcome: "Applied" },
|
|
],
|
|
tags: ["research", "demo"],
|
|
});
|
|
console.log(` ✓ Skill registered`);
|
|
} catch { console.log(` ○ Skill already exists`); }
|
|
|
|
// Phase 3: Loop
|
|
console.log(` [3/5] Running feedback loop...`);
|
|
const loopResult = await agent.run(query, { loopIterations: 5 });
|
|
console.log(` ✓ ${loopResult.iterations} iterations, converged=${loopResult.converged}, rubric=${loopResult.rubricScore ?? "?"}`);
|
|
|
|
// Phase 4: Sharpen
|
|
console.log(` [4/5] Sharpening skills...`);
|
|
const reviews = agent.base.skillSharpener.reviewAll();
|
|
for (const r of reviews) {
|
|
console.log(` ✓ ${r.skillId}: ${r.suggestedChanges.length} suggestions`);
|
|
}
|
|
|
|
// Phase 5: Report
|
|
console.log(` [5/5] System state after 1 compound cycle:\n`);
|
|
const status = agent.getStatus();
|
|
console.log(` Skills: ${status.skills.total}`);
|
|
console.log(` Knowledge base: ${status.knowledge.entries} entries`);
|
|
console.log(` Knowledge tags: ${status.knowledge.tags.slice(0, 5).join(", ")}`);
|
|
console.log(` Episodic memory: ${status.memory?.episodicCount ?? 0} sessions`);
|
|
console.log(` Semantic memory: ${status.memory?.semanticCount ?? 0} insights`);
|
|
console.log(` Context budget: ${status.context.pct}% used`);
|
|
console.log(``);
|
|
console.log(` Compounds every run. The harness, not the model.\n`);
|
|
});
|
|
|
|
// ── Learned ──────────────────────────────────────────────────
|
|
|
|
program
|
|
.command("learned")
|
|
.description("Show everything the system has learned across all storage layers")
|
|
.option("-v, --verbose", "Show full content instead of summaries")
|
|
.action(async (opts: { verbose?: boolean }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const agent = new EnhancedMetaAgent();
|
|
const status = agent.getStatus();
|
|
const memory = agent.memory.getStats();
|
|
|
|
console.log(`\n╔════════════════════════════════════════════╗`);
|
|
console.log(`║ SYSTEM KNOWLEDGE REPORT ║`);
|
|
console.log(`╚════════════════════════════════════════════╝\n`);
|
|
|
|
// Section 1: Skills
|
|
console.log(`Skills: ${status.skills.total} total, ${status.skills.withHistory} with execution history`);
|
|
for (const skill of agent.skillRegistry.list()) {
|
|
console.log(` ${skill.name} v${skill.version}`);
|
|
console.log(` Steps: ${skill.steps.length}`);
|
|
console.log(` Executions: ${skill.metrics.totalExecutions}`);
|
|
console.log(` Quality: ${(skill.metrics.avgQualityScore * 100).toFixed(0)}%`);
|
|
console.log(` Evolutions: ${skill.metrics.evolutionCount}`);
|
|
if (opts.verbose && skill.history.length > 0) {
|
|
console.log(` Recent results:`);
|
|
for (const h of skill.history.slice(-3)) {
|
|
console.log(` ${h.completedAt.slice(0, 10)} q=${h.qualityScore.toFixed(2)} ${h.success ? "pass" : "fail"}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 2: Knowledge Base
|
|
console.log(`\nKnowledge Base: ${status.knowledge.entries} entries`);
|
|
const tags = agent.stateRepository.getTags();
|
|
if (tags.length > 0) {
|
|
console.log(` Tags: ${tags.join(", ")}`);
|
|
}
|
|
|
|
// Section 3: Memory
|
|
console.log(`\nPersistent Memory:`);
|
|
console.log(` Episodic: ${memory.episodicCount} sessions (what happened)`);
|
|
console.log(` Semantic: ${memory.semanticCount} insights (what it means)`);
|
|
console.log(` Procedural: ${memory.proceduralCount} recipes (how to do it)`);
|
|
|
|
// Section 4: Sessions
|
|
const sessions = agent.session.list();
|
|
console.log(`\nSessions: ${sessions.length} total`);
|
|
const recent = sessions.slice(0, 3);
|
|
for (const s of recent) {
|
|
const age = Math.round((Date.now() - new Date(s.createdAt).getTime()) / 60000);
|
|
console.log(` ${s.id.slice(0, 8)}… ${s.status.padEnd(12)} ${age}m ago "${s.task.slice(0, 50)}"`);
|
|
}
|
|
|
|
// Section 5: Compound metrics
|
|
console.log(`\nCompounding:`);
|
|
console.log(` Runs: ${status.compoundIterations}`);
|
|
console.log(` Total skills: ${status.skills.total}`);
|
|
console.log(` Total entries: ${status.knowledge.entries + memory.episodicCount + memory.semanticCount + memory.proceduralCount}`);
|
|
|
|
// Section 6: Cost (if data exists)
|
|
const { CostTracker } = await import("./upgrades/cost-tracker.js");
|
|
const tracker = new CostTracker(agent.store);
|
|
const costSummary = tracker.getSummary(168); // 7 days
|
|
if (costSummary.totalSpend > 0) {
|
|
console.log(`\nCost (7 days): $${costSummary.totalSpend.toFixed(2)}`);
|
|
for (const [model, data] of Object.entries(costSummary.byModel)) {
|
|
console.log(` ${model}: ${data.calls} calls, $${data.cost.toFixed(4)}`);
|
|
}
|
|
} else {
|
|
console.log(`\nCost: No model calls recorded yet`);
|
|
}
|
|
|
|
console.log(``);
|
|
console.log(` Every run compounds. The harness, not the model.\n`);
|
|
});
|
|
|
|
// ── Familiar ────────────────────────────────────────────────
|
|
|
|
const familiar = program
|
|
.command("familiar")
|
|
.description("Familiar knowledge base — inbox-to-wiki processing");
|
|
|
|
familiar
|
|
.command("capture <note>")
|
|
.description("Quick capture — drop a note into inbox")
|
|
.option("-s, --source <src>", "Source label (voice, web, idea, etc.)")
|
|
.action(async (note: string, opts: { source?: string }) => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const f = new FamiliarKnowledge();
|
|
const path = f.quickCapture(note, opts.source);
|
|
console.log(`\n ✓ Captured to: ${path}\n`);
|
|
});
|
|
|
|
familiar
|
|
.command("process")
|
|
.description("Process all inbox notes into wiki pages")
|
|
.action(async () => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const f = new FamiliarKnowledge();
|
|
const notes = f.scanInbox();
|
|
|
|
if (notes.length === 0) {
|
|
console.log(` No notes in inbox/ to process.\n`);
|
|
return;
|
|
}
|
|
|
|
// Default classifier — generates basic wiki pages from inbox content
|
|
const classifier = (content: string) => {
|
|
const firstLine = content.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? "Untitled";
|
|
const tags = ["inbox"];
|
|
if (content.toLowerCase().includes("loop")) tags.push("loops");
|
|
if (content.toLowerCase().includes("agent")) tags.push("agent");
|
|
if (content.toLowerCase().includes("safety")) tags.push("safety");
|
|
|
|
return {
|
|
title: firstLine,
|
|
summary: content.split("\n").slice(1, 3).join(" ").trim().slice(0, 300),
|
|
confidence: 0.5,
|
|
tags,
|
|
mentions: [],
|
|
contradictions: [],
|
|
};
|
|
};
|
|
|
|
const processed = f.processAllInbox(classifier);
|
|
f.autoCommit(`familiar: processed ${processed.length} inbox notes`);
|
|
console.log(`\n Processed ${processed.length} notes:\n`);
|
|
for (const p of processed) {
|
|
console.log(` ✓ ${p.frontmatter.title}`);
|
|
console.log(` Tags: ${p.frontmatter.tags.join(", ")}`);
|
|
console.log(` Wiki: ${p.wikiPath}`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
familiar
|
|
.command("graph")
|
|
.description("Show knowledge graph")
|
|
.action(async () => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const f = new FamiliarKnowledge();
|
|
const graph = f.buildGraphIndex();
|
|
console.log(`\n Knowledge Graph:`);
|
|
console.log(` ${graph.nodes.length} pages, ${graph.edges.length} connections\n`);
|
|
for (const node of graph.nodes) {
|
|
const edges = graph.edges.filter((e) => e.source === node.id);
|
|
console.log(` ${node.title}`);
|
|
for (const e of edges) console.log(` → ${e.target}`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
familiar
|
|
.command("health")
|
|
.description("Audit wiki health — detect orphaned pages, low confidence, missing backlinks")
|
|
.action(async () => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const f = new FamiliarKnowledge();
|
|
const health = f.graphHealth();
|
|
console.log(`\n Graph Health:\n`);
|
|
console.log(` Pages: ${health.stats.totalPages}`);
|
|
console.log(` Edges: ${health.stats.totalConnections}`);
|
|
console.log(` Orphaned: ${health.stats.orphanedPages}`);
|
|
console.log(` No backlinks: ${health.stats.pagesWithoutBacklinks}`);
|
|
console.log(` Low confidence: ${health.stats.pagesWithLowConfidence}\n`);
|
|
if (health.issues.length > 0) {
|
|
console.log(` Issues:`);
|
|
for (const issue of health.issues.slice(0, 5)) {
|
|
console.log(` • ${issue}`);
|
|
}
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
familiar
|
|
.command("briefing")
|
|
.description("Generate daily briefing page")
|
|
.action(async () => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const f = new FamiliarKnowledge();
|
|
const briefing = f.generateBriefing();
|
|
console.log(`\n${briefing}\n`);
|
|
});
|
|
|
|
familiar
|
|
.command("loop")
|
|
.description("Process inbox notes, detect simple contradictions, and queue follow-up goals")
|
|
.option("-p, --priority <n>", "Queued goal priority (1=high, 2=normal, 3=low)", parseInt, 1)
|
|
.option("-d, --dir <path>", "Base directory containing inbox/wiki/resources", process.cwd())
|
|
.action(async (opts: { priority?: number; dir?: string }) => {
|
|
const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js");
|
|
const { GoalQueue } = await import("./upgrades/goal-queue.js");
|
|
const { StateStore } = await import("./core/state-store.js");
|
|
const f = new FamiliarKnowledge(opts.dir);
|
|
const queue = new GoalQueue(new StateStore());
|
|
const priorClaims = loadWikiClaims(opts.dir ?? process.cwd());
|
|
const priority = opts.priority === 2 || opts.priority === 3 ? opts.priority : 1;
|
|
|
|
const processed = f.processAllInbox((content) => {
|
|
const firstLine = content.split("\n")[0]?.replace(/^#\s*/, "").trim() || "Untitled note";
|
|
const claims = splitClaims(content);
|
|
const contradictions = claims.flatMap((claim) => detectSimpleContradictions(claim, priorClaims));
|
|
return {
|
|
title: firstLine.slice(0, 80),
|
|
summary: claims.join("; ").slice(0, 300),
|
|
confidence: contradictions.length > 0 ? 0.9 : 0.6,
|
|
tags: contradictions.length > 0 ? ["second-brain", "contradiction"] : ["second-brain"],
|
|
mentions: [],
|
|
backlinks: [],
|
|
contradictions,
|
|
};
|
|
});
|
|
|
|
const queued: string[] = [];
|
|
for (const note of processed) {
|
|
for (const contradiction of note.contradictions) {
|
|
queued.push(queue.push(`Resolve contradiction in ${path.basename(note.wikiPath)}: ${contradiction}`, {
|
|
priority,
|
|
tags: ["second-brain", "contradiction"],
|
|
}).id);
|
|
}
|
|
}
|
|
|
|
console.log(`\n Familiar loop`);
|
|
console.log(` Processed: ${processed.length} note(s)`);
|
|
console.log(` Queued: ${queued.length} contradiction goal(s)`);
|
|
if (queued.length > 0) console.log(` Goal IDs: ${queued.slice(0, 3).join(", ")}`);
|
|
console.log(``);
|
|
});
|
|
|
|
// ── PAI Pi ───────────────────────────────────────────────────
|
|
|
|
const paiPi = program
|
|
.command("pai-pi")
|
|
.description("PAI Pi integration (PAI via Pi executor)");
|
|
|
|
paiPi
|
|
.command("status")
|
|
.description("Show PAI Pi integration status")
|
|
.action(async () => {
|
|
const { paiPiReport } = await import("./pai/pai-pi-bridge.js");
|
|
console.log(`\n${paiPiReport()}\n`);
|
|
});
|
|
|
|
paiPi
|
|
.command("skills")
|
|
.description("Import PAI Pi skills into FA skill registry")
|
|
.action(async () => {
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
const { importPaiPiSkills } = await import("./pai/pai-pi-bridge.js");
|
|
const agent = new MetaAgent();
|
|
const result = importPaiPiSkills(agent.skillRegistry);
|
|
console.log(`\n PAI Pi → FA Skill Import:`);
|
|
console.log(` Created: ${result.created}`);
|
|
if (result.errors.length > 0) console.log(` Errors: ${result.errors.length}`);
|
|
console.log(``);
|
|
});
|
|
|
|
paiPi
|
|
.command("run <goal>")
|
|
.description("Run a task through PAI Pi system prompt + Pi executor")
|
|
.option("-l, --loop <n>", "Number of feedback loop iterations", parseInt)
|
|
.action(async (goal: string, opts: { loop?: number }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { PiExecutor } = await import("./examples/executors/pi-executor.js");
|
|
const { createPiExecutorForPAI } = await import("./pai/pai-pi-bridge.js");
|
|
|
|
const paiPiConfig = createPiExecutorForPAI();
|
|
const agent = new EnhancedMetaAgent({ selfValidate: false });
|
|
|
|
// Inject PAI Pi system prompt into context
|
|
agent.context.add({
|
|
source: "pai-pi",
|
|
content: paiPiConfig.systemPrompt.slice(0, 2000),
|
|
priority: "critical",
|
|
tokenCount: paiPiConfig.systemPrompt.length,
|
|
});
|
|
|
|
// Create Pi executor with PAI Pi config
|
|
const executor = new PiExecutor({
|
|
provider: paiPiConfig.provider,
|
|
model: paiPiConfig.model,
|
|
systemPrompt: paiPiConfig.systemPrompt.slice(0, 500),
|
|
});
|
|
|
|
// Run through FA's loop with Pi as the executor
|
|
const result = await agent.run(goal, {
|
|
executor,
|
|
loopIterations: opts.loop ?? 8,
|
|
});
|
|
|
|
console.log(`\n PAI Pi → FA Engine:`);
|
|
console.log(` Goal: ${goal}`);
|
|
console.log(` Provider: ${paiPiConfig.provider}`);
|
|
console.log(` Model: ${paiPiConfig.model}`);
|
|
console.log(` Iterations: ${result.iterations}`);
|
|
console.log(` Converged: ${result.converged}`);
|
|
if (result.rubricScore) console.log(` Rubric: ${result.rubricScore}/100`);
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Daemon ──────────────────────────────────────────────────
|
|
|
|
const daemon = program
|
|
.command("daemon")
|
|
.description("Persistent 24/7 autonomous daemon");
|
|
|
|
daemon
|
|
.command("start")
|
|
.description("Start the daemon")
|
|
.option("-d, --detach", "Run in background (detach from terminal)")
|
|
.action(async (opts: { detach?: boolean }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { GoalQueue } = await import("./upgrades/goal-queue.js");
|
|
const { DaemonEngine } = await import("./upgrades/daemon-engine.js");
|
|
|
|
if (opts.detach) {
|
|
// Fork a child process for background operation
|
|
const { spawn } = await import("node:child_process");
|
|
const child = spawn(
|
|
process.execPath,
|
|
["dist/index.js", "daemon", "start"],
|
|
{ detached: true, stdio: "ignore", env: { ...process.env, FABLE_DAEMON: "1" } }
|
|
);
|
|
child.unref();
|
|
const fs = await import("node:fs");
|
|
const path = await import("node:path");
|
|
const pidDir = path.join(process.env.FABLE_DATA_DIR || process.env.HOME || ".", ".fable-agent");
|
|
try { fs.mkdirSync(pidDir, { recursive: true }); } catch {}
|
|
fs.writeFileSync(path.join(pidDir, "daemon.pid"), String(child.pid));
|
|
console.log(`\n Daemon started in background (PID: ${child.pid})`);
|
|
console.log(` Use \`fable-agent daemon stop\` to stop it.\n`);
|
|
return;
|
|
}
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const queue = new GoalQueue(agent.store);
|
|
const engine = new DaemonEngine(agent, queue);
|
|
|
|
console.log(`\n Starting daemon...`);
|
|
console.log(` Queue: ${queue.stats().queued} queued, ${queue.stats().completed} completed`);
|
|
console.log(` Run \`fable-agent daemon queue <goal>\` in another terminal to add goals`);
|
|
console.log(` Press Ctrl+C to stop\n`);
|
|
|
|
// Handle graceful shutdown
|
|
process.on("SIGINT", () => {
|
|
console.log(`\n Stopping daemon...`);
|
|
engine.stop();
|
|
});
|
|
|
|
await engine.start();
|
|
});
|
|
|
|
daemon
|
|
.command("stop")
|
|
.description("Request daemon to stop")
|
|
.action(async () => {
|
|
// Check for PID file first (detached daemon)
|
|
const { readFileSync, existsSync, rmSync } = await import("node:fs");
|
|
const pidPath = (await import("node:path")).join(
|
|
process.env.FABLE_DATA_DIR || process.env.HOME || ".", ".fable-agent", "daemon.pid"
|
|
);
|
|
if (existsSync(pidPath)) {
|
|
try {
|
|
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
process.kill(pid, "SIGTERM");
|
|
rmSync(pidPath);
|
|
console.log(` Daemon (PID ${pid}) stopped.`);
|
|
return;
|
|
} catch (err) {
|
|
console.log(` Could not stop via PID file: ${err}`);
|
|
}
|
|
}
|
|
// Fallback: write stop signal
|
|
const store = new (await import("./core/state-store.js")).StateStore();
|
|
store.write("daemon", "stop-signal.json", { timestamp: new Date().toISOString() });
|
|
console.log(` Stop signal sent. The daemon will stop after the current goal.`);
|
|
});
|
|
|
|
daemon
|
|
.command("status")
|
|
.description("Show daemon status")
|
|
.action(async () => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { GoalQueue } = await import("./upgrades/goal-queue.js");
|
|
const { DaemonEngine } = await import("./upgrades/daemon-engine.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const queue = new GoalQueue(agent.store);
|
|
const engine = new DaemonEngine(agent, queue);
|
|
|
|
console.log(`\n${engine.getReport()}\n`);
|
|
});
|
|
|
|
daemon
|
|
.command("queue <goal>")
|
|
.description("Add a goal to the daemon queue")
|
|
.option("-p, --priority <n>", "Priority (1=high, 2=normal, 3=low)", parseInt)
|
|
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
.action(async (goal: string, opts: { priority?: number; tags?: string }) => {
|
|
const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js");
|
|
const { GoalQueue } = await import("./upgrades/goal-queue.js");
|
|
|
|
const agent = new EnhancedMetaAgent();
|
|
const queue = new GoalQueue(agent.store);
|
|
|
|
const queued = queue.push(goal, {
|
|
priority: (opts.priority ?? 2) as 1 | 2 | 3,
|
|
tags: opts.tags?.split(",").map((t: string) => t.trim()) ?? [],
|
|
});
|
|
|
|
console.log(`\n ✓ Goal queued:`);
|
|
console.log(` ID: ${queued.id}`);
|
|
console.log(` Goal: ${queued.description.slice(0, 60)}`);
|
|
console.log(` Priority: ${queued.priority}`);
|
|
const stats = queue.stats();
|
|
console.log(` Queue: ${stats.queued} pending, ${stats.completed} completed\n`);
|
|
});
|
|
|
|
// ── Factory ─────────────────────────────────────────────────
|
|
|
|
const factory = program
|
|
.command("factory")
|
|
.description("Factory/VPS status and deployment guards");
|
|
|
|
async function printFactoryCheck(): Promise<{ ready: boolean }> {
|
|
const { checkFactory, factoryGateReady, formatFactoryCheck } = await import("./fable5/factory-status.js");
|
|
const result = await checkFactory();
|
|
console.log(formatFactoryCheck(result.factory, result.deploy, result.errors));
|
|
return { ready: factoryGateReady(result) };
|
|
}
|
|
|
|
factory
|
|
.command("check")
|
|
.description("Check factory status and deploy-webhook health")
|
|
.action(async () => {
|
|
await printFactoryCheck();
|
|
});
|
|
|
|
factory
|
|
.command("capabilities")
|
|
.description("Probe the factory capability registry")
|
|
.option("--file <path>", "Capability registry path", "factory-capabilities.yaml")
|
|
.action(async (opts: { file?: string }) => {
|
|
const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js");
|
|
const services = loadFactoryCapabilities(path.resolve(opts.file ?? "factory-capabilities.yaml"));
|
|
const report = await probeFactoryCapabilities(services);
|
|
console.log(formatCapabilityReport(report));
|
|
if (!report.requiredOk) process.exit(1);
|
|
});
|
|
|
|
factory
|
|
.command("gate <task>")
|
|
.description("Run factory check plus local ZTE/repo verification gate")
|
|
.option("--repo <path>", "Repo path to verify", ".")
|
|
.option("--diagnostic-only", "Bypass live factory/deploy readiness failures for non-deploy diagnostics")
|
|
.action(async (task: string, opts: { repo?: string; diagnosticOnly?: boolean }) => {
|
|
const { spawnSync } = await import("node:child_process");
|
|
const repo = path.resolve(opts.repo ?? ".");
|
|
const status = await printFactoryCheck();
|
|
if (!status.ready && !opts.diagnosticOnly) {
|
|
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
|
|
const reason = "live factory/deploy status is not ok";
|
|
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory check"], reason));
|
|
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(` ZTE Gate: ${task}`);
|
|
console.log(` Repo: ${repo}`);
|
|
console.log(``);
|
|
|
|
const args = [process.argv[1], "fable5", "verify", task, "--repo", repo];
|
|
const check = spawnSync(process.execPath, args, { cwd: repo, stdio: "inherit" });
|
|
if (check.status !== 0) process.exit(check.status ?? 1);
|
|
console.log(` ✓ Factory gate passed; deploy still requires explicit token/contract.`);
|
|
console.log(``);
|
|
});
|
|
|
|
// ── Forgejo ─────────────────────────────────────────────────
|
|
|
|
const forgejo = program
|
|
.command("forgejo")
|
|
.description("Forgejo intake and GitOps task receipts");
|
|
|
|
forgejo
|
|
.command("intake <ref>")
|
|
.description("Fetch a Forgejo issue, PR, or wiki page into a scanned local task receipt")
|
|
.option("--repo <repo>", "Repo slug for numeric issue refs, e.g. org/repo")
|
|
.option("--out <path>", "Receipt output directory", ".fable/tasks")
|
|
.option("--dry-run", "Print receipt JSON without writing a file")
|
|
.action(async (ref: string, opts: { repo?: string; out?: string; dryRun?: boolean }) => {
|
|
try {
|
|
const { intakeForgejoItem } = await import("./fable5/forgejo-intake.js");
|
|
const result = await intakeForgejoItem({
|
|
ref,
|
|
repo: opts.repo,
|
|
outDir: opts.out,
|
|
dryRun: opts.dryRun,
|
|
baseUrl: process.env.FORGEJO_URL,
|
|
token: process.env.FORGEJO_TOKEN,
|
|
});
|
|
if (opts.dryRun) console.log(JSON.stringify(result.receipt, null, 2));
|
|
else console.log(` ✓ Forgejo intake receipt written: ${result.path}`);
|
|
if (result.receipt.receipts.quarantine) {
|
|
console.error(` ! Receipt quarantined for human review; no runnable task created.`);
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error(` ✗ Forgejo intake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// ── Fable 5 ─────────────────────────────────────────────────
|
|
|
|
const fable = program
|
|
.command("fable5")
|
|
.description("Fable 5 self-improving system commands (14-step architecture)");
|
|
|
|
fable
|
|
.command("status")
|
|
.description("Show Fable 5 stack status")
|
|
.action(async () => {
|
|
const { CompoundStack } = await import("./fable5/compound-stack.js");
|
|
const stack = new CompoundStack();
|
|
console.log(`\n${stack.getStatus()}\n`);
|
|
});
|
|
|
|
fable
|
|
.command("route <task>")
|
|
.description("Route a task through the model cost-capability matrix")
|
|
.option("-d, --domain <domain>", "Task domain: code|research|analysis|creative|planning|grading")
|
|
.action(async (task: string, opts: { domain?: string }) => {
|
|
const { ModelRouter } = await import("./fable5/model-router.js");
|
|
const router = new ModelRouter();
|
|
const complexity = router.taskComplexity(task);
|
|
const domain = (opts.domain ?? "code") as "code" | "research" | "analysis" | "creative" | "planning" | "grading";
|
|
|
|
const route = router.route({ task, domain: domain as never, complexity, requiresVision: false });
|
|
|
|
console.log(`\n Model Routing for: "${task.slice(0, 60)}..."`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Complexity: ${complexity}`);
|
|
console.log(` Domain: ${domain}`);
|
|
console.log(` `);
|
|
console.log(` Orchestrator: ${route.primary.displayName} (${route.primary.tier})`);
|
|
console.log(` Worker: ${route.fallback.displayName} (${route.fallback.tier})`);
|
|
console.log(` Grader: ${route.grader.displayName} (${route.grader.tier})`);
|
|
console.log(` Reason: ${route.reason}`);
|
|
console.log(` `);
|
|
|
|
if (complexity === "extreme" || complexity === "complex") {
|
|
console.log(` Estimated cost (100K in / 20K out):`);
|
|
console.log(` Primary: $${router.estimateCost(route, 100000, 20000).toFixed(2)}`);
|
|
const cheapRoute = router.route({ task, domain: domain as never, complexity: "simple", requiresVision: false });
|
|
console.log(` If downgraded: $${router.estimateCost(cheapRoute, 100000, 20000).toFixed(2)}`);
|
|
console.log(` `);
|
|
}
|
|
});
|
|
|
|
fable
|
|
.command("models")
|
|
.description("List all models in the routing matrix")
|
|
.action(async () => {
|
|
const { ModelRouter } = await import("./fable5/model-router.js");
|
|
const router = new ModelRouter();
|
|
const models = router.listModels();
|
|
|
|
console.log(`\n Model Routing Matrix (${models.length} models):`);
|
|
console.log(` ─────────────────────────────`);
|
|
for (const m of models) {
|
|
console.log(` ${m.modelId.padEnd(24)} ${m.tier.padEnd(8)} $${String(m.costPer1kIn).padStart(3)}/1K in $${String(m.costPer1kOut).padStart(3)}/1K out`);
|
|
console.log(` ${" ".repeat(26)}${m.recommendedFor.join(", ")}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
fable
|
|
.command("flue")
|
|
.description("Show Flue-inspired interop gaps without vendoring Flue")
|
|
.action(async () => {
|
|
const { formatFlueInteropReport } = await import("./fable5/flue-interop.js");
|
|
console.log(formatFlueInteropReport());
|
|
});
|
|
|
|
fable
|
|
.command("verify <task>")
|
|
.description("Run independent verifier against a task")
|
|
.option("--repo <path>", "Also run the repo verification gate at this path")
|
|
.action(async (task: string, opts: { repo?: string }) => {
|
|
if (opts.repo) {
|
|
const { spawnSync } = await import("node:child_process");
|
|
const repo = path.resolve(opts.repo);
|
|
const checks: Array<[string, string[]]> = [
|
|
["npm", ["run", "-s", "test"]],
|
|
["npm", ["run", "-s", "build"]],
|
|
["npm", ["run", "-s", "docs:check"]],
|
|
["npx", ["tsc", "--noEmit"]],
|
|
[process.execPath, [process.argv[1], "security", "scan", repo]],
|
|
];
|
|
|
|
console.log(`
|
|
Repo Verification Gate: ${repo}`);
|
|
for (const [cmd, args] of checks) {
|
|
console.log(`
|
|
$ ${[cmd, ...args].join(" ")}`);
|
|
// ponytail: Windows npm/npx shims need cmd.exe; commands are fixed, no user shell input.
|
|
const check = process.platform === "win32" && (cmd === "npm" || cmd === "npx")
|
|
? spawnSync("cmd.exe", ["/d", "/s", "/c", [cmd, ...args].join(" ")], { cwd: repo, stdio: "inherit" })
|
|
: spawnSync(cmd, args, { cwd: repo, stdio: "inherit" });
|
|
if (check.status !== 0) {
|
|
console.error(`
|
|
✗ Repo verification failed`);
|
|
process.exit(check.status ?? 1);
|
|
}
|
|
}
|
|
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
|
|
const receipt = createZteReceipt(task, repo, "passed", checks.map(([cmd, args]) => [cmd, ...args].join(" ")));
|
|
const receiptPath = appendZteReceipt(repo, receipt);
|
|
console.log(`
|
|
✓ Repo verification passed`);
|
|
console.log(` ✓ ZTE receipt written: ${receiptPath}
|
|
`);
|
|
}
|
|
|
|
const { IndependentVerifier } = await import("./fable5/independent-verifier.js");
|
|
const verifier = new IndependentVerifier();
|
|
|
|
const dummyIteration = {
|
|
number: 1,
|
|
phase: "refine" as const,
|
|
plan: `Plan for: ${task}`,
|
|
executed: `Executing: ${task}`,
|
|
observation: `Observing results...`,
|
|
reflection: `Task completed: ${task}`,
|
|
refinement: `Refined approach based on reflection`,
|
|
metrics: { durationMs: 1000, successRate: 0.9, qualityScore: 0.7, improvementDelta: 0.1 },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
const result = verifier.verify(dummyIteration, task);
|
|
|
|
console.log(`\n Independent Verifier Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Verdict: ${result.verdict}`);
|
|
console.log(` `);
|
|
console.log(` Criteria:`);
|
|
for (const c of result.criteriaResults) {
|
|
console.log(` ${c.passed ? "✓" : "✗"} ${c.criterionId}: ${c.evidence}`);
|
|
}
|
|
if (result.gaps.length > 0) {
|
|
console.log(` `);
|
|
console.log(` Gaps:`);
|
|
for (const g of result.gaps) console.log(` • ${g}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
fable
|
|
.command("goal <text>")
|
|
.description("Set a /goal with rubric criteria and run evaluation")
|
|
.option("-i, --iterations <n>", "Max iterations", parseInt)
|
|
.option("-s, --min-score <n>", "Minimum score to pass", parseFloat)
|
|
.action(async (text: string, opts: { iterations?: number; minScore?: number }) => {
|
|
const { GoalPattern } = await import("./fable5/goal-pattern.js");
|
|
const { FeedbackLoop } = await import("./tier2-primitives/loops/feedback-loop.js");
|
|
const { StateStore } = await import("./core/state-store.js");
|
|
|
|
const goal = new GoalPattern();
|
|
const goalId = goal.setGoal({
|
|
text,
|
|
criteria: [
|
|
{ label: "Correctness", description: "Output is technically correct", weight: 0.4, required: true },
|
|
{ label: "Completeness", description: "All requirements addressed", weight: 0.3, required: true },
|
|
{ label: "Clarity", description: "Output is clear", weight: 0.2, required: false },
|
|
{ label: "Efficiency", description: "Solution is efficient", weight: 0.1, required: false },
|
|
],
|
|
maxIterations: opts.iterations ?? 5,
|
|
minScore: opts.minScore ?? 0.7,
|
|
});
|
|
|
|
const store = new StateStore();
|
|
const loop = new FeedbackLoop(store, `goal-${goalId}`);
|
|
|
|
console.log(`\n /goal: "${text}"`);
|
|
console.log(` ID: ${goalId}`);
|
|
console.log(` ─────────────────────────────`);
|
|
|
|
for (let i = 0; i < (opts.iterations ?? 5); i++) {
|
|
const iteration = {
|
|
number: i + 1,
|
|
phase: "refine" as const,
|
|
plan: `Iteration ${i + 1} plan`,
|
|
executed: `Executing iteration ${i + 1}`,
|
|
observation: `Observing...`,
|
|
reflection: `Completed iteration ${i + 1}`,
|
|
refinement: `Refined approach`,
|
|
metrics: { durationMs: 500, successRate: 0.8 + i * 0.02, qualityScore: 0.3 + i * 0.1, improvementDelta: 0.05 },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
loop.getAccumulator().record(iteration);
|
|
const result = goal.evaluateIteration(goalId, iteration, loop.getAccumulator());
|
|
|
|
console.log(` Iteration ${i + 1}: score=${result.score.toFixed(2)} ${result.done ? "✓" : "→"}`);
|
|
|
|
if (result.done) {
|
|
console.log(` `);
|
|
console.log(` ✓ Goal ${result.reason.toLowerCase().includes("score") ? "converged" : "reached max iterations"}`);
|
|
console.log(` Final score: ${result.score.toFixed(2)}`);
|
|
console.log(` Reason: ${result.reason}`);
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
}
|
|
console.log(` `);
|
|
console.log(` ○ Goal did not converge within ${opts.iterations ?? 5} iterations`);
|
|
console.log(` `);
|
|
});
|
|
|
|
fable
|
|
.command("zte <task>")
|
|
.description("Generate a Zero-Touch Engineering spec with verifier gates")
|
|
.option("--repo <path>", "Repo path for validation commands", ".")
|
|
.option("--out <file>", "Write spec markdown to a file")
|
|
.action(async (task: string, opts: { repo?: string; out?: string }) => {
|
|
const { createZteSpec } = await import("./fable5/zte-protocol.js");
|
|
const spec = createZteSpec(task, { repo: opts.repo ?? "." });
|
|
|
|
if (opts.out) {
|
|
fs.writeFileSync(path.resolve(opts.out), spec.markdown);
|
|
console.log(` ✓ ZTE spec written: ${path.resolve(opts.out)}`);
|
|
return;
|
|
}
|
|
|
|
console.log(spec.markdown);
|
|
});
|
|
|
|
fable
|
|
.command("worktree")
|
|
.description("Manage git worktree isolation")
|
|
.argument("<action>", "list|create|remove|prune")
|
|
.argument("[name]", "Worktree name")
|
|
.action(async (action: string, name: string) => {
|
|
const { WorktreeManager } = await import("./fable5/worktree-isolation.js");
|
|
const wt = new WorktreeManager();
|
|
|
|
switch (action) {
|
|
case "list": {
|
|
const trees = wt.list();
|
|
if (trees.length === 0) {
|
|
console.log(" No worktrees found.");
|
|
return;
|
|
}
|
|
console.log(`\n Worktrees (${trees.length}):`);
|
|
for (const t of trees) {
|
|
console.log(` ${t.name.padEnd(20)} ${t.branch.padEnd(30)} ${t.isDirty ? "dirty" : "clean"}`);
|
|
}
|
|
console.log(` `);
|
|
break;
|
|
}
|
|
case "create": {
|
|
if (!name) { console.error(" ✗ Name required"); process.exit(1); }
|
|
const spec = wt.createForAgent(name);
|
|
const path = wt.createAndCheckout(spec);
|
|
console.log(`\n ✓ Worktree created:`);
|
|
console.log(` Name: ${spec.name}`);
|
|
console.log(` Branch: ${spec.branch}`);
|
|
console.log(` Path: ${path}`);
|
|
console.log(` `);
|
|
break;
|
|
}
|
|
case "remove": {
|
|
if (!name) { console.error(" ✗ Name required"); process.exit(1); }
|
|
wt.remove(name);
|
|
console.log(` ✓ Worktree removed: ${name}`);
|
|
break;
|
|
}
|
|
case "prune":
|
|
wt.prune();
|
|
console.log(" ✓ Worktrees pruned");
|
|
break;
|
|
default:
|
|
console.error(` ✗ Unknown action: ${action}. Use: list|create|remove|prune`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
fable
|
|
.command("state <project>")
|
|
.description("Manage 5-stage state file")
|
|
.option("--add-fact <text>", "Add a verified fact")
|
|
.option("--add-rule <text>", "Add a general rule")
|
|
.option("--add-failure <text>", "Add a failure entry")
|
|
.option("--add-lesson <text>", "Add a lesson learned")
|
|
.option("--import", "Import from knowledge base")
|
|
.action(async (project: string, opts: Record<string, unknown>) => {
|
|
const { FiveStageStateFile } = await import("./fable5/state-file-5stage.js");
|
|
const state = new FiveStageStateFile();
|
|
|
|
if (opts.addFact) {
|
|
state.addVerifiedFact(project, opts.addFact as string, "cli");
|
|
console.log(` ✓ Verified fact added to "${project}"`);
|
|
} else if (opts.addRule) {
|
|
state.addGeneralRule(project, opts.addRule as string, "cli");
|
|
console.log(` ✓ General rule added to "${project}"`);
|
|
} else if (opts.addFailure) {
|
|
state.addFailure(project, opts.addFailure as string);
|
|
console.log(` ✓ Failure added to "${project}"`);
|
|
} else if (opts.addLesson) {
|
|
state.addLesson(project, opts.addLesson as string);
|
|
console.log(` ✓ Lesson added to "${project}"`);
|
|
} else if (opts.import) {
|
|
const { MetaAgent } = await import("./tier3-compounding/meta-agent.js");
|
|
const agent = new MetaAgent();
|
|
const entries = agent.stateRepository.query({ limit: 50 });
|
|
const count = state.importFromKnowledgeEntries(project, entries);
|
|
console.log(` ✓ Imported ${count} knowledge entries into "${project}" state file`);
|
|
} else {
|
|
const loaded = state.load(project);
|
|
if (!loaded) {
|
|
console.log(` No state file found for "${project}". Use --add-fact/--add-rule to create one.`);
|
|
return;
|
|
}
|
|
console.log(`\n State File: ${project}`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Facts: ${loaded.verifiedFacts.length}`);
|
|
console.log(` Rules: ${loaded.generalRules.length}`);
|
|
console.log(` Fails: ${loaded.openFailures.length}`);
|
|
console.log(` Lessons: ${loaded.lessonsLearned.length}`);
|
|
if (loaded.lastSession.summary) {
|
|
console.log(` Last: ${loaded.lastSession.summary.slice(0, 80)}`);
|
|
}
|
|
console.log(` Updated: ${loaded.lastUpdated.slice(0, 16)}`);
|
|
console.log(` `);
|
|
}
|
|
});
|
|
|
|
fable
|
|
.command("workflow <pattern> <task>")
|
|
.description("Run a dynamic workflow pattern: fan-out|adversarial|loop|godmode|parseltongue")
|
|
.option("-s, --subtasks <list>", "Comma-separated sub-tasks for fan-out pattern")
|
|
.option("-n, --max-iterations <n>", "Max iterations for loop-until-done", parseInt)
|
|
.option("-p, --panel <slug>", "GodMode panel: sprint-3|sprint-5|gauntlet-10|ultra-5tier", "sprint-3")
|
|
.option("--category <name>", "Parseltongue category filter")
|
|
.option("--intensity <level>", "Parseltongue intensity filter: low|medium|high")
|
|
.action(async (pattern: string, task: string, opts: { subtasks?: string; maxIterations?: number; panel?: string; category?: string; intensity?: string }) => {
|
|
const { DynamicWorkflows } = await import("./fable5/dynamic-workflows.js");
|
|
const { IndependentVerifier } = await import("./fable5/independent-verifier.js");
|
|
|
|
switch (pattern) {
|
|
case "fan-out": {
|
|
if (!opts.subtasks) {
|
|
console.error(" ✗ --subtasks required for fan-out pattern");
|
|
process.exit(1);
|
|
}
|
|
const subTasks = opts.subtasks.split(",").map((s) => s.trim());
|
|
const wf = new DynamicWorkflows();
|
|
|
|
const executor = (subTask: string, _i: number, routeContext?: { flow?: "direct" | "planning" | "review"; reason?: string }): LoopIteration => ({
|
|
number: _i + 1,
|
|
phase: "execute" as const,
|
|
plan: `Executing sub-task: ${subTask}`,
|
|
executed: `Completed: ${subTask}`,
|
|
observation: `Sub-task results: ${subTask}`,
|
|
reflection: `Sub-task ${_i + 1} done${routeContext?.flow ? ` (${routeContext.flow})` : ""}`,
|
|
refinement: routeContext?.reason ?? "",
|
|
metrics: { durationMs: 100, successRate: 0.9, qualityScore: 0.7, improvementDelta: 0.05 },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
const synthesizer = (results: LoopIteration[], _task: string): LoopIteration => ({
|
|
number: results.length + 1,
|
|
phase: "refine" as const,
|
|
plan: `Synthesizing ${results.length} results for: ${_task}`,
|
|
executed: `Synthesized: ${results.map((r) => r.observation).join("; ")}`,
|
|
observation: `Synthesis complete for: ${_task}`,
|
|
reflection: `All ${results.length} sub-tasks completed`,
|
|
refinement: `Final synthesis done`,
|
|
metrics: { durationMs: 200, successRate: 0.95, qualityScore: 0.8, improvementDelta: 0.1 },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
const result = await wf.fanOutAndSynthesize(task, subTasks, executor, synthesizer);
|
|
|
|
console.log(`\n Fan-Out-and-Synthesize Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Sub-tasks: ${subTasks.length}`);
|
|
console.log(` Iterations: ${result.iterations.length}`);
|
|
console.log(` Synthesis verdict: ${result.synthesisVerdict.verdict}`);
|
|
if (result.routeContexts.length > 0) {
|
|
const counts = result.routeContexts.reduce(
|
|
(acc, rc) => {
|
|
acc[rc.flow] = (acc[rc.flow] ?? 0) + 1;
|
|
return acc;
|
|
},
|
|
{} as Record<string, number>,
|
|
);
|
|
console.log(` Route flows: direct=${counts.direct ?? 0}, planning=${counts.planning ?? 0}, review=${counts.review ?? 0}`);
|
|
}
|
|
console.log(` `);
|
|
if (result.synthesisVerdict.gaps.length > 0) {
|
|
console.log(` Gaps:`);
|
|
for (const g of result.synthesisVerdict.gaps) console.log(` • ${g}`);
|
|
console.log(` `);
|
|
}
|
|
break;
|
|
}
|
|
case "adversarial": {
|
|
const wf = new DynamicWorkflows();
|
|
const makerIteration: LoopIteration = {
|
|
number: 1,
|
|
phase: "execute" as const,
|
|
plan: `Executing: ${task}`,
|
|
executed: `Completed implementation of: ${task}`,
|
|
observation: `Implementation done for: ${task}`,
|
|
reflection: `Task completed`,
|
|
refinement: "",
|
|
metrics: { durationMs: 500, successRate: 0.85, qualityScore: 0.7, improvementDelta: 0.05 },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
const result = await wf.adversarialVerify(makerIteration, task);
|
|
|
|
console.log(`\n Adversarial Verification Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Maker iteration: #${result.maker.number}`);
|
|
console.log(` Verifier verdict: ${result.verifier.verdict}`);
|
|
console.log(` Passed: ${result.passed ? "✓" : "✗"}`);
|
|
console.log(` `);
|
|
for (const c of result.verifier.criteriaResults) {
|
|
console.log(` ${c.passed ? "✓" : "✗"} ${c.criterionId}: ${c.evidence}`);
|
|
}
|
|
if (result.verifier.gaps.length > 0) {
|
|
console.log(` `);
|
|
console.log(` Gaps: ${result.gapSummary}`);
|
|
}
|
|
console.log(` `);
|
|
break;
|
|
}
|
|
case "loop": {
|
|
const wf = new DynamicWorkflows();
|
|
const executor = (_i: number, _prev: LoopIteration | null): LoopIteration => ({
|
|
number: _i + 1,
|
|
phase: "refine" as const,
|
|
plan: `Iteration ${_i + 1} of: ${task}`,
|
|
executed: `Executed iteration ${_i + 1}`,
|
|
observation: `Observations from iteration ${_i + 1}`,
|
|
reflection: `Completed iteration ${_i + 1}`,
|
|
refinement: `Refined approach for next iteration`,
|
|
metrics: { durationMs: 200, successRate: 0.7 + _i * 0.03, qualityScore: 0.3 + _i * 0.08, improvementDelta: 0.05 },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
const result = await wf.loopUntilDone(task, executor, {
|
|
maxIterations: opts.maxIterations ?? 5,
|
|
convergenceThreshold: 0.75,
|
|
});
|
|
|
|
console.log(`\n Loop-Until-Done Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: ${task.slice(0, 60)}`);
|
|
console.log(` Total rounds: ${result.totalRounds}`);
|
|
console.log(` Final verdict: ${result.finalVerdict.verdict}`);
|
|
console.log(` `);
|
|
for (const c of result.finalVerdict.criteriaResults) {
|
|
console.log(` ${c.passed ? "✓" : "✗"} ${c.criterionId}: ${c.evidence}`);
|
|
}
|
|
console.log(` `);
|
|
break;
|
|
}
|
|
case "godmode": {
|
|
const { GodModeClassic } = await import("./upgrades/godmode-classic.js");
|
|
const wf = new DynamicWorkflows();
|
|
wf.enableGodMode(new GodModeClassic({
|
|
panelSlug: opts.panel as any,
|
|
callModel: async (modelId, prompt) => [
|
|
`Model: ${modelId}`,
|
|
`Task: ${prompt}`,
|
|
"",
|
|
"Candidate:",
|
|
"- Identify the requested outcome.",
|
|
"- Produce a concise implementation plan.",
|
|
"- Verify with deterministic checks before reporting completion.",
|
|
].join("\n"),
|
|
}));
|
|
const result = await wf.godmodeRace(task, opts.panel as any);
|
|
|
|
console.log(`\n GodMode Workflow Results`);
|
|
console.log(` ========================`);
|
|
console.log(` Task: ${task.slice(0, 60)}`);
|
|
console.log(` Mode: ${result.raceType}`);
|
|
console.log(` Winner: ${result.winner.modelId} (${(result.winner.qualityScore * 100).toFixed(0)}/100)`);
|
|
console.log(` Runners: ${result.racers.length}/${result.config.parallelCount}`);
|
|
console.log(` Duration: ${result.totalDurationMs}ms`);
|
|
console.log(``);
|
|
console.log(result.winner.output.slice(0, 1200));
|
|
console.log(``);
|
|
break;
|
|
}
|
|
case "parseltongue": {
|
|
const wf = new DynamicWorkflows();
|
|
const { ContentSafetyGate } = await import("./upgrades/content-safety-gate.js");
|
|
const gate = new ContentSafetyGate();
|
|
const result = await wf.parseltongueTest(
|
|
task,
|
|
(perturbed: string) => gate.evaluate(perturbed).action !== "allow",
|
|
{
|
|
category: opts.category as any,
|
|
intensity: opts.intensity as any,
|
|
},
|
|
);
|
|
|
|
console.log(`\n Parseltongue Workflow Results`);
|
|
console.log(` =============================`);
|
|
console.log(` Tests: ${result.summary.totalTests}`);
|
|
console.log(` Bypasses: ${result.summary.bypassesDetected}`);
|
|
if (result.summary.gateWeaknesses.length > 0) {
|
|
console.log(` Gate weaknesses: ${result.summary.gateWeaknesses.join("; ")}`);
|
|
}
|
|
console.log(``);
|
|
for (const item of result.results) {
|
|
const status = item.bypassed ? "BYPASS" : "BLOCKED";
|
|
console.log(` ${status.padEnd(7)} ${item.category.padEnd(13)} ${item.intensity.padEnd(6)} ${item.techniqueName}`);
|
|
}
|
|
console.log(``);
|
|
break;
|
|
}
|
|
default:
|
|
console.error(` ✗ Unknown pattern: ${pattern}. Use: fan-out|adversarial|loop|godmode|parseltongue`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
fable
|
|
.command("compound <lesson>")
|
|
.description("Write a lesson into the most relevant PAI skill")
|
|
.option("--skill <name>", "Target skill name (auto-detect if omitted)")
|
|
.action(async (lesson: string, opts: { skill?: string }) => {
|
|
const { SkillSync } = await import("./pai/skill-sync.js");
|
|
const ss = new SkillSync();
|
|
|
|
if (opts.skill) {
|
|
const path = ss.compoundLesson(opts.skill, lesson, "cli");
|
|
if (!path) {
|
|
console.error(` ✗ Skill "${opts.skill}" not found`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\n ✓ Lesson compounded into "${opts.skill}"`);
|
|
console.log(` Path: ${path}`);
|
|
console.log(` Lesson: ${lesson.slice(0, 80)}`);
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
|
|
const relevant = ss.findRelevantSkill(lesson);
|
|
if (!relevant) {
|
|
console.log(` No relevant PAI skill found for this lesson.`);
|
|
console.log(` Use --skill <name> to target a specific skill.`);
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
|
|
const path = ss.compoundLesson(relevant.name, lesson, "cli");
|
|
if (path) {
|
|
console.log(`\n ✓ Lesson compounded into "${relevant.name}" (score: ${relevant.score})`);
|
|
console.log(` Path: ${path}`);
|
|
console.log(` `);
|
|
}
|
|
});
|
|
|
|
// ── Tier 3: Multi-Agent Orchestration ──────────────────────
|
|
|
|
fable
|
|
.command("chain <task>")
|
|
.description("Run agent chain: Scout → Plan → Build → Review")
|
|
.option("--stages <list>", "Comma-separated stages (scout,plan,build,review)")
|
|
.action(async (task: string, opts: { stages?: string }) => {
|
|
const { AgentChain } = await import("./fable5/agent-chains.js");
|
|
const chain = new AgentChain();
|
|
const stages = opts.stages?.split(",").map((s) => s.trim()) as import("./fable5/agent-chains.js").ChainStage[] | undefined;
|
|
const result = await chain.run(task, stages);
|
|
|
|
console.log(`\n Agent Chain Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: "${task.slice(0, 60)}..."`);
|
|
console.log(` Duration: ${result.totalDurationMs}ms`);
|
|
console.log(` Passed: ${result.passed ? "✓" : "✗"}`);
|
|
console.log(` `);
|
|
for (const a of result.artifacts) {
|
|
console.log(` ${a.stage.toUpperCase().padEnd(10)} ${a.modelName.padEnd(30)} ${a.durationMs}ms`);
|
|
}
|
|
for (const v of result.verifications) {
|
|
console.log(` Verdict: ${v.verdict} (${v.criteriaResults.filter((c) => c.passed).length}/${v.criteriaResults.length} criteria passed)`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
fable
|
|
.command("meta <task>")
|
|
.description("Meta-agent: configure and execute optimal sub-agent team")
|
|
.option("--stages <list>", "Chain stages for execution")
|
|
.action(async (task: string, opts: { stages?: string }) => {
|
|
const { MetaAgent } = await import("./fable5/meta-agent.js");
|
|
const meta = new MetaAgent();
|
|
|
|
if (opts.stages) {
|
|
const stages = opts.stages.split(",").map((s) => s.trim()) as import("./fable5/agent-chains.js").ChainStage[];
|
|
const result = await meta.run(task, { stages });
|
|
console.log(`\n Meta-Agent Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` ${result.config.rationale}`);
|
|
console.log(` Chain passed: ${result.chainResult.passed ? "✓" : "✗"}`);
|
|
console.log(` `);
|
|
for (const a of result.chainResult.artifacts) {
|
|
console.log(` ${a.stage.toUpperCase().padEnd(10)} ${a.modelName.padEnd(30)} ${a.durationMs}ms`);
|
|
}
|
|
console.log(` `);
|
|
} else {
|
|
const config = meta.configure(task);
|
|
console.log(`\n Meta-Agent Configuration`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` ${config.rationale}`);
|
|
console.log(` `);
|
|
console.log(` Sub-agents:`);
|
|
for (const a of config.config.subAgents) {
|
|
console.log(` ${a.name.padEnd(14)} ${a.modelTier.padEnd(8)} ${a.role.slice(0, 50)}`);
|
|
console.log(` ${" ".repeat(14)} Tools: ${a.tools.join(", ")}`);
|
|
if (a.domainLock) console.log(` ${" ".repeat(14)} Domain: ${a.domainLock}`);
|
|
console.log(` `);
|
|
}
|
|
}
|
|
});
|
|
|
|
fable
|
|
.command("teams <task>")
|
|
.description("Run 3-tier agent team: orchestrator → leads → workers")
|
|
.option("--workers <list>", "Comma-separated worker types (frontend,backend,qa,security,devops)")
|
|
.action(async (task: string, opts: { workers?: string }) => {
|
|
const { AgentTeams } = await import("./fable5/agent-teams.js");
|
|
const teams = new AgentTeams();
|
|
const workers = opts.workers?.split(",").map((s) => s.trim());
|
|
const result = await teams.run(task, workers);
|
|
|
|
console.log(`\n Agent Team Results`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: "${task.slice(0, 60)}..."`);
|
|
console.log(` Passed: ${result.passed ? "✓" : "✗"}`);
|
|
console.log(` Duration: ${result.totalDurationMs}ms`);
|
|
console.log(` `);
|
|
console.log(` Orchestrator:`);
|
|
console.log(` ${result.config.orchestrator.name} (${result.config.orchestrator.tier})`);
|
|
console.log(` `);
|
|
console.log(` Leads:`);
|
|
for (const l of result.config.leads) {
|
|
console.log(` ${l.name.padEnd(20)} ${l.tier}`);
|
|
}
|
|
console.log(` `);
|
|
console.log(` Workers:`);
|
|
for (const w of result.config.workers) {
|
|
console.log(` ${w.name.padEnd(14)} ${w.tier.padEnd(8)} Lock: ${w.domainLock}`);
|
|
}
|
|
console.log(` `);
|
|
console.log(` Verification: ${result.verification?.verdict ?? "N/A"}`);
|
|
for (const c of result.verification?.criteriaResults ?? []) {
|
|
console.log(` ${c.passed ? "✓" : "✗"} ${c.criterionId}: ${c.evidence}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── Stack ──────────────────────────────────────────────────
|
|
|
|
fable
|
|
.command("stack <task>")
|
|
.description("Run the full compound stack (all layers + lifecycle hooks)")
|
|
.option("-p, --project <name>", "Project name for state file", "default")
|
|
.option("--enable-worktrees", "Enable worktree isolation")
|
|
.option("--enable-vision", "Enable vision self-check")
|
|
.option("--enable-workflows", "Enable dynamic workflows")
|
|
.option("--enable-fusion", "Enable fusion panel layer")
|
|
.option("--enable-plinius", "Enable Plinius-inspired upgrade layers")
|
|
.action(async (task: string, opts: { project?: string; enableWorktrees?: boolean; enableVision?: boolean; enableWorkflows?: boolean; enableFusion?: boolean; enablePlinius?: boolean }) => {
|
|
const { CompoundStack } = await import("./fable5/compound-stack.js");
|
|
const enablePlinius = opts.enablePlinius ?? false;
|
|
const stack = new CompoundStack({
|
|
worktree: opts.enableWorktrees ?? false,
|
|
visionCheck: opts.enableVision ?? false,
|
|
dynamicWorkflows: opts.enableWorkflows ?? false,
|
|
fusionPanel: (opts.enableFusion ?? false) || enablePlinius,
|
|
godModeRace: enablePlinius,
|
|
ultraPlinian: enablePlinius,
|
|
parseltongue: enablePlinius,
|
|
autoTune: enablePlinius,
|
|
stmModules: enablePlinius,
|
|
abliteration: enablePlinius,
|
|
promptObservatory: enablePlinius,
|
|
promptLiberation: enablePlinius,
|
|
});
|
|
const p = await stack.run(task, opts.project ?? "default");
|
|
|
|
console.log(`\n Fable 5 Compound Stack Run`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: "${task.slice(0, 60)}..."`);
|
|
console.log(` Session: ${p.sessionId.slice(0, 16)}...`);
|
|
console.log(` `);
|
|
|
|
if (p.layers["safety-boundary"]?.startsWith("BLOCKED")) {
|
|
console.log(` ✗ ${p.layers["safety-boundary"]}`);
|
|
console.log(` `);
|
|
process.exit(1);
|
|
}
|
|
|
|
const layerOrder = [
|
|
"lifecycle",
|
|
"model-router",
|
|
"safety-boundary",
|
|
"goal-pattern",
|
|
"verifier",
|
|
"dynamic-workflows",
|
|
"fusion-panel",
|
|
"godmode-race",
|
|
"ultra-plinian",
|
|
"parseltongue",
|
|
"auto-tune",
|
|
"stm-modules",
|
|
"abliteration-awareness",
|
|
"transparency",
|
|
"prompt-observatory",
|
|
"prompt-liberation",
|
|
"state-file",
|
|
"worktree",
|
|
"vision-check",
|
|
"skill-compounding",
|
|
];
|
|
for (const layer of layerOrder) {
|
|
if (p.layers[layer]) {
|
|
console.log(` ${layer}: ${p.layers[layer]}`);
|
|
}
|
|
}
|
|
|
|
if (p.modelRoute) {
|
|
console.log(` `);
|
|
console.log(` Cost estimate (100K in / 20K out): $${stack.modelRouter.estimateCost(p.modelRoute, 100000, 20000).toFixed(2)}`);
|
|
}
|
|
|
|
// Write exit summary
|
|
await stack.writeExit(`Stack run completed for: ${task.slice(0, 60)}`, [
|
|
"Review stack output for correctness",
|
|
"Update state file with any new facts",
|
|
]);
|
|
|
|
console.log(` `);
|
|
console.log(` ✓ Stack run complete`);
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── RPC ─────────────────────────────────────────────────────
|
|
|
|
fable
|
|
.command("rpc")
|
|
.description("Start a JSONL TCP RPC server for Fable 5 commands")
|
|
.option("-p, --port <n>", "TCP port to listen on (default: 18902)", parseInt)
|
|
.action(async (opts: { port?: number }) => {
|
|
const { startRpcServer } = await import("./fable5/rpc-server.js");
|
|
await startRpcServer(opts.port ?? 18902);
|
|
});
|
|
|
|
// ── Fusion ───────────────────────────────────────────────────
|
|
|
|
const fusion = program
|
|
.command("fusion")
|
|
.description("Multi-model fusion panel — draft → critique → fuse");
|
|
|
|
fusion
|
|
.command("run <task>")
|
|
.description("Run a task through a fusion panel of models")
|
|
.option("-p, --panel <slug>", "Panel: sonnet-opus, opus4.8-4.8, opus4.8-gpt5.5, opus4.8-gpt5.5-gemini, sonnet-haiku-opus, openrouter-fusion, free-gemini-mistral, free-gemini-deepseek, free-omni", "sonnet-opus")
|
|
.option("-j, --judge <model>", "Override judge model")
|
|
.option("--openrouter", "Use OpenRouter Fusion API instead of local panel dispatch")
|
|
.action(async (task: string, opts: { panel?: string; judge?: string; openrouter?: boolean }) => {
|
|
if (opts.openrouter) {
|
|
const { OpenRouterFusionExecutor } = await import("./examples/executors/openrouter-fusion-executor.js");
|
|
const executor = new OpenRouterFusionExecutor({ apiKey: process.env.OPENROUTER_API_KEY });
|
|
|
|
if (!executor.isAvailable()) {
|
|
console.error(" ✗ OpenRouter Fusion requires OPENROUTER_API_KEY env var");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n OpenRouter Fusion API`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: ${task}`);
|
|
console.log(` `);
|
|
|
|
const result = await executor.fuse(task);
|
|
console.log(result);
|
|
console.log(` `);
|
|
return;
|
|
}
|
|
|
|
const { FusionExecutor } = await import("./examples/executors/fusion-executor.js");
|
|
const { callClaudeCli } = await import("./core/claude-cli-caller.js");
|
|
const executor = new FusionExecutor({
|
|
panel: (opts.panel as any) ?? "sonnet-opus",
|
|
judgeModelOverride: opts.judge,
|
|
callModel: (model, prompt) => callClaudeCli(model, prompt),
|
|
verbose: true,
|
|
});
|
|
|
|
console.log(`\n Fusion Panel: ${opts.panel ?? "sonnet-opus"}`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Task: ${task}`);
|
|
console.log(` `);
|
|
|
|
const result = await executor.fuse(task, opts.panel as any);
|
|
|
|
console.log(` Panel: ${result.panelSlug} (${result.panelSize} models)`);
|
|
console.log(` Duration: ${(result.totalDurationMs / 1000).toFixed(1)}s`);
|
|
console.log(` `);
|
|
|
|
for (let i = 0; i < result.panelists.length; i++) {
|
|
const p = result.panelists[i];
|
|
const status = p.error ? "✗" : "✓";
|
|
console.log(` Panelist ${i + 1} (${p.modelId}): ${status} ${p.durationMs}ms`);
|
|
}
|
|
|
|
console.log(` `);
|
|
console.log(` Final Answer:`);
|
|
console.log(` ${result.finalAnswer}`);
|
|
console.log(` `);
|
|
});
|
|
|
|
fusion
|
|
.command("panels")
|
|
.description("List available fusion panel configurations")
|
|
.action(async () => {
|
|
const { FUSION_PANELS } = await import("./core/fusion-types.js");
|
|
console.log(`\n Available Fusion Panels:`);
|
|
console.log(` ─────────────────────────────`);
|
|
for (const [slug, config] of Object.entries(FUSION_PANELS)) {
|
|
console.log(` ${slug}`);
|
|
console.log(` Models: ${config.modelIds.join(", ")}`);
|
|
console.log(` Judge: ${config.judgeModel}`);
|
|
console.log(` `);
|
|
}
|
|
});
|
|
|
|
// ── Generate Media ──────────────────────────────────────────
|
|
|
|
const media = program
|
|
.command("generate")
|
|
.description("Generate media (image/video) via fal.ai — CineFable integration");
|
|
|
|
media
|
|
.command("image <prompt>")
|
|
.description("Generate an image from text prompt")
|
|
.option("-m, --model <id>", "fal.ai model endpoint", "fal-ai/nano-banana-pro")
|
|
.option("--aspect <ratio>", "Aspect ratio")
|
|
.option("-r, --reference <url>", "Reference image URL for edit mode")
|
|
.action(async (prompt: string, opts: { model?: string; aspect?: string; reference?: string }) => {
|
|
const { MediaGenerator } = await import("./upgrades/media-generator.js");
|
|
const generator = new MediaGenerator({ engine: process.env.FAL_KEY ? "fal-ai" : "mock" });
|
|
|
|
console.log(`\n Generate Image`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Prompt: ${prompt}`);
|
|
console.log(` `);
|
|
|
|
const result = await generator.generateImage({
|
|
prompt,
|
|
model: opts.model,
|
|
aspectRatio: opts.aspect,
|
|
referenceImages: opts.reference ? [opts.reference] : undefined,
|
|
});
|
|
|
|
if (result.error) {
|
|
console.log(` Status: ${result.error}`);
|
|
} else {
|
|
console.log(` Status: ✓ ${result.durationMs}ms`);
|
|
console.log(` URL: ${result.url}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
media
|
|
.command("video <source>")
|
|
.description("Generate a video from a source image")
|
|
.option("-m, --model <id>", "fal.ai model endpoint", "alibaba/happy-horse/image-to-video")
|
|
.option("-p, --prompt <text>", "Animation prompt")
|
|
.option("-d, --duration <n>", "Duration in seconds", parseInt)
|
|
.option("--720p", "Use 720p resolution")
|
|
.action(async (source: string, opts: { model?: string; prompt?: string; duration?: number; "720p"?: boolean }) => {
|
|
const { MediaGenerator } = await import("./upgrades/media-generator.js");
|
|
const generator = new MediaGenerator({ engine: process.env.FAL_KEY ? "fal-ai" : "mock" });
|
|
|
|
console.log(`\n Generate Video`);
|
|
console.log(` ─────────────────────────────`);
|
|
console.log(` Source: ${source}`);
|
|
console.log(` `);
|
|
|
|
const result = await generator.generateVideo({
|
|
sourceImage: source,
|
|
model: opts.model,
|
|
prompt: opts.prompt,
|
|
duration: opts.duration,
|
|
resolution: opts["720p"] ? "720p" : "480p",
|
|
});
|
|
|
|
if (result.error) {
|
|
console.log(` Status: ${result.error}`);
|
|
} else {
|
|
console.log(` Status: ✓ ${result.durationMs}ms`);
|
|
console.log(` URL: ${result.url}`);
|
|
}
|
|
console.log(` `);
|
|
});
|
|
|
|
// ── Parse ───────────────────────────────────────────────────
|
|
|
|
// -- Plinius Integrations ------------------------------------------------------
|
|
|
|
const plinius = program
|
|
.command("plinius")
|
|
.description("Safe local integrations inspired by G0DM0D3, OBLITERATUS, L1B3RT4S, and CL4R1T4S");
|
|
|
|
plinius
|
|
.command("godmode <task>")
|
|
.description("Race model candidates with GodMode Classic semantics")
|
|
.option("-p, --panel <slug>", "Panel: sprint-3, sprint-5, gauntlet-10, ultra-5tier", "sprint-3")
|
|
.option("-m, --mode <mode>", "Override race mode: first-past-post, best-quality, weighted-ensemble")
|
|
.option("--proxy", "Use the local OpenAI-compatible proxy instead of mock local responses")
|
|
.option("-v, --verbose", "Show runner-level details")
|
|
.action(async (task: string, opts: { panel?: string; mode?: string; proxy?: boolean; verbose?: boolean }) => {
|
|
const { GODMODE_PANELS } = await import("./core/fusion-types.js");
|
|
const { GodModeClassic } = await import("./upgrades/godmode-classic.js");
|
|
|
|
const panel = opts.panel ?? "sprint-3";
|
|
if (!Object.prototype.hasOwnProperty.call(GODMODE_PANELS, panel)) {
|
|
console.error(` Error: unknown GodMode panel "${panel}"`);
|
|
console.error(` Available: ${Object.keys(GODMODE_PANELS).join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const raceModes = ["first-past-post", "best-quality", "weighted-ensemble"];
|
|
if (opts.mode && !raceModes.includes(opts.mode)) {
|
|
console.error(` Error: unknown race mode "${opts.mode}"`);
|
|
console.error(` Available: ${raceModes.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let callModel: (modelId: string, prompt: string, maxTokens: number) => Promise<string>;
|
|
|
|
if (opts.proxy) {
|
|
const { ProxyClient } = await import("./pai/proxy-client.js");
|
|
const proxy = new ProxyClient();
|
|
if (!(await proxy.health())) {
|
|
console.error(" Error: local proxy is not reachable at http://127.0.0.1:18901");
|
|
process.exit(1);
|
|
}
|
|
callModel = async (modelId, prompt, maxTokens) => {
|
|
const response = await proxy.complete(prompt, modelId, {
|
|
maxTokens,
|
|
temperature: 0.4,
|
|
});
|
|
return response.content;
|
|
};
|
|
} else {
|
|
callModel = async (modelId, prompt) => [
|
|
`Model: ${modelId}`,
|
|
`Task: ${prompt}`,
|
|
"",
|
|
"Candidate:",
|
|
"- Identify the requested outcome.",
|
|
"- Produce a concise implementation plan.",
|
|
"- Verify with deterministic checks before reporting completion.",
|
|
].join("\n");
|
|
}
|
|
|
|
const godmode = new GodModeClassic({
|
|
panelSlug: panel as any,
|
|
raceModeOverride: opts.mode as any,
|
|
callModel,
|
|
verbose: opts.verbose ?? false,
|
|
});
|
|
const result = await godmode.race(task);
|
|
|
|
console.log(`\n GodMode Classic`);
|
|
console.log(` =================`);
|
|
console.log(` Panel: ${panel}`);
|
|
console.log(` Mode: ${result.raceType}`);
|
|
console.log(` Winner: ${result.winner.modelId} (${(result.winner.qualityScore * 100).toFixed(0)}/100)`);
|
|
console.log(` Runners: ${result.racers.length}/${result.config.parallelCount}`);
|
|
console.log(` Duration: ${result.totalDurationMs}ms`);
|
|
console.log(``);
|
|
|
|
for (const racer of result.racers) {
|
|
const status = racer.error ? "ERROR" : "OK";
|
|
console.log(` ${status} ${racer.modelId}: ${(racer.qualityScore * 100).toFixed(0)}/100, ${racer.durationMs}ms`);
|
|
if (racer.error) {
|
|
console.log(` ${racer.error}`);
|
|
}
|
|
}
|
|
|
|
console.log(``);
|
|
console.log(result.winner.output.slice(0, 1200));
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("ultra <task>")
|
|
.description("Evaluate output with the UltraPlinian 5-tier scorer")
|
|
.option("-o, --output <text>", "Output to evaluate; defaults to the task text")
|
|
.action(async (task: string, opts: { output?: string }) => {
|
|
const { UltraPlinian } = await import("./upgrades/ultra-plinian.js");
|
|
const report = await new UltraPlinian().evaluate(task, opts.output ?? task);
|
|
|
|
console.log(`\n UltraPlinian Evaluation`);
|
|
console.log(` =======================`);
|
|
console.log(` Overall: ${(report.composite.overall * 100).toFixed(1)}/100`);
|
|
console.log(` Recommendation: ${report.recommendation}`);
|
|
console.log(``);
|
|
|
|
for (const tier of report.tierResults) {
|
|
console.log(` ${tier.tier.padEnd(5)} ${tier.modelId.padEnd(20)} ${(tier.score * 100).toFixed(1)}/100 - ${tier.rationale}`);
|
|
}
|
|
|
|
if (report.tierGaps.length > 0) {
|
|
console.log(``);
|
|
console.log(` Gaps:`);
|
|
for (const gap of report.tierGaps) console.log(` - ${gap}`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("parseltongue <input>")
|
|
.description("Run perturbation tests against the local content safety gate")
|
|
.option("-c, --category <category>", "encoding, injection, obfuscation, framing, logic_trap, adversarial")
|
|
.option("-i, --intensity <level>", "low, medium, high")
|
|
.action(async (input: string, opts: { category?: string; intensity?: string }) => {
|
|
const { Parseltongue } = await import("./upgrades/parseltongue.js");
|
|
const { ContentSafetyGate } = await import("./upgrades/content-safety-gate.js");
|
|
|
|
const categories = ["encoding", "injection", "obfuscation", "framing", "logic_trap", "adversarial"];
|
|
const intensities = ["low", "medium", "high"];
|
|
if (opts.category && !categories.includes(opts.category)) {
|
|
console.error(` Error: unknown category "${opts.category}"`);
|
|
process.exit(1);
|
|
}
|
|
if (opts.intensity && !intensities.includes(opts.intensity)) {
|
|
console.error(` Error: unknown intensity "${opts.intensity}"`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const parseltongue = new Parseltongue();
|
|
const gate = new ContentSafetyGate();
|
|
const report = await parseltongue.testGate(
|
|
input,
|
|
(candidate) => gate.evaluate(candidate).action !== "allow",
|
|
{
|
|
category: opts.category as any,
|
|
intensity: opts.intensity as any,
|
|
},
|
|
);
|
|
|
|
console.log(`\n Parseltongue Safety-Gate Test`);
|
|
console.log(` =============================`);
|
|
console.log(` Tests: ${report.summary.totalTests}`);
|
|
console.log(` Bypasses: ${report.summary.bypassesDetected}`);
|
|
console.log(``);
|
|
|
|
for (const result of report.results) {
|
|
const status = result.bypassed ? "BYPASS" : "BLOCKED";
|
|
console.log(` ${status.padEnd(7)} ${result.category.padEnd(13)} ${result.intensity.padEnd(6)} ${result.techniqueName}`);
|
|
}
|
|
|
|
if (report.summary.gateWeaknesses.length > 0) {
|
|
console.log(``);
|
|
console.log(` Gate weaknesses: ${report.summary.gateWeaknesses.join("; ")}`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("autotune <domain>")
|
|
.description("Replay rubric scores through the AutoTune sampling-parameter engine")
|
|
.option("-s, --scores <list>", "Comma-separated score list from 0 to 1", "0.35,0.55,0.72")
|
|
.action(async (domain: string, opts: { scores?: string }) => {
|
|
const { AutoTune } = await import("./upgrades/auto-tune.js");
|
|
const scores = (opts.scores ?? "")
|
|
.split(",")
|
|
.map((s) => Number.parseFloat(s.trim()))
|
|
.filter((n) => Number.isFinite(n))
|
|
.map((n) => Math.max(0, Math.min(1, n)));
|
|
|
|
if (scores.length === 0) {
|
|
console.error(" Error: provide at least one numeric score");
|
|
process.exit(1);
|
|
}
|
|
|
|
const tune = new AutoTune();
|
|
console.log(`\n AutoTune`);
|
|
console.log(` ========`);
|
|
console.log(` Domain: ${domain}`);
|
|
console.log(``);
|
|
|
|
for (const score of scores) {
|
|
const params = tune.update(score, domain);
|
|
console.log(` score=${score.toFixed(2)} -> temp=${params.temperature.toFixed(3)}, topP=${params.topP.toFixed(3)}, topK=${params.topK}`);
|
|
}
|
|
|
|
console.log(``);
|
|
console.log(tune.report());
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("stm <text>")
|
|
.description("Run Semantic Transformation Modules over text")
|
|
.option("-m, --modules <list>", "Comma-separated STM module order")
|
|
.option("--max-length <n>", "Max output length for concision module", parseInt)
|
|
.action(async (text: string, opts: { modules?: string; maxLength?: number }) => {
|
|
const { STMPipeline } = await import("./upgrades/stm-modules.js");
|
|
const pipeline = new STMPipeline();
|
|
|
|
if (opts.maxLength) {
|
|
pipeline.configure("concision", { maxLength: opts.maxLength });
|
|
}
|
|
if (opts.modules) {
|
|
const order = opts.modules.split(",").map((s) => s.trim()).filter(Boolean);
|
|
pipeline.setOrder(order as any);
|
|
}
|
|
|
|
const result = await pipeline.run(text);
|
|
console.log(`\n STM Pipeline`);
|
|
console.log(` ============`);
|
|
console.log(` Applied: ${result.modulesApplied.join(", ") || "none"}`);
|
|
console.log(``);
|
|
console.log(result.output);
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("refusal <output>")
|
|
.description("Analyze a model output for refusal patterns")
|
|
.option("-m, --model <id>", "Model ID", "unknown-model")
|
|
.option("-t, --task <text>", "Original task", "unspecified task")
|
|
.action(async (output: string, opts: { model?: string; task?: string }) => {
|
|
const { AbliterationAwareness } = await import("./upgrades/abliteration-awareness.js");
|
|
const awareness = new AbliterationAwareness();
|
|
const analysis = awareness.analyze(output, opts.model ?? "unknown-model", opts.task ?? "unspecified task");
|
|
|
|
console.log(`\n Refusal Analysis`);
|
|
console.log(` ================`);
|
|
if (!analysis.isRefusal || !analysis.refusal) {
|
|
console.log(` No refusal pattern detected.`);
|
|
console.log(``);
|
|
return;
|
|
}
|
|
|
|
const action = awareness.getAction(analysis.refusal);
|
|
console.log(` Category: ${analysis.refusal.refusalCategory}`);
|
|
console.log(` Confidence: ${(analysis.refusal.confidence * 100).toFixed(0)}%`);
|
|
console.log(` Action: ${action}`);
|
|
console.log(` Match: ${analysis.refusal.refusalMessage}`);
|
|
|
|
if (action === "retry_reformulated" || action === "escalate") {
|
|
console.log(``);
|
|
console.log(` Reformulation:`);
|
|
console.log(` ${awareness.generateReformulation(opts.task ?? "unspecified task", analysis.refusal.refusalCategory)}`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("observatory")
|
|
.description("Query the CL4R1T4S-inspired prompt observatory catalog")
|
|
.option("-p, --provider <name>", "Provider filter")
|
|
.option("-m, --model <name>", "Model filter")
|
|
.option("-t, --tag <tag>", "Tag filter")
|
|
.option("-l, --limit <n>", "Max entries", parseInt)
|
|
.action(async (opts: { provider?: string; model?: string; tag?: string; limit?: number }) => {
|
|
const { PromptObservatory } = await import("./upgrades/prompt-observatory.js");
|
|
const observatory = new PromptObservatory();
|
|
const results = observatory.query({
|
|
provider: opts.provider,
|
|
model: opts.model,
|
|
tag: opts.tag,
|
|
limit: opts.limit,
|
|
});
|
|
|
|
console.log(`\n Prompt Observatory`);
|
|
console.log(` ==================`);
|
|
console.log(` Entries: ${results.length}/${observatory.count()}`);
|
|
console.log(` Providers: ${observatory.getProviders().join(", ")}`);
|
|
console.log(``);
|
|
|
|
for (const entry of results) {
|
|
console.log(` ${entry.id}`);
|
|
console.log(` ${entry.provider} / ${entry.model}`);
|
|
console.log(` Tags: ${entry.tags.join(", ")}`);
|
|
console.log(` Snippet: ${entry.promptContent.slice(0, 140)}...`);
|
|
console.log(``);
|
|
}
|
|
});
|
|
|
|
plinius
|
|
.command("liberation")
|
|
.description("List prompt-transparency techniques without running extraction attacks")
|
|
.option("--min-success <n>", "Minimum estimated success rate from 0 to 1", parseFloat, 0.15)
|
|
.action(async (opts: { minSuccess?: number }) => {
|
|
const { PromptLiberation } = await import("./upgrades/prompt-liberation.js");
|
|
const liberation = new PromptLiberation();
|
|
const techniques = liberation.getEffectiveTechniques(opts.minSuccess ?? 0.15);
|
|
const prompts = liberation.getKnownPrompts({ limit: 5 });
|
|
|
|
console.log(`\n Prompt Liberation Metadata`);
|
|
console.log(` ==========================`);
|
|
console.log(` Known prompts: ${liberation.getKnownPrompts().length}`);
|
|
console.log(` Techniques at threshold: ${techniques.length}`);
|
|
console.log(``);
|
|
|
|
console.log(` Catalog sample:`);
|
|
for (const prompt of prompts) {
|
|
console.log(` - ${prompt.provider} / ${prompt.model}: ${liberation.analyzeConstraints(prompt.promptContent).join(", ") || "no constraints detected"}`);
|
|
}
|
|
|
|
console.log(``);
|
|
console.log(` Technique labels (payloads and instructions suppressed):`);
|
|
for (const technique of techniques) {
|
|
console.log(` - ${technique.name} (${(technique.successRate * 100).toFixed(0)}% estimated)`);
|
|
}
|
|
console.log(``);
|
|
});
|
|
|
|
plinius
|
|
.command("transparency <task>")
|
|
.description("Generate a local transparency report for a model-call plan")
|
|
.option("-m, --model <id>", "Model ID", "claude-opus-4-8")
|
|
.option("--save", "Save JSON report under ~/.fable-agent/transparency")
|
|
.action(async (task: string, opts: { model?: string; save?: boolean }) => {
|
|
const { createHash } = await import("node:crypto");
|
|
const { TransparencyModule } = await import("./upgrades/transparency-module.js");
|
|
const transparency = new TransparencyModule();
|
|
const modelId = opts.model ?? "claude-opus-4-8";
|
|
const knownPrompt = transparency.getKnownSystemPrompts().find((prompt) =>
|
|
prompt.model.toLowerCase() === modelId.toLowerCase()
|
|
|| prompt.label.toLowerCase().includes(modelId.toLowerCase())
|
|
);
|
|
const systemPrompt = knownPrompt?.content ?? "No known system prompt entry for this model.";
|
|
const sessionId = `transparency-${Date.now()}`;
|
|
const hash = createHash("sha256").update(systemPrompt).digest("hex");
|
|
|
|
transparency.record({
|
|
sessionId,
|
|
task,
|
|
modelUsed: modelId,
|
|
systemPromptHash: hash,
|
|
systemPromptSnippet: systemPrompt.slice(0, 200),
|
|
routingDecision: knownPrompt ? `matched ${knownPrompt.label}` : "no known prompt catalog match",
|
|
safetyGateVerdicts: [{ gate: "content-safety", action: "allow" }],
|
|
parameters: { temperature: 0.4, topP: 0.8, topK: 20, repetitionPenalty: 1 },
|
|
cost: 0,
|
|
durationMs: 0,
|
|
});
|
|
|
|
const report = transparency.generateReport(sessionId);
|
|
console.log(``);
|
|
console.log(transparency.formatReport(report));
|
|
console.log(``);
|
|
|
|
if (opts.save) {
|
|
console.log(` Saved: ${transparency.saveReport(report)}`);
|
|
console.log(``);
|
|
}
|
|
});
|
|
|
|
|
|
// ── Security ────────────────────────────────────────────────
|
|
|
|
const security = program
|
|
.command("security")
|
|
.description("Security scanners");
|
|
|
|
security
|
|
.command("scan <target>")
|
|
.description("Scan files for prompt-injection markers")
|
|
.option("--include-fixtures", "Include intentional red-team fixtures/generators")
|
|
.action(async (target: string, opts: { includeFixtures?: boolean }) => {
|
|
const { findPromptInjection } = await import("./core/prompt-injection-safety.js");
|
|
const root = path.resolve(target);
|
|
const files: string[] = [];
|
|
|
|
const walk = (file: string) => {
|
|
const st = fs.statSync(file);
|
|
if (st.isDirectory()) {
|
|
for (const name of fs.readdirSync(file)) {
|
|
if ([".git", "node_modules", "dist"].includes(name)) continue;
|
|
walk(path.join(file, name));
|
|
}
|
|
return;
|
|
}
|
|
if (/\.(test|spec)\.(ts|js)$/i.test(file)) return;
|
|
const rel = path.relative(root, file).replace(/\\/g, "/");
|
|
// ponytail: default scan ignores our own intentional prompt-injection generator; use --include-fixtures for audit mode.
|
|
if (!opts.includeFixtures && rel.endsWith("upgrades/parseltongue.ts")) return;
|
|
if (/\.(md|txt|ts|js|json|yaml|yml)$/i.test(file)) files.push(file);
|
|
};
|
|
|
|
walk(root);
|
|
let hits = 0;
|
|
for (const file of files) {
|
|
const found = findPromptInjection(fs.readFileSync(file, "utf-8")).filter((h) => h.kind !== "instruction-smuggling");
|
|
if (found.length === 0) continue;
|
|
hits += found.length;
|
|
console.log(`${file}: ${found.map((h) => `${h.kind}:${h.match}@${h.index}`).join(", ")}`);
|
|
}
|
|
|
|
if (hits > 0) {
|
|
console.error(`
|
|
✗ Prompt-injection marker(s) found: ${hits}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`
|
|
✓ No prompt-injection markers found in ${files.length} file(s)`);
|
|
});
|
|
|
|
function splitClaims(content: string): string[] {
|
|
return content
|
|
.split(/[.!?]\s+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 20);
|
|
}
|
|
|
|
function loadWikiClaims(baseDir: string): string[] {
|
|
const wikiDir = path.join(baseDir, "wiki");
|
|
if (!fs.existsSync(wikiDir)) return [];
|
|
return fs.readdirSync(wikiDir)
|
|
.filter((file) => file.endsWith(".md"))
|
|
.map((file) => fs.readFileSync(path.join(wikiDir, file), "utf-8").toLowerCase());
|
|
}
|
|
|
|
function detectSimpleContradictions(claim: string, prior: string[]): string[] {
|
|
const current = claim.toLowerCase();
|
|
const opposites: Array<[string, string]> = [
|
|
["always", "never"],
|
|
["enabled", "disabled"],
|
|
["online", "offline"],
|
|
["allow", "block"],
|
|
["increase", "decrease"],
|
|
];
|
|
return prior
|
|
.filter((entry) => opposites.some(([a, b]) =>
|
|
(entry.includes(a) && current.includes(b)) || (entry.includes(b) && current.includes(a))
|
|
))
|
|
.slice(0, 2)
|
|
.map((entry) => `Conflicts with prior: ${entry.slice(0, 140)}`);
|
|
}
|
|
|
|
program.parse(process.argv);
|
|
|
|
// Show help if no args
|
|
if (process.argv.length < 3) {
|
|
program.help();
|
|
}
|