@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 +84 -65
- package/package.json +1 -1
- package/skills/log/SKILL.md +18 -16
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1173
|
+
return toolError(`Could not alias: ${e.message}`)
|
|
1155
1174
|
}
|
|
1156
1175
|
const out = await res.json().catch(() => null)
|
|
1157
|
-
if (!res.ok) return
|
|
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
|
|
1201
|
+
return toolError(`Could not snooze: ${e.message}`)
|
|
1183
1202
|
}
|
|
1184
1203
|
const out = await res.json().catch(() => null)
|
|
1185
|
-
if (!res.ok) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1277
|
+
return toolError(`Could not ${action}: ${e.message}`)
|
|
1259
1278
|
}
|
|
1260
1279
|
const out = await res.json().catch(() => null)
|
|
1261
|
-
if (!res.ok) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1384
|
+
return toolError(`Could not decide: ${e.message}`)
|
|
1366
1385
|
}
|
|
1367
1386
|
const out = await res.json().catch(() => null)
|
|
1368
|
-
if (!res.ok) return
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
package/skills/log/SKILL.md
CHANGED
|
@@ -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
|
|
56
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
93
|
+
**Reconciled:** N touched → M authored, K skipped (reason each)
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
## Safety rules
|