@alaarab/cortex 1.13.6 → 1.15.0
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/README.md +16 -15
- package/mcp/dist/cli-config.js +20 -16
- package/mcp/dist/cli-extract.js +15 -10
- package/mcp/dist/cli-govern.js +37 -32
- package/mcp/dist/cli-hooks-citations.js +1 -1
- package/mcp/dist/cli-hooks-retrieval.js +42 -9
- package/mcp/dist/cli-hooks-session.js +46 -41
- package/mcp/dist/cli-hooks.js +26 -21
- package/mcp/dist/cli.js +172 -24
- package/mcp/dist/data-access.js +7 -43
- package/mcp/dist/index.js +23 -15
- package/mcp/dist/link-skills.js +0 -6
- package/mcp/dist/link.js +14 -1
- package/mcp/dist/mcp-finding.js +2 -2
- package/mcp/dist/mcp-search.js +4 -4
- package/mcp/dist/shared-index.js +45 -26
- package/mcp/dist/shared.js +6 -0
- package/mcp/dist/utils.js +49 -5
- package/package.json +11 -11
- package/starter/README.md +2 -2
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ Supports Claude Code, Copilot CLI, Cursor, and Codex.
|
|
|
19
19
|
|
|
20
20
|
<br>
|
|
21
21
|
|
|
22
|
-
Project knowledge, field findings, task queues
|
|
22
|
+
Project knowledge, field findings, task queues. Stored as markdown in a git repo you own. No vendor lock-in, no cloud dependency. One command to set up. Zero commands to use after that.
|
|
23
23
|
|
|
24
|
-
> **Quick start:** `npx @alaarab/cortex init`
|
|
24
|
+
> **Quick start:** `npx @alaarab/cortex init` takes 30 seconds, no account needed.
|
|
25
25
|
|
|
26
26
|
<br>
|
|
27
27
|
</div>
|
|
@@ -98,24 +98,24 @@ On a new machine: clone, run init, done.
|
|
|
98
98
|
|
|
99
99
|
## What's new
|
|
100
100
|
|
|
101
|
-
- **
|
|
102
|
-
- **
|
|
103
|
-
- **
|
|
104
|
-
- **
|
|
105
|
-
- **
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
108
|
-
- **Data portability
|
|
101
|
+
- **Terminal shell**: open `cortex` and get tabs for Backlog, Findings, Memory Queue, and Health. No agent needed
|
|
102
|
+
- **Synonym search**: type "throttling" and find "rate limit" and "429". You don't need to remember what you called it
|
|
103
|
+
- **Bulk operations**: `add_findings`, `add_backlog_items`, `complete_backlog_items`, `remove_findings` for batch work
|
|
104
|
+
- **Memory quality**: confidence scoring, age decay, and a feedback loop. Stale or low-signal entries stop appearing
|
|
105
|
+
- **Starter templates**: `cortex init --template python-project|monorepo|library|frontend`
|
|
106
|
+
- **Multi-agent access control**: four roles (admin, maintainer, contributor, viewer) for shared cortex repos
|
|
107
|
+
- **Deep reference**: `reference/` subdirectories indexed separately so API docs don't drown out your findings
|
|
108
|
+
- **Data portability**: export/import projects as JSON, archive/unarchive anytime
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
112
|
## What makes this different
|
|
113
113
|
|
|
114
|
-
**It runs itself.** Hooks inject context before every prompt and auto-save after every response.
|
|
114
|
+
**It runs itself.** Hooks inject context before every prompt and auto-save after every response. Trust filtering checks confidence, age decay, and citation validity before anything lands in your context.
|
|
115
115
|
|
|
116
116
|
**It's just files.** Markdown in a git repo you own. No database, no vector store, no account. `git log` shows how it grew.
|
|
117
117
|
|
|
118
|
-
**Search that works.** Type "throttling" and it finds "rate limit" and "429".
|
|
118
|
+
**Search that works.** Type "throttling" and it finds "rate limit" and "429". You don't need to remember what you called it.
|
|
119
119
|
|
|
120
120
|
**Every machine, same brain.** Push to a private repo, clone on a new machine, run init. Profiles control which projects each machine sees.
|
|
121
121
|
|
|
@@ -123,15 +123,15 @@ On a new machine: clone, run init, done.
|
|
|
123
123
|
|
|
124
124
|
## What lives in your cortex
|
|
125
125
|
|
|
126
|
-
`cortex init` creates your project store with starter templates. Each project gets its own directory
|
|
126
|
+
`cortex init` creates your project store with starter templates. Each project gets its own directory. Add files as the project grows.
|
|
127
127
|
|
|
128
128
|
| File | What it's for |
|
|
129
129
|
|------|--------------|
|
|
130
|
-
| `summary.md` | Five-line card: what, stack, status, how to run,
|
|
130
|
+
| `summary.md` | Five-line card: what, stack, status, how to run, key insight |
|
|
131
131
|
| `CLAUDE.md` | Full context: architecture, commands, conventions |
|
|
132
132
|
| `REFERENCE.md` | Deep reference: API details, data models, things too long for CLAUDE.md |
|
|
133
133
|
| `FINDINGS.md` | Bugs hit, patterns discovered, things to avoid next time |
|
|
134
|
-
| `CANONICAL_MEMORIES.md` | Pinned
|
|
134
|
+
| `CANONICAL_MEMORIES.md` | Pinned memories that never expire and always inject |
|
|
135
135
|
| `backlog.md` | Task queue that persists across sessions |
|
|
136
136
|
| `MEMORY_QUEUE.md` | Items waiting for your review (see [Memory queue](#memory-queue) below) |
|
|
137
137
|
| `.claude/skills/` | Project-specific slash commands |
|
|
@@ -209,6 +209,7 @@ The server indexes your cortex into a local SQLite FTS5 database. Tools are grou
|
|
|
209
209
|
| `get_related_docs` | Get docs linked to a named entity. |
|
|
210
210
|
| `read_graph` | Read the entity graph for a project or all projects. |
|
|
211
211
|
| `link_findings` | Manually link a finding to an entity. Persists to manual-links.json and survives rebuilds. |
|
|
212
|
+
| `cross_project_entities` | Find entities shared across multiple projects. |
|
|
212
213
|
|
|
213
214
|
### Session management
|
|
214
215
|
|
package/mcp/dist/cli-config.js
CHANGED
|
@@ -2,8 +2,12 @@ import { ensureCortexPath } from "./shared.js";
|
|
|
2
2
|
import { getIndexPolicy, updateIndexPolicy, getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getAccessControl, updateAccessControl, } from "./shared-governance.js";
|
|
3
3
|
import { listMachines as listMachinesStore, listProfiles as listProfilesStore } from "./data-access.js";
|
|
4
4
|
import { setTelemetryEnabled, getTelemetrySummary, resetTelemetry } from "./telemetry.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
let _cortexPath;
|
|
6
|
+
function getCortexPath() {
|
|
7
|
+
if (!_cortexPath)
|
|
8
|
+
_cortexPath = ensureCortexPath();
|
|
9
|
+
return _cortexPath;
|
|
10
|
+
}
|
|
7
11
|
// ── Config router ────────────────────────────────────────────────────────────
|
|
8
12
|
export async function handleConfig(args) {
|
|
9
13
|
const sub = args[0];
|
|
@@ -43,7 +47,7 @@ Subcommands:
|
|
|
43
47
|
// ── Index policy ─────────────────────────────────────────────────────────────
|
|
44
48
|
export async function handleIndexPolicy(args) {
|
|
45
49
|
if (!args.length || args[0] === "get") {
|
|
46
|
-
console.log(JSON.stringify(getIndexPolicy(
|
|
50
|
+
console.log(JSON.stringify(getIndexPolicy(getCortexPath()), null, 2));
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
53
|
if (args[0] === "set") {
|
|
@@ -64,7 +68,7 @@ export async function handleIndexPolicy(args) {
|
|
|
64
68
|
patch.includeHidden = /^(1|true|yes|on)$/i.test(v);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
|
-
const result = updateIndexPolicy(
|
|
71
|
+
const result = updateIndexPolicy(getCortexPath(), patch);
|
|
68
72
|
if (!result.ok) {
|
|
69
73
|
console.log(result.error);
|
|
70
74
|
if (result.code === "PERMISSION_DENIED")
|
|
@@ -80,7 +84,7 @@ export async function handleIndexPolicy(args) {
|
|
|
80
84
|
// ── Memory policy ────────────────────────────────────────────────────────────
|
|
81
85
|
export async function handleRetentionPolicy(args) {
|
|
82
86
|
if (!args.length || args[0] === "get") {
|
|
83
|
-
console.log(JSON.stringify(getRetentionPolicy(
|
|
87
|
+
console.log(JSON.stringify(getRetentionPolicy(getCortexPath()), null, 2));
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
if (args[0] === "set") {
|
|
@@ -101,7 +105,7 @@ export async function handleRetentionPolicy(args) {
|
|
|
101
105
|
patch[k] = value;
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
|
-
const result = updateRetentionPolicy(
|
|
108
|
+
const result = updateRetentionPolicy(getCortexPath(), patch);
|
|
105
109
|
if (!result.ok) {
|
|
106
110
|
console.log(result.error);
|
|
107
111
|
if (result.code === "PERMISSION_DENIED")
|
|
@@ -117,7 +121,7 @@ export async function handleRetentionPolicy(args) {
|
|
|
117
121
|
// ── Memory workflow ──────────────────────────────────────────────────────────
|
|
118
122
|
export async function handleWorkflowPolicy(args) {
|
|
119
123
|
if (!args.length || args[0] === "get") {
|
|
120
|
-
console.log(JSON.stringify(getWorkflowPolicy(
|
|
124
|
+
console.log(JSON.stringify(getWorkflowPolicy(getCortexPath()), null, 2));
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
127
|
if (args[0] === "set") {
|
|
@@ -139,7 +143,7 @@ export async function handleWorkflowPolicy(args) {
|
|
|
139
143
|
patch[k] = Number.isNaN(num) ? v : num;
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
|
-
const result = updateWorkflowPolicy(
|
|
146
|
+
const result = updateWorkflowPolicy(getCortexPath(), patch);
|
|
143
147
|
if (!result.ok) {
|
|
144
148
|
console.log(result.error);
|
|
145
149
|
if (result.code === "PERMISSION_DENIED")
|
|
@@ -155,7 +159,7 @@ export async function handleWorkflowPolicy(args) {
|
|
|
155
159
|
// ── Memory access ────────────────────────────────────────────────────────────
|
|
156
160
|
export async function handleAccessControl(args) {
|
|
157
161
|
if (!args.length || args[0] === "get") {
|
|
158
|
-
console.log(JSON.stringify(getAccessControl(
|
|
162
|
+
console.log(JSON.stringify(getAccessControl(getCortexPath()), null, 2));
|
|
159
163
|
return;
|
|
160
164
|
}
|
|
161
165
|
if (args[0] === "set") {
|
|
@@ -168,7 +172,7 @@ export async function handleAccessControl(args) {
|
|
|
168
172
|
continue;
|
|
169
173
|
patch[k] = v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
170
174
|
}
|
|
171
|
-
const result = updateAccessControl(
|
|
175
|
+
const result = updateAccessControl(getCortexPath(), patch);
|
|
172
176
|
if (!result.ok) {
|
|
173
177
|
console.log(result.error);
|
|
174
178
|
if (result.code === "PERMISSION_DENIED")
|
|
@@ -183,7 +187,7 @@ export async function handleAccessControl(args) {
|
|
|
183
187
|
}
|
|
184
188
|
// ── Machines and profiles ────────────────────────────────────────────────────
|
|
185
189
|
export function handleConfigMachines() {
|
|
186
|
-
const result = listMachinesStore(
|
|
190
|
+
const result = listMachinesStore(getCortexPath());
|
|
187
191
|
if (!result.ok) {
|
|
188
192
|
console.log(result.error);
|
|
189
193
|
return;
|
|
@@ -192,7 +196,7 @@ export function handleConfigMachines() {
|
|
|
192
196
|
console.log(`Registered Machines\n${lines.join("\n")}`);
|
|
193
197
|
}
|
|
194
198
|
export function handleConfigProfiles() {
|
|
195
|
-
const result = listProfilesStore(
|
|
199
|
+
const result = listProfilesStore(getCortexPath());
|
|
196
200
|
if (!result.ok) {
|
|
197
201
|
console.log(result.error);
|
|
198
202
|
return;
|
|
@@ -209,19 +213,19 @@ function handleConfigTelemetry(args) {
|
|
|
209
213
|
const action = args[0];
|
|
210
214
|
switch (action) {
|
|
211
215
|
case "on":
|
|
212
|
-
setTelemetryEnabled(
|
|
216
|
+
setTelemetryEnabled(getCortexPath(), true);
|
|
213
217
|
console.log("Telemetry enabled. Local usage stats will be collected.");
|
|
214
218
|
console.log("No data is sent externally. Stats are stored in .runtime/telemetry.json.");
|
|
215
219
|
return;
|
|
216
220
|
case "off":
|
|
217
|
-
setTelemetryEnabled(
|
|
221
|
+
setTelemetryEnabled(getCortexPath(), false);
|
|
218
222
|
console.log("Telemetry disabled.");
|
|
219
223
|
return;
|
|
220
224
|
case "reset":
|
|
221
|
-
resetTelemetry(
|
|
225
|
+
resetTelemetry(getCortexPath());
|
|
222
226
|
console.log("Telemetry stats reset.");
|
|
223
227
|
return;
|
|
224
228
|
default:
|
|
225
|
-
console.log(getTelemetrySummary(
|
|
229
|
+
console.log(getTelemetrySummary(getCortexPath()));
|
|
226
230
|
}
|
|
227
231
|
}
|
package/mcp/dist/cli-extract.js
CHANGED
|
@@ -8,7 +8,12 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as os from "os";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { execFileSync } from "child_process";
|
|
11
|
-
|
|
11
|
+
let _cortexPath;
|
|
12
|
+
function getCortexPath() {
|
|
13
|
+
if (!_cortexPath)
|
|
14
|
+
_cortexPath = ensureCortexPath();
|
|
15
|
+
return _cortexPath;
|
|
16
|
+
}
|
|
12
17
|
const profile = process.env.CORTEX_PROFILE || "";
|
|
13
18
|
function runGit(cwd, args) {
|
|
14
19
|
return runGitShared(cwd, args, EXEC_TIMEOUT_MS, debugLog);
|
|
@@ -20,7 +25,7 @@ function shouldRetryGh(err) {
|
|
|
20
25
|
function inferProject(arg) {
|
|
21
26
|
if (arg)
|
|
22
27
|
return arg;
|
|
23
|
-
return detectProject(
|
|
28
|
+
return detectProject(getCortexPath(), process.cwd(), profile);
|
|
24
29
|
}
|
|
25
30
|
// ── Git log parsing ──────────────────────────────────────────────────────────
|
|
26
31
|
export function parseGitLogRecords(cwd, days) {
|
|
@@ -219,7 +224,7 @@ export async function handleExtractMemories(projectArg, cwdArg, silent = false)
|
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
221
226
|
const days = Number.parseInt(process.env.CORTEX_MEMORY_EXTRACT_WINDOW_DAYS || "30", 10);
|
|
222
|
-
const threshold = Number.parseFloat(process.env.CORTEX_MEMORY_AUTO_ACCEPT || String(getRetentionPolicy(
|
|
227
|
+
const threshold = Number.parseFloat(process.env.CORTEX_MEMORY_AUTO_ACCEPT || String(getRetentionPolicy(getCortexPath()).autoAcceptThreshold));
|
|
223
228
|
const records = parseGitLogRecords(repoRoot, Number.isNaN(days) ? 30 : days);
|
|
224
229
|
const ghCandidates = isFeatureEnabled("CORTEX_FEATURE_GH_MINING", false)
|
|
225
230
|
? await mineGithubCandidates(repoRoot)
|
|
@@ -232,14 +237,14 @@ export async function handleExtractMemories(projectArg, cwdArg, silent = false)
|
|
|
232
237
|
continue;
|
|
233
238
|
const line = `${candidate.text} (source commit ${rec.hash.slice(0, 8)})`;
|
|
234
239
|
if (candidate.score >= threshold) {
|
|
235
|
-
addFindingToFile(
|
|
240
|
+
addFindingToFile(getCortexPath(), project, line, {
|
|
236
241
|
repo: repoRoot,
|
|
237
242
|
commit: rec.hash,
|
|
238
243
|
});
|
|
239
244
|
accepted++;
|
|
240
245
|
}
|
|
241
246
|
else {
|
|
242
|
-
const qr1 = appendReviewQueue(
|
|
247
|
+
const qr1 = appendReviewQueue(getCortexPath(), project, "Review", [`[confidence ${candidate.score.toFixed(2)}] ${line}`]);
|
|
243
248
|
if (qr1.ok)
|
|
244
249
|
queued += qr1.data;
|
|
245
250
|
}
|
|
@@ -248,10 +253,10 @@ export async function handleExtractMemories(projectArg, cwdArg, silent = false)
|
|
|
248
253
|
const line = `${c.text}${c.commit ? ` (source commit ${c.commit.slice(0, 8)})` : ""}`;
|
|
249
254
|
if (c.text.startsWith("CI failure pattern:")) {
|
|
250
255
|
const key = entryScoreKey(project, "FINDINGS.md", line);
|
|
251
|
-
recordFeedback(
|
|
256
|
+
recordFeedback(getCortexPath(), key, "regression");
|
|
252
257
|
}
|
|
253
258
|
if (c.score >= threshold) {
|
|
254
|
-
addFindingToFile(
|
|
259
|
+
addFindingToFile(getCortexPath(), project, line, {
|
|
255
260
|
repo: repoRoot,
|
|
256
261
|
commit: c.commit,
|
|
257
262
|
file: c.file,
|
|
@@ -259,13 +264,13 @@ export async function handleExtractMemories(projectArg, cwdArg, silent = false)
|
|
|
259
264
|
accepted++;
|
|
260
265
|
}
|
|
261
266
|
else {
|
|
262
|
-
const qr2 = appendReviewQueue(
|
|
267
|
+
const qr2 = appendReviewQueue(getCortexPath(), project, "Review", [`[confidence ${c.score.toFixed(2)}] ${line}`]);
|
|
263
268
|
if (qr2.ok)
|
|
264
269
|
queued += qr2.data;
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
|
-
flushEntryScores(
|
|
268
|
-
appendAuditLog(
|
|
272
|
+
flushEntryScores(getCortexPath());
|
|
273
|
+
appendAuditLog(getCortexPath(), "extract_memories", `project=${project} accepted=${accepted} queued=${queued} window_days=${days}`);
|
|
269
274
|
if (!silent)
|
|
270
275
|
console.log(`Extracted memory candidates for ${project}: accepted=${accepted}, queued=${queued}, window=${days}d`);
|
|
271
276
|
}
|
package/mcp/dist/cli-govern.js
CHANGED
|
@@ -4,13 +4,18 @@ import { filterTrustedFindingsDetailed, migrateLegacyFindings, } from "./shared-
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { handleExtractMemories } from "./cli-extract.js";
|
|
7
|
-
|
|
7
|
+
let _cortexPath;
|
|
8
|
+
function getCortexPath() {
|
|
9
|
+
if (!_cortexPath)
|
|
10
|
+
_cortexPath = ensureCortexPath();
|
|
11
|
+
return _cortexPath;
|
|
12
|
+
}
|
|
8
13
|
const profile = process.env.CORTEX_PROFILE || "";
|
|
9
14
|
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
10
15
|
function targetProjects(projectArg) {
|
|
11
16
|
return projectArg
|
|
12
17
|
? [projectArg]
|
|
13
|
-
: getProjectDirs(
|
|
18
|
+
: getProjectDirs(getCortexPath(), profile).map((p) => path.basename(p)).filter((p) => p !== "global");
|
|
14
19
|
}
|
|
15
20
|
function parseProjectDryRunArgs(args, command, usage) {
|
|
16
21
|
let projectArg;
|
|
@@ -36,7 +41,7 @@ function parseProjectDryRunArgs(args, command, usage) {
|
|
|
36
41
|
function captureFindingBackups(projects) {
|
|
37
42
|
const snapshots = new Map();
|
|
38
43
|
for (const project of projects) {
|
|
39
|
-
const backup = path.join(
|
|
44
|
+
const backup = path.join(getCortexPath(), project, "FINDINGS.md.bak");
|
|
40
45
|
if (!fs.existsSync(backup))
|
|
41
46
|
continue;
|
|
42
47
|
snapshots.set(backup, fs.statSync(backup).mtimeMs);
|
|
@@ -46,14 +51,14 @@ function captureFindingBackups(projects) {
|
|
|
46
51
|
function summarizeBackupChanges(before, projects) {
|
|
47
52
|
const changed = [];
|
|
48
53
|
for (const project of projects) {
|
|
49
|
-
const backup = path.join(
|
|
54
|
+
const backup = path.join(getCortexPath(), project, "FINDINGS.md.bak");
|
|
50
55
|
if (!fs.existsSync(backup))
|
|
51
56
|
continue;
|
|
52
57
|
const current = fs.statSync(backup).mtimeMs;
|
|
53
58
|
const previous = before.get(backup);
|
|
54
59
|
if (previous === undefined || current !== previous) {
|
|
55
60
|
// Normalize to forward slashes for consistent output across platforms
|
|
56
|
-
changed.push(path.relative(
|
|
61
|
+
changed.push(path.relative(getCortexPath(), backup).replace(/\\/g, "/"));
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
return changed.sort();
|
|
@@ -66,16 +71,16 @@ function qualityMarkers(cortexPathLocal) {
|
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
export async function handleGovernMemories(projectArg, silent = false, dryRun = false) {
|
|
69
|
-
const policy = getRetentionPolicy(
|
|
74
|
+
const policy = getRetentionPolicy(getCortexPath());
|
|
70
75
|
const ttlDays = Number.parseInt(process.env.CORTEX_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
|
|
71
76
|
const projects = projectArg
|
|
72
77
|
? [projectArg]
|
|
73
|
-
: getProjectDirs(
|
|
78
|
+
: getProjectDirs(getCortexPath(), profile).map((p) => path.basename(p)).filter((p) => p !== "global");
|
|
74
79
|
let staleCount = 0;
|
|
75
80
|
let conflictCount = 0;
|
|
76
81
|
let reviewCount = 0;
|
|
77
82
|
for (const project of projects) {
|
|
78
|
-
const learningsPath = path.join(
|
|
83
|
+
const learningsPath = path.join(getCortexPath(), project, "FINDINGS.md");
|
|
79
84
|
if (!fs.existsSync(learningsPath))
|
|
80
85
|
continue;
|
|
81
86
|
const content = fs.readFileSync(learningsPath, "utf8");
|
|
@@ -93,18 +98,18 @@ export async function handleGovernMemories(projectArg, silent = false, dryRun =
|
|
|
93
98
|
.filter((l) => /(fixed stuff|updated things|misc|temp|wip|quick note)/i.test(l) || l.length < 16);
|
|
94
99
|
reviewCount += lowValue.length;
|
|
95
100
|
if (!dryRun) {
|
|
96
|
-
appendReviewQueue(
|
|
97
|
-
appendReviewQueue(
|
|
98
|
-
appendReviewQueue(
|
|
101
|
+
appendReviewQueue(getCortexPath(), project, "Stale", stale);
|
|
102
|
+
appendReviewQueue(getCortexPath(), project, "Conflicts", conflicts);
|
|
103
|
+
appendReviewQueue(getCortexPath(), project, "Review", lowValue);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
if (!dryRun) {
|
|
102
|
-
appendAuditLog(
|
|
107
|
+
appendAuditLog(getCortexPath(), "govern_memories", `projects=${projects.length} stale=${staleCount} conflicts=${conflictCount} review=${reviewCount}`);
|
|
103
108
|
for (const project of projects) {
|
|
104
|
-
consolidateProjectFindings(
|
|
109
|
+
consolidateProjectFindings(getCortexPath(), project);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
|
-
const lockSummary = dryRun ? "" : enforceCanonicalLocks(
|
|
112
|
+
const lockSummary = dryRun ? "" : enforceCanonicalLocks(getCortexPath(), projectArg);
|
|
108
113
|
if (!silent) {
|
|
109
114
|
const prefix = dryRun ? "[dry-run] Would govern" : "Governed";
|
|
110
115
|
console.log(`${prefix} memories: stale=${staleCount}, conflicts=${conflictCount}, review=${reviewCount}`);
|
|
@@ -123,19 +128,19 @@ export async function handlePruneMemories(args = []) {
|
|
|
123
128
|
const { projectArg, dryRun } = parseProjectDryRunArgs(args, "prune-memories", usage);
|
|
124
129
|
const projects = targetProjects(projectArg);
|
|
125
130
|
const beforeBackups = dryRun ? new Map() : captureFindingBackups(projects);
|
|
126
|
-
const result = pruneDeadMemories(
|
|
131
|
+
const result = pruneDeadMemories(getCortexPath(), projectArg, dryRun);
|
|
127
132
|
if (!result.ok) {
|
|
128
133
|
console.log(result.error);
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
131
136
|
console.log(result.data);
|
|
132
137
|
// TTL enforcement: move entries older than ttlDays that haven't been retrieved recently
|
|
133
|
-
const policy = getRetentionPolicy(
|
|
138
|
+
const policy = getRetentionPolicy(getCortexPath());
|
|
134
139
|
const ttlDays = policy.ttlDays;
|
|
135
140
|
const retrievalGraceDays = Math.floor(ttlDays / 2);
|
|
136
141
|
const now = Date.now();
|
|
137
142
|
// Load retrieval log once for all projects
|
|
138
|
-
const retrievalLogPath = path.join(
|
|
143
|
+
const retrievalLogPath = path.join(getCortexPath(), ".runtime", "retrieval-log.jsonl");
|
|
139
144
|
let retrievalEntries = [];
|
|
140
145
|
if (fs.existsSync(retrievalLogPath)) {
|
|
141
146
|
try {
|
|
@@ -165,7 +170,7 @@ export async function handlePruneMemories(args = []) {
|
|
|
165
170
|
}
|
|
166
171
|
let ttlExpired = 0;
|
|
167
172
|
for (const project of projects) {
|
|
168
|
-
const learningsPath = path.join(
|
|
173
|
+
const learningsPath = path.join(getCortexPath(), project, "FINDINGS.md");
|
|
169
174
|
if (!fs.existsSync(learningsPath))
|
|
170
175
|
continue;
|
|
171
176
|
const content = fs.readFileSync(learningsPath, "utf8");
|
|
@@ -197,7 +202,7 @@ export async function handlePruneMemories(args = []) {
|
|
|
197
202
|
ttlExpired++;
|
|
198
203
|
}
|
|
199
204
|
if (expiredEntries.length > 0 && !dryRun) {
|
|
200
|
-
appendReviewQueue(
|
|
205
|
+
appendReviewQueue(getCortexPath(), project, "Stale", expiredEntries);
|
|
201
206
|
}
|
|
202
207
|
if (expiredEntries.length > 0 && dryRun) {
|
|
203
208
|
for (const entry of expiredEntries) {
|
|
@@ -221,7 +226,7 @@ export async function handleConsolidateMemories(args = []) {
|
|
|
221
226
|
const { projectArg, dryRun } = parseProjectDryRunArgs(args, "consolidate-memories", usage);
|
|
222
227
|
const projects = targetProjects(projectArg);
|
|
223
228
|
const beforeBackups = dryRun ? new Map() : captureFindingBackups(projects);
|
|
224
|
-
const results = projects.map((p) => consolidateProjectFindings(
|
|
229
|
+
const results = projects.map((p) => consolidateProjectFindings(getCortexPath(), p, dryRun));
|
|
225
230
|
console.log(results.map((r) => r.ok ? r.data : r.error).join("\n"));
|
|
226
231
|
if (dryRun)
|
|
227
232
|
return;
|
|
@@ -238,7 +243,7 @@ export async function handleMigrateFindings(args) {
|
|
|
238
243
|
}
|
|
239
244
|
const pinCanonical = args.includes("--pin");
|
|
240
245
|
const dryRun = args.includes("--dry-run");
|
|
241
|
-
const result = migrateLegacyFindings(
|
|
246
|
+
const result = migrateLegacyFindings(getCortexPath(), project, { pinCanonical, dryRun });
|
|
242
247
|
console.log(result.ok ? result.data : result.error);
|
|
243
248
|
}
|
|
244
249
|
function printMaintainMigrationUsage() {
|
|
@@ -299,7 +304,7 @@ function parseMaintainMigrationArgs(args) {
|
|
|
299
304
|
return { kind: "data", project: positional[0], pinCanonical, dryRun };
|
|
300
305
|
}
|
|
301
306
|
function describeGovernanceMigrationPlan() {
|
|
302
|
-
const govDir = path.join(
|
|
307
|
+
const govDir = path.join(getCortexPath(), ".governance");
|
|
303
308
|
if (!fs.existsSync(govDir))
|
|
304
309
|
return [];
|
|
305
310
|
const files = [
|
|
@@ -335,7 +340,7 @@ function runGovernanceMigration(dryRun) {
|
|
|
335
340
|
const details = pending.map((entry) => `${entry.file} (${entry.from} -> ${entry.to})`).join(", ");
|
|
336
341
|
return `[dry-run] Would migrate ${pending.length} governance file(s): ${details}`;
|
|
337
342
|
}
|
|
338
|
-
const migrated = migrateGovernanceFiles(
|
|
343
|
+
const migrated = migrateGovernanceFiles(getCortexPath());
|
|
339
344
|
if (!migrated.length)
|
|
340
345
|
return "Governance files are already up to date.";
|
|
341
346
|
return `Migrated ${migrated.length} governance file(s): ${migrated.join(", ")}`;
|
|
@@ -347,7 +352,7 @@ export async function handleMaintainMigrate(args) {
|
|
|
347
352
|
lines.push(`Governance migration: ${runGovernanceMigration(parsed.dryRun)}`);
|
|
348
353
|
}
|
|
349
354
|
if (parsed.kind === "data" || parsed.kind === "all") {
|
|
350
|
-
const result = migrateLegacyFindings(
|
|
355
|
+
const result = migrateLegacyFindings(getCortexPath(), parsed.project, {
|
|
351
356
|
pinCanonical: parsed.pinCanonical,
|
|
352
357
|
dryRun: parsed.dryRun,
|
|
353
358
|
});
|
|
@@ -404,7 +409,7 @@ function findBackups(projects) {
|
|
|
404
409
|
const results = [];
|
|
405
410
|
const now = Date.now();
|
|
406
411
|
for (const project of projects) {
|
|
407
|
-
const dir = path.join(
|
|
412
|
+
const dir = path.join(getCortexPath(), project);
|
|
408
413
|
if (!fs.existsSync(dir))
|
|
409
414
|
continue;
|
|
410
415
|
for (const f of fs.readdirSync(dir)) {
|
|
@@ -450,35 +455,35 @@ async function handleRestoreBackup(args) {
|
|
|
450
455
|
fs.copyFileSync(b.fullPath, target);
|
|
451
456
|
console.log(`Restored ${b.project}/${b.file.replace(/\.bak$/, "")} from backup`);
|
|
452
457
|
}
|
|
453
|
-
appendAuditLog(
|
|
458
|
+
appendAuditLog(getCortexPath(), "restore_backup", `project=${projectArg} files=${projectBackups.length}`);
|
|
454
459
|
}
|
|
455
460
|
// ── Background maintenance ───────────────────────────────────────────────────
|
|
456
461
|
export async function handleBackgroundMaintenance(projectArg) {
|
|
457
|
-
const markers = qualityMarkers(
|
|
462
|
+
const markers = qualityMarkers(getCortexPath());
|
|
458
463
|
const startedAt = new Date().toISOString();
|
|
459
464
|
try {
|
|
460
465
|
const governance = await handleGovernMemories(projectArg, true);
|
|
461
|
-
const pruneResult = pruneDeadMemories(
|
|
466
|
+
const pruneResult = pruneDeadMemories(getCortexPath(), projectArg);
|
|
462
467
|
const pruneMsg = pruneResult.ok ? pruneResult.data : pruneResult.error;
|
|
463
468
|
fs.writeFileSync(markers.done, new Date().toISOString() + "\n");
|
|
464
|
-
updateRuntimeHealth(
|
|
469
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
465
470
|
lastGovernance: {
|
|
466
471
|
at: startedAt,
|
|
467
472
|
status: "ok",
|
|
468
473
|
detail: `projects=${governance.projects} stale=${governance.staleCount} conflicts=${governance.conflictCount} review=${governance.reviewCount}; ${pruneMsg}`,
|
|
469
474
|
},
|
|
470
475
|
});
|
|
471
|
-
appendAuditLog(
|
|
476
|
+
appendAuditLog(getCortexPath(), "background_maintenance", `status=ok projects=${governance.projects} stale=${governance.staleCount} conflicts=${governance.conflictCount} review=${governance.reviewCount}`);
|
|
472
477
|
}
|
|
473
478
|
catch (err) {
|
|
474
|
-
updateRuntimeHealth(
|
|
479
|
+
updateRuntimeHealth(getCortexPath(), {
|
|
475
480
|
lastGovernance: {
|
|
476
481
|
at: startedAt,
|
|
477
482
|
status: "error",
|
|
478
483
|
detail: err?.message || String(err),
|
|
479
484
|
},
|
|
480
485
|
});
|
|
481
|
-
appendAuditLog(
|
|
486
|
+
appendAuditLog(getCortexPath(), "background_maintenance_failed", `error=${err?.message || String(err)}`);
|
|
482
487
|
}
|
|
483
488
|
finally {
|
|
484
489
|
try {
|
|
@@ -20,7 +20,7 @@ export function clearCitationValidCache() {
|
|
|
20
20
|
citationValidCache.clear();
|
|
21
21
|
}
|
|
22
22
|
/** @deprecated Legacy citation formats. Use `<!-- cortex:cite {...} -->` instead. */
|
|
23
|
-
const CITATION_PATTERN = /<!-- source: ([^:]+):(\d+) -->|\[file:([^:]+):(\d+)\]/g;
|
|
23
|
+
const CITATION_PATTERN = /<!-- source: ((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+) -->|\[file:((?:[a-zA-Z]:[\\\/])?[^:]+):(\d+)\]/g;
|
|
24
24
|
export function parseCitations(text) {
|
|
25
25
|
const results = [];
|
|
26
26
|
let m;
|
|
@@ -122,7 +122,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
122
122
|
const queryTokens = tokenizeForOverlap(prompt);
|
|
123
123
|
if (!queryTokens.length)
|
|
124
124
|
return [];
|
|
125
|
-
const sampleLimit =
|
|
125
|
+
const sampleLimit = 100;
|
|
126
126
|
const docs = project
|
|
127
127
|
? queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE project = ? LIMIT ?", [project, sampleLimit]) || []
|
|
128
128
|
: queryDocRows(db, "SELECT project, filename, type, content, path FROM docs LIMIT ?", [sampleLimit]) || [];
|
|
@@ -132,7 +132,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
132
132
|
const score = overlapScore(queryTokens, corpus);
|
|
133
133
|
return { doc, score };
|
|
134
134
|
})
|
|
135
|
-
.filter((x) => x.score >= 0.
|
|
135
|
+
.filter((x) => x.score >= 0.25)
|
|
136
136
|
.sort((a, b) => b.score - a.score)
|
|
137
137
|
.slice(0, 8)
|
|
138
138
|
.map((x) => x.doc);
|
|
@@ -219,6 +219,22 @@ function mostRecentDate(content) {
|
|
|
219
219
|
return "0000-00-00";
|
|
220
220
|
return matches.map((m) => m.slice(3)).sort().reverse()[0];
|
|
221
221
|
}
|
|
222
|
+
function crossProjectAgeMultiplier(doc, detectedProject) {
|
|
223
|
+
if (doc.type !== "findings" || !detectedProject || doc.project === detectedProject)
|
|
224
|
+
return 1;
|
|
225
|
+
const decayDaysRaw = Number.parseInt(process.env.CORTEX_CROSS_PROJECT_DECAY_DAYS ?? "30", 10);
|
|
226
|
+
const decayDays = Number.isFinite(decayDaysRaw) && decayDaysRaw > 0 ? decayDaysRaw : 30;
|
|
227
|
+
const latest = mostRecentDate(doc.content);
|
|
228
|
+
const todayUtc = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate());
|
|
229
|
+
let ageInDays = 90;
|
|
230
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(latest) && latest !== "0000-00-00") {
|
|
231
|
+
const entryUtc = Date.parse(`${latest}T00:00:00Z`);
|
|
232
|
+
if (!Number.isNaN(entryUtc)) {
|
|
233
|
+
ageInDays = Math.max(0, Math.floor((todayUtc - entryUtc) / 86_400_000));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return Math.max(0.1, 1 - (ageInDays / decayDays));
|
|
237
|
+
}
|
|
222
238
|
export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLocal, db, cwd, query) {
|
|
223
239
|
let ranked = [...rows];
|
|
224
240
|
if (detectedProject) {
|
|
@@ -260,31 +276,48 @@ export function rankResults(rows, intent, gitCtx, detectedProject, cortexPathLoc
|
|
|
260
276
|
if (byDate !== 0)
|
|
261
277
|
return byDate;
|
|
262
278
|
}
|
|
279
|
+
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
280
|
+
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
281
|
+
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
282
|
+
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
283
|
+
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
284
|
+
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
285
|
+
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
286
|
+
const scoreA = (intentBoost(intent, a.type) +
|
|
287
|
+
fileRelevanceBoost(a.path, changedFiles) +
|
|
288
|
+
branchMatchBoost(a.content, gitCtx?.branch) +
|
|
289
|
+
globBoostA +
|
|
290
|
+
getQualityMultiplier(cortexPathLocal, keyA) +
|
|
291
|
+
entityA -
|
|
292
|
+
lowValuePenalty(a.content, a.type)) * crossProjectAgeMultiplier(a, detectedProject);
|
|
293
|
+
const scoreB = (intentBoost(intent, b.type) +
|
|
294
|
+
fileRelevanceBoost(b.path, changedFiles) +
|
|
295
|
+
branchMatchBoost(b.content, gitCtx?.branch) +
|
|
296
|
+
globBoostB +
|
|
297
|
+
getQualityMultiplier(cortexPathLocal, keyB) +
|
|
298
|
+
entityB -
|
|
299
|
+
lowValuePenalty(b.content, b.type)) * crossProjectAgeMultiplier(b, detectedProject);
|
|
300
|
+
const scoreDelta = scoreB - scoreA;
|
|
301
|
+
if (Math.abs(scoreDelta) > 0.01)
|
|
302
|
+
return scoreDelta;
|
|
263
303
|
const intentDelta = intentBoost(intent, b.type) - intentBoost(intent, a.type);
|
|
264
304
|
if (intentDelta !== 0)
|
|
265
305
|
return intentDelta;
|
|
266
|
-
const changedFiles = gitCtx?.changedFiles || new Set();
|
|
267
306
|
const fileDelta = fileRelevanceBoost(b.path, changedFiles) - fileRelevanceBoost(a.path, changedFiles);
|
|
268
307
|
if (fileDelta !== 0)
|
|
269
308
|
return fileDelta;
|
|
270
309
|
const branchDelta = branchMatchBoost(b.content, gitCtx?.branch) - branchMatchBoost(a.content, gitCtx?.branch);
|
|
271
310
|
if (branchDelta !== 0)
|
|
272
311
|
return branchDelta;
|
|
273
|
-
const globBoostA = getProjectGlobBoost(cortexPathLocal, a.project, cwd, gitCtx?.changedFiles);
|
|
274
|
-
const globBoostB = getProjectGlobBoost(cortexPathLocal, b.project, cwd, gitCtx?.changedFiles);
|
|
275
312
|
const globDelta = globBoostB - globBoostA;
|
|
276
313
|
if (Math.abs(globDelta) > 0.01)
|
|
277
314
|
return globDelta;
|
|
278
|
-
const keyA = entryScoreKey(a.project, a.filename, a.content);
|
|
279
|
-
const keyB = entryScoreKey(b.project, b.filename, b.content);
|
|
280
315
|
const qualityDelta = getQualityMultiplier(cortexPathLocal, keyB) - getQualityMultiplier(cortexPathLocal, keyA);
|
|
281
316
|
if (qualityDelta !== 0)
|
|
282
317
|
return qualityDelta;
|
|
283
318
|
const penaltyDelta = lowValuePenalty(a.content, a.type) - lowValuePenalty(b.content, b.type);
|
|
284
319
|
if (penaltyDelta !== 0)
|
|
285
320
|
return penaltyDelta;
|
|
286
|
-
const entityA = entityBoostPaths.has(a.path) ? 1.3 : 1;
|
|
287
|
-
const entityB = entityBoostPaths.has(b.path) ? 1.3 : 1;
|
|
288
321
|
if (entityB !== entityA)
|
|
289
322
|
return entityB - entityA;
|
|
290
323
|
return 0;
|