feat: add familiar second-brain loop

This commit is contained in:
artale 2026-06-15 13:49:04 +02:00
parent ed208d56e4
commit 275de5d0e8
2 changed files with 81 additions and 0 deletions

View File

@ -158,6 +158,9 @@ fable-agent plinius godmode "improve explanation quality"
- `familiar graph` - `familiar graph`
- `familiar health` - `familiar health`
- `familiar briefing` - `familiar briefing`
- `familiar loop`
- `-p, --priority <n>`
- `-d, --dir <path>`
### `pai-pi` ### `pai-pi`

View File

@ -1185,6 +1185,52 @@ familiar
console.log(`\n${briefing}\n`); 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 ─────────────────────────────────────────────────── // ── PAI Pi ───────────────────────────────────────────────────
const paiPi = program const paiPi = program
@ -2626,6 +2672,38 @@ security
No prompt-injection markers found in ${files.length} file(s)`); 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); program.parse(process.argv);
// Show help if no args // Show help if no args