@theronap/cortex-mcp 0.9.82 → 0.9.84

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # cortex-mcp
2
2
 
3
- Connect your AI assistant to **Cortex** — your org's projects, recent activity, gaps, and directives, scoped to exactly what you're permitted to see.
3
+ Connect your AI assistant to **Agnoclast** — your org's projects, recent activity, gaps, and directives, scoped to exactly what you're permitted to see.
4
4
 
5
5
  ## Setup
6
6
 
7
- 1. Get your personal token from the Cortex console → **Connect your AI**.
7
+ 1. Get your personal token from the Agnoclast console → **Connect your AI**.
8
8
  2. Add this to your Claude Code config (`~/.claude.json`, under `mcpServers`):
9
9
 
10
10
  ```json
@@ -21,7 +21,7 @@ Connect your AI assistant to **Cortex** — your org's projects, recent activity
21
21
 
22
22
  3. Restart Claude Code. Your AI now sees your org context automatically, the managed startup skill
23
23
  will prefer query-centered `session_context` on substantive session opens, and each session start
24
- will log the exact baseline Cortex context to `~/.cortex/context-snapshots/`.
24
+ will log the exact baseline Agnoclast context to `~/.cortex/context-snapshots/`.
25
25
 
26
26
  No clone, no path, no build step — `npx` fetches and runs it.
27
27
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * cortex-mcp — connect your AI assistant to Cortex.
3
+ * cortex-mcp — connect your AI assistant to Agnoclast.
4
4
  *
5
5
  * Subcommands:
6
6
  * (none) run the MCP server (stdio) — used by your Claude config
@@ -8,14 +8,14 @@
8
8
  * doctor live health check — is the token actually working? (no restart needed)
9
9
  * capture the Stop-hook capturer (invoked by Claude Code, not by hand)
10
10
  * ingest-folder <path> ingest a local markdown folder as your authored records
11
- * snapshot-context save the exact startup context served by Cortex to a local log file
11
+ * snapshot-context save the exact startup context served by Agnoclast to a local log file
12
12
  * --version | -v
13
13
  * --help | -h
14
14
  *
15
15
  * Zero-install onboarding:
16
16
  * npx -y @theronap/cortex-mcp setup <your-token>
17
17
  *
18
- * Get your token from the Cortex console → Connect your AI.
18
+ * Get your token from the Agnoclast console → Connect your AI.
19
19
  */
20
20
 
21
21
  import { readFileSync } from 'node:fs'
@@ -35,31 +35,32 @@ if (cmd === '--version' || cmd === '-v') {
35
35
 
36
36
  if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
37
37
  process.stdout.write(
38
- `cortex-mcp ${VERSION} — connect your AI assistant to Cortex\n\n` +
38
+ `cortex-mcp ${VERSION} — connect your AI assistant to Agnoclast\n\n` +
39
39
  `Onboard (one command, no token to copy):\n` +
40
40
  ` npx -y @theronap/cortex-mcp login\n\n` +
41
41
  `This opens your browser, you click Approve, and it wires everything up. Restart\n` +
42
- `Claude Code after, and your AI sees your Cortex context while your sessions flow\n` +
42
+ `Claude Code after, and your AI sees your Agnoclast context while your sessions flow\n` +
43
43
  `into the org automatically.\n\n` +
44
44
  `Subcommands:\n` +
45
45
  ` login [--label <name>] browser-approved sign-in — gets a token for you, then runs setup\n` +
46
46
  ` setup <token> wire MCP server + capture hook into ~/.claude config (single editor)\n` +
47
- ` install [<token>] [--editor auto|all|<id,...>] wire Cortex into EVERY detected editor + write the capability manifest\n` +
47
+ ` install [<token>] [--editor auto|all|<id,...>] wire Agnoclast into EVERY detected editor + write the capability manifest\n` +
48
48
  ` repair re-run setup at the latest version using your existing token (no token needed)\n` +
49
- ` uninstall remove ALL Cortex wiring (MCP, hooks, skills, launchd, cron). --dry-run to preview, --purge to also wipe ~/.cortex + npx cache\n` +
49
+ ` uninstall remove ALL Agnoclast wiring (MCP, hooks, skills, launchd, cron). --dry-run to preview, --purge to also wipe ~/.cortex + npx cache\n` +
50
50
  ` doctor live health check — confirm your token works (no restart needed)\n` +
51
51
  ` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
52
- ` skills install/repair the managed Cortex skillsbundled + org-published (also wired by setup)\n` +
52
+ ` use-brain [<brain>] where your session captures are saved no arg shows the current setting\n` +
53
+ ` skills install/repair the managed Agnoclast skills — bundled + org-published (also wired by setup)\n` +
53
54
  ` skills push <file> publish a SKILL.md to your org (owner/manager/admin)\n` +
54
- ` docs-scan detect new/changed local docs pending Cortex authoring (used by /cortex-author-docs)\n` +
55
- ` graphify-sync [path] rebuild the local code graph (graphify) + log an evidence-tier timeline event\n` +
56
- ` snapshot-context save the exact startup context Cortex served to a local snapshot\n` +
55
+ ` docs-scan detect new/changed local docs pending Agnoclast authoring (used by /cortex-author-docs)\n` +
56
+ ` graphify-sync [path] [--brain <name-or-id>] rebuild the local code graph + log a timeline event\n` +
57
+ ` snapshot-context save the exact startup context Agnoclast served to a local snapshot\n` +
57
58
  ` hydrate UserPromptSubmit hook — inject query-centered context on the first substantive turn\n` +
