@theronap/cortex-mcp 0.9.58 → 0.9.60

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/lib/server.mjs CHANGED
@@ -28,6 +28,18 @@ async function redLinkTriage(BASE, TOKEN, name) {
28
28
  }
29
29
  }
30
30
 
31
+ // MCP tool results carry an `isError` flag, and a failure that omits it is indistinguishable from a
32
+ // success at the protocol level — the caller just sees a normal result whose text happens to begin
33
+ // "Could not". This file had 41 tools, 64 failure returns and ZERO uses of isError, so nothing
34
+ // downstream could tell a rejected write from a completed one. Measured 2026-07-31: a walker over
35
+ // 397 transcripts scored 18 REJECTED `author` calls as successful writes for exactly this reason,
36
+ // and an agent skimming its own tool result is exposed the same way. Route every failure through
37
+ // here so the flag cannot be forgotten at a new call site.
38
+ //
39
+ // NAME: deliberately not `fail` — runServer already has a local `fail(verb, res)` for the
40
+ // file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
41
+ const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
42
+
31
43
  // The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
32
44
  // context to their AI assistant. CORTEX_TOKEN identifies the user + org.
33
45
 
@@ -406,12 +418,12 @@ export async function runServer(version) {
406
418
  if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
407
419
  if (!vr.ok) {
408
420
  const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
409
- return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${d.message}` }] }
421
+ return toolError(`Could not read version "${version}" of "${name}": ${d.message}`)
410
422
  }
411
423
  const v = await vr.json()
412
424
  return { content: [{ type: 'text', text: `# ${name} — historical version (rev ${v.revNo} · ${v.op} · ${String(v.createdAt).slice(0, 10)} · ${v.tier})\nversion: ${v.version}\n\n${v.body}\n\n— This is a HISTORICAL snapshot, not the current page. \`read_page "${name}"\` (no version) shows what's live; \`rollback_page\` restores this one as a new version.` }] }
413
425
  } catch (e) {
414
- return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${e.message}` }] }
426
+ return toolError(`Could not read version "${version}" of "${name}": ${e.message}`)
415
427
  }
416
428
  }
417
429
  // PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
@@ -422,7 +434,7 @@ export async function runServer(version) {
422
434
  if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
423
435
  if (!r.ok) {
424
436
  const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
425
- return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${d.message}` }] }
437
+ return toolError(`Could not read the timeline for "${name}": ${d.message}`)
426
438
  }
427
439
  const t = await r.json()
