@theronap/cortex-mcp 0.9.105 → 0.9.107
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/folder_claims.mjs +66 -0
- package/lib/server.mjs +54 -0
- package/package.json +5 -5
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The read_page footer line about FOLDER CLAIMS — pure, so CI can execute the decision.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. A page claiming a whole repo and a page claiming `web/lib/engine/` read
|
|
4
|
+
// identically, and the difference decides where every future commit lands and what tier it gets.
|
|
5
|
+
// Measured against prod 2026-08-22: 88 Agnoclast pages carry a `[[repo:…]]` stamp and 10 hold any
|
|
6
|
+
// path claim, so ~78 silently take delivery of the entire monorepo — `cortex` alone governs 66%.
|
|
7
|
+
//
|
|
8
|
+
// WHY THE FOOTER AND NOT A BACKLOG, A SCRIPT, OR A MINING PASS. It cannot be mined. Today's
|
|
9
|
+
// attachments come FROM the mother route, so mining them re-derives the mother (circular for
|
|
10
|
+
// `cortex` and `Theron Peterson`); and for every other page a commit spanning two packages makes all
|
|
11
|
+
// of its paths look like evidence for both (`cortex-mcp` mines to `web/app/api/` at 52 records — it
|
|
12
|
+
// is a package). Co-occurrence is not aboutness. Which folders a page is ABOUT is judgment, and the
|
|
13
|
+
// agent holding the page open is the one with the context to make it — the same argument, and the
|
|
14
|
+
// same evidence, as the per-node backlog nudge that ships beside it.
|
|
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
|
+
//
|
|
25
|
+
// SELF-EXTINGUISHING BY DESIGN. Once a claim exists the line stops asking and starts reviewing, so a
|
|
26
|
+
// page that has been narrowed does not keep nagging every reader forever. That is the difference
|
|
27
|
+
// between a nudge and wallpaper, and this repo has already recorded the day-one backlog saturating
|
|
28
|
+
// the D9 signal as the failure mode to avoid.
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PURE. The footer line for a page's folder claims, or null when there is nothing worth saying.
|
|
32
|
+
*
|
|
33
|
+
* @param stamps repo stamps found in the page body, e.g. ['[[repo:theronap/cortex]]']
|
|
34
|
+
* @param claims ALL routing claims the node holds (repo: and file:), from the server
|
|
35
|
+
* @returns the line to append, or null
|
|
36
|
+
*/
|
|
37
|
+
export function folderClaimLine(stamps, claims) {
|
|
38
|
+
// ⚠ SILENCE WHEN THE SERVER DID NOT ANSWER, which is not the same as "no claims". A client newer
|
|
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
|
|
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:'))
|
|
47
|
+
const hasStamp = Array.isArray(stamps) && stamps.length > 0
|
|
48
|
+
|
|
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
|
+
}
|
|
59
|
+
|
|
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.`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null
|
|
66
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from 'os'
|
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { createHash, randomUUID } from 'crypto'
|
|
8
8
|
import { fetchCortex, classify, resolveBase, resolveEnvToken, setSessionKey } from './diagnose.mjs'
|
|
9
|
+
import { folderClaimLine } from './folder_claims.mjs'
|
|
9
10
|
import { resolveSessionKey, resolveLogSessionId } from './session_key.mjs'
|
|
10
11
|
import { runSendImessage } from './imessage_send.mjs'
|
|
11
12
|
import { formatGrepHits } from './grep_cli.mjs'
|
|
@@ -516,6 +517,42 @@ export async function runServer(version) {
|
|
|
516
517
|
},
|
|
517
518
|
)
|
|
518
519
|
|
|
520
|
+
server.registerTool(
|
|
521
|
+
'gate4_status',
|
|
522
|
+
{
|
|
523
|
+
title: 'Gate 4 claim-coverage monitor',
|
|
524
|
+
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.',
|
|
525
|
+
inputSchema: {
|
|
526
|
+
days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
|
|
527
|
+
brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
async ({ days, brain }) => {
|
|
531
|
+
const qs = new URLSearchParams()
|
|
532
|
+
if (days != null) qs.set('days', String(days))
|
|
533
|
+
if (brain) qs.set('brain', brain)
|
|
534
|
+
const suffix = qs.size ? `?${qs}` : ''
|
|
535
|
+
const res = await fetchCortex(`${BASE}/api/gates/4/status${suffix}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
536
|
+
if (!res.ok) {
|
|
537
|
+
const body = await res.text()
|
|
538
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
539
|
+
}
|
|
540
|
+
const status = await res.json()
|
|
541
|
+
const pct = (x) => `${(x * 100).toFixed(1)}%`
|
|
542
|
+
// The denominator and the claim breakdown are in EVERY branch, including the healthy one. A
|
|
543
|
+
// reader must never be handed a ratio without the count it was taken over.
|
|
544
|
+
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`
|
|
545
|
+
const text = status.status === 'insufficient_volume'
|
|
546
|
+
? `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.`
|
|
547
|
+
: status.status === 'regressed'
|
|
548
|
+
? `⚠ 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.`
|
|
549
|
+
: status.machineEvidenceReady
|
|
550
|
+
? `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.`
|
|
551
|
+
: `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.`
|
|
552
|
+
return { content: [{ type: 'text', text }] }
|
|
553
|
+
},
|
|
554
|
+
)
|
|
555
|
+
|
|
519
556
|
server.registerTool(
|
|
520
557
|
'session_context',
|
|
521
558
|
{
|
|
@@ -1260,6 +1297,23 @@ export async function runServer(version) {
|
|
|
1260
1297
|
const allBody = m.tiers.flatMap((t) => (t.sections ?? []).map((s) => s.body)).join('\n')
|
|
1261
1298
|
const stamps = [...new Set((allBody.match(/\[\[repo:[a-z0-9][a-z0-9-]*\/[a-z0-9_.-]+\]\]/gi) ?? []).map((s) => s.toLowerCase()))]
|
|
1262
1299
|
if (stamps.length) footer += `\n— This page carries ${stamps.join(', ')} — \`read_page "${name}"\` with history: true for its event timeline (page = present, timeline = history).`
|
|
1300
|
+
// FOLDER CLAIMS (2026-08-22). A page claiming a whole repo and a page claiming
|
|
1301
|
+
// `web/lib/engine/` read identically, and the difference decides where every future commit
|
|
1302
|
+
// lands. Measured that day: 88 Agnoclast pages carry a repo stamp, 10 hold any path claim —
|
|
1303
|
+
// so ~78 silently take delivery of the entire monorepo, and `cortex` alone governs 66%.
|
|
1304
|
+
//
|
|
1305
|
+
// WHY THE FOOTER RATHER THAN A BACKLOG OR A SCRIPT. This cannot be mined: today's attachments
|
|
1306
|
+
// come FROM the mother route, so mining them re-derives the mother (circular), and for
|
|
1307
|
+
// everyone else a commit spanning two packages makes all its paths look like evidence for
|
|
1308
|
+
// both pages (`cortex-mcp` mines to `web/app/api/` at 52 records — it is a package). Which
|
|
1309
|
+
// folders a page is ABOUT is judgment, and the agent holding the page open is the one with
|
|
1310
|
+
// the context to make it. Same reasoning as the backlog nudge two lines down, and the same
|
|
1311
|
+
// evidence: a session read a page through a [[link]], saw the footer, and repaired the page.
|
|
1312
|
+
// The ask is deliberately self-extinguishing — once a claim exists the line changes to a
|
|
1313
|
+
// review prompt rather than a request, so a corrected page stops nagging.
|
|
1314
|
+
// routingClaims (0.9.107+) carries repo: AND file:; pathRoutes is the 0.9.106 file:-only field.
|
|
1315
|
+
const claimLine = folderClaimLine(stamps, m.routingClaims ?? m.pathRoutes)
|
|
1316
|
+
if (claimLine) footer += `\n— ${claimLine}`
|
|
1263
1317
|
// Gate 4's per-node backlog nudge (also gate 3's KWA-36). The SERVER decides whether this
|
|
1264
1318
|
// fires — it sends `nudge` only when 25+ records are unclaimed AND the page has gone 7+ days
|
|
1265
1319
|
// unedited — so there is no threshold logic here to drift out of sync. It rides in the footer
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theronap/cortex-mcp",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Connect your AI assistant to Cortex
|
|
3
|
+
"version": "0.9.107",
|
|
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": {
|
|
7
7
|
"cortex-mcp": "bin/cortex-mcp.mjs"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"scripts": {
|
|
32
|
-
"release": "
|
|
33
|
-
"promote": "
|
|
34
|
-
"rollback": "
|
|
32
|
+
"release": "bash scripts/release.sh",
|
|
33
|
+
"promote": "bash scripts/promote.sh",
|
|
34
|
+
"rollback": "bash scripts/rollback.sh"
|
|
35
35
|
}
|
|
36
36
|
}
|