diff --git a/COMMANDS.md b/COMMANDS.md index 1081344..2a98cfb 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -158,6 +158,9 @@ fable-agent plinius godmode "improve explanation quality" - `familiar graph` - `familiar health` - `familiar briefing` +- `familiar loop` + - `-p, --priority ` + - `-d, --dir ` ### `pai-pi` diff --git a/src/index.ts b/src/index.ts index 9c0e1c5..0c86c2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1185,6 +1185,52 @@ familiar console.log(`\n${briefing}\n`); }); +familiar + .command("loop") + .description("Process inbox notes, detect simple contradictions, and queue follow-up goals") + .option("-p, --priority ", "Queued goal priority (1=high, 2=normal, 3=low)", parseInt, 1) + .option("-d, --dir ", "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 @@ -2626,6 +2672,38 @@ security ✓ 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