428
440
  if (!t.identifiers?.length) {
@@ -435,7 +447,7 @@ export async function runServer(version) {
435
447
  if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
436
448
  return { content: [{ type: 'text', text: lines.join('\n') }] }
437
449
  } catch (e) {
438
- return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${e.message}` }] }
450
+ return toolError(`Could not read the timeline for "${name}": ${e.message}`)
439
451
  }
440
452
  }
441
453
  // IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
@@ -474,14 +486,14 @@ export async function runServer(version) {
474
486
  res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
475
487
  { headers: { Authorization: `Bearer ${TOKEN}` } })
476
488
  } catch (e) {
477
- return { content: [{ type: 'text', text: `Could not read "${name}": ${e.message}` }] }
489
+ return toolError(`Could not read "${name}": ${e.message}`)
478
490
  }
479
491
  if (res.status === 404) {
480
492
  return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
481
493
  }
482
494
  if (!res.ok) {
483
495
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
484
- return { content: [{ type: 'text', text: `Could not read "${name}": ${d.message}` }] }
496
+ return toolError(`Could not read "${name}": ${d.message}`)
485
497
  }
486
498
  const page = await res.json()
487
499
  // Multi-brain (decision 3A): /api/brain/page returns matches[] — one entry per brain that has a
@@ -518,7 +530,14 @@ export async function runServer(version) {
518
530
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
519
531
  return [head, t.summary, secs].filter(Boolean).join('\n')
520
532
  })
521
- let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — re-author just those sections now with \`author\` (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). Pass this page's \`version\` as base_version when you re-author — if the save comes back stale, the page changed under you: re-read, then re-author. Reading a stale page you can fix IS the trigger to fix it. Re-authoring is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
533
+ // REPAIR TOOL (2026-07-31). This footer used to say "re-author just those sections with
534
+ // `author`" — which `author` cannot do: computeDroppedSections rejects a partial-section
535
+ // write unconditionally, and the 409 then hands back every section's body, so the advice
536
+ // routed the reader straight into retyping the whole page. Measured 2026-07-30: one such
537
+ // retype silently deleted a sentence, a [[link]] (a graph edge), a command list and the
538
+ // word "today" from sections it was never meant to touch. This footer renders on EVERY
539
+ // page read in the system, so it was the single widest surface pointing the wrong way.
540
+ let footer = `— Follow any [[links]] above with read_page to go deeper.\n— If you hold fresher FIRST-HAND truth than this page — something you established THIS session (ran the command, made the change, hold the artifact) that a section here gets concretely wrong — fix just that passage with \`edit_page\`: quote the wrong text as old_string and pass this page's \`version\` as base_version (any node you are permitted to edit; concise; a material contradiction, not merely extra detail). If the save comes back stale the page changed under you — the conflict hands back what changed, so re-anchor from that instead of re-reading. Reading a stale page you can fix IS the trigger to fix it. Editing is pre-authorized — do NOT ask the user before updating (edits are versioned + reversible via page_history/rollback_page); update, then briefly report it. Use \`author\` only to CREATE a page or rewrite one wholesale: it re-emits every section, so untouched sections get retyped on the way through and drift.\n— Citing code? Use a SYMBOL and file (\`formConnections\` in \`web/app/api/ingest/route.ts\`), never a line number — line numbers drift with every commit above them. And cite only what you opened THIS session; re-emitting a reference you read on another page is how a stale claim gains a second source and starts looking corroborated.`
522
541
  // slice 4: when the page carries identifier stamps, the history projection is one flag away.
523
542
  const allBody = m.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
524
543
  const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
@@ -552,12 +571,12 @@ export async function runServer(version) {
552
571
  const qs = new URLSearchParams({ kind: k, key: name, ...(limit ? { limit: String(limit) } : {}) })
553
572
  res = await fetchCortex(`${BASE}/api/brain/page-history?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
554
573
  } catch (e) {
555
- return { content: [{ type: 'text', text: `Could not read history for "${name}": ${e.message}` }] }
574
+ return toolError(`Could not read history for "${name}": ${e.message}`)
556
575
  }
557
576
  if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}". Pass kind (person/org) if it isn't a project.` }] }
558
577
  if (!res.ok) {
559
578
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
560
- return { content: [{ type: 'text', text: `Could not read history for "${name}": ${d.message}` }] }
579
+ return toolError(`Could not read history for "${name}": ${d.message}`)
561
580
  }
562
581
  const out = await res.json()
563
582
  const revs = out.revisions ?? []
@@ -605,12 +624,12 @@ export async function runServer(version) {
605
624
  })
606
625
  res = await fetchCortex(`${BASE}/api/brain/page-diff?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
607
626
  } catch (e) {
608
- return { content: [{ type: 'text', text: `Could not diff "${name}": ${e.message}` }] }
627
+ return toolError(`Could not diff "${name}": ${e.message}`)
609
628
  }
610
629
  if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}", or it has no version "${to}". Run \`page_history "${name}"\` to list its versions.` }] }
611
630
  if (!res.ok) {
612
631
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
613
- return { content: [{ type: 'text', text: `Could not diff "${name}": ${d.message}` }] }
632
+ return toolError(`Could not diff "${name}": ${d.message}`)
614
633
  }
615
634
  const out = await res.json()
616
635
  const d = out.diff
@@ -662,10 +681,10 @@ export async function runServer(version) {
662
681
  body: JSON.stringify({ kind: k, name, to_version, ...(tier ? { tier } : {}), ...(reason ? { reason } : {}) }),
663
682
  })
664
683
  } catch (e) {
665
- return { content: [{ type: 'text', text: `Could not roll back "${name}": ${e.message}` }] }
684
+ return toolError(`Could not roll back "${name}": ${e.message}`)
666
685
  }
667
686
  const out = await res.json().catch(() => null)
668
- if (!res.ok) return { content: [{ type: 'text', text: `Could not roll back "${name}": ${out?.error ?? res.status}` }] }
687
+ if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
669
688
  return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
670
689
  },
671
690
  )
@@ -698,7 +717,7 @@ export async function runServer(version) {
698
717
  }),
699
718
  })
700
719
  } catch (e) {
701
- return { content: [{ type: 'text', text: `Could not rename the section: ${e.message}` }] }
720
+ return toolError(`Could not rename the section: ${e.message}`)
702
721
  }
703
722
  const out = await res.json().catch(() => null)
