total-recall-brain 3.41.4 → 3.41.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +10 -6
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
- package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +11 -0
- package/frontend/src/App.tsx +47 -122
- package/frontend/src/components/plugins/plugin-frame.ts +1 -0
- package/frontend/src/index.css +152 -86
- package/frontend/src/pages/HelpPage.tsx +13 -10
- package/frontend/src/pages/MemoryPage.tsx +28 -30
- package/frontend/src/pages/NetworkPage.tsx +3 -3
- package/frontend/src/pages/PluginsPage.tsx +6 -5
- package/frontend/src/pages/RulesPage.tsx +16 -12
- package/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +55 -23
- package/src/cli/doctor.mjs +5 -2
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +62 -50
- package/src/cli/memory-entry.mjs +2 -2
- package/src/cli/preflight.mjs +152 -16
- package/src/cli/release.mjs +386 -0
- package/src/cli/remember.mjs +19 -1
- package/src/cli/rules.mjs +54 -12
- package/src/cli/startup.mjs +1 -1
- package/src/cli/status.mjs +12 -2
- package/src/core/claude-hooks.mjs +41 -3
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +145 -0
- package/src/core/context-policy.mjs +20 -0
- package/src/core/duplicate-guard.mjs +350 -0
- package/src/core/meta-harness.mjs +31 -11
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +29 -1
- package/src/core/scaffold-plugins.mjs +7 -0
- package/src/core/stdio-flush.mjs +23 -0
- package/src/core/surface.mjs +33 -10
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
- package/src/server/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
- package/frontend/src/components/DaemonLogsTab.tsx +0 -54
- package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
- package/frontend/src/components/TaskQueueTab.tsx +0 -193
- package/frontend/src/components/VoiceInput.tsx +0 -112
- package/frontend/src/pages/ChatPage.tsx +0 -1059
- package/frontend/src/pages/CollabPage.tsx +0 -470
- package/frontend/src/pages/DesignDocsPage.tsx +0 -621
- package/frontend/src/pages/FilesPage.tsx +0 -393
- package/frontend/src/pages/HealthPage.tsx +0 -359
- package/frontend/src/pages/InboxPage.tsx +0 -83
- package/frontend/src/pages/InstructionsPage.tsx +0 -583
- package/frontend/src/pages/IntegrationsPage.tsx +0 -761
- package/frontend/src/pages/MeshPage.css +0 -58
- package/frontend/src/pages/MeshPage.tsx +0 -1219
- package/frontend/src/pages/OkfPage.tsx +0 -535
- package/frontend/src/pages/SandboxPage.tsx +0 -113
- package/frontend/src/pages/SkillsPage.tsx +0 -1994
- package/frontend/src/pages/TasksPage.tsx +0 -427
- package/frontend/src/pages/UsagePage.tsx +0 -702
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
|
@@ -14,6 +14,7 @@ export const CLI_COMMANDS = {
|
|
|
14
14
|
restore: 'restore.mjs',
|
|
15
15
|
upgrade: 'upgrade.mjs',
|
|
16
16
|
update: 'update.mjs',
|
|
17
|
+
release: 'release.mjs',
|
|
17
18
|
friction: 'friction.mjs',
|
|
18
19
|
chat: 'chat.mjs',
|
|
19
20
|
status: 'status.mjs',
|
|
@@ -22,6 +23,7 @@ export const CLI_COMMANDS = {
|
|
|
22
23
|
edit: 'edit.mjs',
|
|
23
24
|
recall: 'recall.mjs',
|
|
24
25
|
context: 'context.mjs',
|
|
26
|
+
compact: 'compact.mjs',
|
|
25
27
|
preflight: 'preflight.mjs',
|
|
26
28
|
'tr-preflight': 'preflight.mjs',
|
|
27
29
|
'context-stats': 'context-stats.mjs',
|
|
@@ -508,13 +508,14 @@ export async function buildCliSemanticIndex({
|
|
|
508
508
|
skillsDir = null,
|
|
509
509
|
projectRoot = process.cwd(),
|
|
510
510
|
force = false,
|
|
511
|
-
semantic = true
|
|
511
|
+
semantic = true,
|
|
512
|
+
harvest = harvestAllCliVerbs
|
|
512
513
|
} = {}) {
|
|
513
514
|
if (!derivedDir) {
|
|
514
515
|
throw new Error('derivedDir is required to build CLI semantic index');
|
|
515
516
|
}
|
|
516
517
|
|
|
517
|
-
const harvested =
|
|
518
|
+
const harvested = harvest({ projectRoot, skillsDir });
|
|
518
519
|
const existing = force ? null : loadCliSemanticIndex(derivedDir);
|
|
519
520
|
const existingMap = new Map();
|
|
520
521
|
|
|
@@ -291,17 +291,6 @@ export function autoResolveConflict(conflict) {
|
|
|
291
291
|
const newIsProtected = isProtectedInvariant(newNode);
|
|
292
292
|
const existingIsProtected = isProtectedInvariant(existingNode);
|
|
293
293
|
|
|
294
|
-
// Tier 0: New user-created node always wins and auto-supersedes
|
|
295
|
-
if (newAuth === 'user') {
|
|
296
|
-
return {
|
|
297
|
-
resolved: true,
|
|
298
|
-
action: 'supersede',
|
|
299
|
-
winner: newNode.slug,
|
|
300
|
-
loser: existingNode.slug,
|
|
301
|
-
reason: `User-created candidate node '${newNode.slug}' automatically supersedes existing node '${existingNode.slug}' to keep the developer in the loop within the IDE context.`,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
294
|
// Tier 1: Both are protected invariants — require human review
|
|
306
295
|
if (newIsProtected && existingIsProtected) {
|
|
307
296
|
return {
|
|
@@ -318,7 +307,7 @@ export function autoResolveConflict(conflict) {
|
|
|
318
307
|
action: 'keep',
|
|
319
308
|
winner: existingNode.slug,
|
|
320
309
|
loser: newNode.slug,
|
|
321
|
-
reason: `Existing node '${existingNode.slug}' is a protected invariant.
|
|
310
|
+
reason: `Existing node '${existingNode.slug}' is a protected invariant. Non-protected node rejected.`,
|
|
322
311
|
};
|
|
323
312
|
}
|
|
324
313
|
if (newIsProtected && !existingIsProtected) {
|
|
@@ -331,6 +320,28 @@ export function autoResolveConflict(conflict) {
|
|
|
331
320
|
};
|
|
332
321
|
}
|
|
333
322
|
|
|
323
|
+
// Tier 0: New user-created node always wins against machine-generated node
|
|
324
|
+
if (newAuth === 'user' && existingAuth === 'machine') {
|
|
325
|
+
return {
|
|
326
|
+
resolved: true,
|
|
327
|
+
action: 'supersede',
|
|
328
|
+
winner: newNode.slug,
|
|
329
|
+
loser: existingNode.slug,
|
|
330
|
+
reason: `User-created candidate node '${newNode.slug}' automatically supersedes existing node '${existingNode.slug}' to keep the developer in the loop within the IDE context.`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Tier 0b: New user-created node against other user node (non-protected)
|
|
335
|
+
if (newAuth === 'user' && existingAuth === 'user') {
|
|
336
|
+
return {
|
|
337
|
+
resolved: true,
|
|
338
|
+
action: 'supersede',
|
|
339
|
+
winner: newNode.slug,
|
|
340
|
+
loser: existingNode.slug,
|
|
341
|
+
reason: `User-created candidate node '${newNode.slug}' automatically supersedes existing user node '${existingNode.slug}' to reflect latest intent.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
334
345
|
// Tier 3: User vs machine — user intent always wins
|
|
335
346
|
if (newAuth === 'user' && existingAuth === 'machine') {
|
|
336
347
|
return {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Budgeted task capsules: rank rules, keep verbatim text, and account for every rule left out. */
|
|
2
|
+
|
|
3
|
+
import { estimateTokens } from './context-policy.mjs';
|
|
4
|
+
import { cosineSimilarity } from './embeddings.mjs';
|
|
5
|
+
|
|
6
|
+
const DUPLICATE_SIMILARITY = 0.92;
|
|
7
|
+
const REFERENCE_SHARE = 0.4;
|
|
8
|
+
const TOP_FULL_SHARE = 0.3;
|
|
9
|
+
const SUMMARY_MAX_CHARS = 200;
|
|
10
|
+
const SUMMARY_MIN_CHARS = 80;
|
|
11
|
+
// A period here does not end a sentence.
|
|
12
|
+
const SENTENCE_END = /(?<!\be\.g)(?<!\bi\.e)(?<!\bvs)[.!?](?=\s|$)/g;
|
|
13
|
+
|
|
14
|
+
/** The leading sentences of a rule, verbatim. Never rewords, because rewording can change a constraint. */
|
|
15
|
+
export function summarizeRule(text) {
|
|
16
|
+
const flat = String(text || '').replace(/^#+\s.*$/gm, ' ').replace(/\s+/g, ' ').trim();
|
|
17
|
+
if (flat.length <= SUMMARY_MAX_CHARS) return flat;
|
|
18
|
+
let end = 0;
|
|
19
|
+
for (const match of flat.matchAll(SENTENCE_END)) {
|
|
20
|
+
const candidate = match.index + 1;
|
|
21
|
+
if (candidate > SUMMARY_MAX_CHARS) break;
|
|
22
|
+
end = candidate;
|
|
23
|
+
if (end >= SUMMARY_MIN_CHARS) return flat.slice(0, end);
|
|
24
|
+
}
|
|
25
|
+
if (end) return flat.slice(0, end);
|
|
26
|
+
const cut = flat.lastIndexOf(' ', SUMMARY_MAX_CHARS);
|
|
27
|
+
return `${flat.slice(0, cut > 0 ? cut : SUMMARY_MAX_CHARS)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Relevance alone orders the text; ties fall to importance, then slug, so output is stable. */
|
|
31
|
+
function rank(items) {
|
|
32
|
+
return [...items].sort((a, b) =>
|
|
33
|
+
(b.score || 0) - (a.score || 0) ||
|
|
34
|
+
(b.importance || 3) - (a.importance || 3) ||
|
|
35
|
+
a.slug.localeCompare(b.slug));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findDuplicates(ranked) {
|
|
39
|
+
const duplicateOf = new Map();
|
|
40
|
+
const normalize = text => String(text).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
41
|
+
ranked.forEach((item, i) => {
|
|
42
|
+
if (item.priority === 'absolute') return;
|
|
43
|
+
for (const earlier of ranked.slice(0, i)) {
|
|
44
|
+
if (duplicateOf.has(earlier.slug)) continue;
|
|
45
|
+
const same = normalize(item.body) === normalize(earlier.body) ||
|
|
46
|
+
(item.vector && earlier.vector && cosineSimilarity(item.vector, earlier.vector) >= DUPLICATE_SIMILARITY);
|
|
47
|
+
if (same) { duplicateOf.set(item.slug, earlier.slug); return; }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return duplicateOf;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Assign each rule a tier so the printed output fits `budget`, measuring the real
|
|
55
|
+
* render after every change. `header(counts, tokens)` supplies the status lines.
|
|
56
|
+
*/
|
|
57
|
+
export function compactRules(items, { budget, expand = [], header, complete = false }) {
|
|
58
|
+
if (!Number.isFinite(budget) || budget < 1) throw new Error('Compact budget must be a positive token estimate');
|
|
59
|
+
const ranked = rank(items);
|
|
60
|
+
const known = new Set(ranked.map(item => item.slug));
|
|
61
|
+
const unknown = expand.filter(slug => !known.has(slug));
|
|
62
|
+
if (unknown.length) throw new Error(`Cannot expand rules not in this capsule: ${unknown.join(', ')}`);
|
|
63
|
+
const duplicateOf = findDuplicates(ranked.filter(item => !expand.includes(item.slug)));
|
|
64
|
+
const tiers = new Map(ranked.map(item => [item.slug, 'omitted']));
|
|
65
|
+
|
|
66
|
+
const render = () => {
|
|
67
|
+
const of = tier => ranked.filter(item => tiers.get(item.slug) === tier);
|
|
68
|
+
const full = of('full'), summary = of('summary'), reference = of('reference'), duplicate = of('duplicate');
|
|
69
|
+
const counts = { rules: ranked.length, full: full.length, summarized: summary.length, referenced: reference.length,
|
|
70
|
+
duplicate: duplicate.length, omitted: of('omitted').length };
|
|
71
|
+
const blocks = [
|
|
72
|
+
...full.map(item => item.text.trim()),
|
|
73
|
+
...summary.map(item => `[${item.slug}] ${summarizeRule(item.body)}`),
|
|
74
|
+
];
|
|
75
|
+
if (reference.length) blocks.push(`Referenced: ${reference.map(item => item.slug).join(', ')}`);
|
|
76
|
+
if (duplicate.length) blocks.push(`Duplicates: ${duplicate.map(item => `${item.slug} (of ${duplicateOf.get(item.slug)})`).join(', ')}`);
|
|
77
|
+
const body = blocks.join('\n\n---\n\n');
|
|
78
|
+
// The token count sits inside the text it counts, so iterate to a stable value.
|
|
79
|
+
let tokens = estimateTokens(body);
|
|
80
|
+
let context = '';
|
|
81
|
+
for (let i = 0; i < 5; i++) {
|
|
82
|
+
context = `${header(counts, tokens)}${body ? `\n${body}` : ''}`;
|
|
83
|
+
const measured = estimateTokens(`${context}\n`);
|
|
84
|
+
if (measured === tokens) break;
|
|
85
|
+
tokens = measured;
|
|
86
|
+
}
|
|
87
|
+
return { context, counts, tokens };
|
|
88
|
+
};
|
|
89
|
+
const used = () => render().tokens;
|
|
90
|
+
const tryTier = (item, tier, limit = budget) => {
|
|
91
|
+
const previous = tiers.get(item.slug);
|
|
92
|
+
tiers.set(item.slug, tier);
|
|
93
|
+
if (used() <= limit) return true;
|
|
94
|
+
tiers.set(item.slug, previous);
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// A complete capsule measures its own, higher floor below, so a retry at the reported number succeeds.
|
|
99
|
+
const floor = used();
|
|
100
|
+
if (!complete && floor > budget) {
|
|
101
|
+
const error = new Error(`Budget ${budget} cannot hold the capsule header; the minimum is ${floor}`);
|
|
102
|
+
error.requiredBudget = floor;
|
|
103
|
+
error.ruleCount = items.length;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const slug of expand) {
|
|
108
|
+
if (!tryTier(ranked.find(item => item.slug === slug), 'full')) {
|
|
109
|
+
const error = new Error(`Budget ${budget} cannot hold expanded rule ${slug}; raise --budget or expand fewer rules`);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// A complete capsule accounts for every rule before ranking spends any budget, so
|
|
114
|
+
// ranking decides how much text a rule gets and never whether it applies.
|
|
115
|
+
if (complete) {
|
|
116
|
+
for (const item of ranked) {
|
|
117
|
+
if (tiers.get(item.slug) !== 'omitted') continue;
|
|
118
|
+
tiers.set(item.slug, duplicateOf.has(item.slug) ? 'duplicate' : item.priority === 'absolute' ? 'summary' : 'reference');
|
|
119
|
+
}
|
|
120
|
+
const completeFloor = used();
|
|
121
|
+
if (completeFloor > budget) {
|
|
122
|
+
const error = new Error(`Budget ${budget} cannot hold every rule; the minimum is ${completeFloor}`);
|
|
123
|
+
error.requiredBudget = completeFloor;
|
|
124
|
+
error.ruleCount = items.length;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const candidates = ranked.filter(item => !duplicateOf.has(item.slug) && tiers.get(item.slug) !== 'full');
|
|
129
|
+
|
|
130
|
+
// Slugs alone must not consume a small budget: the top rules need their text.
|
|
131
|
+
const referenceLimit = used() + Math.floor((budget - used()) * REFERENCE_SHARE);
|
|
132
|
+
// Absolute rules are named first, so a small budget still shows they exist.
|
|
133
|
+
const unplaced = candidates.filter(item => tiers.get(item.slug) === 'omitted');
|
|
134
|
+
const namedFirst = [...unplaced.filter(item => item.priority === 'absolute'), ...unplaced.filter(item => item.priority !== 'absolute')];
|
|
135
|
+
for (const item of namedFirst) if (!tryTier(item, 'reference', referenceLimit)) break;
|
|
136
|
+
const topLimit = used() + Math.floor((budget - used()) * TOP_FULL_SHARE);
|
|
137
|
+
for (const item of candidates) tryTier(item, 'full', topLimit);
|
|
138
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'summary');
|
|
139
|
+
for (const item of candidates) if (tiers.get(item.slug) !== 'full') tryTier(item, 'full');
|
|
140
|
+
for (const item of namedFirst) if (tiers.get(item.slug) === 'omitted') tryTier(item, 'reference');
|
|
141
|
+
for (const slug of duplicateOf.keys()) tryTier(ranked.find(item => item.slug === slug), 'duplicate');
|
|
142
|
+
|
|
143
|
+
const { context, counts, tokens } = render();
|
|
144
|
+
return { context, counts, tokens, tiers: Object.fromEntries(tiers) };
|
|
145
|
+
}
|
|
@@ -40,6 +40,24 @@ export function ruleAppliesToRepo(node, repo) {
|
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const CRITICAL_TOP_RULES = new Set([
|
|
44
|
+
'invariants-8cd098fd',
|
|
45
|
+
'invariants-dc116371',
|
|
46
|
+
'never-ask-for-a-secret-before-searching-all-tr-stores',
|
|
47
|
+
'push-only-on-request',
|
|
48
|
+
'report-breakage-then-fix-at-the-source',
|
|
49
|
+
'invariants-bbbbec36',
|
|
50
|
+
'invariants-5d8df45d',
|
|
51
|
+
'invariants-7780389f',
|
|
52
|
+
'rule-total-recall-for-everything-always',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function rulePrecedence(slug) {
|
|
56
|
+
if (slug === 'invariants-8cd098fd' || slug === 'invariants-dc116371') return 100;
|
|
57
|
+
if (CRITICAL_TOP_RULES.has(slug)) return 50;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/** Applicability is explicit validated memory tags, independent of obligation.
|
|
44
62
|
* Unknown rules remain required in task capsules. No inference from prose.
|
|
45
63
|
*/
|
|
@@ -51,6 +69,7 @@ export function selectRules(nodes, { actions = [], projectRoot = process.cwd(),
|
|
|
51
69
|
if (!ruleAppliesToRepo(node, repo)) return false;
|
|
52
70
|
const tags = Array.isArray(node.tags) ? node.tags : [];
|
|
53
71
|
if (tags.includes('context:universal')) return true;
|
|
72
|
+
if (node.priority === 'absolute' || node.immutable === true) return true;
|
|
54
73
|
if (bootstrap) return false;
|
|
55
74
|
const triggers = tags.filter(t => typeof t === 'string' && t.startsWith('context:action:')).map(t => t.slice(15));
|
|
56
75
|
// Unknown actions conservatively activate every conditional restriction.
|
|
@@ -59,6 +78,7 @@ export function selectRules(nodes, { actions = [], projectRoot = process.cwd(),
|
|
|
59
78
|
});
|
|
60
79
|
return applicable.filter(node => !applicable.some(other => other !== node && other.subject && other.subject === node.subject &&
|
|
61
80
|
other.supersedes?.includes(node.slug))).sort((a, b) =>
|
|
81
|
+
(rulePrecedence(b.slug) - rulePrecedence(a.slug)) ||
|
|
62
82
|
Number(b.priority === 'absolute') - Number(a.priority === 'absolute') || (b.importance || 3) - (a.importance || 3) || String(a.slug).localeCompare(String(b.slug)));
|
|
63
83
|
}
|
|
64
84
|
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes-Grade Fail-Closed Duplicate Memory Guard
|
|
3
|
+
*
|
|
4
|
+
* Implements pre-write semantic collision checking co-opted from Nous Hermes / Hindsight
|
|
5
|
+
* (_find_related_observations).
|
|
6
|
+
*
|
|
7
|
+
* Prevents agents from polluting the vault with duplicate rules or disguising
|
|
8
|
+
* rule-violation incidents by re-recording existing invariants.
|
|
9
|
+
*
|
|
10
|
+
* Invariant:
|
|
11
|
+
* If an active rule or invariant with high token similarity or vector semantic proximity
|
|
12
|
+
* already exists, the write MUST FAIL CLOSED (Exit Code 1) with ZERO disk bytes written.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { getNodes } from './vault-cache.mjs';
|
|
18
|
+
import { loadEmbeddingsIndex, cosineSimilarity } from './embeddings.mjs';
|
|
19
|
+
import { logger } from './logger.mjs';
|
|
20
|
+
|
|
21
|
+
const RULE_CATEGORIES = new Set(['invariants', 'anti-patterns', 'preferences', 'rules']);
|
|
22
|
+
|
|
23
|
+
const STOPWORDS = new Set([
|
|
24
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'has', 'he',
|
|
25
|
+
'in', 'is', 'it', 'its', 'of', 'on', 'that', 'the', 'to', 'was', 'were',
|
|
26
|
+
'will', 'with', 'before', 'after', 'using', 'used', 'use', 'any', 'all',
|
|
27
|
+
'should', 'must', 'each', 'this', 'these', 'those', 'such', 'than', 'then'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize and stem a token to its canonical semantic root.
|
|
32
|
+
*/
|
|
33
|
+
export function stemToken(word) {
|
|
34
|
+
let w = word.toLowerCase();
|
|
35
|
+
// Strip trailing punctuation
|
|
36
|
+
w = w.replace(/[^a-z0-9_-]/g, '');
|
|
37
|
+
if (w.length <= 2) return w;
|
|
38
|
+
|
|
39
|
+
// Synonyms and domain stems
|
|
40
|
+
if (w === 'docs' || w === 'documentation') return 'doc';
|
|
41
|
+
if (w === 'apis' || w === 'api') return 'api';
|
|
42
|
+
if (w === 'models' || w === 'model') return 'model';
|
|
43
|
+
if (w === 'names' || w === 'strings' || w === 'string') return 'name';
|
|
44
|
+
if (w.startsWith('search')) return 'search';
|
|
45
|
+
if (w.startsWith('hardcod')) return 'hardcode';
|
|
46
|
+
if (w.startsWith('pretrain')) return 'pretrain';
|
|
47
|
+
if (w.startsWith('reli') || w.startsWith('rely')) return 'rely';
|
|
48
|
+
if (w.startsWith('specif')) return 'spec';
|
|
49
|
+
if (w.startsWith('push')) return 'push';
|
|
50
|
+
if (w.startsWith('commit')) return 'commit';
|
|
51
|
+
if (w.startsWith('verif')) return 'verify';
|
|
52
|
+
if (w.startsWith('check')) return 'check';
|
|
53
|
+
|
|
54
|
+
// Standard suffix stripping
|
|
55
|
+
if (w.endsWith('ing') && w.length > 5) return w.slice(0, -3);
|
|
56
|
+
if (w.endsWith('es') && w.length > 4) return w.slice(0, -2);
|
|
57
|
+
if (w.endsWith('s') && !w.endsWith('ss') && w.length > 3) return w.slice(0, -1);
|
|
58
|
+
if (w.endsWith('ed') && w.length > 4) return w.slice(0, -2);
|
|
59
|
+
if (w.endsWith('ly') && w.length > 4) return w.slice(0, -2);
|
|
60
|
+
|
|
61
|
+
return w;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tokenize text into stemmed, stopword-filtered semantic tokens.
|
|
66
|
+
*/
|
|
67
|
+
export function tokenize(text) {
|
|
68
|
+
if (!text || typeof text !== 'string') return new Set();
|
|
69
|
+
const rawWords = text
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9\s_-]/g, ' ')
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter((w) => w.length > 1 && !STOPWORDS.has(w));
|
|
74
|
+
|
|
75
|
+
const stems = new Set();
|
|
76
|
+
for (const word of rawWords) {
|
|
77
|
+
const s = stemToken(word);
|
|
78
|
+
if (s && !STOPWORDS.has(s)) {
|
|
79
|
+
stems.add(s);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return stems;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute Jaccard similarity between two token sets.
|
|
87
|
+
*/
|
|
88
|
+
export function jaccard(setA, setB) {
|
|
89
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
90
|
+
let intersection = 0;
|
|
91
|
+
for (const item of setA) {
|
|
92
|
+
if (setB.has(item)) intersection++;
|
|
93
|
+
}
|
|
94
|
+
const union = setA.size + setB.size - intersection;
|
|
95
|
+
return union === 0 ? 0 : intersection / union;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compute containment ratio (how much of candidate's core tokens exist in target).
|
|
100
|
+
*/
|
|
101
|
+
export function containment(candidateSet, targetSet) {
|
|
102
|
+
if (candidateSet.size === 0) return 0;
|
|
103
|
+
let matches = 0;
|
|
104
|
+
for (const token of candidateSet) {
|
|
105
|
+
if (targetSet.has(token)) matches++;
|
|
106
|
+
}
|
|
107
|
+
return matches / candidateSet.size;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find the location of an existing slug in compiled instruction surfaces.
|
|
112
|
+
*/
|
|
113
|
+
export function findSurfaceLocation(existingSlug, projectRoot = process.cwd()) {
|
|
114
|
+
const homeDir = process.env.HOME || '';
|
|
115
|
+
const candidates = [
|
|
116
|
+
path.join(projectRoot, 'AGENTS.md'),
|
|
117
|
+
path.join(projectRoot, 'INSTRUCTIONS.md'),
|
|
118
|
+
path.join(projectRoot, 'CLAUDE.md'),
|
|
119
|
+
path.join(projectRoot, '.cursorrules'),
|
|
120
|
+
path.join(homeDir, '.agent', 'INSTRUCTIONS.md'),
|
|
121
|
+
path.join(homeDir, '.agent', 'AGENTS.md'),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (const candidatePath of candidates) {
|
|
125
|
+
if (fs.existsSync(candidatePath)) {
|
|
126
|
+
try {
|
|
127
|
+
const content = fs.readFileSync(candidatePath, 'utf8');
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
if (lines[i].includes(existingSlug)) {
|
|
131
|
+
return {
|
|
132
|
+
file: path.basename(candidatePath),
|
|
133
|
+
path: candidatePath,
|
|
134
|
+
line: i + 1,
|
|
135
|
+
snippet: lines[i].trim().slice(0, 100),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore read errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Record a rule violation incident into the persistent incidents log.
|
|
149
|
+
*/
|
|
150
|
+
export function logIncident(incident, derivedDir) {
|
|
151
|
+
try {
|
|
152
|
+
fs.mkdirSync(derivedDir, { recursive: true });
|
|
153
|
+
const logPath = path.join(derivedDir, 'incidents.jsonl');
|
|
154
|
+
fs.appendFileSync(logPath, JSON.stringify(incident) + '\n', 'utf8');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.warn('duplicate-guard', `Failed to record incident to ${derivedDir}: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if a candidate rule/invariant node is a duplicate of an existing active node.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} candidate - The candidate node to be remembered.
|
|
164
|
+
* @param {object} options
|
|
165
|
+
* @param {string} options.vaultDir - Path to current vault directory.
|
|
166
|
+
* @param {string} [options.derivedDir] - Path to memory-derived directory.
|
|
167
|
+
* @param {string} [options.globalVaultDir] - Global vault, as resolved by the caller's brain owner. Also searched when it differs from vaultDir.
|
|
168
|
+
* @param {string} [options.projectRoot] - Repository root directory.
|
|
169
|
+
* @param {number} [options.jaccardThreshold=0.45] - Threshold for token overlap.
|
|
170
|
+
* @param {number} [options.containmentThreshold=0.60] - Threshold for candidate containment.
|
|
171
|
+
* @param {number} [options.cosineThreshold=0.82] - Threshold for embedding cosine similarity.
|
|
172
|
+
* @returns {object} { isDuplicate: boolean, matchedNode?: object, similarity?: number, surfaceLoc?: object }
|
|
173
|
+
*/
|
|
174
|
+
export function checkDuplicateRule(candidate, options = {}) {
|
|
175
|
+
const {
|
|
176
|
+
vaultDir,
|
|
177
|
+
derivedDir,
|
|
178
|
+
globalVaultDir,
|
|
179
|
+
projectRoot = process.cwd(),
|
|
180
|
+
jaccardThreshold = 0.45,
|
|
181
|
+
containmentThreshold = 0.60,
|
|
182
|
+
cosineThreshold = 0.82,
|
|
183
|
+
} = options;
|
|
184
|
+
|
|
185
|
+
if (!candidate || !RULE_CATEGORIES.has(candidate.category)) {
|
|
186
|
+
return { isDuplicate: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Load existing active nodes from the target vault
|
|
190
|
+
const vaultNodes = getNodes(vaultDir).filter(
|
|
191
|
+
(n) => n.status === 'active' && RULE_CATEGORIES.has(n.category) && n.slug !== candidate.slug
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// If in a project brain, also check global vault to prevent duplicating global invariants
|
|
195
|
+
let allNodes = [...vaultNodes];
|
|
196
|
+
if (globalVaultDir && vaultDir !== globalVaultDir && fs.existsSync(globalVaultDir)) {
|
|
197
|
+
try {
|
|
198
|
+
const globalNodes = getNodes(globalVaultDir).filter(
|
|
199
|
+
(n) => n.status === 'active' && RULE_CATEGORIES.has(n.category) && n.slug !== candidate.slug
|
|
200
|
+
);
|
|
201
|
+
allNodes = [...allNodes, ...globalNodes];
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore global load errors if any
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (allNodes.length === 0) {
|
|
208
|
+
return { isDuplicate: false };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const candidateBody = (candidate.body || '').trim();
|
|
212
|
+
const candidateNorm = candidateBody.toLowerCase().replace(/\s+/g, ' ');
|
|
213
|
+
const candidateTokens = tokenize(
|
|
214
|
+
`${candidate.title || ''} ${candidateBody} ${candidate.subject || ''} ${candidate.predicate || ''} ${candidate.object || ''}`
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Optional vector index check
|
|
218
|
+
let embeddingsIndex = null;
|
|
219
|
+
if (derivedDir && fs.existsSync(path.join(derivedDir, 'embeddings.db'))) {
|
|
220
|
+
try {
|
|
221
|
+
embeddingsIndex = loadEmbeddingsIndex(derivedDir);
|
|
222
|
+
} catch {
|
|
223
|
+
embeddingsIndex = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const existing of allNodes) {
|
|
228
|
+
const existingBody = (existing.body || '').trim();
|
|
229
|
+
const existingNorm = existingBody.toLowerCase().replace(/\s+/g, ' ');
|
|
230
|
+
|
|
231
|
+
// 1. Exact or near-exact normalized body match
|
|
232
|
+
if (candidateNorm.length > 10 && (candidateNorm === existingNorm || existingNorm.includes(candidateNorm) || candidateNorm.includes(existingNorm))) {
|
|
233
|
+
const surfaceLoc = findSurfaceLocation(existing.slug, projectRoot);
|
|
234
|
+
return {
|
|
235
|
+
isDuplicate: true,
|
|
236
|
+
matchedNode: existing,
|
|
237
|
+
similarity: 1.0,
|
|
238
|
+
surfaceLoc,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. Token Jaccard similarity & Containment
|
|
243
|
+
const existingTokens = tokenize(
|
|
244
|
+
`${existing.title || ''} ${existingBody} ${existing.subject || ''} ${existing.predicate || ''} ${existing.object || ''}`
|
|
245
|
+
);
|
|
246
|
+
const tokenSim = jaccard(candidateTokens, existingTokens);
|
|
247
|
+
const containSim = containment(candidateTokens, existingTokens);
|
|
248
|
+
|
|
249
|
+
if (tokenSim >= jaccardThreshold || containSim >= containmentThreshold) {
|
|
250
|
+
const surfaceLoc = findSurfaceLocation(existing.slug, projectRoot);
|
|
251
|
+
const effectiveSim = Math.max(tokenSim, containSim);
|
|
252
|
+
return {
|
|
253
|
+
isDuplicate: true,
|
|
254
|
+
matchedNode: existing,
|
|
255
|
+
similarity: Math.round(effectiveSim * 1000) / 1000,
|
|
256
|
+
surfaceLoc,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 3. Vector Embedding similarity (if available)
|
|
261
|
+
if (embeddingsIndex && embeddingsIndex[candidate.slug] && embeddingsIndex[existing.slug]) {
|
|
262
|
+
try {
|
|
263
|
+
const vecSim = cosineSimilarity(
|
|
264
|
+
embeddingsIndex[candidate.slug].embedding,
|
|
265
|
+
embeddingsIndex[existing.slug].embedding
|
|
266
|
+
);
|
|
267
|
+
if (vecSim >= cosineThreshold) {
|
|
268
|
+
const surfaceLoc = findSurfaceLocation(existing.slug, projectRoot);
|
|
269
|
+
return {
|
|
270
|
+
isDuplicate: true,
|
|
271
|
+
matchedNode: existing,
|
|
272
|
+
similarity: Math.round(vecSim * 1000) / 1000,
|
|
273
|
+
surfaceLoc,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Fall back to token match
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { isDuplicate: false };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Enforces that a candidate rule is not a duplicate.
|
|
287
|
+
* Throws an Error and logs an incident if a collision occurs.
|
|
288
|
+
*/
|
|
289
|
+
export function assertNoDuplicateRule(candidate, options = {}) {
|
|
290
|
+
const check = checkDuplicateRule(candidate, options);
|
|
291
|
+
if (!check.isDuplicate) return;
|
|
292
|
+
|
|
293
|
+
const { matchedNode, similarity, surfaceLoc } = check;
|
|
294
|
+
const surfaceStr = surfaceLoc ? `${surfaceLoc.file} at Line ${surfaceLoc.line}` : 'Active Invariant Pool';
|
|
295
|
+
|
|
296
|
+
const incident = {
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
session_id: process.env.TR_SESSION_ID || 'unknown',
|
|
299
|
+
event: 'rule_write_rejected',
|
|
300
|
+
reason: 'An attempt was made to write a rule that already exists in active memory.',
|
|
301
|
+
attempted: {
|
|
302
|
+
category: candidate.category,
|
|
303
|
+
title: candidate.title,
|
|
304
|
+
body: candidate.body,
|
|
305
|
+
priority: candidate.priority || 'normal',
|
|
306
|
+
},
|
|
307
|
+
existing: {
|
|
308
|
+
slug: matchedNode.slug,
|
|
309
|
+
title: matchedNode.title,
|
|
310
|
+
body: matchedNode.body,
|
|
311
|
+
priority: matchedNode.priority || 'normal',
|
|
312
|
+
category: matchedNode.category,
|
|
313
|
+
},
|
|
314
|
+
similarity,
|
|
315
|
+
surface_location: surfaceStr,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (options.derivedDir) {
|
|
319
|
+
logIncident(incident, options.derivedDir);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const message = [
|
|
323
|
+
'',
|
|
324
|
+
'================================================================================',
|
|
325
|
+
'🚨 VIOLATION INCIDENT DETECTED: DUPLICATE RULE REJECTED',
|
|
326
|
+
'--------------------------------------------------------------------------------',
|
|
327
|
+
`Attempted Rule: "${candidate.body}"`,
|
|
328
|
+
`Existing Rule: [${matchedNode.slug}] "${matchedNode.body}"`,
|
|
329
|
+
`Similarity: ${Math.round(similarity * 100)}%`,
|
|
330
|
+
`Surface Status: Active (${surfaceStr})`,
|
|
331
|
+
'',
|
|
332
|
+
'ROOT-CAUSE ANALYSIS:',
|
|
333
|
+
'The agent attempted to write a rule that ALREADY exists in the active instruction',
|
|
334
|
+
'surface. This indicates the agent failed to follow an invariant that was already',
|
|
335
|
+
'active in its context.',
|
|
336
|
+
'',
|
|
337
|
+
'Refusing to write duplicate node. Zero disk bytes written.',
|
|
338
|
+
'Fix the agent\'s behavior at the source.',
|
|
339
|
+
'================================================================================',
|
|
340
|
+
'',
|
|
341
|
+
].join('\n');
|
|
342
|
+
|
|
343
|
+
console.error(message);
|
|
344
|
+
|
|
345
|
+
const error = new Error(`Rule already exists: [${matchedNode.slug}] "${matchedNode.body}" (${Math.round(similarity * 100)}% match)`);
|
|
346
|
+
error.code = 'ERR_DUPLICATE_RULE';
|
|
347
|
+
error.incident = incident;
|
|
348
|
+
error.matchedNode = matchedNode;
|
|
349
|
+
throw error;
|
|
350
|
+
}
|