58
59
  ` statusline ambient presence line for the Claude Code statusline (local read only)\n` +
59
60
  ` capture Stop-hook capturer (invoked by Claude Code)\n` +
60
61
  ` ingest-folder <path> ingest a local markdown folder as your authored records\n` +
61
62
  ` (no args) run the MCP server (used by your Claude config)\n\n` +
62
- `Get your token from the Cortex console → Connect your AI.\n`,
63
+ `Get your token from the Agnoclast console → Connect your AI.\n`,
63
64
  )
64
65
  process.exit(0)
65
66
  }
@@ -84,14 +85,14 @@ if (cmd === 'login') {
84
85
  const { closeFetch } = await import('../lib/diagnose.mjs')
85
86
  await closeFetch()
86
87
  } else if (cmd === 'install') {
87
- // The cross-editor hub installer: wire Cortex into every detected editor via the adapter
88
+ // The cross-editor hub installer: wire Agnoclast into every detected editor via the adapter
88
89
  // registry, then write the capability manifest (~/.cortex/editors.json). Superset of `setup`.
89
90
  const { runInstall } = await import('../lib/install.mjs')
90
91
  await runInstall(rest, VERSION)
91
92
  const { closeFetch } = await import('../lib/diagnose.mjs')
92
93
  await closeFetch()
93
94
  } else if (cmd === 'uninstall' || cmd === 'remove') {
94
- // Full reverse of setup: strip every Cortex touch-point (MCP entries, hooks, skills, launchd, cron).
95
+ // Full reverse of setup: strip every Agnoclast touch-point (MCP entries, hooks, skills, launchd, cron).
95
96
  // --dry-run prints the plan and changes nothing; --purge also removes ~/.cortex, the npx cache, and
96
97
  // backups. No network — safe to run even when the token is dead or the server is unreachable.
97
98
  const { runUninstall } = await import('../lib/uninstall.mjs')
@@ -122,7 +123,7 @@ if (cmd === 'login') {
122
123
  } else if (cmd === 'resolve') {
123
124
  // Entity identity dedup: judge the server-flagged fuzzy duplicate pairs locally via `claude -p`.
124
125
  const { runResolve } = await import('../lib/resolve.mjs')
125
- await runResolve()
126
+ await runResolve(rest)
126
127
  const { closeFetch } = await import('../lib/diagnose.mjs')
127
128
  await closeFetch()
128
129
  } else if (cmd === 'materialize') {
@@ -153,7 +154,7 @@ if (cmd === 'login') {
153
154
  const { closeFetch } = await import('../lib/diagnose.mjs')
154
155
  await closeFetch()
155
156
  } else if (cmd === 'hydrate') {
156
- // UserPromptSubmit hook (① discovery): hydrate the model with query-centered Cortex context on the
157
+ // UserPromptSubmit hook (① discovery): hydrate the model with query-centered Agnoclast context on the
157
158
  // FIRST substantive turn, before it answers — then never again this session (topic-shift refresh stays
158
159
  // the cortex-context skill's job). Synchronous by necessity, but once-per-session + 8s + fail-open.
159
160
  const { runHydrate } = await import('../lib/hydrate.mjs')
@@ -166,7 +167,7 @@ if (cmd === 'login') {
166
167
  const { runStatusline } = await import('../lib/statusline.mjs')
167
168
  process.exitCode = runStatusline()
168
169
  } else if (cmd === 'skills') {
169
- // Install / repair the managed Cortex skills — bundled + org-published (`skills push` publishes).
170
+ // Install / repair the managed Agnoclast skills — bundled + org-published (`skills push` publishes).
170
171
  // Org sync is network-fail-soft so the SessionStart hook stays safe offline.
171
172
  const { runSkills } = await import('../lib/skills.mjs')
172
173
  process.exitCode = await runSkills(rest)
@@ -177,12 +178,27 @@ if (cmd === 'login') {
177
178
  // The cortex-author-docs skill authors the pending docs into wiki pages. No network.
178
179
  const { runDocsScan } = await import('../lib/docs_scan.mjs')
179
180
  process.exitCode = await runDocsScan(rest)
181
+ } else if (cmd === 'use-brain') {
182
+ // Set (or show) which brain this machine's unattended session captures land in. Registered here,
183
+ // BEFORE the default branch — an unrecognised subcommand falls through to "start the MCP server",
184
+ // which is how `connect-calendar` once silently became the file-watching daemon on a pilot user's
185
+ // machine and reported that daemon's errors instead of its own.
186
+ const { runUseBrain } = await import('../lib/use_brain.mjs')
187
+ process.exitCode = await runUseBrain(rest)
188
+ const { closeFetch } = await import('../lib/diagnose.mjs')
189
+ await closeFetch()
180
190
  } else if (cmd === 'precompact') {
181
- // PreCompact hook (③ live wiki authoring, best-effort): print an "author now" reminder so the session
182
- // sweeps its understanding into the wiki BEFORE compaction drops it. A hook cannot force a model turn
183
- // (D2) this is the documented fallback; /log is the hard backstop. No network; prints + exits.
184
- const { runPrecompactReminder } = await import('../lib/precompact.mjs')
185
- runPrecompactReminder()
191
+ // RETIRED deliberately kept as a silent no-op, do not delete yet.
192
+ //
193
+ // The reminder it used to print never reached a model: PreCompact takes a blocking `decision` and has
194
+ // no additionalContext channel, so its stdout went nowhere (verified 2026-08-10 against 7,232 local
195
+ // transcripts — see the PreCompact block in lib/editors/claude.mjs for the measurement and control).
196
+ //
197
+ // Why this branch survives the removal: seats installed before this release still have
198
+ // `PreCompact: npx -y @theronap/cortex-mcp@latest precompact` in ~/.claude/settings.json, and @latest
199
+ // resolves to THIS build. Deleting the branch would turn a harmless no-op into an unknown-command
200
+ // error on every compaction for anyone who has not re-run install. mergeClaudeSettings unwires them
201
+ // on their next install/repair; drop this branch a release after that has had time to propagate.
186
202
  } else {
187
203
  // Default: run the MCP server (stays alive; never exits).
188
204
  const { runServer } = await import('../lib/server.mjs')
package/lib/capture.mjs CHANGED
@@ -35,7 +35,7 @@ export function projectFrom(cwd) {
35
35
  return base ?? 'general'
36
36
  }
37
37
 
38
- // Claude Code Stop hook → POSTs a session digest to Cortex cloud, which
38
+ // Claude Code Stop hook → POSTs a session digest to Agnoclast cloud, which
39
39
  // summarizes server-side and upserts ONE record per session. Node-native
40
40
  // SHR-01/T6 — parse a git remote URL into GitHub 'owner/name', or null.
41
41
  //
@@ -4,7 +4,7 @@ import { join } from 'path'
4
4
 
5
5
  // Thin local wrapper around the `graphify` CLI's read-only query subcommands. Deliberately NOT a
6
6
  // fetchCortex client like grep/read_page: this is LOCAL-MACHINE data (a tree-sitter AST graph of
7
- // whatever repo the session's cwd happens to be in), not org-shared Cortex content, and it never
7
+ // whatever repo the session's cwd happens to be in), not org-shared Agnoclast content, and it never
8
8
  // becomes the wiki graph — see cortex-wiki-primary-spec (structural/extracted data is evidence,
9
9
  // never auto-promoted into authored pages). No LLM, no network call; graphify already built the
10
10
  // graph on disk, this just queries it.
@@ -49,7 +49,7 @@ export async function runSnapshotContext() {
49
49
  const out = (m) => process.stdout.write(m + '\n')
50
50
  const token = resolveToken()
51
51
  if (!token) {
52
- out('Cortex: context snapshot skipped — no token found.')
52
+ out('Agnoclast: context snapshot skipped — no token found.')
53
53
  return 0
54
54
  }
55
55
 
@@ -58,13 +58,13 @@ export async function runSnapshotContext() {
58
58
  try {
59
59
  res = await fetchCortex(`${base}/api/mcp-context`, { headers: { Authorization: `Bearer ${token}` } })
60
60
  } catch (e) {
61
- out(`Cortex: context snapshot failed — ${e?.message ?? String(e)}`)
61
+ out(`Agnoclast: context snapshot failed — ${e?.message ?? String(e)}`)
62
62
  return 0
63
63
  }
64
64
 
65
65
  if (!res.ok) {
66
66
  const body = await res.text()
67
- out(`Cortex: context snapshot failed — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
67
+ out(`Agnoclast: context snapshot failed — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
68
68
  return 0
69
69
  }
70
70
 
@@ -76,7 +76,7 @@ export async function runSnapshotContext() {
76
76
  const capturedAt = new Date().toISOString()
77
77
  const file = join(dir, `${stamp()}.md`)
78
78
  const header = [
79
- '# Cortex startup context snapshot',
79
+ '# Agnoclast startup context snapshot',
80
80
  `- Captured: ${capturedAt}`,
81
81
  `- Source: ${base}/api/mcp-context`,
82
82
  '',
@@ -94,6 +94,6 @@ export async function runSnapshotContext() {
94
94
 
95
95
  pruneSnapshots(dir)
96
96
 
97
- out(`Cortex: logged startup context → ${file}`)
97
+ out(`Agnoclast: logged startup context → ${file}`)
98
98
  return 0
99
99
  }
package/lib/diagnose.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  // Shared health / diagnosis helpers for the cortex MCP client.
2
2
  //
3
3
  // The whole reason this file exists: a transient infrastructure block (Vercel firewall / bot
4
- // protection / deployment protection) once surfaced as a bare "Cortex API 403: unknown", which
4
+ // protection / deployment protection) once surfaced as a bare "Agnoclast API 403: unknown", which
5
5
  // read like an auth failure and sent everyone chasing token regeneration for hours. The fix is
6
6
  // to tell the truth about WHAT failed.
7
7
  //
8
- // KEY SIGNAL: the Cortex app ALWAYS returns JSON ({ error: ... }). So a NON-JSON body on a
9
- // 4xx/5xx means infrastructure handled the request, not Cortex auth — re-running setup or
8
+ // KEY SIGNAL: the Agnoclast app ALWAYS returns JSON ({ error: ... }). So a NON-JSON body on a
9
+ // 4xx/5xx means infrastructure handled the request, not Agnoclast auth — re-running setup or
10
10
  // regenerating the token will not help; it is usually transient and worth a retry.
11
11
 
12
12
  import { readFileSync } from 'fs'
@@ -85,15 +85,24 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
85
85
  // A non-OK response → an actionable diagnosis: { kind, retriable, message }.
86
86
  // kind: 'auth' → token bad/revoked/wrong-deployment (NOT retriable)
87
87
  // 'infra' → blocked by infrastructure, non-JSON body (retriable, usually transient)
88
- // 'app' → a real Cortex API error with a JSON message (retriable only if 5xx)
88
+ // 'app' → a real Agnoclast API error with a JSON message (retriable only if 5xx)
89
89
  export function classify(status, contentType, bodyText, requestId) {
90
90
  const isJson = (contentType ?? '').includes('application/json')
91
91
  let appError = null
92
92
  let appHint = null
93
+ let appMessage = null
94
+ let appBrains = null
93
95
  if (isJson) {
94
96
  try {
95
97
  const parsed = JSON.parse(bodyText)
96
98
  appError = parsed?.error ?? null
99
+ // Same lesson as `hint`, one layer up. brain_choice_response.ts writes a full explanation to
100
+ // `message` and every brain's NAME / PAGE COUNT / SAMPLE TITLES to `brains` — its comment says
101
+ // "THE BODY IS THE ANSWER TO ITS OWN QUESTION", precisely so a model can pick a brain from what
102
+ // each one HOLDS. Only `error` survived here, so the agent got the bare code `brain_required`
103
+ // and could not answer it. Measured on the CLI twin of this bug: `✗ brain_required`, full stop.
104
+ appMessage = typeof parsed?.message === 'string' && parsed.message.trim() ? parsed.message.trim() : null
105
+ appBrains = Array.isArray(parsed?.brains) ? parsed.brains : null
97
106
  // `hint` carries the RECOVERY instruction for the errors an agent is meant to act on, not
98
107
  // just report: 409 would_drop ("re-author including the dropped sections…") and 413 too_large
99
108
  // ("split the section…"). It used to be dropped here — only `error` survived — so the agent
@@ -109,20 +118,39 @@ export function classify(status, contentType, bodyText, requestId) {
109
118
  kind: 'auth', retriable: false,
110
119
  message: `Token rejected (HTTP ${status}: ${appError ?? 'unauthorized'}). The token is invalid, ` +
111
120
  `revoked, or for a different deployment — not an infrastructure problem. Get a fresh token from ` +
112
- `the Cortex console → Connect your AI, then re-run setup.${rid}`,
121
+ `the Agnoclast console → Connect your AI, then re-run setup.${rid}`,
113
122
  }
114
123
  }
115
124
  if (!isJson) {
116
125
  return {
117
126
  kind: 'infra', retriable: true,
118
- message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by Cortex auth. This is ` +
127
+ message: `Blocked by infrastructure (HTTP ${status}, non-JSON response) — NOT by Agnoclast auth. This is ` +
119
128
  `usually a transient firewall / bot-protection hiccup; retrying often clears it. If it persists, check ` +
120
129
  `Vercel firewall / deployment protection on the API route. Re-running setup will not help.${rid}`,
121
130
  }
122
131
  }
132
+ // A brain-choice refusal is ANSWERABLE, so it must arrive as the question it is rather than as a
133
+ // code. Deliberately narrow: only these two errors reshape the message, so every other classify()
134
+ // output keeps its existing wording (and its tests).
135
+ if (isJson && (appError === 'brain_required' || appError === 'unknown_brain') && (appMessage || appBrains)) {
136
+ const list = (appBrains ?? []).map((b) => {
137
+ const pages = typeof b?.pageCount === 'number' ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
138
+ const titles = Array.isArray(b?.sampleTitles) && b.sampleTitles.length
139
+ ? `: ${b.sampleTitles.slice(0, 2).join('; ')}` : ''
140
+ // Name AND id: brain names are NOT unique (one account holds two called "Personal"), so a name
141
+ // alone can come back as unknown_brain. The id always resolves.
142
+ return ` - ${b?.name ?? '(unnamed)'}${pages}${titles}${b?.orgId ? ` [${b.orgId}]` : ''}`
143
+ })
144
+ return {
145
+ kind: 'app', retriable: false,
146
+ message: `Agnoclast API ${status}: ${appMessage ?? appError}${list.length ? `\n${list.join('\n')}` : ''}` +
147
+ `\nRe-run this tool with \`brain\` set to one of the names or ids above.${rid}`,
148
+ }
149
+ }
150
+
123
151
  return {
124
152
  kind: 'app', retriable: status >= 500,
125
- message: `Cortex API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
153
+ message: `Agnoclast API ${status}: ${appError ?? 'unknown error'}.${appHint ? ` ${appHint}` : ''}${rid}`,
126
154
  }
127
155
  }
128
156
 
@@ -176,7 +204,7 @@ export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 4
176
204
  }
177
205
  }
178
206
  throw new Error(
179
- `Could not reach Cortex at ${url} — ${lastErr?.message ?? 'network error'}. ` +
207
+ `Could not reach Agnoclast at ${url} — ${lastErr?.message ?? 'network error'}. ` +
180
208
  `Check your connection (and CORTEX_URL if you set it).`,
181
209
  )
182
210
  }
@@ -190,7 +218,7 @@ export async function checkToken(token, base) {
190
218
  }
191
219
  if (!isUuid(token)) {
192
220
  return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
193
- message: `Token "${String(token).slice(0, 8)}…" is not a valid Cortex token (expected a UUID). ` +
221
+ message: `Token "${String(token).slice(0, 8)}…" is not a valid Agnoclast token (expected a UUID). ` +
194
222
  `Re-run setup with the token from the console.` } }
195
223
  }
196
224
  const url = `${(base ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')}/api/mcp-context`
@@ -207,12 +235,54 @@ export async function checkToken(token, base) {
207
235
  return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
208
236
  }
209
237
  let projectCount
238
+ let captureNotice = null
210
239
  try {
211
- const ctx = JSON.parse(body).context ?? ''
240
+ const parsed = JSON.parse(body)
241
+ const ctx = parsed.context ?? ''
212
242
  const m = ctx.match(/## Projects \((\d+)\)/)
213
243
  if (m) projectCount = Number(m[1])
244
+ // The server's "your work is not landing" advisory. Optional by design: an older server does not
245
+ // send it and this must stay a health check, so a missing field is simply no notice.
246
+ if (parsed.captureNotice && typeof parsed.captureNotice.message === 'string') {
247
+ captureNotice = parsed.captureNotice
248
+ }
214
249
  } catch { /* context shape changed — non-fatal for a health check */ }
215
- return { ok: true, status: 200, projectCount, requestId }
250
+ return { ok: true, status: 200, projectCount, requestId, captureNotice }
251
+ }
252
+
253
+ // Probe the org-skills surface. SEPARATE FROM checkToken ON PURPOSE.
254
+ //
255
+ // WHY THIS EXISTS. `doctor` checked exactly one endpoint — /api/mcp-context — and reported PASS on
256
+ // the strength of it. Meanwhile /api/skills answered 409 to every multi-brain caller for three days
257
+ // (measured 2026-08-08: 14 requests, 14 × 409, zero successes; the local cache sat frozen from Aug 5
258
+ // to Aug 8). Org-published skills silently never installed, the client swallowed the status, and
259
+ // doctor — the tool setup.txt tells people to run FIRST when something is wrong — said everything
260
+ // was fine, every single time. A check that probes one surface is a health check for that surface,
261
+ // not for the system, and must not be reported as the latter.
262
+ //
263
+ // Returns checkToken's shape so the caller renders both the same way. `skillCount` is
264
+ // informational: an org with zero published skills is perfectly healthy, so it is NOT a failure.
265
+ export async function checkSkills(token, base) {
266
+ if (!token || !isUuid(token)) {
267
+ return { ok: false, status: 0, diagnosis: { kind: 'config', retriable: false,
268
+ message: 'no usable token — the context check above already covers this' } }
269
+ }
270
+ const url = `${(base ?? CANONICAL_BASE).replace(/\/$/, '')}/api/skills`
271
+ let res
272
+ try {
273
+ res = await fetchCortex(url, { headers: { Authorization: `Bearer ${token}` } })
274
+ } catch (e) {
275
+ return { ok: false, status: 0, diagnosis: { kind: 'network', retriable: true, message: e.message } }
276
+ }
277
+ const requestId = res.headers.get('x-vercel-id') ?? null
278
+ const contentType = res.headers.get('content-type')
279
+ const body = await res.text()
280
+ if (!res.ok) {
281
+ return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
282
+ }
283
+ let skillCount
284
+ try { skillCount = (JSON.parse(body)?.skills ?? []).length } catch { /* shape drift — non-fatal */ }
285
+ return { ok: true, status: 200, skillCount, requestId }
216
286
  }
217
287
 
218
288
  // Release the keep-alive sockets the global fetch (undici) holds, so a short-lived CLI command
package/lib/docs_scan.mjs CHANGED
@@ -6,7 +6,7 @@ import { join, resolve, dirname } from 'path'
6
6
  // Documentation ingestion — detection half (D1 of docs/documentation-ingestion-spec.md).
7
7
  //
8
8
  // Specs/plans/design docs get written to disk (repo docs/, ~/.gstack/projects/…) and never reach
9
- // Cortex as pages. This subcommand DETECTS new/changed markdown under registered roots by content
9
+ // Agnoclast as pages. This subcommand DETECTS new/changed markdown under registered roots by content
10
10
  // hash; the AUTHORING is done by the live session (the cortex-author-docs skill reads each pending
11
11
  // doc and calls the `author` MCP tool) — the agent is the pipe, never a raw-markdown dump.
12
12
  //
@@ -90,7 +90,7 @@ export function markFiles(state, paths, now = new Date().toISOString()) {
90
90
  }
91
91
 
92
92
  // Heuristic soak guardrail (spec D5): the Robin parity experiment forbids re-syncing the local
93
- // brain into Cortex during the window, so warn when a root looks like the brain repo.
93
+ // brain into Agnoclast during the window, so warn when a root looks like the brain repo.
94
94
  const looksLikeBrain = (dir) => /\/Documents\/brain(\/|$)/.test(dir)
95
95
 
96
96
  export async function runDocsScan(argv = []) {
@@ -111,7 +111,7 @@ export async function runDocsScan(argv = []) {
111
111
  const abs = resolve(dir)
112
112
  if (!existsSync(abs)) { process.stderr.write(`Not a directory: ${abs}\n`); return 1 }
113
113
  if (looksLikeBrain(abs)) {
114
- out(`⚠ ${abs} looks like the local brain repo — the Robin parity soak forbids re-syncing it into Cortex.`)
114
+ out(`⚠ ${abs} looks like the local brain repo — the Robin parity soak forbids re-syncing it into Agnoclast.`)
115
115
  out(' Registering anyway is on you; the cortex-author-docs skill will also warn.')
116
116
  }
117
117
  if (!state.roots.includes(abs)) state.roots.push(abs)
@@ -163,7 +163,7 @@ export async function runDocsScan(argv = []) {
163
163
  }
164
164
  if (!pending.length) out(`✓ up to date — ${scanned} doc(s) scanned, nothing pending`)
165
165
  else {
166
- out(`${pending.length} doc(s) pending Cortex authoring (of ${scanned} scanned):`)
166
+ out(`${pending.length} doc(s) pending Agnoclast authoring (of ${scanned} scanned):`)
167
167
  for (const p of pending) out(` ${p.status === 'new' ? '+ ' : '~ '}${p.path}`)
168
168
  out('Author them via the cortex-author-docs skill, then: docs-scan --mark <file>...')
169
169
  }
package/lib/doctor.mjs CHANGED
@@ -1,17 +1,20 @@
1
1
  import { readFileSync, existsSync } from 'fs'
2
2
  import { homedir } from 'os'
3
3
  import { join } from 'path'
4
- import { checkToken, resolveBase } from './diagnose.mjs'
4
+ import { checkToken, checkSkills, resolveBase } from './diagnose.mjs'
5
5
 
6
6
  // `npx @theronap/cortex-mcp doctor` — a live, one-command health check.
7
7
  //
8
8
  // This is the "is it ACTUALLY working?" tool that was missing: it reads your token, calls the
9
- // real Cortex API, and prints a clear PASS/FAIL with the actual cause. No Claude Code restart
9
+ // real Agnoclast API, and prints a clear PASS/FAIL with the actual cause. No Claude Code restart
10
10
  // needed — so onboarding can confirm the connection independently of "did the server load."
11
11
 
12
12
  // Token resolution: env first, then the Claude config the setup command wrote (so `doctor`
13
13
  // works the moment after `setup`, before any restart). Returns { token, source }.
14
- function resolveToken() {
14
+ // Exported so `use-brain` resolves the token EXACTLY as doctor/status do. There is already a second,
15
+ // subtly different copy of this in context_log.mjs (returns a bare token, not {token, source}); a
16
+ // third copy is how a machine ends up "connected" to one command and "no token found" to another.
17
+ export function resolveToken() {
15
18
  if (process.env.CORTEX_TOKEN) return { token: process.env.CORTEX_TOKEN, source: 'CORTEX_TOKEN env' }
16
19
  const claudeJson = join(homedir(), '.claude.json')
17
20
  if (existsSync(claudeJson)) {
@@ -24,7 +27,7 @@ function resolveToken() {
24
27
  return { token: null, source: null }
25
28
  }
26
29
 
27
- // `status` — the one-line SessionStart variant of doctor: a visible "is Cortex capturing?"
30
+ // `status` — the one-line SessionStart variant of doctor: a visible "is Agnoclast capturing?"
28
31
  // signal inside Claude Code itself (three-machine dry-run finding 2026-06-09: with no
29
32
  // indicator, a user can't tell whether their sessions are flowing to the org).
30
33
  // Always returns 0 — a status line must never break a session start.
@@ -33,19 +36,29 @@ export async function runStatus() {
33
36
  const out = (m) => process.stdout.write(m + '\n')
34
37
  const { token } = resolveToken()
35
38
  if (!token) {
36
- out('Cortex: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
39
+ out('Agnoclast: NOT connected — no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
37
40
  return 0
38
41
  }
39
42
  try {
40
43
  const r = await checkToken(token, base)
41
44
  if (r.ok) {
45
+ // ⚠ THE NOTICE REPLACES THE HAPPY LINE RATHER THAN FOLLOWING IT.
46
+ // "connected — sessions on this machine are captured to your org" was printed truthfully to
47
+ // three people whose sessions were, at that moment, landing in NO brain: connected is a fact
48
+ // about the TOKEN, and every reader takes it as a fact about their WORK. Printing both would
49
+ // leave the reassurance that caused four days of silent loss sitting directly above the
50
+ // warning that contradicts it.
51
+ if (r.captureNotice?.message) {
52
+ out(`Agnoclast: ⚠ ${r.captureNotice.message}`)
53
+ return 0
54
+ }
42
55
  const n = typeof r.projectCount === 'number' ? ` · ${r.projectCount} project${r.projectCount === 1 ? '' : 's'} visible` : ''
43
- out(`Cortex: connected — sessions on this machine are captured to your org${n}.`)
56
+ out(`Agnoclast: connected — sessions on this machine are captured to your org${n}.`)
44
57
  } else {
45
- out(`Cortex: NOT connected — ${r.diagnosis?.message ?? 'check failed'}. Run: npx -y @theronap/cortex-mcp doctor`)
58
+ out(`Agnoclast: NOT connected — ${r.diagnosis?.message ?? 'check failed'}. Run: npx -y @theronap/cortex-mcp doctor`)
46
59
  }
47
60
  } catch (e) {
48
- out(`Cortex: status check failed (${e?.message ?? String(e)}) — run doctor.`)
61
+ out(`Agnoclast: status check failed (${e?.message ?? String(e)}) — run doctor.`)
49
62
  }
50
63
  return 0
51
64
  }
@@ -55,7 +68,7 @@ export async function runDoctor() {
55
68
  const out = (m) => process.stdout.write(m + '\n')
56
69
 
57
70
  out('')
58
- out('Cortex doctor — checking your connection…')
71
+ out('Agnoclast doctor — checking your connection…')
59
72
  const { token, source } = resolveToken()
60
73
  out(` token source: ${source ?? 'NONE FOUND'}`)
61
74
  out(` endpoint: ${base}`)
@@ -64,11 +77,35 @@ export async function runDoctor() {
64
77
  const r = await checkToken(token, base)
65
78
 
66
79
  if (r.ok) {
67
- out(' ✓ PASS — your token authenticates and Cortex returned your context.')
80
+ out(' ✓ context — your token authenticates and Agnoclast returned your context.')
68
81
  if (typeof r.projectCount === 'number') out(` You can currently see ${r.projectCount} project(s).`)
69
82
  if (r.requestId) out(` (request id: ${r.requestId})`)
83
+
84
+ // A SECOND surface, because one endpoint answering is not "the system is healthy". /api/skills
85
+ // 409'd every multi-brain caller for three days while this command printed PASS on the strength
86
+ // of /api/mcp-context alone. See checkSkills' header for the full record.
87
+ const s = await checkSkills(token, base)
88
+ if (s.ok) {
89
+ const n = typeof s.skillCount === 'number'
90
+ ? ` (${s.skillCount} org skill${s.skillCount === 1 ? '' : 's'} published)` : ''
91
+ out(` ✓ skills — the org-skill surface answers${n}.`)
92
+ } else {
93
+ // NOT a total failure — context works, so capture and retrieval are fine. But it must never
94
+ // again render as PASS, because org skills silently stop updating when this breaks.
95
+ out(` ✗ skills — ${s.diagnosis?.kind ?? 'error'}${s.status ? ` (HTTP ${s.status})` : ''}`)
96
+ out(` ${s.diagnosis?.message ?? 'unknown error'}`)
97
+ out('')
98
+ out(' ⚠ PARTIAL — your connection is fine and capture/retrieval work, but org-published')
99
+ out(' skills cannot be fetched, so they will silently stop updating (the client falls')
100
+ out(' back to its last-good cache). Report this rather than ignoring it.')
101
+ out('')
102
+ return 1
103
+ }
104
+
105
+ out('')
106
+ out(' ✓ PASS — every surface checked is answering.')
70
107
  out('')
71
- out(' If your AI still does not see Cortex, the server just is not loaded yet —')
108
+ out(' If your AI still does not see Agnoclast, the server just is not loaded yet —')
72
109
  out(' fully quit and reopen Claude Code (the MCP server starts on launch).')
73
110
  out('')
74
111
  return 0
@@ -55,7 +55,7 @@ export function renderAntigravityPlist({ home = homedir() } = {}) {
55
55
  * NOTE the AGENT_DIR line is the portability blocker documented above. */
56
56
  export function renderAntigravitySyncSh() {
57
57
  return `#!/bin/bash
58
- # Cortex ⇄ Antigravity one-shot sync, triggered by launchd WatchPaths on the Antigravity
58
+ # Agnoclast ⇄ Antigravity one-shot sync, triggered by launchd WatchPaths on the Antigravity
59
59
  # trajectory store. There is no resident daemon — launchd wakes this on change and it exits.
60
60
  # Token is resolved from the wired MCP config at runtime (never stored in plist/script).
61
61
  # ⚠ AGENT_DIR points at a local dev checkout — see antigravity.mjs BLOCKER note (not coworker-portable).
@@ -6,7 +6,7 @@ import { existsSync } 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 Cortex MCP server into a ~/.claude.json object. Pure + idempotent: sets only the
9
+ /** Merge the Agnoclast MCP server into a ~/.claude.json object. Pure + idempotent: sets only the
10
10
  * `cortex` entry (type:'stdio'), preserves every other server. `spec` = the package@dist-tag string. */
11
11
  export function mergeClaudeMcp(existing, spec, token) {
12
12
  const cfg = existing && typeof existing === 'object' ? { ...existing } : {}
@@ -15,14 +15,16 @@ export function mergeClaudeMcp(existing, spec, token) {
15
15
  return cfg
16
16
  }
17
17
 
18
- /** The Cortex tools every seat may run WITHOUT a permission prompt: the read surface, the live
18
+ /** The Agnoclast tools every seat may run WITHOUT a permission prompt: the read surface, the live
19
19
  * authoring core, and trivially-reversible maintenance. The contract this enforces: authoring is
20
20
  * EXPECTED agent behavior — a page update must never stall on a yes/no dialog the user won't read
21
21
  * (the ask-permission failure mode is how pages go stale). Safe because every page edit is
22
22
  * CAS-protected + snapshotted (page_revisions → page_history/rollback_page).
23
23
  * Deliberately EXCLUDED — these keep prompting: send_imessage (external side effect),
24
24
  * set_page_privacy / set_record_privacy / grant_page_access (visibility widening — the
25
- * unowned-project accessible-default sharp edge, 2026-07-02), rollback_page, decide_page_merge /
25
+ * unowned-project accessible-default sharp edge, 2026-07-02), replace_variant (destroys a page body
26
+ * and deletes a variant row — the one write here that is not merely CAS-protected but genuinely
27
+ * lossy at the row level, so the prompt IS the guard D1 argued for), rollback_page, decide_page_merge /
26
28
  * decide_file_request / request_file / get_file, create_brain / set_active_brain, alias_page,
27
29
  * set_writing_style. */
28
30
  export const CORTEX_ALLOWED_TOOLS = [
@@ -36,10 +38,11 @@ export const CORTEX_ALLOWED_TOOLS = [
36
38
  'set_page_validity', 'snooze_red_link', 'attribute_thread',
37
39
  ].map((t) => `mcp__cortex__${t}`)
38
40
 
39
- /** Merge Cortex's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
41
+ /** Merge Agnoclast's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
40
42
  * any prior cortex entry (old token/path/version) from each hook array before appending the current
41
43
  * one — capture (Stop), status + skills-repair + snapshot-context (SessionStart), hydrate
42
- * (UserPromptSubmit), precompact (PreCompact). Commands carry NO inline token (each subcommand
44
+ * (UserPromptSubmit). Also UNWIRES the retired PreCompact reminder from seats that still carry it.
45
+ * Commands carry NO inline token (each subcommand
43
46
  * self-resolves it). Mirrors setup.mjs
44
47
  * step 2 exactly; foreign hooks are never touched. Also merges the CORTEX_ALLOWED_TOOLS permission
45
48
  * allowlist — additive-only: a user's own allow entries (even extra mcp__cortex__* ones) are never
@@ -103,16 +106,28 @@ export function mergeClaudeSettings(existing, spec) {
103
106
  hgrp.hooks = hgrp.hooks ?? []
104
107
  hgrp.hooks.push({ type: 'command', command: hydrateCmd })
105
108
 
106
- // PreCompact — "author now" reminder.
107
- s.hooks.PreCompact = Array.isArray(s.hooks.PreCompact) ? s.hooks.PreCompact : []
108
- const precompactCmd = `npx -y ${spec} precompact`
109
- for (const pg of s.hooks.PreCompact) {
110
- if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? precompact/.test(h.command ?? ''))
109
+ // PreCompact — REMOVED, and this block is the migration that unwires existing seats.
110
+ //
111
+ // The "author now" reminder wrote to stdout on the assumption the harness surfaced it as context for
112
+ // the next turn. It does not: PreCompact accepts a blocking `decision` and nothing else — it has no
113
+ // additionalContext channel so the reminder never reached a model. Measured 2026-08-10 across 7,232
114
+ // local transcripts: the reminder text appears 5 times, every one of them a tool_result from someone
115
+ // READING the file, assistant prose about it, or a compaction summary that absorbed such prose. Zero
116
+ // injections, against >=13 transcripts that demonstrably compacted. The control is what makes that
117
+ // conclusive rather than merely absent: SessionStart's hook output, on a channel that IS injected,
118
+ // appears in 912 transcripts of the same corpus.
119
+ //
120
+ // So: filter, never append. Every install/repair strips the stale entry from seats that already have
121
+ // it, which is why this runs unconditionally instead of shipping as a separate migration.
122
+ if (Array.isArray(s.hooks.PreCompact)) {
123
+ for (const pg of s.hooks.PreCompact) {
124
+ if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? precompact/.test(h.command ?? ''))
125
+ }
126
+ // Drop groups we just emptied, then the key itself if no foreign hook remains — a bare
127
+ // `PreCompact: []` reads as "cortex wires this event" to the next person to open settings.json.
128
+ s.hooks.PreCompact = s.hooks.PreCompact.filter((pg) => (pg.hooks ?? []).length > 0)
129
+ if (s.hooks.PreCompact.length === 0) delete s.hooks.PreCompact
111
130
  }
112
- let pgrp = s.hooks.PreCompact.find((g) => (g.matcher ?? '') === '')
113
- if (!pgrp) { pgrp = { matcher: '', hooks: [] }; s.hooks.PreCompact.push(pgrp) }
114
- pgrp.hooks = pgrp.hooks ?? []
115
- pgrp.hooks.push({ type: 'command', command: precompactCmd })
116
131
 
117
132
  // Permissions — pre-authorize the read + authoring core so a page update never stalls on a
118
133
  // permission prompt. Append-missing only (no filter-and-rebuild like the hooks above): removals