704
723
  if (!res.ok) {
@@ -711,7 +730,7 @@ export async function runServer(version) {
711
730
  out?.currentVersion ? `current version: ${out.currentVersion}` : '',
712
731
  ].filter(Boolean).join(' · ')
713
732
  const hint = out?.hint ? `\n${out.hint}` : ''
714
- return { content: [{ type: 'text', text: `Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}` }] }
733
+ return toolError(`Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
715
734
  }
716
735
  return { content: [{ type: 'text', text: `Renamed on "${name}" (${out.brain} · ${out.tier} tier): "${out.from}" -> "${out.to}". The section kept its body, position and as-of date. New version: ${out.version}` }] }
717
736
  },
@@ -746,7 +765,7 @@ export async function runServer(version) {
746
765
  }),
747
766
  })
748
767
  } catch (e) {
749
- return { content: [{ type: 'text', text: `Could not edit "${name}": ${e.message}` }] }
768
+ return toolError(`Could not edit "${name}": ${e.message}`)
750
769
  }
751
770
  const out = await res.json().catch(() => null)
752
771
  if (!res.ok) {
@@ -777,7 +796,7 @@ export async function runServer(version) {
777
796
  Array.isArray(out?.concurrentRemoved) && out.concurrentRemoved.length ? `sections REMOVED elsewhere: ${out.concurrentRemoved.join(', ')}` : '',
778
797
  Array.isArray(out?.concurrentTruncated) && out.concurrentTruncated.length ? `(truncated, see page_diff for all of: ${out.concurrentTruncated.join(', ')})` : '',
779
798
  ].filter(Boolean).join('\n')
780
- return { content: [{ type: 'text', text: `Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}` }] }
799
+ return toolError(`Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}`)
781
800
  }
782
801
  const red = Array.isArray(out.redLinks) && out.redLinks.length
783
802
  ? `\nRed-links now on this page: ${out.redLinks.map((r) => `[[${r}]]`).join(', ')}` : ''
@@ -797,11 +816,11 @@ export async function runServer(version) {
797
816
  try {
798
817
  res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
799
818
  } catch (e) {
800
- return { content: [{ type: 'text', text: `Could not load writing style: ${e.message}` }] }
819
+ return toolError(`Could not load writing style: ${e.message}`)
801
820
  }
802
821
  if (!res.ok) {
803
822
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
804
- return { content: [{ type: 'text', text: `Could not load writing style: ${d.message}` }] }
823
+ return toolError(`Could not load writing style: ${d.message}`)
805
824
  }
806
825
  const { style } = await res.json()
807
826
  return { content: [{ type: 'text', text: style }] }
@@ -824,11 +843,11 @@ export async function runServer(version) {
824
843
  body: JSON.stringify({ style_md }),
825
844
  })
826
845
  } catch (e) {
827
- return { content: [{ type: 'text', text: `Could not save writing style: ${e.message}` }] }
846
+ return toolError(`Could not save writing style: ${e.message}`)
828
847
  }
829
848
  if (!res.ok) {
830
849
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
831
- return { content: [{ type: 'text', text: `Could not save writing style: ${d.message}` }] }
850
+ return toolError(`Could not save writing style: ${d.message}`)
832
851
  }
833
852
  const r = await res.json()
834
853
  return { content: [{ type: 'text', text: r.saved ? `Saved your writing-style profile (${r.chars} chars).` : 'Cleared your writing-style profile.' }] }
@@ -847,11 +866,11 @@ export async function runServer(version) {
847
866
  try {
848
867
  res = await fetchCortex(`${BASE}/api/brains`, { headers: { Authorization: `Bearer ${TOKEN}` } })
849
868
  } catch (e) {
850
- return { content: [{ type: 'text', text: `Could not list brains: ${e.message}` }] }
869
+ return toolError(`Could not list brains: ${e.message}`)
851
870
  }
852
871
  if (!res.ok) {
853
872
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
854
- return { content: [{ type: 'text', text: `Could not list brains: ${d.message}` }] }
873
+ return toolError(`Could not list brains: ${d.message}`)
855
874
  }
856
875
  const { brains, activeIsExplicit, activeSource, sessionOrgId, accountOrgId } = await res.json()
857
876
  if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
@@ -891,11 +910,11 @@ export async function runServer(version) {
891
910
  try {
892
911
  res = await fetchCortex(`${BASE}/api/brains/pages?orgId=${encodeURIComponent(org_id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
893
912
  } catch (e) {
894
- return { content: [{ type: 'text', text: `Could not list pages: ${e.message}` }] }
913
+ return toolError(`Could not list pages: ${e.message}`)
895
914
  }
896
915
  if (!res.ok) {
897
916
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
898
- return { content: [{ type: 'text', text: `Could not list pages: ${d.message}` }] }
917
+ return toolError(`Could not list pages: ${d.message}`)
899
918
  }
900
919
  const { nodeCount, rowCount, pages } = await res.json()
901
920
  if (!pages?.length) return { content: [{ type: 'text', text: `Brain ${org_id} has no authored pages.` }] }
@@ -932,11 +951,11 @@ export async function runServer(version) {
932
951
  body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
933
952
  })
