@theronap/cortex-mcp 0.9.143 → 0.9.145
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/edge_extract.mjs +22 -3
- package/lib/obligations_render.mjs +20 -2
- package/lib/server.mjs +43 -0
- package/package.json +1 -1
package/lib/edge_extract.mjs
CHANGED
|
@@ -170,8 +170,12 @@ const PAGES_FRAGMENT =
|
|
|
170
170
|
const OBLIGATIONS_FRAGMENT =
|
|
171
171
|
'"obligations" (array of things someone must DO by a date, stated in this session. Each an object ' +
|
|
172
172
|
'with "subject" (what must be done, one short phrase), ' +
|
|
173
|
-
'"due_at" (
|
|
174
|
-
'date that was not
|
|
173
|
+
'"due_at" (a DATE, YYYY-MM-DD, when only a day is stated; a date-time, YYYY-MM-DDTHH:MM, ONLY when a ' +
|
|
174
|
+
'time of day is actually said; otherwise null — do NOT invent or infer a date or a time that was not ' +
|
|
175
|
+
'said. A day with no time is kept as a bare date and treated as all-day), ' +
|
|
176
|
+
'"due_phrase" (the exact words that say WHEN it is due — "on the 15th", "by Tuesday the 22nd", ' +
|
|
177
|
+
'"Saturday", "by 5 pm tomorrow" — copied VERBATIM from the evidence sentence; null if the evidence ' +
|
|
178
|
+
'names no time. The date itself is worked out from these words, so copy them, do not rephrase), ' +
|
|
175
179
|
'"evidence" (the sentence from the session that says so, copied EXACTLY and VERBATIM — it is ' +
|
|
176
180
|
'checked against the transcript and the whole item is discarded if it does not match), ' +
|
|
177
181
|
'"obligated_party" (exactly one of: "self" if the person whose session this is must do it; ' +
|
|
@@ -179,6 +183,9 @@ const OBLIGATIONS_FRAGMENT =
|
|
|
179
183
|
'The hard part is "none", so read it carefully: a real date about a real person is still "none" ' +
|
|
180
184
|
'when nobody owes anything — "so-and-so\'s birthday is today" has every surface feature of a ' +
|
|
181
185
|
'deadline and is not one. Historical and course-content dates are "none" too. ' +
|
|
186
|
+
'If the SAME item is given a deadline more than once, or its deadline is changed later ("due ' +
|
|
187
|
+
'Saturday", then later "actually, plan on Tuesday the 22nd"), report it ONCE, using the LAST statement ' +
|
|
188
|
+
'for its evidence, due_phrase and due_at. ' +
|
|
182
189
|
'Include an item when the session genuinely states an obligation, and do not stretch to find one; ' +
|
|
183
190
|
'an empty array is correct for a session that contains no deadlines.)'
|
|
184
191
|
|
|
@@ -278,6 +285,8 @@ export function verbatimIn(needle, haystack) {
|
|
|
278
285
|
* `obligated_party` outside the preset is malformed and goes — the server's CHECK constraint would
|
|
279
286
|
* reject it anyway, and failing here means the record still lands with its other keys intact.
|
|
280
287
|
*/
|
|
288
|
+
const normWs = (x) => x.toLowerCase().replace(/[’']/g, '').replace(/\s+/g, ' ').trim()
|
|
289
|
+
|
|
281
290
|
export function keepVerifiableObligations(items, shown) {
|
|
282
291
|
if (!Array.isArray(items)) return []
|
|
283
292
|
const out = []
|
|
@@ -289,7 +298,17 @@ export function keepVerifiableObligations(items, shown) {
|
|
|
289
298
|
if (!subject || !evidence || !OBLIGATION_PARTIES.has(party)) continue
|
|
290
299
|
if (!verbatimIn(evidence, shown)) continue
|
|
291
300
|
const due = typeof it.due_at === 'string' && it.due_at.trim() ? it.due_at.trim() : null
|
|
292
|
-
|
|
301
|
+
// ADR-0059 §5.2: the WHEN words, which the server resolves into a date in code. Kept only if they
|
|
302
|
+
// appear inside the evidence sentence — the check that stops a paraphrase ("Sept 22" for "the
|
|
303
|
+
// 22nd") or a phrase from some other sentence from deciding the date. Dropped otherwise: the server
|
|
304
|
+
// then falls back to due_at, exactly as it did before phrases existed.
|
|
305
|
+
const phraseRaw = typeof it.due_phrase === 'string' ? it.due_phrase.trim() : ''
|
|
306
|
+
const due_phrase = phraseRaw && normWs(evidence).includes(normWs(phraseRaw)) ? phraseRaw.slice(0, 120) : null
|
|
307
|
+
out.push({
|
|
308
|
+
subject: subject.slice(0, 200), due_at: due,
|
|
309
|
+
...(due_phrase ? { due_phrase } : {}),
|
|
310
|
+
evidence: evidence.slice(0, 500), obligated_party: party,
|
|
311
|
+
})
|
|
293
312
|
if (out.length >= MAX_OBLIGATIONS) break
|
|
294
313
|
}
|
|
295
314
|
return out
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
//
|
|
11
11
|
// `dueAt` arrives in the OWNER's zone with its offset (web/lib/engine/due_dates.ts), so slice(0, 10)
|
|
12
12
|
// below is the owner's calendar date and new Date() is still the exact instant.
|
|
13
|
+
//
|
|
14
|
+
// ADR-0059 §5.2 (Theron, 2026-09-10): a deadline with no stated time is ALL DAY, starting 00:00 local.
|
|
15
|
+
// It prints "(all day)" instead of a time, reads DUE TODAY for the whole of its day, and turns OVERDUE
|
|
16
|
+
// only at `overdueAfter` (the next local midnight). Without `overdueAfter`, the old rule — overdue at
|
|
17
|
+
// dueAt — would call an all-day deadline OVERDUE from the first minute of its own day. A server older
|
|
18
|
+
// than these fields sends neither, and gets exactly the old output.
|
|
13
19
|
export function renderObligations(obs, now = Date.now()) {
|
|
14
20
|
if (!obs?.length) return 'Nothing open.'
|
|
15
21
|
const proposed = obs.filter((o) => o.state === 'proposed').length
|
|
@@ -20,8 +26,7 @@ export function renderObligations(obs, now = Date.now()) {
|
|
|
20
26
|
'',
|
|
21
27
|
]
|
|
22
28
|
for (const o of obs) {
|
|
23
|
-
const
|
|
24
|
-
const when = o.dueAt ? `${overdue ? 'OVERDUE ' : 'due '}${o.dueAt.slice(0, 10)}` : 'no deadline'
|
|
29
|
+
const when = describeDue(o, now)
|
|
25
30
|
lines.push(`${o.id}`)
|
|
26
31
|
lines.push(` ${o.subject} — ${when}${o.state === 'snoozed' ? ' (snooze expired)' : ''}${o.state === 'proposed' ? ' [PROPOSED]' : ''}`)
|
|
27
32
|
// ⚠ EVIDENCE IS RENDERED AS A QUESTION, NEVER AS A VERDICT. The phrasing is the feature:
|
|
@@ -37,3 +42,16 @@ export function renderObligations(obs, now = Date.now()) {
|
|
|
37
42
|
}
|
|
38
43
|
return lines.join('\n')
|
|
39
44
|
}
|
|
45
|
+
|
|
46
|
+
function describeDue(o, now) {
|
|
47
|
+
if (!o.dueAt) return 'no deadline'
|
|
48
|
+
const day = o.dueAt.slice(0, 10)
|
|
49
|
+
const flagged = typeof o.dueAllDay === 'boolean' // absent = a server older than all-day deadlines
|
|
50
|
+
const at = new Date(o.dueAt).getTime()
|
|
51
|
+
const after = o.overdueAfter ? new Date(o.overdueAfter).getTime() : at
|
|
52
|
+
if (!flagged) return `${at <= now ? 'OVERDUE ' : 'due '}${day}`
|
|
53
|
+
const what = o.dueAllDay ? `${day} (all day)` : `${day} ${o.dueAt.slice(11, 16)}`
|
|
54
|
+
if (after <= now) return `OVERDUE ${what}`
|
|
55
|
+
if (o.dueAllDay && at <= now) return `DUE TODAY ${what}`
|
|
56
|
+
return `due ${what}`
|
|
57
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -77,6 +77,28 @@ export const sectionCurrencyStamp = (s, day) => {
|
|
|
77
77
|
// invisible on exactly the sections that were healthy.
|
|
78
78
|
export const renderSection = (s, day) => `### ${s.heading}${sectionCurrencyStamp(s, day)}\n${s.body}`
|
|
79
79
|
|
|
80
|
+
// gate5_status's transport half, pulled out of the tool handler so it can be exercised directly rather
|
|
81
|
+
// than only string-matched (gate5_status_transport.drift.test.mjs). Takes the raw fetch Response from
|
|
82
|
+
// /api/gates/5/status; the registerTool callback's only remaining job is fetching it. Same three
|
|
83
|
+
// branches the drift test names: 404 (a deployment older than this tool), a generic non-ok response
|
|
84
|
+
// (classified and thrown, same as every other tool here), and 200 with or without a usable `text`.
|
|
85
|
+
export async function gate5StatusFromResponse(res) {
|
|
86
|
+
// A deployment older than this tool has no such route. That is a real state (the package and the
|
|
87
|
+
// web deploy move independently — #693) and it must read as "no reading", not as an error dump.
|
|
88
|
+
if (res.status === 404) {
|
|
89
|
+
return { content: [{ type: 'text', text: 'This tool is newer than the deployment it is talking to: /api/gates/5/status does not exist there yet, so NO reading was taken. This is not an all-clear. Re-check once the console has redeployed.' }] }
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const body = await res.text()
|
|
93
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
94
|
+
}
|
|
95
|
+
const body = await res.json()
|
|
96
|
+
const text = typeof body?.text === 'string' && body.text.trim()
|
|
97
|
+
? body.text
|
|
98
|
+
: 'gate5_status received a response with no reading in it, so NO reading was taken. This is not an all-clear.'
|
|
99
|
+
return { content: [{ type: 'text', text }] }
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
// The Agnoclast MCP server (stdio). Serves the signed-in employee's scoped org
|
|
81
103
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
82
104
|
|
|
@@ -674,6 +696,27 @@ function renderNudge(payload) {
|
|
|
674
696
|
},
|
|
675
697
|
)
|
|
676
698
|
|
|
699
|
+
// ── gate5_status — v1 gate 5's operator-access alarm (scope lock D3) ─────────────────────────────
|
|
700
|
+
//
|
|
701
|
+
// THE VERDICT AND THE WORDS ARE RENDERED SERVER-SIDE, and that is the design rather than a shortcut.
|
|
702
|
+
// gate2_status and gate3_status build their prose here, in the client, and both ended up with wrong
|
|
703
|
+
// sentences frozen in published seats that could only be shimmed around from the route (see the
|
|
704
|
+
// notes above them). /api/gates/5/status returns the finished `text`, so its wording and verdict
|
|
705
|
+
// logic deploy with the web app. This handler only transports it, and its one rule is that "no
|
|
706
|
+
// reading" must never come out as silence or as an all-clear.
|
|
707
|
+
server.registerTool(
|
|
708
|
+
'gate5_status',
|
|
709
|
+
{
|
|
710
|
+
title: 'Gate 5 operator-access alarm (ops staff only)',
|
|
711
|
+
description: 'Read v1 gate 5\'s operator-access alarm: privileged database sessions (postgres / supabase_admin, sampled every 30s) that NO break-glass declaration covers, counted since a dated, attributed, expiring baseline (ADR-0060 option 1, accepted by Theron Peterson 2026-09-10). Aggregate-only — counts by likely source, the newest unexplained time and the window; never addresses, org names or session contents. OPS STAFF ONLY: the data is cross-org, so any other caller gets an operator-only answer, which is neither an alarm nor an all-clear. It also checks this app\'s OWN database connection: if the app itself is being recorded as operator access, the alarm is reported NOT ARMED rather than red or green. A failed read is reported as NO READING, never as clean. It decides nothing — gate 5 closes by Theron\'s decision, and what it cannot see is listed on [[Agnoclast v1 — the currency gate]].',
|
|
712
|
+
inputSchema: {},
|
|
713
|
+
},
|
|
714
|
+
async () => {
|
|
715
|
+
const res = await fetchCortex(`${BASE}/api/gates/5/status`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
716
|
+
return gate5StatusFromResponse(res)
|
|
717
|
+
},
|
|
718
|
+
)
|
|
719
|
+
|
|
677
720
|
server.registerTool(
|
|
678
721
|
'session_context',
|
|
679
722
|
{
|
package/package.json
CHANGED