@theronap/cortex-mcp 0.9.108 → 0.9.110
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 +117 -7
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -501,11 +501,45 @@ export async function runServer(version) {
|
|
|
501
501
|
},
|
|
502
502
|
)
|
|
503
503
|
|
|
504
|
+
// ── WHY THIS TOOL IS STILL CALLED `gate2_status` WHEN IT IS NO LONGER A GATE STATUS ────────────
|
|
505
|
+
//
|
|
506
|
+
// It measures a standing attribution watch now, not a gate, and the honest name would be something
|
|
507
|
+
// like `unattributed_revision_alarm`. Renaming it is a COMPATIBILITY EVENT and the trade is not
|
|
508
|
+
// close:
|
|
509
|
+
//
|
|
510
|
+
// • `gate2_status` is live on BOTH dist-tags: 0.9.107 @stable and 0.9.109 @latest, checked
|
|
511
|
+
// 2026-08-25. Every desktop-wired seat follows @stable, which deliberately LAGS for soak — so a
|
|
512
|
+
// rename does not reach those seats when this merges, it reaches them whenever someone
|
|
513
|
+
// publishes AND THEN promotes, two manual steps apart. Until then a renamed tool is simply
|
|
514
|
+
// ABSENT from their tool list, and a caller that asks for it gets "unknown tool".
|
|
515
|
+
// • That trades a misleading sentence for a hard failure. The misleading sentence is fixable from
|
|
516
|
+
// here — the strings below are recomputed on every call — while the missing tool is not fixable
|
|
517
|
+
// from here at all.
|
|
518
|
+
// • Registering BOTH names is worse, not a compromise: two tools that do the same thing sit in
|
|
519
|
+
// every seat's tool list forever, and every future reader has to work out which is which. This
|
|
520
|
+
// server has no aliasing primitive, so "aliasing" means literally two `registerTool` calls.
|
|
521
|
+
// • And the rename would not fix the actual defect. What seeded the two-day misdiagnosis on
|
|
522
|
+
// 2026-08-22 was this tool's DESCRIPTION — measurably wrong in both its op set ("absorb, retier,
|
|
523
|
+
// replace" when the counted ops were delete/retier/system) and its date ("since the 2026-08-17
|
|
524
|
+
// fix" when `since` reports the 2026-08-20 anchor). Nobody was misled by the six characters
|
|
525
|
+
// `gate2_`. A wrong description under a new name is still a wrong description.
|
|
526
|
+
//
|
|
527
|
+
// So: name pinned, prose replaced. This is the same call `gate3_status` made on 2026-08-24 (3f8df6a)
|
|
528
|
+
// for the same reason, and the route path `/api/gates/2/status` is pinned for the same reason on
|
|
529
|
+
// the other side of the wire — see web/app/api/gates/2/status/route.ts.
|
|
530
|
+
//
|
|
531
|
+
// ⚠ NO THRESHOLD LANGUAGE, NO CLOSURE LANGUAGE. Gate 2 CLOSED 2026-08-25 on ENUMERATION
|
|
532
|
+
// (`page_history_managed_writers_test.ts` discovers all 18 brain_documents DML sites and fails the
|
|
533
|
+
// build on a nineteenth), NOT on the session counter this tool used to report. That counter is
|
|
534
|
+
// retired. It was not merely stuck: measured against prod 2026-08-25 it stood at 2 of its 3
|
|
535
|
+
// required sessions and was CLIMBING, because PR #713's newly-managed routes write counted ops with
|
|
536
|
+
// real sessions — so one more ordinary edit would have had this tool inviting a human to close a
|
|
537
|
+
// gate that had been closed for days. Exactly gate 3's failure, three days later.
|
|
504
538
|
server.registerTool(
|
|
505
539
|
'gate2_status',
|
|
506
540
|
{
|
|
507
|
-
title: '
|
|
508
|
-
description: 'Read the aggregate-only
|
|
541
|
+
title: 'Unattributed-revision alarm (NOT a gate status — gate 2 is CLOSED)',
|
|
542
|
+
description: 'Read the aggregate-only unattributed-revision alarm for this brain: how many MCP-authored page revisions in the window carry NO session key, i.e. record an edit without recording WHICH SESSION made it. It spans EVERY mcp op, `author` included — scoping it to the "repaired" paths would let it go all-clear while attribution died on the path doing ~98% of the writing. Operator and migration writes are excluded: they legitimately have no session, and imputing one would violate the 0093 don\'t-impute rule. Aggregate-only — it never exposes page titles, refs, reasons or session keys. ⚠ IT IS NOT A GATE STATUS AND DECIDES NOTHING. Gate 2 CLOSED for v1 on 2026-08-25, on ENUMERATION of all 18 brain_documents DML sites, and the session counter this tool used to report was retired the same day as a named residual — it had begun measuring a threshold nothing was waiting on. What this alarm IS: one of the two things the closure names as able to REOPEN gate 2. So "regressed" means attribution is being dropped again — find the writer. Gate state lives on [[Agnoclast v1 — the currency gate]]; read that page, not this tool, to learn where a gate stands.',
|
|
509
543
|
inputSchema: {
|
|
510
544
|
days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
|
|
511
545
|
brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
|
|
@@ -522,11 +556,22 @@ export async function runServer(version) {
|
|
|
522
556
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
523
557
|
}
|
|
524
558
|
const status = await res.json()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
559
|
+
// A tool ahead of its deployment must SAY SO rather than render `undefined`. `mcpRevisions` is
|
|
560
|
+
// new with the alarm; an older deployment of /api/gates/2/status does not send it. #693 is the
|
|
561
|
+
// standing lesson (0.9.104 shipped a tool against an unmerged route and was withdrawn) — the
|
|
562
|
+
// package and the web deploy move independently, so this skew is a real state, not a hypothetical.
|
|
563
|
+
const text = status.mcpRevisions === undefined
|
|
564
|
+
? `This tool is newer than the deployment it is talking to: /api/gates/2/status still returns the retired gate-2 closure counter, not the unattributed-revision alarm. Its verdict field says "${status.status}". Do NOT read that as a gate status — gate 2 closed 2026-08-25 on enumeration. Re-check once the console has redeployed.`
|
|
565
|
+
: status.status === 'regressed'
|
|
566
|
+
// The alarm. This is the reopen detector, and its wording is the one thing here that must
|
|
567
|
+
// stay blunt: it caught the 2026-08-19 regression in two days.
|
|
568
|
+
? `⚠ ATTRIBUTION IS BEING DROPPED: ${status.unattributedMcpRevisions} of ${status.mcpRevisions} MCP-authored revisions since ${status.since} carry NO session (${status.unattributedRepairedRevisions} of them on ops delete/retier/system; any remainder is elsewhere, most likely \`author\`). Find the writer. An unattributed MCP revision is one of the two conditions that REOPEN gate 2, which closed 2026-08-25.`
|
|
569
|
+
: status.mcpRevisions === 0
|
|
570
|
+
// "Nothing was written" and "plenty was written and none of it lost a session" are
|
|
571
|
+
// opposite conditions, and a bare "0 unattributed" renders them identically. Gate 4's
|
|
572
|
+
// denominator rule, applied here because silence is not a clean bill of health.
|
|
573
|
+
? `No MCP-authored revisions at all in this brain in the last ${status.days} days (window floor ${status.since}), so there is NOTHING the alarm could have checked. This is an empty measurement, not an all-clear.`
|
|
574
|
+
: `Attribution is holding: 0 of ${status.mcpRevisions} MCP-authored revisions since ${status.since} lost its session, across every mcp op including \`author\`. This is a standing regression watch and it decides NOTHING — it is not a gate criterion. Gate 2 closed 2026-08-25 on enumeration of all 18 brain_documents DML sites. For where any gate actually stands, read [[Agnoclast v1 — the currency gate]].`
|
|
530
575
|
return { content: [{ type: 'text', text }] }
|
|
531
576
|
},
|
|
532
577
|
)
|
|
@@ -1540,6 +1585,71 @@ export async function runServer(version) {
|
|
|
1540
1585
|
},
|
|
1541
1586
|
)
|
|
1542
1587
|
|
|
1588
|
+
server.registerTool(
|
|
1589
|
+
'merge_sections',
|
|
1590
|
+
{
|
|
1591
|
+
title: 'Condense two sections into one, or drop a spent section',
|
|
1592
|
+
description: 'CONDENSE a page: fold one section into another, or remove one outright. This is the only route that can make a page SMALLER — `author` refuses any write that omits a section (409 would_drop_sections), `edit_page` reaches inside one section, and `rename_section` touches only a heading, so without this every correction is additive and pages grow without bound. Use it when two sections overlap and one shorter section would say the same thing, or when a section is spent — closed work, a retraction whose lesson now lives elsewhere, a superseded status block. Pass `into` + `new_body` to condense (the surviving section gets `new_body`, `from` is removed); omit BOTH to drop `from` outright. GUARDS, all of which refuse rather than guess: `base_version` is the same page-level CAS `edit_page` uses; a merge never crosses tiers; the last remaining section cannot be dropped; and if the removal would orphan a `[[link]]` — leaving it nowhere on the page — it 409s with the list, because a [[link]] is a graph EDGE and losing one silently is the exact 2026-07-30 failure the no-drop rule was written for. CURRENCY: the surviving section inherits the OLDER of the two as-of stamps, never now() — relocating a claim does not re-verify it. Fully reversible: the prior revision keeps the removed section in full, so `rollback_page` restores it.',
|
|
1593
|
+
inputSchema: {
|
|
1594
|
+
name: z.string().describe('the exact page name, as read_page shows it'),
|
|
1595
|
+
from: z.string().describe('the heading of the section to REMOVE (match is case- and whitespace-insensitive)'),
|
|
1596
|
+
into: z.string().optional().describe('the heading of the section that SURVIVES and absorbs the content. Omit to drop `from` outright without folding it anywhere.'),
|
|
1597
|
+
new_body: z.string().optional().describe('the condensed body the surviving section should read AFTER the merge — you write it, this route does not summarize. Required with `into`; must be absent without it.'),
|
|
1598
|
+
base_version: z.string().describe('the `version` read_page prints for this page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved. A per-SECTION hash is not valid here.'),
|
|
1599
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only when the page exists at MORE THAN ONE tier — which one to condense in. A merge never moves content between tiers.'),
|
|
1600
|
+
from_ordinal: z.number().optional().describe('only when `from`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
|
|
1601
|
+
into_ordinal: z.number().optional().describe('only when `into`\'s heading appears more than once on the page — which occurrence (the error lists the ordinals).'),
|
|
1602
|
+
reason: z.string().optional().describe('WHY you are condensing, in one short phrase — recorded in page_history. Say what became redundant, not what you did.'),
|
|
1603
|
+
drop_links_ack: z.boolean().optional().describe('ONLY after a 409 drops_links refusal, and only if the answer is genuinely yes: those [[links]] are meant to stop being edges of this page. Otherwise carry them into `new_body` instead.'),
|
|
1604
|
+
identity_claim_ack: z.boolean().optional().describe('ONLY after a 409 identity_claim refusal, and only if the answer is genuinely yes: this text is meant to publish an email address or phone number as page content at this tier.'),
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
async ({ name, from, into, new_body, base_version, tier, from_ordinal, into_ordinal, reason, drop_links_ack, identity_claim_ack }) => {
|
|
1608
|
+
let res
|
|
1609
|
+
try {
|
|
1610
|
+
res = await fetchCortex(`${BASE}/api/brain/merge-sections`, {
|
|
1611
|
+
method: 'POST',
|
|
1612
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1613
|
+
body: JSON.stringify({
|
|
1614
|
+
name, from, base_version,
|
|
1615
|
+
...(into ? { into } : {}), ...(new_body !== undefined ? { new_body } : {}),
|
|
1616
|
+
...(tier ? { tier } : {}),
|
|
1617
|
+
...(from_ordinal !== undefined ? { from_ordinal } : {}),
|
|
1618
|
+
...(into_ordinal !== undefined ? { into_ordinal } : {}),
|
|
1619
|
+
...(reason ? { reason } : {}),
|
|
1620
|
+
...(drop_links_ack ? { drop_links_ack: true } : {}),
|
|
1621
|
+
...(identity_claim_ack ? { identity_claim_ack: true } : {}),
|
|
1622
|
+
}),
|
|
1623
|
+
})
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
return toolError(`Could not condense the section: ${e.message}`)
|
|
1626
|
+
}
|
|
1627
|
+
const out = await res.json().catch(() => null)
|
|
1628
|
+
if (!res.ok) {
|
|
1629
|
+
// Surface every disambiguator the server named, so a 409 is directly actionable rather than
|
|
1630
|
+
// something to retry blindly. `dropped` is the one that matters most: it names the graph edges
|
|
1631
|
+
// that would be lost, which is the decision the caller actually has to make.
|
|
1632
|
+
const extra = [
|
|
1633
|
+
Array.isArray(out?.dropped) ? `would orphan these [[links]]: ${out.dropped.join(', ')}` : '',
|
|
1634
|
+
Array.isArray(out?.available) ? `existing headings: ${out.available.join(' · ')}` : '',
|
|
1635
|
+
Array.isArray(out?.identityClaims) ? `identity identifiers this would publish: ${out.identityClaims.join(', ')}` : '',
|
|
1636
|
+
Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
|
|
1637
|
+
Array.isArray(out?.ordinals) ? `ordinals: ${out.ordinals.join(', ')}` : '',
|
|
1638
|
+
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
1639
|
+
].filter(Boolean).join('\n')
|
|
1640
|
+
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
1641
|
+
return toolError(`Could not condense "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
1642
|
+
}
|
|
1643
|
+
const what = out.kept
|
|
1644
|
+
? `Condensed "${out.removed}" into "${out.kept}"`
|
|
1645
|
+
: `Removed section "${out.removed}"`
|
|
1646
|
+
const undated = out.undated ? '\n⚠ The surviving section carries no explicit calendar date — add one so a reader can see when it became true.' : ''
|
|
1647
|
+
const red = Array.isArray(out.redLinks) && out.redLinks.length
|
|
1648
|
+
? `\nRed links now on this page: ${out.redLinks.join(', ')}` : ''
|
|
1649
|
+
return { content: [{ type: 'text', text: `${what} on "${name}" (${out.brain} · ${out.tier} tier). Reclaimed ${out.chars_reclaimed} chars. New version: ${out.version}${undated}${red}\nReversible: \`page_history "${name}"\` then \`rollback_page\` restores the removed section in full.` }] }
|
|
1650
|
+
},
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1543
1653
|
server.registerTool(
|
|
1544
1654
|
'set_summary',
|
|
1545
1655
|
{
|
package/package.json
CHANGED