@alaarab/cortex 1.15.4 → 1.15.5
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/mcp/dist/cli-hooks-output.js +12 -1
- package/mcp/dist/cli-hooks-session.js +52 -2
- package/mcp/dist/data-access.js +22 -17
- package/mcp/dist/mcp-backlog.js +61 -11
- package/mcp/dist/shared-governance.js +31 -1
- package/package.json +1 -1
|
@@ -47,7 +47,18 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
|
-
|
|
50
|
+
// Position-aware injection: place most relevant at START and END (LaRA benchmark).
|
|
51
|
+
// Input `selected` is already ranked by relevance (best first).
|
|
52
|
+
// Reorder so: [0] stays first, [1] goes last, middle positions get [2..N-1].
|
|
53
|
+
let ordered = selected;
|
|
54
|
+
if (selected.length >= 3) {
|
|
55
|
+
ordered = [
|
|
56
|
+
selected[0], // most relevant → start
|
|
57
|
+
...selected.slice(2), // remaining → middle
|
|
58
|
+
selected[1], // second most → end
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
for (const injected of ordered) {
|
|
51
62
|
const { doc, snippet, key } = injected;
|
|
52
63
|
recordInjection(cortexPathLocal, key, sessionId);
|
|
53
64
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debugLog, appendAuditLog, runtimeFile, EXEC_TIMEOUT_MS, ensureCortexPath, } from "./shared.js";
|
|
1
|
+
import { debugLog, appendAuditLog, runtimeFile, sessionMarker, EXEC_TIMEOUT_MS, ensureCortexPath, } from "./shared.js";
|
|
2
2
|
import { appendReviewQueue, recordFeedback, getQualityMultiplier, updateRuntimeHealth, withFileLock, } from "./shared-governance.js";
|
|
3
3
|
import { detectProject, } from "./shared-index.js";
|
|
4
4
|
import { addFindingToFile, } from "./shared-content.js";
|
|
@@ -526,7 +526,37 @@ export async function handleHookTool() {
|
|
|
526
526
|
// best effort
|
|
527
527
|
}
|
|
528
528
|
const cwd = (data.cwd ?? input.cwd ?? undefined);
|
|
529
|
-
|
|
529
|
+
let activeProject = cwd ? detectProject(getCortexPath(), cwd, profile) : null;
|
|
530
|
+
// Check cooldown
|
|
531
|
+
const cooldownMs = Number.parseInt(process.env.CORTEX_HOOK_TOOL_COOLDOWN_MS || "30000", 10) || 30000;
|
|
532
|
+
const cooldownFile = runtimeFile(getCortexPath(), "hook-tool-cooldown");
|
|
533
|
+
try {
|
|
534
|
+
if (fs.existsSync(cooldownFile)) {
|
|
535
|
+
const age = Date.now() - fs.statSync(cooldownFile).mtimeMs;
|
|
536
|
+
if (age < cooldownMs) {
|
|
537
|
+
debugLog(`hook-tool: cooldown active (${Math.round(age / 1000)}s < ${Math.round(cooldownMs / 1000)}s), skipping extraction`);
|
|
538
|
+
// Skip to the log-only path
|
|
539
|
+
activeProject = null; // prevent extraction below
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch { /* best effort */ }
|
|
544
|
+
// Check session cap
|
|
545
|
+
const sessionCap = Number.parseInt(process.env.CORTEX_HOOK_TOOL_SESSION_CAP || "10", 10) || 10;
|
|
546
|
+
if (activeProject && sessionId) {
|
|
547
|
+
try {
|
|
548
|
+
const capFile = sessionMarker(getCortexPath(), `tool-findings-${sessionId}`);
|
|
549
|
+
let count = 0;
|
|
550
|
+
if (fs.existsSync(capFile)) {
|
|
551
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
552
|
+
}
|
|
553
|
+
if (count >= sessionCap) {
|
|
554
|
+
debugLog(`hook-tool: session cap reached (${count}/${sessionCap}), skipping extraction`);
|
|
555
|
+
activeProject = null; // prevent extraction below
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch { /* best effort */ }
|
|
559
|
+
}
|
|
530
560
|
if (activeProject) {
|
|
531
561
|
try {
|
|
532
562
|
const candidates = extractToolFindings(toolName, input, responseStr);
|
|
@@ -540,6 +570,26 @@ export async function handleHookTool() {
|
|
|
540
570
|
debugLog(`hook-tool: queued candidate (conf=${confidence}): ${text.slice(0, 60)}`);
|
|
541
571
|
}
|
|
542
572
|
}
|
|
573
|
+
// Update cooldown and session counter
|
|
574
|
+
if (candidates.length > 0) {
|
|
575
|
+
try {
|
|
576
|
+
fs.writeFileSync(cooldownFile, Date.now().toString());
|
|
577
|
+
}
|
|
578
|
+
catch { /* best effort */ }
|
|
579
|
+
if (sessionId) {
|
|
580
|
+
try {
|
|
581
|
+
const capFile = sessionMarker(getCortexPath(), `tool-findings-${sessionId}`);
|
|
582
|
+
let count = 0;
|
|
583
|
+
try {
|
|
584
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
585
|
+
}
|
|
586
|
+
catch { /* new file */ }
|
|
587
|
+
count += candidates.filter(c => c.confidence >= 0.85).length; // only count directly added ones
|
|
588
|
+
fs.writeFileSync(capFile, count.toString());
|
|
589
|
+
}
|
|
590
|
+
catch { /* best effort */ }
|
|
591
|
+
}
|
|
592
|
+
}
|
|
543
593
|
}
|
|
544
594
|
catch (err) {
|
|
545
595
|
debugLog(`hook-tool: learning extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -805,25 +805,30 @@ export function removeProjectFromProfile(cortexPath, profile, project) {
|
|
|
805
805
|
return cortexOk(`Removed ${project} from profile ${profile}.`);
|
|
806
806
|
});
|
|
807
807
|
}
|
|
808
|
+
function buildProjectCard(dir) {
|
|
809
|
+
const name = path.basename(dir);
|
|
810
|
+
const summaryFile = path.join(dir, "summary.md");
|
|
811
|
+
const claudeFile = path.join(dir, "CLAUDE.md");
|
|
812
|
+
const summarySource = fs.existsSync(summaryFile)
|
|
813
|
+
? fs.readFileSync(summaryFile, "utf8")
|
|
814
|
+
: fs.existsSync(claudeFile)
|
|
815
|
+
? fs.readFileSync(claudeFile, "utf8")
|
|
816
|
+
: "";
|
|
817
|
+
const summary = summarySource
|
|
818
|
+
.split("\n")
|
|
819
|
+
.map((line) => line.trim())
|
|
820
|
+
.find((line) => line && !line.startsWith("#")) || "";
|
|
821
|
+
const docs = ["CLAUDE.md", "FINDINGS.md", "LEARNINGS.md", "summary.md", "backlog.md", "MEMORY_QUEUE.md"]
|
|
822
|
+
.filter((file) => fs.existsSync(path.join(dir, file)));
|
|
823
|
+
return { name, summary, docs };
|
|
824
|
+
}
|
|
808
825
|
export function listProjectCards(cortexPath, profile) {
|
|
809
826
|
const dirs = getProjectDirs(cortexPath, profile).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
810
|
-
const cards =
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const summarySource = fs.existsSync(summaryFile)
|
|
816
|
-
? fs.readFileSync(summaryFile, "utf8")
|
|
817
|
-
: fs.existsSync(claudeFile)
|
|
818
|
-
? fs.readFileSync(claudeFile, "utf8")
|
|
819
|
-
: "";
|
|
820
|
-
const summary = summarySource
|
|
821
|
-
.split("\n")
|
|
822
|
-
.map((line) => line.trim())
|
|
823
|
-
.find((line) => line && !line.startsWith("#")) || "";
|
|
824
|
-
const docs = ["CLAUDE.md", "FINDINGS.md", "LEARNINGS.md", "summary.md", "backlog.md", "MEMORY_QUEUE.md"]
|
|
825
|
-
.filter((file) => fs.existsSync(path.join(dir, file)));
|
|
826
|
-
cards.push({ name, summary, docs });
|
|
827
|
+
const cards = dirs.map(buildProjectCard);
|
|
828
|
+
// Prepend global as a pinned entry so it's always accessible from the shell
|
|
829
|
+
const globalDir = path.join(cortexPath, "global");
|
|
830
|
+
if (fs.existsSync(globalDir)) {
|
|
831
|
+
cards.unshift(buildProjectCard(globalDir));
|
|
827
832
|
}
|
|
828
833
|
return cards;
|
|
829
834
|
}
|
package/mcp/dist/mcp-backlog.js
CHANGED
|
@@ -5,7 +5,8 @@ import * as path from "path";
|
|
|
5
5
|
import { isValidProjectName } from "./utils.js";
|
|
6
6
|
import { addBacklogItem as addBacklogItemStore, addBacklogItems as addBacklogItemsBatch, backlogMarkdown, completeBacklogItem as completeBacklogItemStore, completeBacklogItems as completeBacklogItemsBatch, readBacklog, readBacklogs, updateBacklogItem as updateBacklogItemStore, } from "./data-access.js";
|
|
7
7
|
const BACKLOG_SECTION_ORDER = ["Active", "Queue", "Done"];
|
|
8
|
-
|
|
8
|
+
const DEFAULT_BACKLOG_LIMIT = 20;
|
|
9
|
+
function buildBacklogView(doc, status, limit) {
|
|
9
10
|
let includedSections;
|
|
10
11
|
if (status === "all") {
|
|
11
12
|
includedSections = BACKLOG_SECTION_ORDER;
|
|
@@ -22,11 +23,23 @@ function buildBacklogView(doc, status) {
|
|
|
22
23
|
else {
|
|
23
24
|
includedSections = ["Active", "Queue"];
|
|
24
25
|
}
|
|
26
|
+
const effectiveLimit = limit ?? DEFAULT_BACKLOG_LIMIT;
|
|
27
|
+
let truncated = false;
|
|
25
28
|
const items = {
|
|
26
|
-
Active:
|
|
27
|
-
Queue:
|
|
28
|
-
Done:
|
|
29
|
+
Active: [],
|
|
30
|
+
Queue: [],
|
|
31
|
+
Done: [],
|
|
29
32
|
};
|
|
33
|
+
for (const section of includedSections) {
|
|
34
|
+
const sectionItems = doc.items[section];
|
|
35
|
+
if (sectionItems.length > effectiveLimit) {
|
|
36
|
+
items[section] = sectionItems.slice(0, effectiveLimit);
|
|
37
|
+
truncated = true;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
items[section] = sectionItems;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
30
43
|
const totalItems = BACKLOG_SECTION_ORDER.reduce((sum, section) => sum + items[section].length, 0);
|
|
31
44
|
return {
|
|
32
45
|
doc: {
|
|
@@ -35,8 +48,26 @@ function buildBacklogView(doc, status) {
|
|
|
35
48
|
},
|
|
36
49
|
includedSections,
|
|
37
50
|
totalItems,
|
|
51
|
+
truncated,
|
|
38
52
|
};
|
|
39
53
|
}
|
|
54
|
+
function buildBacklogSummary(doc, includedSections) {
|
|
55
|
+
const lines = [`## ${doc.project}`];
|
|
56
|
+
for (const section of includedSections) {
|
|
57
|
+
const items = doc.items[section];
|
|
58
|
+
const highCount = items.filter(i => i.priority === "high").length;
|
|
59
|
+
const medCount = items.filter(i => i.priority === "medium").length;
|
|
60
|
+
lines.push(`**${section}**: ${items.length} items${highCount ? ` (${highCount} high` + (medCount ? `, ${medCount} medium` : "") + ")" : ""}`);
|
|
61
|
+
// Show first 3 items as preview
|
|
62
|
+
for (const item of items.slice(0, 3)) {
|
|
63
|
+
const prio = item.priority ? ` [${item.priority}]` : "";
|
|
64
|
+
lines.push(` - ${item.line.slice(0, 80)}${item.line.length > 80 ? "\u2026" : ""}${prio}`);
|
|
65
|
+
}
|
|
66
|
+
if (items.length > 3)
|
|
67
|
+
lines.push(` ... and ${items.length - 3} more`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
40
71
|
export function register(server, ctx) {
|
|
41
72
|
const { cortexPath, profile, withWriteQueue, updateFileInIndex } = ctx;
|
|
42
73
|
server.registerTool("get_backlog", {
|
|
@@ -47,8 +78,10 @@ export function register(server, ctx) {
|
|
|
47
78
|
id: z.string().optional().describe("Backlog item ID like A1, Q3, D2. Requires project."),
|
|
48
79
|
item: z.string().optional().describe("Exact backlog item text. Requires project."),
|
|
49
80
|
status: z.enum(["all", "active", "queue", "done", "active+queue"]).optional().describe("Which backlog sections to include. Defaults to 'active+queue'."),
|
|
81
|
+
limit: z.number().optional().describe("Max items per section to return. Default 20."),
|
|
82
|
+
summary: z.boolean().optional().describe("If true, return counts and titles only (no full content). Reduces token usage."),
|
|
50
83
|
}),
|
|
51
|
-
}, async ({ project, id, item, status }) => {
|
|
84
|
+
}, async ({ project, id, item, status, limit, summary }) => {
|
|
52
85
|
// Single item lookup
|
|
53
86
|
if (id || item) {
|
|
54
87
|
if (!project)
|
|
@@ -78,7 +111,7 @@ export function register(server, ctx) {
|
|
|
78
111
|
if (!result.ok)
|
|
79
112
|
return mcpResponse({ ok: false, error: result.error });
|
|
80
113
|
const doc = result.data;
|
|
81
|
-
const view = buildBacklogView(doc, status);
|
|
114
|
+
const view = buildBacklogView(doc, status, limit);
|
|
82
115
|
if (!fs.existsSync(doc.path)) {
|
|
83
116
|
return mcpResponse({
|
|
84
117
|
ok: true,
|
|
@@ -86,26 +119,43 @@ export function register(server, ctx) {
|
|
|
86
119
|
data: { project, items: view.doc.items, includedSections: view.includedSections, totalItems: view.totalItems },
|
|
87
120
|
});
|
|
88
121
|
}
|
|
122
|
+
if (summary) {
|
|
123
|
+
return mcpResponse({
|
|
124
|
+
ok: true,
|
|
125
|
+
message: buildBacklogSummary(doc, view.includedSections),
|
|
126
|
+
data: { project, includedSections: view.includedSections, totalItems: view.totalItems, summary: true },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const truncationNote = view.truncated ? `\n\n_Showing first ${limit ?? DEFAULT_BACKLOG_LIMIT} items per section. Pass a higher limit to see more._` : "";
|
|
89
130
|
return mcpResponse({
|
|
90
131
|
ok: true,
|
|
91
|
-
message: `## ${project}\n${backlogMarkdown(view.doc)}`,
|
|
92
|
-
data: { project, items: view.doc.items, issues: doc.issues, includedSections: view.includedSections, totalItems: view.totalItems },
|
|
132
|
+
message: `## ${project}\n${backlogMarkdown(view.doc)}${truncationNote}`,
|
|
133
|
+
data: { project, items: view.doc.items, issues: doc.issues, includedSections: view.includedSections, totalItems: view.totalItems, truncated: view.truncated },
|
|
93
134
|
});
|
|
94
135
|
}
|
|
95
136
|
// All projects
|
|
96
137
|
const docs = readBacklogs(cortexPath, profile);
|
|
97
138
|
if (!docs.length)
|
|
98
139
|
return mcpResponse({ ok: true, message: "No backlogs found.", data: { projects: [] } });
|
|
99
|
-
const views = docs.map((doc) => ({ project: doc.project, view: buildBacklogView(doc, status), issues: doc.issues }));
|
|
100
|
-
const
|
|
140
|
+
const views = docs.map((doc) => ({ project: doc.project, doc, view: buildBacklogView(doc, status, limit), issues: doc.issues }));
|
|
141
|
+
const anyTruncated = views.some(({ view }) => view.truncated);
|
|
142
|
+
let parts;
|
|
143
|
+
if (summary) {
|
|
144
|
+
parts = views.map(({ doc, view }) => buildBacklogSummary(doc, view.includedSections));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
parts = views.map(({ project, view }) => `## ${project}\n${backlogMarkdown(view.doc)}`);
|
|
148
|
+
}
|
|
149
|
+
const truncationNote = anyTruncated && !summary ? `\n\n_Showing first ${limit ?? DEFAULT_BACKLOG_LIMIT} items per section. Pass a higher limit to see more._` : "";
|
|
101
150
|
const projectData = views.map(({ project, view, issues }) => ({
|
|
102
151
|
project,
|
|
103
152
|
items: view.doc.items,
|
|
104
153
|
issues,
|
|
105
154
|
includedSections: view.includedSections,
|
|
106
155
|
totalItems: view.totalItems,
|
|
156
|
+
truncated: view.truncated,
|
|
107
157
|
}));
|
|
108
|
-
return mcpResponse({ ok: true, message: parts.join("\n\n"), data: { projects: projectData } });
|
|
158
|
+
return mcpResponse({ ok: true, message: parts.join("\n\n") + truncationNote, data: { projects: projectData, summary: summary || false } });
|
|
109
159
|
});
|
|
110
160
|
server.registerTool("add_backlog_item", {
|
|
111
161
|
title: "◆ cortex · add task",
|
|
@@ -835,6 +835,8 @@ export function getQualityMultiplier(cortexPath, key) {
|
|
|
835
835
|
let helpful = entry ? entry.helpful : 0;
|
|
836
836
|
let repromptPenalty = entry ? entry.repromptPenalty : 0;
|
|
837
837
|
let regressionPenalty = entry ? entry.regressionPenalty : 0;
|
|
838
|
+
let impressions = entry ? entry.impressions : 0;
|
|
839
|
+
let lastUsedAt = entry ? entry.lastUsedAt : "";
|
|
838
840
|
// Include unflushed journal deltas
|
|
839
841
|
const journalEntries = readScoreJournal(cortexPath).filter(e => e.key === key);
|
|
840
842
|
for (const je of journalEntries) {
|
|
@@ -844,11 +846,39 @@ export function getQualityMultiplier(cortexPath, key) {
|
|
|
844
846
|
repromptPenalty += je.delta.repromptPenalty;
|
|
845
847
|
if (je.delta.regressionPenalty)
|
|
846
848
|
regressionPenalty += je.delta.regressionPenalty;
|
|
849
|
+
if (je.delta.impressions)
|
|
850
|
+
impressions += je.delta.impressions;
|
|
851
|
+
// Track most recent journal timestamp as lastUsedAt
|
|
852
|
+
if (je.at && (!lastUsedAt || je.at > lastUsedAt))
|
|
853
|
+
lastUsedAt = je.at;
|
|
847
854
|
}
|
|
848
855
|
if (!entry && journalEntries.length === 0)
|
|
849
856
|
return 1;
|
|
857
|
+
// ACT-R activation scoring components:
|
|
858
|
+
// 1. Temporal decay: recency of last retrieval
|
|
859
|
+
let recencyBoost = 0;
|
|
860
|
+
if (lastUsedAt) {
|
|
861
|
+
const lastUsedMs = new Date(lastUsedAt).getTime();
|
|
862
|
+
if (!Number.isNaN(lastUsedMs)) {
|
|
863
|
+
const daysSinceUse = Math.max(0, (Date.now() - lastUsedMs) / 86_400_000);
|
|
864
|
+
if (daysSinceUse <= 7) {
|
|
865
|
+
recencyBoost = 0.15; // recently used: boost
|
|
866
|
+
}
|
|
867
|
+
else if (daysSinceUse <= 30) {
|
|
868
|
+
recencyBoost = 0; // neutral
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
recencyBoost = -0.1 * Math.min(3, (daysSinceUse - 30) / 30); // decay up to -0.3
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// 2. Usage frequency: log-scaled impressions (diminishing returns)
|
|
876
|
+
const frequencyBoost = impressions > 0 ? Math.min(0.2, Math.log2(impressions + 1) * 0.05) : 0;
|
|
877
|
+
// 3. Feedback signals (existing)
|
|
850
878
|
const penalties = repromptPenalty + regressionPenalty * 2;
|
|
851
|
-
const
|
|
879
|
+
const feedbackScore = helpful * 0.15 - penalties * 0.2;
|
|
880
|
+
// Combine all components
|
|
881
|
+
const raw = 1 + feedbackScore + recencyBoost + frequencyBoost;
|
|
852
882
|
return Math.max(0.2, Math.min(1.5, raw));
|
|
853
883
|
}
|
|
854
884
|
export function recordRetrieval(cortexPath, file, section) {
|