@theronap/agnoclast-mcp 0.9.150 → 0.9.152
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 +94 -17
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -78,6 +78,55 @@ export const sectionCurrencyStamp = (s, day) => {
|
|
|
78
78
|
// invisible on exactly the sections that were healthy.
|
|
79
79
|
export const renderSection = (s, day) => `### ${s.heading}${sectionCurrencyStamp(s, day)}\n${s.body}`
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* ADR-0062 §5.1. A materialize PREVIEW goes to its own route: a server too old to know it answers 404,
|
|
83
|
+
* where /api/intake/materialize would ignore a `mode` it does not know and publish.
|
|
84
|
+
*/
|
|
85
|
+
export function intakeMaterializePath(mode) {
|
|
86
|
+
return mode === 'preview' ? '/api/intake/preview' : '/api/intake/materialize'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** ADR-0062 §5.2. Which page governs a materialized record, and who chose it — or '' for an older server. */
|
|
90
|
+
export function renderGoverningLine(governing) {
|
|
91
|
+
if (!governing || typeof governing !== 'object') return ''
|
|
92
|
+
const name = governing.title || 'a page'
|
|
93
|
+
const who = governing.designatedBy === 'session' ? 'chosen by you'
|
|
94
|
+
: governing.designatedBy === 'human' ? 'chosen by a person'
|
|
95
|
+
: 'chosen by the ranking — pass governingPage to decide it yourself'
|
|
96
|
+
return `Governed by ${name} (${governing.tier}) — ${who}.`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The one line a preview leads with, so it cannot be mistaken for a publish. */
|
|
100
|
+
export function renderIntakePreviewLine(body) {
|
|
101
|
+
const p = (body && typeof body === 'object' && body.placement) || {}
|
|
102
|
+
const pages = Array.isArray(p.pages) ? p.pages.length : 0
|
|
103
|
+
const g = p.governing
|
|
104
|
+
const gov = g ? `${g.title || g.kind} (${g.tier})${p.governingJudged ? ', your choice' : ''}` : 'none'
|
|
105
|
+
return `PREVIEW — nothing was written. Would attach ${pages} page(s); governing: ${gov}; born ${p.tier ?? 'unknown'}.`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// TOP-OF-PAGE NOTES — ADR-0065's size note, and ADR-0064 §3's "Also visible to you" callout.
|
|
109
|
+
//
|
|
110
|
+
// ⚠ PRINTED VERBATIM, NEVER COMPOSED HERE. The server decides whether a note fires and what it says
|
|
111
|
+
// (`headerNotes` on /api/brain/page), so changing a threshold or a sentence is a deploy, not an npm release
|
|
112
|
+
// plus a desktop pin bump. An older server sends no field, and this prints nothing.
|
|
113
|
+
//
|
|
114
|
+
// ⚠ AT THE TOP, NOT IN THE FOOTER. Claude Code saves an over-cap tool result to a file and the agent reads
|
|
115
|
+
// it from offset 0, so the footer of a huge page is the part nobody reaches — and these notes exist for
|
|
116
|
+
// exactly those pages. Shared by read_page and project_status so the two cannot drift, which is how every
|
|
117
|
+
// earlier fix to this file's page rendering landed on one surface and not the other.
|
|
118
|
+
// v1 SCOPE (Theron, 2026-09-16): page splitting moves to v2 and the server refuses it with
|
|
119
|
+
// `splitting_unavailable`. split_page is not registered, so agents do not see a verb that can only refuse.
|
|
120
|
+
// The definition stays for v2: flip this together with the server's SPLITTING_AVAILABLE.
|
|
121
|
+
export const SPLIT_PAGE_TOOL_AVAILABLE = false
|
|
122
|
+
|
|
123
|
+
export const renderHeaderNotes = (notes) => {
|
|
124
|
+
const lines = (Array.isArray(notes) ? notes : [])
|
|
125
|
+
.filter((n) => typeof n === 'string' && n.trim() !== '')
|
|
126
|
+
.map((n) => n.replace(/\s*\n\s*/g, ' ').trim())
|
|
127
|
+
return lines.length ? `\n\n${lines.join('\n')}` : ''
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
// gate5_status's transport half, pulled out of the tool handler so it can be exercised directly rather
|
|
82
131
|
// than only string-matched (gate5_status_transport.drift.test.mjs). Takes the raw fetch Response from
|
|
83
132
|
// /api/gates/5/status; the registerTool callback's only remaining job is fetching it. Same three
|
|
@@ -860,7 +909,8 @@ function renderNudge(payload) {
|
|
|
860
909
|
{
|
|
861
910
|
title: 'Claim private intake items',
|
|
862
911
|
description:
|
|
863
|
-
'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only. Requires x-cortex-session-key (set automatically by this MCP server). Aged work never blocks a claim: the response reports cleanupDueCount and how far behind the oldest unit is, and servicing it is expected but always your call. Claiming is a commitment to process — hand back anything you will not finish with intake_release, or intake_defer if it is the owner\'s decision to make.'
|
|
912
|
+
'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only. Requires x-cortex-session-key (set automatically by this MCP server). Aged work never blocks a claim: the response reports cleanupDueCount and how far behind the oldest unit is, and servicing it is expected but always your call. Claiming is a commitment to process — hand back anything you will not finish with intake_release, or intake_defer if it is the owner\'s decision to make. ' +
|
|
913
|
+
'YOU ARE THE JUDGE of every unit you claim (ADR-0062): nothing is published until you materialize, and materializing is when it becomes visible. Each unit carries a `proposal` — the brain its signals point to and, for that brain, the pages it would attach to, the page that would govern it, and the `tier` it would be born with — computed without writing anything. Decide the brain, the pages (confirm, drop or add), which page should govern (the page whose reasons for restricting its audience fit the content, not a person page), and obligations. Pages only tighten a tier; if a record should be LOOSER than `tierReason.baseline`, that is the owner\'s call — intake_defer with the question.',
|
|
864
914
|
inputSchema: {
|
|
865
915
|
claimKind: z.enum(['relevance', 'cleanup']).optional().describe('default relevance'),
|
|
866
916
|
limit: z.number().optional().describe('max items (default 10). Ignored when intakeItemIds is given.'),
|
|
@@ -930,7 +980,9 @@ function renderNudge(payload) {
|
|
|
930
980
|
{
|
|
931
981
|
title: 'Materialize a private intake item',
|
|
932
982
|
description:
|
|
933
|
-
'Atomically publish a claimed intake item into one brain. Deterministic identifier homes in that brain are always attached; documentIds may add further pages.
|
|
983
|
+
'Atomically publish a claimed intake item into one brain — the disclosure moment (ADR-0062). Deterministic identifier homes in that brain are always attached; documentIds may add further pages. The record is born at the strictest of its source baseline, what the producer asked for, and every attached page; a source floor (iMessage, claude-code) pins it. ' +
|
|
984
|
+
'Name the governing page with governingPage (one of documentIds, never a person page) — otherwise the ranking picks it. ' +
|
|
985
|
+
'Pass mode "preview" first when your choice differs from the claim\'s proposal: it returns the pages, the governing page and the tier this exact call would produce, and writes nothing. Never writes private intake into search/history before this call.',
|
|
934
986
|
inputSchema: {
|
|
935
987
|
intakeItemId: z.string().describe('intake item uuid'),
|
|
936
988
|
orgId: z.string().describe('destination brain org uuid'),
|
|
@@ -941,10 +993,21 @@ function renderNudge(payload) {
|
|
|
941
993
|
recordType: z.string().optional(),
|
|
942
994
|
dedupeKey: z.string().optional(),
|
|
943
995
|
origin: z.enum(['deterministic', 'llm', 'user', 'session']).optional(),
|
|
996
|
+
governingPage: z.string().optional().describe(
|
|
997
|
+
'the page that should GOVERN this record (ADR-0062) — a page `ref:` or document id, and one of documentIds. ' +
|
|
998
|
+
'Choose the page whose reasons for restricting its audience fit the content; never a person page. It can only ' +
|
|
999
|
+
'tighten the record, and the ranking will not move it. Omit to leave the choice to the ranking.',
|
|
1000
|
+
),
|
|
1001
|
+
mode: z.enum(['preview', 'apply']).optional().describe(
|
|
1002
|
+
'"preview": report the pages, governing page and tier this call would produce, and write nothing. Default "apply".',
|
|
1003
|
+
),
|
|
944
1004
|
},
|
|
945
1005
|
},
|
|
946
1006
|
async (args) => {
|
|
947
|
-
|
|
1007
|
+
// A preview goes to its OWN route: a server too old to know it answers 404, where the materialize
|
|
1008
|
+
// route would ignore a `mode` it does not know and publish.
|
|
1009
|
+
const preview = args.mode === 'preview'
|
|
1010
|
+
const res = await fetchCortex(`${BASE}${intakeMaterializePath(args.mode)}`, {
|
|
948
1011
|
method: 'POST',
|
|
949
1012
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
950
1013
|
body: JSON.stringify({
|
|
@@ -959,13 +1022,20 @@ function renderNudge(payload) {
|
|
|
959
1022
|
dedupe_key: args.dedupeKey,
|
|
960
1023
|
},
|
|
961
1024
|
attachmentMeta: (args.documentIds ?? []).map(() => ({ origin: args.origin ?? 'llm' })),
|
|
1025
|
+
...(args.governingPage ? { governingDocumentId: args.governingPage } : {}),
|
|
962
1026
|
}),
|
|
963
1027
|
})
|
|
964
1028
|
if (!res.ok) {
|
|
965
1029
|
const body = await res.text()
|
|
1030
|
+
if (preview && res.status === 404) {
|
|
1031
|
+
return toolError('This server cannot preview a materialization yet — nothing was written. Materialize without mode, or wait for the server to update.')
|
|
1032
|
+
}
|
|
966
1033
|
return toolError(body)
|
|
967
1034
|
}
|
|
968
1035
|
const body = await res.json()
|
|
1036
|
+
if (preview) {
|
|
1037
|
+
return { content: [{ type: 'text', text: `${renderIntakePreviewLine(body)}\n\n${JSON.stringify(body, null, 2)}` }] }
|
|
1038
|
+
}
|
|
969
1039
|
// WHERE THE SESSION'S OWN WORK LANDED, in words rather than buried in the JSON dump.
|
|
970
1040
|
//
|
|
971
1041
|
// Three outcomes that look identical in raw JSON and mean completely different things:
|
|
@@ -982,6 +1052,8 @@ function renderNudge(payload) {
|
|
|
982
1052
|
// the ordinary case (observed 2026-09-03 on the first handoff record materialised). Silence is
|
|
983
1053
|
// correct here, and it also covers a server too old to send the field at all.
|
|
984
1054
|
const lines = []
|
|
1055
|
+
const governingLine = renderGoverningLine(body?.governing)
|
|
1056
|
+
if (governingLine) lines.push(governingLine)
|
|
985
1057
|
const sa = body?.sessionAttachments
|
|
986
1058
|
if (sa === null) {
|
|
987
1059
|
lines.push('⚠ Session-page attachment FAILED for this record — it was written, but not filed under the pages this session worked on. Re-run the reconciler.')
|
|
@@ -1348,7 +1420,7 @@ function renderNudge(payload) {
|
|
|
1348
1420
|
return [head, secs].filter(Boolean).join('\n')
|
|
1349
1421
|
})
|
|
1350
1422
|
const brainTag = matches.length > 1 ? ` · brain: ${m.brain}` : ''
|
|
1351
|
-
return `# ${m.title ?? key} (authored page${brainTag})\n\n${blocks.join('\n\n---\n\n')}`
|
|
1423
|
+
return `# ${m.title ?? key} (authored page${brainTag})${renderHeaderNotes(m.headerNotes)}\n\n${blocks.join('\n\n---\n\n')}`
|
|
1352
1424
|
}
|
|
1353
1425
|
return { content: [{ type: 'text', text: matches.map(renderMatch).join('\n\n═══\n\n') }] }
|
|
1354
1426
|
}
|
|
@@ -1579,7 +1651,7 @@ function renderNudge(payload) {
|
|
|
1579
1651
|
// Always emitted, not only in the multi-brain case: the ref is what makes the brain question
|
|
1580
1652
|
// moot, so withholding it until brains collide is exactly backwards.
|
|
1581
1653
|
const refLine = m.ref ? `\nref: ${m.ref}` : ''
|
|
1582
|
-
return `# ${m.title ?? name} (full authored page${brainTag})${refLine}\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}`
|
|
1654
|
+
return `# ${m.title ?? name} (full authored page${brainTag})${refLine}${renderHeaderNotes(m.headerNotes)}\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}`
|
|
1583
1655
|
}
|
|
1584
1656
|
// ── THE PRE-CLAIM NUDGE ───────────────────────────────────────────────────────────────────
|
|
1585
1657
|
//
|
|
@@ -1863,19 +1935,19 @@ function renderNudge(payload) {
|
|
|
1863
1935
|
},
|
|
1864
1936
|
)
|
|
1865
1937
|
|
|
1866
|
-
server.registerTool(
|
|
1938
|
+
if (SPLIT_PAGE_TOOL_AVAILABLE) server.registerTool(
|
|
1867
1939
|
'split_page',
|
|
1868
1940
|
{
|
|
1869
1941
|
title: 'Move sections onto a new child page',
|
|
1870
|
-
description: 'SPLIT a page: move whole sections onto a NEW page, leaving the original in place. Use it when a page has
|
|
1942
|
+
description: 'SPLIT a page: move whole sections onto a NEW page, leaving the original in place. Use it when a page has outgrown one read — an agent that cannot read the whole authority answers from part of it. It targets the tail (5.2% of pages hold a third of all authored text), not routine hygiene, and it does not make pages go wrong less often: it makes each fix cheaper (~42,000 tokens to read a 165k-char page, ~5,000 for a 20k child). It NEVER retires the original, moves governance, rewrites inbound [[links]] (the reader following one knows which half it meant), or links the child FROM the source. EVERY REFUSAL IS DECIDED BEFORE ANYTHING IS WRITTEN (ADR-0064 §7): you need EDIT access to the version you read (edit_forbidden); pages with several tier versions are paused (multi_version_page); `headings` must match the STORED heading exactly, INCLUDING any `· as of <date>` suffix, and a heading two sections share is refused (ambiguous_heading); moving every section is refused as a rename; `new_title` must not name any existing or archived page (title_taken); a STRICTER child is refused, and a LOOSER one needs the source page\'s owner (widening_requires_owner); oversize sections and identity identifiers are refused (too_large, identity_claim). The child keeps the source page\'s OWNER, tier and grants, so the same people can read it, and gets an `In short` section. If a split stops after the child exists (source_trim_failed), finish with `merge_sections` (`from` only) for each heading in `remaining` — never re-run split_page. Reversible via `page_history` + `rollback_page`.',
|
|
1871
1943
|
inputSchema: {
|
|
1872
1944
|
name: z.string().describe('the exact page name to split, as read_page shows it'),
|
|
1873
|
-
headings: z.array(z.string()).min(1).describe('the headings of the sections to MOVE, matched EXACTLY against the stored heading — include any `· as of <date>` suffix. Everything not listed stays on the original.'),
|
|
1874
|
-
new_title: z.string().describe('the title of the new child page. It must not
|
|
1945
|
+
headings: z.array(z.string()).min(1).describe('the headings of the sections to MOVE, matched EXACTLY against the stored heading — include any `· as of <date>` suffix. A heading two sections share is refused: rename one with rename_section first. Everything not listed stays on the original.'),
|
|
1946
|
+
new_title: z.string().describe('the title of the new child page. It must not name ANY existing page in this brain, or an archived project: a split never adopts or revives one.'),
|
|
1875
1947
|
base_version: z.string().describe('the `version` read_page prints for the SOURCE page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved.'),
|
|
1876
1948
|
reason: z.string().describe('WHY you are splitting, in one short phrase — recorded in page_history. Say what outgrew the page.'),
|
|
1877
|
-
owner: z.string().optional().describe('
|
|
1878
|
-
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('omit to COPY the source page tier, which is almost always right. A LOOSER tier
|
|
1949
|
+
owner: z.string().optional().describe('omit to keep the SOURCE page\'s owner, so the child has exactly the same audience — almost always right. Naming a different user is an ownership change: it needs the page owner, or someone above them who can read the page, and must be an active member of this brain.'),
|
|
1950
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('omit to COPY the source page tier, which is almost always right. A LOOSER tier widens who can read the moved text, so only the source page\'s owner may choose it; a STRICTER one is refused, since a split cannot tighten access.'),
|
|
1879
1951
|
},
|
|
1880
1952
|
},
|
|
1881
1953
|
async ({ name, headings, new_title, base_version, reason, owner, tier }) => {
|
|
@@ -1895,20 +1967,25 @@ function renderNudge(payload) {
|
|
|
1895
1967
|
const out = await res.json().catch(() => null)
|
|
1896
1968
|
if (!res.ok) {
|
|
1897
1969
|
// `child_created` + `remaining` is the one failure worth reading carefully: it means the split
|
|
1898
|
-
// is HALF DONE and nothing was lost. Surface it first so the caller
|
|
1899
|
-
// from
|
|
1970
|
+
// is HALF DONE and nothing was lost. Surface it first so the caller FINISHES with merge_sections
|
|
1971
|
+
// (`from` only) rather than re-running split_page, which refuses the now-taken title (ADR-0064 §7.4).
|
|
1900
1972
|
const extra = [
|
|
1901
1973
|
out?.child_created ? `the child "${out.child_created}" EXISTS and holds every moved section — nothing was lost` : '',
|
|
1902
|
-
Array.isArray(out?.remaining) ? `still on BOTH pages, re-run
|
|
1974
|
+
Array.isArray(out?.remaining) ? `still on BOTH pages — finish with merge_sections (\`from\` only) for each, do NOT re-run split_page: ${out.remaining.join(' · ')}` : '',
|
|
1903
1975
|
Array.isArray(out?.detail) ? `detail: ${out.detail.join(' · ')}` : '',
|
|
1904
1976
|
out?.message ?? '',
|
|
1905
1977
|
].filter(Boolean).join('\n')
|
|
1906
1978
|
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
1907
1979
|
return toolError(`Could not split "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
1908
1980
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1981
|
+
// ADR-0064 §7.5: an omitted owner now keeps the SOURCE page's owner, and the server's `note` says so.
|
|
1982
|
+
// Only a NAMED owner changes the audience, so that is the one worth a warning. `owner_defaulted` is
|
|
1983
|
+
// still read for a server older than #1041, where it meant the child fell to the caller.
|
|
1984
|
+
const ownerNote = out.owner_changed
|
|
1985
|
+
? '\n⚠ The child is owned by the member you named, which changes who can read it if it is scoped or confidential.'
|
|
1986
|
+
: out.owner_defaulted
|
|
1987
|
+
? '\n⚠ The child is owned by you because no `owner` was given. Set one deliberately if it should belong to someone else — transfer is unreachable once an owner is deactivated.'
|
|
1988
|
+
: ''
|
|
1912
1989
|
const grants = out.grants_copied ? ` ${out.grants_copied} access grant(s) copied.` : ''
|
|
1913
1990
|
return { content: [{ type: 'text', text: `Split "${name}" (${out.brain} · ${out.tier} tier) → new page "${out.child}". Moved: ${out.moved.join(' · ')}.${grants} Child version: ${out.version}\n${out.note}\nNEXT: ${out.next}${ownerNote}\nReversible: \`page_history "${name}"\` then \`rollback_page\` restores the source; the child can be retired with set_page_validity.` }] }
|
|
1914
1991
|
},
|
package/package.json
CHANGED