@theronap/cortex-mcp 0.9.106 → 0.9.108

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.
@@ -13,6 +13,15 @@
13
13
  // agent holding the page open is the one with the context to make it — the same argument, and the
14
14
  // same evidence, as the per-node backlog nudge that ships beside it.
15
15
  //
16
+ // ⚠ CORRECTED 2026-08-22, HOURS AFTER SHIPPING. The first version said "every commit to that repo
17
+ // routes here" to every page carrying a [[repo:…]] body stamp. That is true of exactly ONE page.
18
+ // Measured: 88 pages carry the stamp; **one** (`cortex`) holds a `repo:` ROUTE. A body stamp stopped
19
+ // being an attach home when ADR-0026 moved homes into page_routing_identifiers — it still feeds the
20
+ // node TIMELINE (node_timeline.ts unions identifier-derived events with explicit attachments) but
21
+ // files nothing. So the notice was telling 87 pages to stem a flood that was not happening, and an
22
+ // agent acting on it would have added folder claims to pages that receive no commits at all. Three
23
+ // states, three different true things to say.
24
+ //
16
25
  // SELF-EXTINGUISHING BY DESIGN. Once a claim exists the line stops asking and starts reviewing, so a
17
26
  // page that has been narrowed does not keep nagging every reader forever. That is the difference
18
27
  // between a nudge and wallpaper, and this repo has already recorded the day-one backlog saturating
@@ -22,27 +31,36 @@
22
31
  * PURE. The footer line for a page's folder claims, or null when there is nothing worth saying.
23
32
  *
24
33
  * @param stamps repo stamps found in the page body, e.g. ['[[repo:theronap/cortex]]']
25
- * @param pathRoutes `file:` claims the node holds, from the server
34
+ * @param claims ALL routing claims the node holds (repo: and file:), from the server
26
35
  * @returns the line to append, or null
27
36
  */
28
- export function folderClaimLine(stamps, pathRoutes) {
37
+ export function folderClaimLine(stamps, claims) {
29
38
  // ⚠ SILENCE WHEN THE SERVER DID NOT ANSWER, which is not the same as "no claims". A client newer
30
- // than its server gets `undefined` here, and treating that as an empty array would tell every
31
- // repo-stamped page to add folder claims including the ones that already have them, whose claims
32
- // this build simply cannot see. Say nothing rather than something confidently wrong; the line
33
- // reappears on its own once the server is serving the field.
34
- if (pathRoutes === undefined || pathRoutes === null) return null
39
+ // than its server gets `undefined`, and treating that as an empty list would tell every stamped
40
+ // page something false about claims this build cannot see. The line reappears on its own once the
41
+ // server serves the field.
42
+ if (claims === undefined || claims === null) return null
35
43
 
44
+ const list = Array.isArray(claims) ? claims.filter((c) => typeof c === 'string' && c) : []
45
+ const paths = list.filter((c) => c.startsWith('file:'))
46
+ const repos = list.filter((c) => c.startsWith('repo:'))
36
47
  const hasStamp = Array.isArray(stamps) && stamps.length > 0
37
- const routes = Array.isArray(pathRoutes) ? pathRoutes.filter((r) => typeof r === 'string' && r) : []
38
48
 
39
- // Nothing to say about a page that claims no repo and no folders it is not in this population,
40
- // and a line here would be noise on every unrelated page in the wiki.
41
- if (!hasStamp && !routes.length) return null
49
+ // Narrowed already: show the claims and ask whether they are right. Applies whether or not a
50
+ // repo: route also exists the specific claims are what a reader can actually judge.
51
+ if (paths.length) {
52
+ return `Folder claims on this page: ${paths.join(', ')}. If one of these is not actually what this page is about, correct it — a wrong claim quietly routes other people's commits here and sets their tier.`
53
+ }
54
+
55
+ // The genuine catch-all: it holds the repo ROUTE, so every commit really does get filed here.
56
+ if (repos.length) {
57
+ return `⚠ This page holds the repo route ${repos.join(', ')} and no folder claims, so EVERY commit to that repo is filed here — it is the catch-all. If you can tell which folders this page is actually about, claim them with \`set_routing_identifier\` (\`file:owner/repo:path/to/dir/\` — the trailing slash makes it a subtree). Claims are additive, so narrowing never drops the catch-all placement.`
58
+ }
42
59
 
43
- if (routes.length) {
44
- return `Folder claims on this page: ${routes.join(', ')}. If one of these is not actually what this page is about, correct it — a wrong claim quietly routes other people's commits here and sets their tier.`
60
+ // Body stamp only: a timeline, but no attachments. Saying "commits route here" would be false.
61
+ if (hasStamp) {
62
+ return `This page carries a repo stamp but claims no folders, so this repo's commits appear in its TIMELINE and none are filed to it. If specific folders or files belong to this page, claim them with \`set_routing_identifier\` (\`file:owner/repo:path/to/dir/\` — the trailing slash makes it a subtree) and they will start landing here. Many pages are about a decision rather than a directory; if that is this one, there is nothing to claim.`
45
63
  }
46
64
 
47
- return `⚠ This page claims a WHOLE REPO and no folders within it, so every commit to that repo routes here. If you can tell which folders or files this page is actually about, claim them with \`set_routing_identifier\` (\`file:owner/repo:path/to/dir/\` — the trailing slash is what makes it a subtree). Claims are additive and reversible, and a narrower claim also decides the tier of records that land on it. Judge from what the page IS about, not from what happens to be attached to it.`
65
+ return null
48
66
  }
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
  )
@@ -517,6 +531,42 @@ export async function runServer(version) {
517
531
  },
518
532
  )
