@theronap/cortex-mcp 0.9.109 → 0.9.111

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 +64 -12
  2. 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: 'Gate 2 edit-accountability monitor',
508
- description: 'Read the aggregate-only Gate 2 status for this brain whether every edit records WHICH SESSION made it. It counts only the REPAIRED write paths (absorb, retier, replace) since the 2026-08-17 fix, because the author path was never broken and would certify a repair it never exercised. Operator and migration writes are excluded: they legitimately have no session, and imputing one would violate the 0093 don\'t-impute rule. It never exposes page titles, refs, reasons or session keys. "regressed" means a repaired path lost its session attribution again and the gate must NOT be closed; "machine_evidence_ready" means enough ordinary-work evidence has accumulated to request a human closure decision.',
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
- const text = status.status === 'regressed'
526
- ? `\u26a0 Gate 2 has REGRESSED: ${status.unattributedMcpRevisions} MCP-authored revisions since ${status.since} carry NO session (${status.unattributedRepairedRevisions} of them on the repaired absorb/retier/replace paths). Attribution is being dropped again — do not close this gate; find the writer.`
527
- : status.machineEvidenceReady
528
- ? `Gate 2 machine evidence is ready: ${status.repairedPathSessions} distinct sessions exercised the repaired write paths across ${status.repairedPathRevisions} revisions since ${status.since}, and NONE lost its session. A human should confirm these were ordinary work before closing the gate.`
529
- : `Gate 2 is still collecting evidence: ${status.repairedPathSessions}/${status.requiredRepairedPathSessions} distinct sessions have exercised the repaired write paths (absorb/retier/replace) since ${status.since}, across ${status.repairedPathRevisions} revisions, 0 unattributed. No gate decision has been made.`
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
  )
@@ -1136,7 +1181,10 @@ export async function runServer(version) {
1136
1181
  // drift again.
1137
1182
  const secs = (t.sections ?? []).map((s) => renderSection(s, day)).join('\n\n')
1138
1183
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}]`
1139
- return [head, t.summary, secs].filter(Boolean).join('\n')
1184
+ // ADR-0030 step 3b (2026-08-24): the summary header is NO LONGER RENDERED. It duplicated the
1185
+ // `In short` section byte-for-byte on 6 of 6 pages sampled, so every read paid for the same
1186
+ // text twice. The column stays vestigial; this just stops serving it.
1187
+ return [head, secs].filter(Boolean).join('\n')
1140
1188
  })
1141
1189
  const brainTag = matches.length > 1 ? ` · brain: ${m.brain}` : ''
1142
1190
  return `# ${m.title ?? key} (authored page${brainTag})\n\n${blocks.join('\n\n---\n\n')}`
@@ -1291,7 +1339,11 @@ export async function runServer(version) {
1291
1339
  ? `\nversion: ${t.version}`
1292
1340
  : `\nversion: none (this variant predates content-hash tracking — base_version writes will always fail here; ask an admin about the ADR-0018 backfill)`
1293
1341
  const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}${versionLine}]`
1294
- return [head, t.summary, secs].filter(Boolean).join('\n')
1342
+ // ADR-0030 step 3b (2026-08-24): the summary header is NO LONGER RENDERED here either.
1343
+ // ⚠ There were TWO of these — project_status and read_page — and the first patch fixed only
1344
+ // project_status, leaving the primary surface untouched. Same granularity miss this repo
1345
+ // keeps recording: a fix applied to the occurrence you happened to find first.
1346
+ return [head, secs].filter(Boolean).join('\n')
1295
1347
  })
1296
1348
  // REPAIR TOOL (2026-07-31). This footer used to say "re-author just those sections with
1297
1349
  // `author`" — which `author` cannot do: computeDroppedSections rejects a partial-section
@@ -1300,7 +1352,7 @@ export async function runServer(version) {
1300
1352
  // retype silently deleted a sentence, a [[link]] (a graph edge), a command list and the
1301
1353
  // word "today" from sections it was never meant to touch. This footer renders on EVERY
1302
1354
  // page read in the system, so it was the single widest surface pointing the wrong way.
1303
- 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. If what went false is the SUMMARY at the top rather than a section, use \`set_summary\` — \`edit_page\` cannot reach it, and worse, it carries the old summary FORWARD into every revision it writes, so fixing a section silently re-certifies a summary that already contradicts 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.`
1355
+ 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.`
1304
1356
  // ADDRESSING (2026-08-04). The server accepts `ref` on every page/node route and its ambiguity
1305
1357
  // 409s hand refs back — but read_page never PRINTED one, so the only way to obtain a ref was to
1306
1358
  // trigger the error first. That made ID addressing reachable in principle and unusable in
@@ -1609,7 +1661,7 @@ export async function runServer(version) {
1609
1661
  'set_summary',
1610
1662
  {
1611
1663
  title: 'Rewrite a wiki page summary without touching its sections',
1612
- description: 'Replace a page\'s SUMMARY the one-sentence line at the top — in place, leaving every section untouched. Use this the moment you notice a summary that no longer matches the page: `edit_page` structurally CANNOT reach it (it edits section bodies; the summary is not one), and `author` reaches it only by retyping every section on the way through, which is how a 2026-07-30 write silently deleted a sentence and a [[link]] from a section nobody meant to touch. Correcting a body with `edit_page` actively carries the OLD summary forward into the new revision, so a stale summary does not decay quietly — each unrelated fix re-certifies it. Fixing it matters more than it looks: page retrieval FTS-matches on title + summary ALONE — the tsvector is built over title and summary, and section bodies are not in that index — so a stale summary decides whether the page is found by that path at all, and it is the first line every reader sees before a word of the body. Requires base_version (the `version` read_page prints) that is also how it finds the right brain, so it can never write to the wrong one. Recorded in page_history as a correction, and reversible with rollback_page.',
1664
+ description: 'DEPRECATED retired by ADR-0030 (decided 2026-08-24: no part of a page summarises the page). Do not call it. It still works so seats on older clients do not break, and it will be removed in a later release. This description previously claimed page retrieval matches on title + summary ALONE and that a stale summary decides whether a page is found that was true only of the digest branch deleted by ADR-0030 step 2, and on 2026-08-24 a session read it, believed it, and asserted the opposite of the truth to its operator. `grep` searches SECTION BODIES and always has. Put the content in a section.',
1613
1665
  inputSchema: {
1614
1666
  name: z.string().describe('the exact page name, as read_page shows it'),
1615
1667
  summary: z.string().describe('the new summary: one sentence saying what this is and where it stands. May contain [[links]]. Max 2,000 chars — detail belongs in a section.'),
@@ -3102,7 +3154,7 @@ export async function runServer(version) {
3102
3154
  inputSchema: {
3103
3155
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
3104
3156
  name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Agnoclast" or "Theron Peterson"'),
3105
- summary: z.string().describe('one-sentence summary of what this is and its current state (may contain [[links]])'),
3157
+ summary: z.string().optional().describe('DEPRECATED and no longer required (ADR-0030, decided 2026-08-24: no part of a page summarises the page). Omit it. Accepted only so older callers keep working; the column is vestigial and nothing reads it. Put what the page is in its FIRST SECTION grep indexes section bodies, and never indexed this.'),
3106
3158
  sections: z.array(z.object({
3107
3159
  heading: z.string().describe('e.g. Overview, Current state, Decisions, Open threads, People'),
3108
3160
  body: z.string().describe('dense markdown WITH inline [[links]] where the prose references another node'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.109",
3
+ "version": "0.9.111",
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": {