@theronap/cortex-mcp 0.9.107 → 0.9.109

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.
Files changed (2) hide show
  1. package/lib/server.mjs +84 -5
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -462,8 +462,8 @@ export async function runServer(version) {
462
462
  server.registerTool(
463
463
  'gate3_status',
464
464
  {
465
- title: 'Gate 3 currency monitor',
466
- description: 'Read the aggregate-only Gate 3 currency-monitor status for this brain. It counts explicit maintenance-candidate reviews and their same-session durable corrections in the rolling window; it never exposes project names, evidence content, or session keys. "machine_evidence_ready" means enough ordinary-work evidence has accumulated to request a human closure decision, not that the monitor closes the gate itself.',
465
+ title: 'Maintenance-review counter (informational — NOT a gate 3 criterion)',
466
+ description: 'Counts explicit maintenance-candidate reviews and their same-session durable corrections in a rolling window, aggregate-only; it never exposes project names, evidence content, or session keys. IT DOES NOT DECIDE ANYTHING, AND IT NO LONGER BEARS ON GATE 3. Gate 3 CLOSED for v1 on 2026-08-22 with five residuals named inside the closure, and this counter was dropped from its closure criteria the same day because it measures gate 4\'s machinery rather than currency. It is also the wrong SHAPE of signal: it counts agents catching the wiki being wrong and fixing it, so a rising number means failure is being discovered more often, not that currency is improving. Read it as "how often did anyone review maintenance candidates", nothing more. Gate state lives on [[Agnoclast v1 — the currency gate]]; read that page, not this tool, to learn where a gate stands.',
467
467
  inputSchema: {
468
468
  days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
469
469
  brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
@@ -480,9 +480,23 @@ export async function runServer(version) {
480
480
  throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
481
481
  }
482
482
  const status = await res.json()
483
- const text = status.machineEvidenceReady
484
- ? `Gate 3 machine evidence is ready: ${status.maintenanceReviewSessions} independent maintenance-review sessions and ${status.correctionSessions} correction sessions in ${status.days} days. A human should confirm these were ordinary work before closing the gate.`
485
- : `Gate 3 is still collecting evidence: ${status.maintenanceReviewSessions}/${status.requiredReviewSessions} independent maintenance-review sessions and ${status.correctionSessions}/${status.requiredCorrectionSessions} correction sessions in ${status.days} days. No gate decision has been made.`
483
+ // NO THRESHOLD LANGUAGE, DELIBERATELY. Until 2026-08-24 this rendered
484
+ // "machine evidence is ready before closing the gate" a sentence written when the counter
485
+ // WAS gate 3's criterion. Gate 3 closed 2026-08-22 and the counter was dropped from its
486
+ // criteria the same day, and nothing told the tool. Called on 2026-08-24 it still invited a
487
+ // reader to close a gate that had been closed for two days, on grounds that no longer applied.
488
+ // The numbers were never wrong; the sentence around them was, and it was recomputed fresh on
489
+ // every call while its framing stayed frozen — which is what made it read as current.
490
+ // So: report the count, name what it is not, and point at where gate state actually lives.
491
+ const text =
492
+ `Maintenance reviews (informational — NOT a gate criterion): ` +
493
+ `${status.maintenanceReviewSessions} session${status.maintenanceReviewSessions === 1 ? '' : 's'} reviewed maintenance candidates ` +
494
+ `and ${status.correctionSessions} made a same-session durable correction, in ${status.days} days.\n` +
495
+ `⚠ This does not bear on gate 3. Gate 3 CLOSED for v1 on 2026-08-22 (five residuals named inside the closure), ` +
496
+ `and this counter was dropped from its closure criteria the same day — it measures gate 4's machinery, not currency. ` +
497
+ `It is also inverted as a health signal: it counts agents catching the wiki being WRONG, so a rising number means ` +
498
+ `failure is being discovered more often, not that currency is improving.\n` +
499
+ `For where any gate actually stands, read [[Agnoclast v1 — the currency gate]].`
486
500
  return { content: [{ type: 'text', text }] }
487
501
  },
488
502
  )
@@ -1526,6 +1540,71 @@ export async function runServer(version) {
1526
1540
  },
1527
1541
  )
1528
1542
 
1543
+ server.registerTool(
1544
+ 'merge_sections',
1545
+ {
1546
+ title: 'Condense two sections into one, or drop a spent section',
1547
+ 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.',
1548
+ inputSchema: {
1549
+ name: z.string().describe('the exact page name, as read_page shows it'),
1550
+ from: z.string().describe('the heading of the section to REMOVE (match is case- and whitespace-insensitive)'),
1551
+ 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.'),
1552
+ 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.'),
1553
+ 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.'),
1554
+ 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.'),
1555
+ 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).'),
1556
+ 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).'),
1557
+ 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.'),
1558
+ 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.'),
1559
+ 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.'),
1560
+ },
1561
+ },
1562
+ async ({ name, from, into, new_body, base_version, tier, from_ordinal, into_ordinal, reason, drop_links_ack, identity_claim_ack }) => {
1563
+ let res
1564
+ try {
1565
+ res = await fetchCortex(`${BASE}/api/brain/merge-sections`, {
1566
+ method: 'POST',
1567
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1568
+ body: JSON.stringify({
1569
+ name, from, base_version,
1570
+ ...(into ? { into } : {}), ...(new_body !== undefined ? { new_body } : {}),
1571
+ ...(tier ? { tier } : {}),
1572
+ ...(from_ordinal !== undefined ? { from_ordinal } : {}),
1573
+ ...(into_ordinal !== undefined ? { into_ordinal } : {}),
1574
+ ...(reason ? { reason } : {}),
1575
+ ...(drop_links_ack ? { drop_links_ack: true } : {}),
1576
+ ...(identity_claim_ack ? { identity_claim_ack: true } : {}),
1577
+ }),
1578
+ })
1579
+ } catch (e) {
1580
+ return toolError(`Could not condense the section: ${e.message}`)
1581
+ }
1582
+ const out = await res.json().catch(() => null)
1583
+ if (!res.ok) {
1584
+ // Surface every disambiguator the server named, so a 409 is directly actionable rather than
1585
+ // something to retry blindly. `dropped` is the one that matters most: it names the graph edges
1586
+ // that would be lost, which is the decision the caller actually has to make.
1587
+ const extra = [
1588
+ Array.isArray(out?.dropped) ? `would orphan these [[links]]: ${out.dropped.join(', ')}` : '',
1589
+ Array.isArray(out?.available) ? `existing headings: ${out.available.join(' · ')}` : '',
1590
+ Array.isArray(out?.identityClaims) ? `identity identifiers this would publish: ${out.identityClaims.join(', ')}` : '',
1591
+ Array.isArray(out?.tiers) ? `tiers: ${out.tiers.join(', ')}` : '',
1592
+ Array.isArray(out?.ordinals) ? `ordinals: ${out.ordinals.join(', ')}` : '',
1593
+ out?.currentVersion ? `current version: ${out.currentVersion}` : '',
1594
+ ].filter(Boolean).join('\n')
1595
+ const hint = out?.hint ? `\n${out.hint}` : ''
1596
+ return toolError(`Could not condense "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
1597
+ }
1598
+ const what = out.kept
1599
+ ? `Condensed "${out.removed}" into "${out.kept}"`
1600
+ : `Removed section "${out.removed}"`
1601
+ const undated = out.undated ? '\n⚠ The surviving section carries no explicit calendar date — add one so a reader can see when it became true.' : ''
1602
+ const red = Array.isArray(out.redLinks) && out.redLinks.length
1603
+ ? `\nRed links now on this page: ${out.redLinks.join(', ')}` : ''
1604
+ 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.` }] }
1605
+ },
1606
+ )
1607
+
1529
1608
  server.registerTool(
1530
1609
  'set_summary',
1531
1610
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.107",
3
+ "version": "0.9.109",
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": {