519
533
 
534
+ server.registerTool(
535
+ 'gate4_status',
536
+ {
537
+ title: 'Gate 4 claim-coverage monitor',
538
+ description: 'Read the aggregate-only Gate 4 status for this brain — what SHARE of the records that arrived in the window have reached a claim, and who claimed them. The metric is coverage (claimed / arrived), never a count of claiming sessions: measured across brains, claiming-session count tracks where a backlog is being drained rather than whether the stream is being kept up with, and the brain with the fewest claimers had the best coverage. The denominator is always reported beside the ratio, because "nothing arrived to claim" and "plenty arrived and nothing was claimed" are opposite conditions that a bare percentage renders identical. Below 30 arrived records the ratio is withheld as `insufficient_volume` rather than stated. Session / sweep / unattributed claims are broken out, and the session share is reported but never thresholded — the alarm is on total coverage, and whether a clause about SESSIONS is satisfied by an automated sweep is a human judgment. It never exposes record ids, titles, payloads, reasons or session keys. "regressed" means coverage has fallen and gate 4 must NOT be considered closed.',
539
+ inputSchema: {
540
+ days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
541
+ brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
542
+ },
543
+ },
544
+ async ({ days, brain }) => {
545
+ const qs = new URLSearchParams()
546
+ if (days != null) qs.set('days', String(days))
547
+ if (brain) qs.set('brain', brain)
548
+ const suffix = qs.size ? `?${qs}` : ''
549
+ const res = await fetchCortex(`${BASE}/api/gates/4/status${suffix}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
550
+ if (!res.ok) {
551
+ const body = await res.text()
552
+ throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
553
+ }
554
+ const status = await res.json()
555
+ const pct = (x) => `${(x * 100).toFixed(1)}%`
556
+ // The denominator and the claim breakdown are in EVERY branch, including the healthy one. A
557
+ // reader must never be handed a ratio without the count it was taken over.
558
+ const shape = `${status.arrived} records arrived in ${status.days}d (excluding the last ${status.settlingHours}h, which the daily sweep has not reached yet) — ${status.unclaimed} unclaimed, ${status.sessionClaimed} session-claimed, ${status.sweepClaimed} swept, ${status.unattributedClaimed} unattributed`
559
+ const text = status.status === 'insufficient_volume'
560
+ ? `Gate 4 coverage is NOT MEASURABLE for this brain: ${shape}. Below ${status.minArrivedRecords} arrivals the ratio is noise, so none is reported — this is not a low score, it is no score.`
561
+ : status.status === 'regressed'
562
+ ? `⚠ Gate 4 has REGRESSED: coverage ${pct(status.coverage)} of ${shape}. That is under the ${pct(status.regressedBelow)} floor — the claim clause closed ${status.gateClosedAt} does NOT currently hold and the gate must not be treated as closed. Sessions accounted for ${pct(status.sessionShare)} of arrivals. Find what stopped claiming before citing the clause.`
563
+ : status.machineEvidenceReady
564
+ ? `Gate 4's claim coverage holds: ${pct(status.coverage)} of ${shape}. Sessions accounted for ${pct(status.sessionShare)} of arrivals — if that is near zero, an automated sweep is carrying a clause about sessions, which is a human judgment and not something this monitor decides. Closed ${status.gateClosedAt}; this is the evidence continuing to hold, not a new closure.`
565
+ : `Gate 4 coverage is ${pct(status.coverage)} of ${shape} — above the ${pct(status.regressedBelow)} alarm floor but below the ${pct(status.healthyAt)} the closure rested on. Sessions accounted for ${pct(status.sessionShare)} of arrivals. Not an alarm and not evidence; re-check before citing the clause as live.`
566
+ return { content: [{ type: 'text', text }] }
567
+ },
568
+ )
569
+
520
570
  server.registerTool(
521
571
  'session_context',
522
572
  {
@@ -1275,7 +1325,8 @@ export async function runServer(version) {
1275
1325
  // evidence: a session read a page through a [[link]], saw the footer, and repaired the page.
1276
1326
  // The ask is deliberately self-extinguishing — once a claim exists the line changes to a
1277
1327
  // review prompt rather than a request, so a corrected page stops nagging.
1278
- const claimLine = folderClaimLine(stamps, m.pathRoutes)
1328
+ // routingClaims (0.9.107+) carries repo: AND file:; pathRoutes is the 0.9.106 file:-only field.
1329
+ const claimLine = folderClaimLine(stamps, m.routingClaims ?? m.pathRoutes)
1279
1330
  if (claimLine) footer += `\n— ${claimLine}`
1280
1331
  // Gate 4's per-node backlog nudge (also gate 3's KWA-36). The SERVER decides whether this
1281
1332
  // fires — it sends `nudge` only when 25+ records are unclaimed AND the page has gone 7+ days
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.106",
3
+ "version": "0.9.108",
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": {