fable-agent/src/index.ts

3325 lines
141 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);
// ── Verify / Attest ─────────────────────────────────────────
program
.command("verify <target>")
.description("Verify a target and emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
if (receipt.verification_status === "failed") process.exit(1);
});
program
.command("attest <target>")
.description("Alias for verify: emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
if (receipt.verification_status === "failed") process.exit(1);
});
// ── 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("duel <task>")
.description("Record a two-implementer eval winner")
.requiredOption("--a <label>", "First contender label")
.requiredOption("--b <label>", "Second contender label")
.requiredOption("--winner <a|b|tie>", "Winning output")
.requiredOption("--reason <text>", "Why this output won")
.option("--reviewer <human|agent>", "Reviewer type", "human")
.option("--out <path>", "Receipt output path")
.action(async (task: string, opts: { a: string; b: string; winner: "a" | "b" | "tie"; reason: string; reviewer?: "human" | "agent"; out?: string }) => {
if (!["a", "b", "tie"].includes(opts.winner)) throw new Error("--winner must be a, b, or tie");
if (!["human", "agent"].includes(opts.reviewer ?? "human")) throw new Error("--reviewer must be human or agent");
const { createDuelEvalReceipt, writeDuelEvalReceipt } = await import("./fable5/eval-trace.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "duel-evals", stamp + ".json");
const receipt = createDuelEvalReceipt({ task, a: opts.a, b: opts.b, winner: opts.winner, reason: opts.reason, reviewer: opts.reviewer });
writeDuelEvalReceipt(out, receipt);
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Receipt: " + out + "\n");
});
benchmark
.command("duel-tally")
.description("Tally two-implementer eval receipts")
.option("--dir <path>", "Duel receipt directory", path.join(".fable", "duel-evals"))
.action(async (opts: { dir?: string }) => {
const { tallyDuelReceipts } = await import("./fable5/eval-trace.js");
const tally = tallyDuelReceipts(opts.dir ?? path.join(".fable", "duel-evals"));
console.log(JSON.stringify(tally, null, 2));
});
benchmark
.command("set-grade")
.description("Grade expected vs actual set recovery with Jaccard similarity")
.requiredOption("--expected <csv>", "Expected comma-separated items")
.requiredOption("--actual <csv>", "Actual comma-separated items")
.option("--threshold <n>", "Pass threshold", parseFloat, 0.8)
.action(async (opts: { expected: string; actual: string; threshold?: number }) => {
const { gradeSetRecovery } = await import("./fable5/eval-trace.js");
const parse = (v: string) => v.split(",").map((s) => s.trim()).filter(Boolean);
const receipt = gradeSetRecovery(parse(opts.expected), parse(opts.actual), opts.threshold ?? 0.8);
console.log(JSON.stringify(receipt, null, 2));
if (!receipt.passed) process.exit(1);
});
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 emitRunEvent(runId: string | undefined, event: { source: "agent" | "tool" | "workflow" | "gate" | "intake" | "mission"; type: "started" | "output" | "receipt" | "error" | "done"; data: unknown }): Promise<void> {
if (!runId) return;
const { emitChannelEvent } = await import("./fable5/channel.js");
emitChannelEvent(runId, event);
}
async function printFactoryCheck(runId?: string): Promise<{ ready: boolean }> {
const { checkFactory, factoryGateReady, formatFactoryCheck } = await import("./fable5/factory-status.js");
const result = await checkFactory();
const ready = factoryGateReady(result);
console.log(formatFactoryCheck(result.factory, result.deploy, result.errors));
await emitRunEvent(runId, { source: "gate", type: ready ? "receipt" : "error", data: { kind: "factory-check", ready, ...result } });
return { ready };
}
factory
.command("check")
.description("Check factory status and 8099 deploy route health")
.option("--run <id>", "Emit factory check event to .runs/<id>/channel.jsonl")
.action(async (opts: { run?: string }) => {
await printFactoryCheck(opts.run);
});
factory
.command("capabilities")
.description("Probe the factory capability registry")
.option("--file <path>", "Capability registry path", "factory-capabilities.yaml")
.option("--run <id>", "Emit capability probe event to .runs/<id>/channel.jsonl")
.action(async (opts: { file?: string; run?: 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));
await emitRunEvent(opts.run, { source: "gate", type: report.requiredOk ? "receipt" : "error", data: { kind: "factory-capabilities", report } });
if (!report.requiredOk) process.exit(1);
});
factory
.command("rsi")
.description("Reconcile live RSI/self-improvement evidence into a receipt")
.option("--host <host>", "Factory SSH host", "77.42.112.29")
.option("--port <n>", "Factory SSH port", (v) => Number(v), 2222)
.option("--out <path>", "Receipt output path")
.option("--run <id>", "Emit RSI receipt event to .runs/<id>/channel.jsonl")
.action(async (opts: { host?: string; port?: number; out?: string; run?: string }) => {
const { reconcileRsi } = await import("./fable5/rsi-reconcile.js");
const out = opts.out ?? path.join(".fable", "rsi", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = reconcileRsi({ host: opts.host ?? "77.42.112.29", port: opts.port, out });
console.log(JSON.stringify(receipt, null, 2));
console.log(`\n Receipt: ${out}\n`);
await emitRunEvent(opts.run, { source: "gate", type: receipt.decision === "degraded" ? "error" : "receipt", data: { kind: "factory-rsi", receipt, path: out } });
if (receipt.decision === "degraded") process.exit(1);
});
factory
.command("verify-cycle")
.description("Prove factory RSI/skill recovery and write a receipt")
.option("--host <host>", "Factory SSH host", "77.42.112.29")
.option("--port <n>", "Factory SSH port", (v) => Number(v), 2222)
.option("--skill <name>", "Skill test to verify", "rsi_canary")
.option("--out <path>", "Receipt output path")
.option("--allow-dirty", "Allow a dirty git worktree in the receipt")
.option("--run <id>", "Emit verify-cycle receipt event to .runs/<id>/channel.jsonl")
.action(async (opts: { host?: string; port?: number; skill?: string; out?: string; allowDirty?: boolean; run?: string }) => {
const { verifyCycle } = await import("./fable5/verify-cycle.js");
const out = opts.out ?? path.join(".fable", "verify-cycle", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = await verifyCycle({ host: opts.host ?? "77.42.112.29", port: opts.port, skill: opts.skill, out, allowDirty: opts.allowDirty });
console.log(`${receipt.result.toUpperCase()} factory verify-cycle`);
console.log(`receipt: ${out}`);
console.log(`deploy: ${receipt.deploy}`);
await emitRunEvent(opts.run, { source: "gate", type: receipt.result === "passed" ? "receipt" : "error", data: { kind: "factory-verify-cycle", receipt, path: out } });
if (receipt.result !== "passed") process.exit(1);
});
factory
.command("verify <target>")
.description("Verify a target and emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
if (receipt.verification_status === "failed") process.exit(1);
});
factory
.command("attest <target>")
.description("Alias for factory verify: emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
if (receipt.verification_status === "failed") process.exit(1);
});
factory
.command("deploy <command>")
.description("Post an approved command to the factory deploy webhook")
.option("--token <token>", "Deploy token; defaults to FACTORY_DEPLOY_TOKEN or DEPLOY_TOKEN")
.requiredOption("--gate-receipt <path>", "ZTE/factory gate receipt JSONL containing a passed status")
.option("--url <url>", "Deploy route URL", "http://77.42.112.29:8099/deploy")
.option("--out <path>", "Deploy receipt path")
.option("--yes", "Confirm this is an approved deploy command")
.option("--run <id>", "Emit deploy receipt event to .runs/<id>/channel.jsonl")
.action(async (command: string, opts: { token?: string; gateReceipt: string; url?: string; out?: string; yes?: boolean; run?: string }) => {
const { deployToFactory } = await import("./fable5/factory-deploy.js");
const out = opts.out ?? path.join(".fable", "deploy", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = await deployToFactory({
command,
token: opts.token ?? process.env.FACTORY_DEPLOY_TOKEN ?? process.env.DEPLOY_TOKEN,
gateReceipt: opts.gateReceipt,
url: opts.url,
yes: opts.yes,
out,
});
console.log(JSON.stringify(receipt, null, 2));
console.log(`\n Receipt: ${out}\n`);
await emitRunEvent(opts.run, { source: "gate", type: receipt.status === "posted" ? "receipt" : "error", data: { kind: "factory-deploy", receipt, path: out } });
if (receipt.status !== "posted") 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("--host <host>", "Factory SSH host for cyber preflight", "77.42.112.29")
.option("--port <n>", "Factory SSH port for cyber preflight", (v) => Number(v), 2222)
.option("--capabilities <path>", "Capability registry path", "factory-capabilities.yaml")
.option("--diagnostic-only", "Bypass live factory/deploy readiness failures for non-deploy diagnostics")
.option("--run <id>", "Emit gate events to .runs/<id>/channel.jsonl")
.action(async (task: string, opts: { repo?: string; host?: string; port?: number; capabilities?: string; diagnosticOnly?: boolean; run?: string }) => {
const { spawnSync } = await import("node:child_process");
const repo = path.resolve(opts.repo ?? ".");
await emitRunEvent(opts.run, { source: "gate", type: "started", data: { kind: "factory-gate", task, repo } });
const status = await printFactoryCheck(opts.run);
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));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason } });
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
process.exit(1);
}
const { formatCapabilityReport, loadFactoryCapabilities, probeFactoryCapabilities } = await import("./fable5/factory-capabilities.js");
const capabilitiesPath = path.resolve(opts.capabilities ?? "factory-capabilities.yaml");
const capabilities = await probeFactoryCapabilities(loadFactoryCapabilities(capabilitiesPath));
console.log(formatCapabilityReport(capabilities));
await emitRunEvent(opts.run, { source: "gate", type: capabilities.requiredOk ? "receipt" : "error", data: { kind: "factory-capabilities", report: capabilities } });
if (!capabilities.requiredOk && !opts.diagnosticOnly) {
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const reason = "required factory capabilities are missing";
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["factory capabilities"], reason));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason } });
console.error(` ✗ Factory gate failed: ${reason}; use --diagnostic-only for non-deploy diagnostics.`);
process.exit(1);
}
const { runCyberPreflight } = await import("./fable5/cyber-preflight.js");
const { reconcileCyberPreflight, writeCyberReconcileReceipt } = await import("./fable5/cyber-reconcile.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const preflightPath = path.join(repo, ".fable", "cyber-preflight", `factory-gate-${stamp}.json`);
const reconcilePath = path.join(repo, ".fable", "cyber-preflight", `factory-gate-${stamp}-reconcile.json`);
const preflight = runCyberPreflight({ host: opts.host ?? "77.42.112.29", port: opts.port, out: preflightPath });
const reconcile = reconcileCyberPreflight(preflightPath, capabilitiesPath);
writeCyberReconcileReceipt(reconcilePath, reconcile);
console.log(` Cyber preflight: ${preflight.decision} (${preflightPath})`);
console.log(` Cyber reconcile: ${reconcile.decision} (${reconcilePath})`);
await emitRunEvent(opts.run, { source: "gate", type: preflight.decision === "allow_read_only" && reconcile.decision === "allow_read_only" ? "receipt" : "error", data: { kind: "cyber-preflight", preflightPath, reconcilePath, preflight: preflight.decision, reconcile: reconcile.decision } });
if ((preflight.decision !== "allow_read_only" || reconcile.decision !== "allow_read_only") && !opts.diagnosticOnly) {
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const reason = "cyber preflight/reconcile did not pass";
appendZteReceipt(repo, createZteReceipt(task, repo, "blocked", ["cyber preflight", "cyber reconcile"], reason));
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", 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, ...(opts.run ? ["--run", opts.run] : [])];
const check = spawnSync(process.execPath, args, { cwd: repo, stdio: "inherit" });
if (check.status !== 0) {
await emitRunEvent(opts.run, { source: "gate", type: "error", data: { kind: "factory-gate", reason: "repo verification failed", status: check.status } });
process.exit(check.status ?? 1);
}
await emitRunEvent(opts.run, { source: "gate", type: "done", data: { kind: "factory-gate", task, repo } });
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")
.option("--run <id>", "Emit intake events to .runs/<id>/channel.jsonl")
.action(async (ref: string, opts: { repo?: string; out?: string; dryRun?: boolean; run?: string }) => {
try {
const { intakeForgejoItem } = await import("./fable5/forgejo-intake.js");
const result = await intakeForgejoItem({
ref,
repo: opts.repo,
outDir: opts.out,
dryRun: opts.dryRun,
runId: opts.run,
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("channels")
.description("Show Flue-style channel event log location")
.option("--run <id>", "Run id", "demo")
.action(async (opts: { run?: string }) => {
const { channelPath, readChannelEvents } = await import("./fable5/channel.js");
const runId = opts.run ?? "demo";
const events = readChannelEvents(runId);
console.log(`
Channel: ${channelPath(runId)}`);
console.log(` Events: ${events.length}`);
console.log(``);
});
fable
.command("memory-b-cell <file>")
.description("Assess a file through the memory B cell pattern cache")
.option("--cache <path>", "Cache path", path.join(".fable", "memory-b-cells.json"))
.action(async (file: string, opts: { cache?: string }) => {
const { assessMemoryBCell } = await import("./fable5/memory-b-cell.js");
const full = path.resolve(file);
const result = assessMemoryBCell(full, fs.readFileSync(full, "utf-8"), opts.cache);
console.log(JSON.stringify(result, null, 2));
if (result.verdict === "known_unsafe") process.exit(1);
});
fable
.command("receipt-health")
.description("Check receipt consumer health over a recent time window")
.option("--root <path>", "Receipt root", ".fable")
.option("--window-minutes <n>", "Freshness window", (v) => Number(v), 15)
.action(async (opts: { root?: string; windowMinutes?: number }) => {
const { checkReceiptHealth } = await import("./fable5/receipt-consumer.js");
const health = checkReceiptHealth(opts.root ?? ".fable", opts.windowMinutes ?? 15);
console.log(JSON.stringify(health, null, 2));
if (health.status !== "healthy") process.exit(1);
});
fable
.command("heartbeat")
.description("Write a mission-control heartbeat locally and optionally POST it remotely")
.requiredOption("--id <id>", "Agent id")
.option("--name <name>", "Agent display name")
.option("--status <status>", "idle|working|blocked|done|error", "working")
.option("--task <text>", "Current task")
.option("--completed <n>", "Tasks completed", (v) => Number(v), 0)
.option("--cost <n>", "Total cost", (v) => Number(v), 0)
.option("--run <id>", "Local channel run id")
.option("--url <url>", "Mission Control /api/agents/state URL; defaults to MISSION_CONTROL_URL")
.option("--secret <secret>", "Mission Control secret; defaults to MISSION_CONTROL_SECRET")
.option("--out <path>", "Receipt output path")
.action(async (opts: { id: string; name?: string; status?: string; task?: string; completed?: number; cost?: number; run?: string; url?: string; secret?: string; out?: string }) => {
const { sendMissionHeartbeat } = await import("./fable5/mission-heartbeat.js");
const out = opts.out ?? path.join(".fable", "mission", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = await sendMissionHeartbeat({
id: opts.id,
name: opts.name,
status: (opts.status ?? "working") as never,
currentTask: opts.task,
tasksCompleted: opts.completed,
totalCost: opts.cost,
runId: opts.run,
url: opts.url ?? process.env.MISSION_CONTROL_URL,
secret: opts.secret ?? process.env.MISSION_CONTROL_SECRET,
out,
});
console.log(JSON.stringify(receipt, null, 2));
console.log(`\n Receipt: ${out}\n`);
if (receipt.remote_attempted && !receipt.remote_ok) process.exit(1);
});
fable
.command("verify <task>")
.description("Run independent verifier against a task")
.option("--repo <path>", "Also run the repo verification gate at this path")
.option("--run <id>", "Emit verify events to .runs/<id>/channel.jsonl")
.option("--trace-out <path>", "Eval trace receipt path")
.action(async (task: string, opts: { repo?: string; run?: string; traceOut?: string }) => {
if (opts.run) {
const { emitChannelEvent } = await import("./fable5/channel.js");
emitChannelEvent(opts.run, { source: "gate", type: "started", data: { task, repo: opts.repo } });
}
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]],
];
const commandResults: Array<{ command: string; status: number | null }> = [];
console.log(`
Repo Verification Gate: ${repo}`);
for (const [cmd, args] of checks) {
const command = [cmd, ...args].join(" ");
console.log(`
$ ${command}`);
// 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", command], { cwd: repo, stdio: "inherit" })
: spawnSync(cmd, args, { cwd: repo, stdio: "inherit" });
commandResults.push({ command, status: check.status });
if (check.status !== 0) break;
}
const { appendZteReceipt, createZteReceipt } = await import("./fable5/zte-protocol.js");
const { createEvalTraceReceipt, writeEvalTraceReceipt } = await import("./fable5/eval-trace.js");
const gitHead = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf-8" }).stdout.trim() || undefined;
const trace = createEvalTraceReceipt(task, repo, commandResults, gitHead);
const tracePath = opts.traceOut ?? path.join(repo, ".fable", "eval-traces", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
writeEvalTraceReceipt(tracePath, trace);
if (opts.run) {
const { emitChannelEvent } = await import("./fable5/channel.js");
emitChannelEvent(opts.run, { source: "gate", type: trace.status === "passed" ? "receipt" : "error", data: trace });
}
const receipt = createZteReceipt(task, repo, trace.status, commandResults.map((r) => r.command), trace.status === "failed" ? "repo verification failed" : undefined);
const receiptPath = appendZteReceipt(repo, receipt);
if (trace.status === "failed") {
console.error(`
✗ Repo verification failed`);
console.error(` Eval trace: ${tracePath}`);
process.exit(commandResults.find((r) => r.status !== 0)?.status ?? 1);
}
console.log(`
✓ Repo verification passed`);
console.log(` ✓ ZTE receipt written: ${receiptPath}`);
console.log(` ✓ Eval trace written: ${tracePath}
`);
}
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("goal-audit <objective>")
.description("Audit goal completion evidence and reject hallucinated done claims")
.requiredOption("--output <text>", "Agent completion output to audit")
.option("--require <label:evidence>", "Required evidence contract; repeatable", (v, p: string[] = []) => [...p, v], [])
.option("--context <text>", "Verified context/facts; repeatable", (v, p: string[] = []) => [...p, v], [])
.option("--out <path>", "Receipt output path")
.action(async (objective: string, opts: { output: string; require?: string[]; context?: string[]; out?: string }) => {
const { auditGoalCompletion, writeGoalAuditReceipt } = await import("./fable5/goal-audit.js");
const criteria = (opts.require ?? []).map((raw) => {
const [label, ...rest] = raw.split(":");
return { label: label.trim(), evidence: rest.join(":").trim(), required: true };
});
const receipt = auditGoalCompletion({ objective, output: opts.output, criteria, context: opts.context });
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "goal-audits", stamp + ".json");
writeGoalAuditReceipt(out, receipt);
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Receipt: " + out + "\n");
if (receipt.status === "rejected") process.exit(1);
});
fable
.command("spec <task>")
.description("Generate a six-section anti-hallucination 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(` ✓ Spec written: ${path.resolve(opts.out)}`);
return;
}
console.log(spec.markdown);
});
fable
.command("autowiki")
.description("Generate a local repo wiki from current source files")
.option("--repo <path>", "Repo path to document", ".")
.option("--out <path>", "Wiki output directory")
.option("--max-files <n>", "Max files to survey", (v) => Number(v), 80)
.action(async (opts: { repo?: string; out?: string; maxFiles?: number }) => {
const { generateAutoWiki } = await import("./fable5/autowiki.js");
const receipt = generateAutoWiki({ repo: opts.repo ?? ".", outDir: opts.out, maxFiles: opts.maxFiles });
console.log(JSON.stringify(receipt, null, 2));
});
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("--approved <text>", "Append approved feedback memory")
.option("--rejected <text>", "Append rejected feedback memory")
.option("--performance <text>", "Append performance feedback memory")
.option("--repo <path>", "Repo path for .fable/memory", ".")
.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.approved || opts.rejected || opts.performance) {
const { appendFeedbackMemory } = await import("./fable5/feedback-memory.js");
const type = opts.approved ? "approved" : opts.rejected ? "rejected" : "performance";
const text = (opts.approved ?? opts.rejected ?? opts.performance) as string;
appendFeedbackMemory(path.resolve(opts.repo as string), type, `[${project}] ${text}`);
console.log(`${type} feedback memory appended`);
} else 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("safety <input>")
.description("Evaluate input with the local content safety gate")
.action(async (input: string) => {
const { ContentSafetyGate } = await import("./upgrades/content-safety-gate.js");
const verdict = new ContentSafetyGate().evaluate(input);
console.log(JSON.stringify(verdict, null, 2));
if (verdict.action !== "allow") process.exit(1);
});
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");
// ── Cyber ───────────────────────────────────────────────────
const cyber = program
.command("cyber")
.description("Read-only defensive cyber evidence collection");
cyber
.command("preflight")
.description("Collect read-only factory evidence and write a fail-closed receipt")
.requiredOption("--host <host>", "Target host to inspect over SSH")
.option("--port <n>", "SSH port", (v) => Number(v), 2222)
.option("--mode <mode>", "Only read-only is supported", "read-only")
.option("--output <file>", "Receipt output file")
.action(async (opts: { host: string; port?: number; mode?: string; output?: string }) => {
if (opts.mode !== "read-only") {
console.error(" ✗ cyber preflight only supports --mode read-only");
process.exit(1);
}
const { runCyberPreflight } = await import("./fable5/cyber-preflight.js");
const out = opts.output ?? path.join(".fable", "cyber-preflight", `${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
const receipt = runCyberPreflight({ host: opts.host, port: opts.port, out });
console.log(JSON.stringify(receipt, null, 2));
console.log(`\n Receipt: ${out}\n`);
if (receipt.decision !== "allow_read_only") process.exit(1);
});
cyber
.command("reconcile")
.description("Compare cyber preflight evidence against factory capability expectations")
.requiredOption("--preflight <file>", "Cyber preflight receipt JSON")
.option("--capabilities <file>", "Capability registry path", "factory-capabilities.yaml")
.option("--output <file>", "Reconcile receipt output file")
.action(async (opts: { preflight: string; capabilities?: string; output?: string }) => {
const { reconcileCyberPreflight, writeCyberReconcileReceipt } = await import("./fable5/cyber-reconcile.js");
const receipt = reconcileCyberPreflight(opts.preflight, opts.capabilities ?? "factory-capabilities.yaml");
if (opts.output) writeCyberReconcileReceipt(opts.output, receipt);
console.log(JSON.stringify(receipt, null, 2));
if (opts.output) console.log(`\n Receipt: ${opts.output}\n`);
if (receipt.decision !== "allow_read_only") process.exit(1);
});
security
.command("ast <file>")
.description("Summarize JS/TS structure and risky capabilities")
.action(async (file: string) => {
const { scanAstFile } = await import("./core/ast-safety.js");
const report = scanAstFile(path.resolve(file));
console.log(JSON.stringify(report, null, 2));
if (report.decision !== "allow") process.exit(1);
});
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 { assessLegibilityRisk } = await import("./core/unicode-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 content = fs.readFileSync(file, "utf-8");
const found = findPromptInjection(content).filter((h) => h.kind !== "instruction-smuggling");
const legibility = assessLegibilityRisk(content);
if (found.length === 0 && !legibility.risky) continue;
hits += found.length + (legibility.risky ? 1 : 0);
if (found.length > 0) console.log(`${file}: ${found.map((h) => `${h.kind}:${h.match}@${h.index}`).join(", ")}`);
if (legibility.risky) console.log(`${file}: human-legibility-risk:${legibility.reason}`);
}
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();
}