934
953
  } catch (e) {
935
- return { content: [{ type: 'text', text: `Could not set active brain: ${e.message}` }] }
954
+ return toolError(`Could not set active brain: ${e.message}`)
936
955
  }
937
956
  if (!res.ok) {
938
957
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
939
- return { content: [{ type: 'text', text: `Could not set active brain: ${d.message}` }] }
958
+ return toolError(`Could not set active brain: ${d.message}`)
940
959
  }
941
960
  const r = await res.json()
942
961
  // Always say WHICH scope changed. The default is session-only, so a caller expecting the old
@@ -967,11 +986,11 @@ export async function runServer(version) {
967
986
  body: JSON.stringify({ name }),
968
987
  })
969
988
  } catch (e) {
970
- return { content: [{ type: 'text', text: `Could not create brain: ${e.message}` }] }
989
+ return toolError(`Could not create brain: ${e.message}`)
971
990
  }
972
991
  if (!res.ok) {
973
992
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
974
- return { content: [{ type: 'text', text: `Could not create brain: ${d.message}` }] }
993
+ return toolError(`Could not create brain: ${d.message}`)
975
994
  }
976
995
  const r = await res.json()
977
996
  return { content: [{ type: 'text', text: `Created brain "${name}" [${r.orgId}]. Reads already span it. Edits to pages in it route themselves; set_active_brain only if you want NEW pages to default here.` }] }
@@ -1000,11 +1019,11 @@ export async function runServer(version) {
1000
1019
  try {
1001
1020
  res = await fetchCortex(`${BASE}/api/records?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1002
1021
  } catch (e) {
1003
- return { content: [{ type: 'text', text: `Could not list records: ${e.message}` }] }
1022
+ return toolError(`Could not list records: ${e.message}`)
1004
1023
  }
1005
1024
  if (!res.ok) {
1006
1025
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1007
- return { content: [{ type: 'text', text: `Could not list records: ${d.message}` }] }
1026
+ return toolError(`Could not list records: ${d.message}`)
1008
1027
  }
1009
1028
  const { text } = await res.json()
1010
1029
  return { content: [{ type: 'text', text }] }
@@ -1032,11 +1051,11 @@ export async function runServer(version) {
1032
1051
  try {
1033
1052
  res = await fetchCortex(`${BASE}/api/records/daily?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1034
1053
  } catch (e) {
1035
- return { content: [{ type: 'text', text: `Could not build daily log: ${e.message}` }] }
1054
+ return toolError(`Could not build daily log: ${e.message}`)
1036
1055
  }
1037
1056
  if (!res.ok) {
1038
1057
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1039
- return { content: [{ type: 'text', text: `Could not build daily log: ${d.message}` }] }
1058
+ return toolError(`Could not build daily log: ${d.message}`)
1040
1059
  }
1041
1060
  const { text } = await res.json()
1042
1061
  return { content: [{ type: 'text', text }] }
@@ -1057,11 +1076,11 @@ export async function runServer(version) {
1057
1076
  headers: { Authorization: `Bearer ${TOKEN}` },
1058
1077
  })
1059
1078
  } catch (e) {
1060
- return { content: [{ type: 'text', text: `Could not list your sessions: ${e.message}` }] }
1079
+ return toolError(`Could not list your sessions: ${e.message}`)
1061
1080
  }
1062
1081
  if (!res.ok) {
1063
1082
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1064
- return { content: [{ type: 'text', text: `Could not list your sessions: ${d.message}` }] }
1083
+ return toolError(`Could not list your sessions: ${d.message}`)
1065
1084
  }
1066
1085
  const { text } = await res.json()
1067
1086
  return { content: [{ type: 'text', text }] }
@@ -1087,11 +1106,11 @@ export async function runServer(version) {
1087
1106
  body: JSON.stringify({ privacy }),
1088
1107
  })
1089
1108
  } catch (e) {
1090
- return { content: [{ type: 'text', text: `Could not set privacy: ${e.message}` }] }
1109
+ return toolError(`Could not set privacy: ${e.message}`)
1091
1110
  }
1092
1111
  if (!res.ok) {
1093
1112
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1094
- return { content: [{ type: 'text', text: `Could not set privacy: ${d.message}` }] }
1113
+ return toolError(`Could not set privacy: ${d.message}`)
1095
1114
  }
