@theronap/cortex-mcp 0.9.79 → 0.9.80
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 +3 -3
- package/bin/cortex-mcp.mjs +38 -22
- package/lib/capture.mjs +1 -1
- package/lib/code_graph_cli.mjs +1 -1
- package/lib/context_log.mjs +5 -5
- package/lib/diagnose.mjs +81 -11
- package/lib/docs_scan.mjs +4 -4
- package/lib/doctor.mjs +48 -11
- package/lib/editors/antigravity.mjs +1 -1
- package/lib/editors/claude.mjs +29 -14
- package/lib/editors/codex.mjs +1 -1
- package/lib/editors/cursor.mjs +1 -1
- package/lib/editors/index.mjs +1 -1
- package/lib/graphify_sync.mjs +49 -4
- package/lib/grep_cli.mjs +1 -1
- package/lib/hydrate.mjs +1 -1
- package/lib/imessage_send.mjs +1 -1
- package/lib/ingest_folder.mjs +2 -2
- package/lib/install.mjs +6 -6
- package/lib/redact.mjs +1 -1
- package/lib/resolve.mjs +62 -12
- package/lib/server.mjs +171 -41
- package/lib/setup.mjs +11 -10
- package/lib/skills.mjs +89 -17
- package/lib/timeline_claim_receipt.mjs +25 -0
- package/lib/uninstall.mjs +3 -3
- package/lib/use_brain.mjs +82 -0
- package/package.json +1 -1
- package/skills/author-docs/SKILL.md +5 -5
- package/skills/context/SKILL.md +4 -4
- package/skills/log/SKILL.md +6 -6
- package/skills/walkthrough/SKILL.md +1 -1
- package/lib/precompact.mjs +0 -16
package/lib/editors/codex.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { readJson, backupFile, ensureDir, writeJson } from './_fsutil.mjs'
|
|
8
8
|
|
|
9
|
-
/** Merge the
|
|
9
|
+
/** Merge the Agnoclast MCP server into a Codex config.toml. Pure + idempotent: strips any existing
|
|
10
10
|
* [mcp_servers.cortex] / [mcp_servers.cortex.env] tables (so re-runs update in place rather than
|
|
11
11
|
* duplicating — a duplicate TOML table would break Codex's parser), preserves every other table,
|
|
12
12
|
* and appends a fresh block. Only touches the cortex tables; never rewrites the user's config. */
|
package/lib/editors/cursor.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { backupFile } from './_fsutil.mjs'
|
|
8
8
|
|
|
9
|
-
/** Merge the
|
|
9
|
+
/** Merge the Agnoclast MCP server into a Cursor mcp.json object. Pure + idempotent: sets only the
|
|
10
10
|
* `cortex` entry, preserves every other server. `spec` is the package@dist-tag STRING — the same
|
|
11
11
|
* convention claude/codex/setup.mjs use, so one install driver can pass a single spec to every adapter. */
|
|
12
12
|
export function mergeCursorMcp(existing, spec, token) {
|
package/lib/editors/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Editor adapter registry (P0-b task 1). One place that knows every editor
|
|
1
|
+
// Editor adapter registry (P0-b task 1). One place that knows every editor Agnoclast can wire.
|
|
2
2
|
// Adding an editor = add a module here; the install driver + capability manifest fall out of it.
|
|
3
3
|
//
|
|
4
4
|
// EditorAdapter shape (see the four modules):
|
package/lib/graphify_sync.mjs
CHANGED
|
@@ -26,8 +26,42 @@ function repoFullNameFromRemote(cwd) {
|
|
|
26
26
|
return m ? `${m[1]}/${m[2]}` : null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// `--brain <name-or-org-id>` — which brain an UNROUTED repo's graph event lands in.
|
|
30
|
+
//
|
|
31
|
+
// Deliberately a flag and nothing cleverer. /api/timeline/graphify routes deterministically by repo
|
|
32
|
+
// (resolveSourceRoute) and only asks when the repo has no route; its comment is blunt about why it
|
|
33
|
+
// must not be guessed: this writes a `records` row unique on (org_id, dedupe_key), so "a repo whose
|
|
34
|
+
// graph updates land in two brains becomes two record sets that never reconcile, and re-pointing
|
|
35
|
+
// later converges on nothing (ADR-0020). It is the one write class where a wrong answer is genuinely
|
|
36
|
+
// unrecoverable." So: no sweep (unlike `resolve`, which is a maintenance pass over everything) and
|
|
37
|
+
// no auto-pick.
|
|
38
|
+
//
|
|
39
|
+
// It does NOT create a source route as a side effect, though that would stop the question recurring:
|
|
40
|
+
// routes are append-only precisely because "re-pointing a live source splits its history
|
|
41
|
+
// irreparably", and a near-irreversible write should not fall out of a CLI flag. This command runs
|
|
42
|
+
// from a per-repo cron/launchd job, so the flag lives in that job's definition — answered once,
|
|
43
|
+
// where it is visible. (Route creation currently has NO client on any surface; that is a separate
|
|
44
|
+
// gap, not this command's to paper over.)
|
|
45
|
+
// Split argv into { cwd, brain }. Pure + exported so the ordering trap below is unit-testable
|
|
46
|
+
// without a git repo, a graphify binary or a network.
|
|
47
|
+
//
|
|
48
|
+
// THE TRAP: argv[0] doubles as the optional repo path, and `--brain`'s VALUE has no leading '-'.
|
|
49
|
+
// Parsed naively, `graphify-sync --brain Personal` reads "Personal" as the path and syncs whatever
|
|
50
|
+
// happens to be there. So the flag and its value are stripped BEFORE the positional check.
|
|
51
|
+
export function parseGraphifyArgs(argv = [], fallbackCwd = process.cwd()) {
|
|
52
|
+
const bIdx = argv.indexOf('--brain')
|
|
53
|
+
const brain = bIdx === -1 ? null : argv[bIdx + 1]
|
|
54
|
+
if (bIdx !== -1 && (!brain || brain.startsWith('-'))) {
|
|
55
|
+
return { error: 'Usage: graphify-sync [path] [--brain <name-or-org-id>]' }
|
|
56
|
+
}
|
|
57
|
+
const rest = bIdx === -1 ? argv : argv.filter((_, i) => i !== bIdx && i !== bIdx + 1)
|
|
58
|
+
return { cwd: rest[0] && !rest[0].startsWith('-') ? rest[0] : fallbackCwd, brain }
|
|
59
|
+
}
|
|
60
|
+
|
|
29
61
|
export async function runGraphifySync(argv = []) {
|
|
30
|
-
const
|
|
62
|
+
const parsed = parseGraphifyArgs(argv)
|
|
63
|
+
if (parsed.error) { process.stderr.write(parsed.error + '\n'); return 1 }
|
|
64
|
+
const { cwd, brain } = parsed
|
|
31
65
|
const TOKEN = process.env.CORTEX_TOKEN
|
|
32
66
|
const BASE = resolveBase(process.env.CORTEX_URL)
|
|
33
67
|
if (!TOKEN) {
|
|
@@ -73,16 +107,27 @@ export async function runGraphifySync(argv = []) {
|
|
|
73
107
|
const res = await fetchCortex(`${BASE}/api/timeline/graphify`, {
|
|
74
108
|
method: 'POST',
|
|
75
109
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ repo, commitSha, nodeCount, edgeCount, communityCount }),
|
|
110
|
+
body: JSON.stringify({ repo, commitSha, nodeCount, edgeCount, communityCount, ...(brain ? { brain } : {}) }),
|
|
77
111
|
})
|
|
78
112
|
if (!res.ok) {
|
|
79
113
|
const body = await res.text()
|
|
80
|
-
|
|
114
|
+
const d = classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id'))
|
|
115
|
+
process.stderr.write(d.message + '\n')
|
|
116
|
+
// classify names the brains but speaks in MCP terms ("re-run this tool with `brain`"). Say the
|
|
117
|
+
// actual flag, and where to put it — this runs unattended from cron, so the person reading this
|
|
118
|
+
// is looking at a log after the fact, not a prompt.
|
|
119
|
+
if (res.status === 409 && !brain) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`\n${repo} has no routing decision yet, so it cannot be filed without one.\n` +
|
|
122
|
+
`Re-run with: cortex-mcp graphify-sync ${cwd === process.cwd() ? '' : `${cwd} `}--brain "<name-or-org-id>"\n` +
|
|
123
|
+
`and add that flag to this repo's cron/launchd job so it stops asking.\n`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
81
126
|
return 1
|
|
82
127
|
}
|
|
83
128
|
const payload = await res.json()
|
|
84
129
|
process.stdout.write(
|
|
85
|
-
`Logged to
|
|
130
|
+
`Logged to Agnoclast timeline: ${repo}@${commitSha.slice(0, 7)} (${nodeCount} nodes, ${edgeCount} edges, ${communityCount} communities)` +
|
|
86
131
|
`${payload.inserted ? '' : ' (already logged)'}\n`,
|
|
87
132
|
)
|
|
88
133
|
return 0
|
package/lib/grep_cli.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export async function runGrep(rest = []) {
|
|
|
56
56
|
const TOKEN = process.env.CORTEX_TOKEN
|
|
57
57
|
const BASE = resolveBase(process.env.CORTEX_URL)
|
|
58
58
|
if (!TOKEN) {
|
|
59
|
-
process.stderr.write('cortex grep: CORTEX_TOKEN is required (get yours from the
|
|
59
|
+
process.stderr.write('cortex grep: CORTEX_TOKEN is required (get yours from the Agnoclast console).\n')
|
|
60
60
|
return 1
|
|
61
61
|
}
|
|
62
62
|
const { query, mode, max } = parseGrepArgs(rest)
|
package/lib/hydrate.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { formatReceipt, renderUncounted, subjectOf } from './presence.mjs'
|
|
|
7
7
|
import { writePresence } from './statusline.mjs'
|
|
8
8
|
|
|
9
9
|
// UserPromptSubmit hook (① discovery): on the FIRST substantive turn of a session, hydrate the model
|
|
10
|
-
// with query-centered
|
|
10
|
+
// with query-centered Agnoclast context BEFORE it answers — then never again this session. It closes the
|
|
11
11
|
// gap that read-triggered authoring can't: an agent that never READS the wiki (works from code + memory)
|
|
12
12
|
// never triggers a currency update, and re-derives already-authored truth. cortex-context is a SKILL the
|
|
13
13
|
// model may forget to invoke; this makes the first hydration non-discretionary (the same move that made
|
package/lib/imessage_send.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// send_imessage — outbound iMessage via Messages.app. This is a personal automation, NOT org
|
|
2
|
-
// intelligence: it writes nothing to
|
|
2
|
+
// intelligence: it writes nothing to Agnoclast. Three layers of safety (eng-review D3/D6/D10):
|
|
3
3
|
// D6 argv-safe: recipient + body are passed as osascript `on run argv` arguments, NEVER
|
|
4
4
|
// interpolated into the script source → no AppleScript injection, no quote/newline breakage.
|
|
5
5
|
// D3 draft-by-default: nothing sends unless the caller explicitly passes send:true.
|
package/lib/ingest_folder.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { join, relative, basename, extname } from 'node:path'
|
|
|
3
3
|
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
4
4
|
|
|
5
5
|
// `cortex-mcp ingest-folder <path>` — walk a local markdown folder and upsert each file as an
|
|
6
|
-
// AUTHORED record in
|
|
6
|
+
// AUTHORED record in Agnoclast (source='brain'), so a user's personal digest is built from THEIR
|
|
7
7
|
// notes (Robin parity). Only an EXCERPT of each file leaves the machine (frontmatter + first
|
|
8
8
|
// ~800 chars), never the full body. The server clamps privacy for source='brain' and treats
|
|
9
9
|
// sessionId (the file's relative path) as the stable dedupe key, so re-running updates in place.
|
|
@@ -100,7 +100,7 @@ export async function runIngestFolder(argv) {
|
|
|
100
100
|
if (!path) {
|
|
101
101
|
process.stderr.write(
|
|
102
102
|
'usage: cortex-mcp ingest-folder <path>\n' +
|
|
103
|
-
' Ingest a local markdown folder as your authored
|
|
103
|
+
' Ingest a local markdown folder as your authored Agnoclast records.\n',
|
|
104
104
|
)
|
|
105
105
|
return
|
|
106
106
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `cortex install` — the cross-editor hub installer (P0-b). One command that wires the
|
|
1
|
+
// `cortex install` — the cross-editor hub installer (P0-b). One command that wires the Agnoclast MCP
|
|
2
2
|
// server + client shim into EVERY detected editor via the adapter registry, then writes a capability
|
|
3
3
|
// manifest so later pillars (session sync, skill sync, doc sync) know which channels each editor
|
|
4
4
|
// supports. `setup <token>` stays as the single-editor path the console prints; `install` is the
|
|
@@ -78,7 +78,7 @@ export async function runInstall(argv, version) {
|
|
|
78
78
|
process.stderr.write(
|
|
79
79
|
'Usage: npx @theronap/cortex-mcp install [<CORTEX_TOKEN>] [--editor auto|all|<id,...>]\n\n' +
|
|
80
80
|
'No token was given and none is already wired.\n' +
|
|
81
|
-
'Get your token from the
|
|
81
|
+
'Get your token from the Agnoclast console → Connect your AI.\n',
|
|
82
82
|
)
|
|
83
83
|
process.exit(1)
|
|
84
84
|
}
|
|
@@ -92,7 +92,7 @@ export async function runInstall(argv, version) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
log('')
|
|
95
|
-
log('
|
|
95
|
+
log('Agnoclast install — wiring your AI editors…')
|
|
96
96
|
if (editor === 'auto' && targets.length === 0) {
|
|
97
97
|
log(' (no supported editors detected — pass --editor all to force, or install an editor first)')
|
|
98
98
|
}
|
|
@@ -135,7 +135,7 @@ export async function runInstall(argv, version) {
|
|
|
135
135
|
// Verify the token against the live API — writing config proves "files written", not "it works".
|
|
136
136
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
137
137
|
log('')
|
|
138
|
-
log('Verifying your token against
|
|
138
|
+
log('Verifying your token against Agnoclast…')
|
|
139
139
|
try {
|
|
140
140
|
const health = await checkToken(token, base)
|
|
141
141
|
if (health.ok) {
|
|
@@ -147,13 +147,13 @@ export async function runInstall(argv, version) {
|
|
|
147
147
|
log(' The files are in place; fix the above, then re-check with `doctor`.')
|
|
148
148
|
}
|
|
149
149
|
} catch (e) {
|
|
150
|
-
log(` ⚠ Could not reach
|
|
150
|
+
log(` ⚠ Could not reach Agnoclast to verify (${e.message}). Config is written; re-check with 'doctor'.`)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const wired = entries.filter((e) => targetIds.has(e.adapter.id) && e.wired).map((e) => e.adapter.displayName)
|
|
154
154
|
log('')
|
|
155
155
|
log(`⟳ Wired ${wired.length} editor(s): ${wired.join(', ') || '(none)'}`)
|
|
156
|
-
log(' Fully quit and reopen each editor to load the
|
|
156
|
+
log(' Fully quit and reopen each editor to load the Agnoclast server.')
|
|
157
157
|
log(` Console: ${base}`)
|
|
158
158
|
log('')
|
|
159
159
|
}
|
package/lib/redact.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const PATTERNS = [
|
|
|
21
21
|
[/xox[baprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack]'],
|
|
22
22
|
[/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED:jwt]'],
|
|
23
23
|
[/(Bearer\s+)[A-Za-z0-9._-]{20,}/g, '$1[REDACTED]'],
|
|
24
|
-
//
|
|
24
|
+
// Agnoclast's own login token wherever it appears in KEY=value / KEY: value form (hooks.json,
|
|
25
25
|
// config.toml, shell commands — the 2026-07-02 finding: a grep of hooks.json put the live token
|
|
26
26
|
// in a transcript and nothing below caught it). Specific pattern first for the accurate label.
|
|
27
27
|
[/(CORTEX_TOKEN["']?\s*[=:]\s*["']?)[0-9a-fA-F][0-9a-fA-F-]{30,}/g, '$1[REDACTED:cortex-token]'],
|
package/lib/resolve.mjs
CHANGED
|
@@ -8,47 +8,97 @@ import { edgeSafeEnv } from './edge_extract.mjs'
|
|
|
8
8
|
// and pushes decisions back (POST /api/resolve-apply): confident-same → entity_merges, else → rejected
|
|
9
9
|
// so the pair never re-flags. Conservative by construction. Always exits cleanly.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Which brains to sweep. BOTH endpoints require the brain to be NAMED (ADR-0022 requireBrain —
|
|
12
|
+
// answering a scoped read out of an ARBITRARY brain is the defect that whole class exists to
|
|
13
|
+
// prevent). Until 2026-08-09 this command named none, so a multi-brain caller got 409 on step 1 and
|
|
14
|
+
// the dedup sweep did nothing at all, silently, forever.
|
|
15
|
+
//
|
|
16
|
+
// Naming ONE brain would have been the smaller fix and the wrong one: `resolve` is a MAINTENANCE
|
|
17
|
+
// SWEEP over the user's entities, so doing one brain and reporting success is ADR-0022's other
|
|
18
|
+
// failure — "silently truncating a result set and presenting it as complete". With no --brain we
|
|
19
|
+
// enumerate the caller's brains and sweep EACH, naming it explicitly. Not a guess: every request
|
|
20
|
+
// still names exactly one brain, and all of them actually get done.
|
|
21
|
+
//
|
|
22
|
+
// A sole-brain caller sees today's behaviour: one pass, no flag, no prompt.
|
|
23
|
+
export async function brainsToSweep(base, token, wanted, deps = {}) {
|
|
24
|
+
const fetchFn = deps.fetchCortex ?? fetchCortex
|
|
25
|
+
if (wanted) return [{ orgId: wanted, name: wanted }] // explicit: pass through (name or org id)
|
|
26
|
+
const res = await fetchFn(`${base}/api/brains`, { headers: { Authorization: `Bearer ${token}` } })
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
29
|
+
throw new Error(`could not list your brains — ${d.message}`)
|
|
30
|
+
}
|
|
31
|
+
const j = await res.json().catch(() => ({}))
|
|
32
|
+
const brains = Array.isArray(j.brains) ? j.brains : []
|
|
33
|
+
// Carry the ORG ID, never the name: brain names are NOT unique (this account holds two called
|
|
34
|
+
// "Personal"), and a duplicate name comes back as unknown_brain.
|
|
35
|
+
return brains.filter((b) => b?.orgId).map((b) => ({ orgId: b.orgId, name: b.name ?? b.orgId }))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runResolve(argv = []) {
|
|
12
39
|
// recursion guard (we spawn `claude --print`; if its Stop hook fires capture, that no-ops on this flag)
|
|
13
40
|
if (process.env.CORTEX_SUMMARIZING) { process.stderr.write('cortex: summarizer subprocess, skipping\n'); return }
|
|
14
41
|
const token = process.env.CORTEX_TOKEN
|
|
15
42
|
if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
|
|
16
43
|
const base = resolveBase(process.env.CORTEX_URL)
|
|
17
44
|
|
|
45
|
+
const bIdx = argv.indexOf('--brain')
|
|
46
|
+
const wanted = bIdx === -1 ? null : argv[bIdx + 1]
|
|
47
|
+
if (bIdx !== -1 && (!wanted || wanted.startsWith('-'))) {
|
|
48
|
+
process.stderr.write('Usage: resolve [--brain <name-or-org-id>]\n'); return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let targets
|
|
52
|
+
try {
|
|
53
|
+
targets = await brainsToSweep(base, token, wanted)
|
|
54
|
+
} catch (e) { process.stderr.write(`cortex: resolve — ${e.message}\n`); return }
|
|
55
|
+
if (!targets.length) { process.stderr.write('cortex: no brains to sweep\n'); return }
|
|
56
|
+
|
|
57
|
+
for (const t of targets) {
|
|
58
|
+
// Label each line with the brain when sweeping several: an unlabelled "merged 3" cannot be acted
|
|
59
|
+
// on, because you cannot tell WHERE three entities just merged.
|
|
60
|
+
await resolveOneBrain(base, token, t, targets.length > 1)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function resolveOneBrain(base, token, brain, labelled) {
|
|
65
|
+
const tag = labelled ? `[${brain.name}] ` : ''
|
|
66
|
+
const qs = `?brain=${encodeURIComponent(brain.orgId)}`
|
|
67
|
+
|
|
18
68
|
// 1. pull the flagged candidate pairs
|
|
19
69
|
let candidates = []
|
|
20
70
|
try {
|
|
21
|
-
const res = await fetchCortex(`${base}/api/resolve-candidates`, { headers: { Authorization: `Bearer ${token}` } })
|
|
71
|
+
const res = await fetchCortex(`${base}/api/resolve-candidates${qs}`, { headers: { Authorization: `Bearer ${token}` } })
|
|
22
72
|
if (!res.ok) {
|
|
23
73
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
24
|
-
process.stderr.write(`cortex: resolve-candidates failed — ${d.message}\n`); return
|
|
74
|
+
process.stderr.write(`cortex: ${tag}resolve-candidates failed — ${d.message}\n`); return
|
|
25
75
|
}
|
|
26
76
|
const j = await res.json().catch(() => ({}))
|
|
27
77
|
candidates = Array.isArray(j.candidates) ? j.candidates : []
|
|
28
|
-
} catch (e) { process.stderr.write(`cortex: resolve fetch failed — ${e.message}\n`); return }
|
|
78
|
+
} catch (e) { process.stderr.write(`cortex: ${tag}resolve fetch failed — ${e.message}\n`); return }
|
|
29
79
|
|
|
30
|
-
if (!candidates.length) { process.stderr.write(
|
|
31
|
-
process.stderr.write(`cortex: judging ${candidates.length} candidate pair(s) locally…\n`)
|
|
80
|
+
if (!candidates.length) { process.stderr.write(`cortex: ${tag}no duplicate candidates to judge\n`); return }
|
|
81
|
+
process.stderr.write(`cortex: ${tag}judging ${candidates.length} candidate pair(s) locally…\n`)
|
|
32
82
|
|
|
33
83
|
// 2. judge locally on the subscription
|
|
34
84
|
const decisions = judgeCandidates(candidates)
|
|
35
|
-
if (decisions === null) { process.stderr.write(
|
|
85
|
+
if (decisions === null) { process.stderr.write(`cortex: ${tag}judge unavailable (is \`claude\` on PATH?) — skipping\n`); return }
|
|
36
86
|
|
|
37
|
-
// 3. apply
|
|
87
|
+
// 3. apply — SAME brain the candidates came from, or the merges land in the wrong graph
|
|
38
88
|
try {
|
|
39
|
-
const res = await fetchCortex(`${base}/api/resolve-apply`, {
|
|
89
|
+
const res = await fetchCortex(`${base}/api/resolve-apply${qs}`, {
|
|
40
90
|
method: 'POST',
|
|
41
91
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
42
92
|
body: JSON.stringify({ decisions }),
|
|
43
93
|
})
|
|
44
94
|
if (res.ok) {
|
|
45
95
|
const j = await res.json().catch(() => ({}))
|
|
46
|
-
process.stderr.write(`cortex: dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
|
|
96
|
+
process.stderr.write(`cortex: ${tag}dedup — merged ${j.merged ?? 0}, rejected ${j.rejected ?? 0}, skipped ${j.skipped ?? 0}\n`)
|
|
47
97
|
} else {
|
|
48
98
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
49
|
-
process.stderr.write(`cortex: resolve-apply failed — ${d.message}\n`)
|
|
99
|
+
process.stderr.write(`cortex: ${tag}resolve-apply failed — ${d.message}\n`)
|
|
50
100
|
}
|
|
51
|
-
} catch (e) { process.stderr.write(`cortex: resolve-apply failed — ${e.message}\n`) }
|
|
101
|
+
} catch (e) { process.stderr.write(`cortex: ${tag}resolve-apply failed — ${e.message}\n`) }
|
|
52
102
|
}
|
|
53
103
|
|
|
54
104
|
// ONE `claude --print` call judges every pair. Returns DedupDecision[] for the apply endpoint, or null
|