1096
1115
  const out = await res.json()
1097
1116
  return { content: [{ type: 'text', text: `Done — record ${out.id} is now "${out.privacy}".` }] }
@@ -1120,11 +1139,11 @@ export async function runServer(version) {
1120
1139
  body: JSON.stringify({ kind, name, validity, modality, superseded_by }),
1121
1140
  })
1122
1141
  } catch (e) {
1123
- return { content: [{ type: 'text', text: `Could not set validity: ${e.message}` }] }
1142
+ return toolError(`Could not set validity: ${e.message}`)
1124
1143
  }
1125
1144
  if (!res.ok) {
1126
1145
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1127
- return { content: [{ type: 'text', text: `Could not set validity: ${d.message}` }] }
1146
+ return toolError(`Could not set validity: ${d.message}`)
1128
1147
  }
1129
1148
  const out = await res.json()
1130
1149
  return { content: [{ type: 'text', text: `Done — "${out.name}" is now ${out.validity}${out.modality ? ` / ${out.modality}` : ''} (${out.docs_updated} tier-doc(s) updated).` }] }
@@ -1151,10 +1170,10 @@ export async function runServer(version) {
1151
1170
  body: JSON.stringify({ name, target_name, ...(target_kind ? { target_kind } : {}) }),
1152
1171
  })
1153
1172
  } catch (e) {
1154
- return { content: [{ type: 'text', text: `Could not alias: ${e.message}` }] }
1173
+ return toolError(`Could not alias: ${e.message}`)
1155
1174
  }
1156
1175
  const out = await res.json().catch(() => null)
1157
- if (!res.ok) return { content: [{ type: 'text', text: `Could not alias "${name}": ${out?.error ?? res.status}` }] }
1176
+ if (!res.ok) return toolError(`Could not alias "${name}": ${out?.error ?? res.status}`)
1158
1177
  if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
1159
1178
  return { content: [{ type: 'text', text: `Done — [[${out.alias}]] now resolves to "${out.target}" (${out.target_kind}). It's out of the wanted-page backlog.` }] }
1160
1179
  },
@@ -1179,10 +1198,10 @@ export async function runServer(version) {
1179
1198
  body: JSON.stringify({ name, ...(days ? { days } : {}) }),
1180
1199
  })
1181
1200
  } catch (e) {
1182
- return { content: [{ type: 'text', text: `Could not snooze: ${e.message}` }] }
1201
+ return toolError(`Could not snooze: ${e.message}`)
1183
1202
  }
1184
1203
  const out = await res.json().catch(() => null)
1185
- if (!res.ok) return { content: [{ type: 'text', text: `Could not snooze "${name}": ${out?.error ?? res.status}` }] }
1204
+ if (!res.ok) return toolError(`Could not snooze "${name}": ${out?.error ?? res.status}`)
1186
1205
  if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
1187
1206
  return { content: [{ type: 'text', text: `Snoozed "${out.name}" for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
1188
1207
  },
@@ -1211,7 +1230,7 @@ export async function runServer(version) {
1211
1230
  body: JSON.stringify({ kind, name, tier, ...(source_tier ? { source_tier } : {}), ...(absorb ? { absorb: true } : {}), ...(target_version ? { target_version } : {}) }),
1212
1231
  })
1213
1232
  } catch (e) {
1214
- return { content: [{ type: 'text', text: `Could not set page privacy: ${e.message}` }] }
1233
+ return toolError(`Could not set page privacy: ${e.message}`)
1215
1234
  }
1216
1235
  const out = await res.json().catch(() => null)
1217
1236
  if (!res.ok) {
@@ -1221,10 +1240,10 @@ export async function runServer(version) {
1221
1240
  const extra = out.collision === 'readable' && out.blocking
1222
1241
  ? `\nYour ${out.source?.tier} page: ${out.source?.summary ?? out.source?.title}\nExisting ${out.blocking.tier} page (current version: ${out.blocking.version ?? 'none — this page predates content-hash tracking and cannot be re-authored via base_version; ask an admin about a backfill'}): ${out.blocking.summary ?? out.blocking.title}`
1223
1242
  : ''
1224
- return { content: [{ type: 'text', text: `Could not set page privacy: ${out.error}${extra}` }] }
1243
+ return toolError(`Could not set page privacy: ${out.error}${extra}`)
1225
1244
  }
1226
1245
  const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
1227
- return { content: [{ type: 'text', text: `Could not set page privacy: ${d.message}` }] }
1246
+ return toolError(`Could not set page privacy: ${d.message}`)
1228
1247
  }
1229
1248
  const g = out.live_grants?.length
1230
1249
  ? ` Grants still active for: ${out.live_grants.map((x) => x.grantee_name ?? x.grantee_user_id).join(', ')}.`
@@ -1255,10 +1274,10 @@ export async function runServer(version) {
1255
1274
  body: JSON.stringify({ kind, name, grantee, action, ...(tier ? { tier } : {}) }),
1256
1275
  })
1257
1276
  } catch (e) {
1258
- return { content: [{ type: 'text', text: `Could not ${action}: ${e.message}` }] }
1277
+ return toolError(`Could not ${action}: ${e.message}`)
1259
1278
  }
1260
1279
  const out = await res.json().catch(() => null)
1261
- if (!res.ok) return { content: [{ type: 'text', text: `Could not ${action}: ${out?.error ?? res.status}` }] }
1280
+ if (!res.ok) return toolError(`Could not ${action}: ${out?.error ?? res.status}`)
1262
1281
  const verb = { granted: 'now has access to', already_granted: 'already had access to', revoked: 'no longer has access to', not_granted: 'had no grant on' }[out.action]
1263
1282
  return { content: [{ type: 'text', text: `Done — ${out.grantee} ${verb} the ${out.tier} page.` }] }
1264
1283
  },
@@ -1281,10 +1300,10 @@ export async function runServer(version) {
1281
1300
  headers: { Authorization: `Bearer ${TOKEN}` },
1282
1301
  })
1283
1302
  } catch (e) {
1284
- return { content: [{ type: 'text', text: `Could not list grants: ${e.message}` }] }
1303
+ return toolError(`Could not list grants: ${e.message}`)
1285
1304
  }
1286
1305
  const out = await res.json().catch(() => null)
1287
- if (!res.ok) return { content: [{ type: 'text', text: `Could not list grants: ${out?.error ?? res.status}` }] }
1306
+ if (!res.ok) return toolError(`Could not list grants: ${out?.error ?? res.status}`)
1288
1307
  if (!out.grants?.length) return { content: [{ type: 'text', text: 'No grants on this page.' }] }
1289
1308
  const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} variant (since ${String(g.created_at).slice(0, 10)})`)
1290
1309
  return { content: [{ type: 'text', text: `Grants (${out.grants.length}):\n${lines.join('\n')}` }] }
@@ -1303,11 +1322,11 @@ export async function runServer(version) {
1303
1322
  try {
1304
1323
  res = await fetchCortex(`${BASE}/api/brain/retier-notices`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1305
1324
  } catch (e) {
1306
- return { content: [{ type: 'text', text: `Could not list notices: ${e.message}` }] }
1325
+ return toolError(`Could not list notices: ${e.message}`)
1307
1326
  }
1308
1327
  if (!res.ok) {
1309
1328
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1310
- return { content: [{ type: 'text', text: `Could not list notices: ${d.message}` }] }
1329
+ return toolError(`Could not list notices: ${d.message}`)
1311
1330
  }
1312
1331
  const { notices } = await res.json()
1313
1332
  if (!notices?.length) return { content: [{ type: 'text', text: 'No re-tier notices.' }] }
@@ -1329,11 +1348,11 @@ export async function runServer(version) {
1329
1348
  try {
1330
1349
  res = await fetchCortex(`${BASE}/api/brain/page-merge-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1331
1350
  } catch (e) {
1332
- return { content: [{ type: 'text', text: `Could not list merge requests: ${e.message}` }] }
1351
+ return toolError(`Could not list merge requests: ${e.message}`)
1333
1352
  }
1334
1353
  if (!res.ok) {
1335
1354
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1336
- return { content: [{ type: 'text', text: `Could not list merge requests: ${d.message}` }] }
1355
+ return toolError(`Could not list merge requests: ${d.message}`)
1337
1356
  }
1338
1357
  const { requests } = await res.json()
1339
1358
  if (!requests?.length) return { content: [{ type: 'text', text: 'No pending page merge requests.' }] }
@@ -1362,17 +1381,17 @@ export async function runServer(version) {
1362
1381
  body: JSON.stringify({ id, decision }),
1363
1382
  })
1364
1383
  } catch (e) {
1365
- return { content: [{ type: 'text', text: `Could not decide: ${e.message}` }] }
1384
+ return toolError(`Could not decide: ${e.message}`)
1366
1385
  }
1367
1386
  const out = await res.json().catch(() => null)
1368
- if (!res.ok) return { content: [{ type: 'text', text: `Could not decide: ${out?.error ?? res.status}` }] }
1387
+ if (!res.ok) return toolError(`Could not decide: ${out?.error ?? res.status}`)
1369
1388
  return { content: [{ type: 'text', text: out.decision === 'denied' ? 'Denied — nothing moved.' : `Approved — removed variant(s): ${out.removed_variants.join(', ') || 'none remained'}. ${out.note}` }] }
1370
1389
  },
1371
1390
  )
1372
1391
 
1373
1392
  // ── File requests: ask the owner for a record's FULL original (lives on their machine) ──
1374
1393
  const fail = (verb, res) => async () =>
1375
- ({ content: [{ type: 'text', text: `Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}` }] })
1394
+ toolError(`Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}`)
1376
1395
 
1377
1396
  server.registerTool(
1378
1397
  'request_file',
@@ -1388,7 +1407,7 @@ export async function runServer(version) {
1388
1407
  method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1389
1408
  body: JSON.stringify({ recordId: record_id }),
1390
1409
  })
1391
- } catch (e) { return { content: [{ type: 'text', text: `Could not request: ${e.message}` }] } }
1410
+ } catch (e) { return toolError(`Could not request: ${e.message}`) }
1392
1411
  if (res.status === 409) {
1393
1412
  const j = await res.json().catch(() => ({}))
1394
1413
  if (j.error === 'you_own_it') return { content: [{ type: 'text', text: `You own this record — open the original directly:\n${j.uri}` }] }
@@ -1410,7 +1429,7 @@ export async function runServer(version) {
1410
1429
  async () => {
1411
1430
  let res
1412
1431
  try { res = await fetchCortex(`${BASE}/api/file-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
1413
- catch (e) { return { content: [{ type: 'text', text: `Could not load file requests: ${e.message}` }] } }
1432
+ catch (e) { return toolError(`Could not load file requests: ${e.message}`) }
1414
1433
  if (!res.ok) return (await fail('load file requests', res))()
1415
1434
  const { mine, inbox } = await res.json()
1416
1435
  const parts = []
@@ -1434,7 +1453,7 @@ export async function runServer(version) {
1434
1453
  method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1435
1454
  body: JSON.stringify({ action: decision }),
1436
1455
  })
1437
- } catch (e) { return { content: [{ type: 'text', text: `Could not decide: ${e.message}` }] } }
1456
+ } catch (e) { return toolError(`Could not decide: ${e.message}`) }
1438
1457
  if (!res.ok) return (await fail('decide', res))()
1439
1458
  return { content: [{ type: 'text', text: `Request ${decision === 'approve' ? 'approved' : 'denied'}.` }] }
1440
1459
  },
@@ -1450,7 +1469,7 @@ export async function runServer(version) {
1450
1469
  async ({ request_id }) => {
1451
1470
  let res
1452
1471
  try { res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/download`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
1453
- catch (e) { return { content: [{ type: 'text', text: `Could not download: ${e.message}` }] } }
1472
+ catch (e) { return toolError(`Could not download: ${e.message}`) }
1454
1473
  if (res.status === 409) return { content: [{ type: 'text', text: 'Not ready — the owner hasn\'t fulfilled this yet. Try again after they approve.' }] }
1455
1474
  if (res.status === 410) return { content: [{ type: 'text', text: 'This file has expired (downloads are available for 7 days). Request it again.' }] }
1456
1475
  if (!res.ok) return (await fail('download', res))()
@@ -1509,11 +1528,11 @@ export async function runServer(version) {
1509
1528
  try {
1510
1529
  res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1511
1530
  } catch (e) {
1512
- return { content: [{ type: 'text', text: `Could not fetch authoring context: ${e.message}` }] }
1531
+ return toolError(`Could not fetch authoring context: ${e.message}`)
1513
1532
  }
1514
1533
  if (!res.ok) {
1515
1534
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1516
- return { content: [{ type: 'text', text: `Could not fetch authoring context: ${d.message}` }] }
1535
+ return toolError(`Could not fetch authoring context: ${d.message}`)
1517
1536
  }
1518
1537
  const { connectionRules, namespace, retiredLinks } = await res.json()
1519
1538
  const ns = Array.isArray(namespace) ? namespace : []
@@ -1564,11 +1583,11 @@ export async function runServer(version) {
1564
1583
  body: JSON.stringify({ kind, name, pages, reason, change_kind }),
1565
1584
  })
1566
1585
  } catch (e) {
1567
- return { content: [{ type: 'text', text: `Could not author "${name}": ${e.message}` }] }
1586
+ return toolError(`Could not author "${name}": ${e.message}`)
1568
1587
  }
1569
1588
  if (!res.ok) {
1570
1589
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1571
- return { content: [{ type: 'text', text: `Could not author "${name}": ${d.message}` }] }
1590
+ return toolError(`Could not author "${name}": ${d.message}`)
1572
1591
  }
1573
1592
  const out = await res.json()
1574
1593
  const blue = out?.links?.blue ?? 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.58",
3
+ "version": "0.9.60",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,9 +52,8 @@ No arguments. Read the conversation context.
52
52
  docs are pending, follow the `cortex-author-docs` skill (author each into its page, then
53
53
  `docs-scan --mark`). Specs/plans written to disk this session must not die on disk — a spec IS
54
54
  a page. If no roots are registered or nothing is pending, skip silently.
55
- 7. **Reconcile & verify (prove the sweep don't trust it).** Step 5 relies on your in-the-moment
56
- judgment of "what advanced"; this step closes the loop so nothing is silently missed and no stale
57
- write slips through. Before printing the Output:
55
+ 7. **Reconcile the sweep (don't trust it).** Step 5 relies on your in-the-moment judgment of "what
56
+ advanced"; this step closes the loop so nothing is silently missed. Before printing the Output:
58
57
  a. **Enumerate what you touched** — from the transcript, list the concrete entities this session
59
58
  advanced: the project(s), notable files/artifacts, and the people you coordinated with. Derive
60
59
  this checklist from what actually *happened*, not from what you remember authoring — the whole
@@ -62,18 +61,21 @@ No arguments. Read the conversation context.
62
61
  b. **Assert one outcome per entity** — every item gets exactly `authored [[Page]]` **or**
63
62
  `skipped — <reason>` (e.g. "no material change", "not a node", "already current"). Nothing may be
64
63
  left unaccounted for. If an entity that genuinely advanced has neither, `author` it now (Step 5).
65
- c. **Verify the writes landed and are right** — for each page you claim you authored, `read_page` it
66
- (or check `page_history`) and confirm both: (i) **the change applied** — an `author` that returned
67
- "no change" when you *intended* an update means it did NOT land (stale `base_version`, wrong
68
- brain/namespace, or nothing actually differed) re-check rather than assume; and (ii) **the page
69
- reflects THIS session's first-hand findings**, not a prior you copied forward. This is the
70
- read-after-write half of the read-before-write rule the guard against laundering stale priors
71
- into the wiki. Fix a page you can edit; `set_page_validity` on one you can't. (Cross-brain note:
72
- `read_page` can resolve a page in another brain that `author` won't write if a "verify" read
73
- looks right but your write reported no-op, run `my_brains` / check `authoring_context` before
74
- trusting the read.)
75
- Carry the tally into the Output. If any intended write did not land, say so never report a clean
76
- sweep you didn't confirm.
64
+ Carry the tally into the Output.
65
+
66
+ > **A read-back verification sub-step lived here and was REMOVED 2026-07-31. Do not re-add it
67
+ > without new evidence.** It asked you to `read_page` every page you had just authored, to confirm
68
+ > the write landed. Measured over 397 local transcripts 18 sessions that wrote a page,
69
+ > 2026-07-29→31 **18 `author` calls were rejected and all 18 were retried to success: zero
70
+ > silently lost.** An end-of-session pass would have caught nothing, because the rejection reason
71
+ > arrives *in the tool result at the moment of the call*. Session end is the weakest place to
72
+ > verify a write; the result you already have in hand is the strongest.
73
+ >
74
+ > **What replaces it — at the moment of each write, not at the end:** a rejected `author` comes back
75
+ > as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Cortex API 409:
76
+ > <reason>` — and `No change to "<page>"` is a **200 OK that wrote nothing**. Neither is an error at
77
+ > the protocol level, so nothing will interrupt you. **Read the result text of every write; never
78
+ > skim it.** That inline read is where this step's value actually was.
77
79
 
78
80
  ## Output
79
81
 
@@ -88,7 +90,7 @@ After calling `log_session`, show a short structured summary:
88
90
  **Coordinated with:** people involved
89
91
  **Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
90
92
  **Wiki authored:** [[Node A]], [[Node B]] — pages updated (or "— nothing advanced this session")
91
- **Reconciled:** N touched → M authored, K skipped (reason each); writes verified ✅ (or ⚠ <what didn't land>)
93
+ **Reconciled:** N touched → M authored, K skipped (reason each)
92
94
  ```
93
95
 
94
96
  ## Safety rules