@theronap/cortex-mcp 0.9.134 → 0.9.136

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 +46 -5
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -2418,7 +2418,7 @@ function renderNudge(payload) {
2418
2418
  'capture_meeting',
2419
2419
  {
2420
2420
  title: 'Capture a meeting transcript, joined to its calendar event',
2421
- description: "Capture a meeting transcript you were given — a paste, an export, notes — as a real record joined to the calendar event it came from. ⚠ CALL IT TWICE. Without `calendar_record_id` it WRITES NOTHING and returns candidate calendar events near that time; you pick one and call again. Overlap alone is not an answer — 90 of 120 calendar records are recurring birthdays and an all-day event overlaps its whole day, so each candidate carries `caveats` saying why it might be wrong. Read them before choosing. WHY THE JOIN MATTERS: a pasted transcript has no attendee list, so on its own it is reachable from nobody. The calendar event's attendee list is authoritative, and the captured meeting inherits it — that is what makes the transcript findable from the people who were in it. The brain follows from the matched event; you never pick one. Attendees who have no page come back as `unclaimed` — each is a person in the room nobody has authored, and authoring one reaches every past event they were in, not just this meeting. Meetings are stored `scoped`, never org-wide.",
2421
+ description: "Capture a meeting transcript you were given — a paste, an export, notes — as a real record joined to the calendar event it came from. ⚠ CALL IT TWICE. Without `calendar_record_id` it WRITES NOTHING and returns candidate calendar events near that time; you pick one and call again. Overlap alone is not an answer — 90 of 120 calendar records are recurring birthdays and an all-day event overlaps its whole day, so each candidate carries `caveats` saying why it might be wrong. Read them before choosing. WHY THE JOIN MATTERS: a pasted transcript has no attendee list, so on its own it is reachable from nobody. The calendar event's attendee list is authoritative, and the captured meeting inherits it — that is what makes the transcript findable from the people who were in it. The brain follows from the matched event; you never pick one. Attendees who have no page come back as `unclaimed` — each is a person in the room nobody has authored, and authoring one reaches their EXISTING records too, not only future meetings: claiming their identifier heals history in the same statement. Measured 2026-09-03 — ten calendar records spanning two months attached 49ms after the claim. ⚠ THE CONFIRMATION DOES NOT TELL YOU THAT. It says only that future events will attach, so a heal that just moved ten records is indistinguishable from one that did nothing. Verify by reading the page, never by trusting the reply. Meetings are stored `scoped`, never org-wide.",
2422
2422
  inputSchema: {
2423
2423
  occurred_at: z.string().describe('when the meeting STARTED, ISO 8601. Required for both the proposal and the capture.'),
2424
2424
  ends_at: z.string().optional().describe('when it ended, ISO 8601. Omit and the calendar event\'s end is inherited. NEVER guess one — unknown must stay unknown.'),
@@ -3339,7 +3339,19 @@ function renderNudge(payload) {
3339
3339
  const out = await res.json().catch(() => null)
3340
3340
  if (!res.ok) return toolError(`Could not set routing identifier: ${out?.error ?? res.status}`)
3341
3341
  const set = out?.set?.join(', ') ?? identifier
3342
- return { content: [{ type: 'text', text: `Routing identifier set on document ${out?.documentId ?? '?'}: ${set}. Future matching events will attach here (body mentions alone will not).` }] }
3342
+ // SAY WHAT ALREADY HAPPENED, NOT ONLY WHAT WILL. The claim heals history in the same
3343
+ // statement, so by the time this renders, existing records are ALREADY attached. The previous
3344
+ // wording mentioned only future events — true, and load-bearingly incomplete, which is harder
3345
+ // to catch than a false statement because checking it confirms it.
3346
+ // A ZERO IS EXPLAINED, not printed bare: an unexplained 0 recreates the same ambiguity one
3347
+ // level down, and identity claims (email:, thread:) attach only the CLAIMANT'S OWN records.
3348
+ const healed = typeof out?.attachedExisting === 'number' ? out.attachedExisting : null
3349
+ const healedLine = healed === null
3350
+ ? ''
3351
+ : healed > 0
3352
+ ? ` ${healed} existing record(s) ALREADY attached to this page as a result.`
3353
+ : ' 0 existing records attached — either nothing carries this identifier yet, or records do but belong to another owner (an identity claim only attaches your own).'
3354
+ return { content: [{ type: 'text', text: `Routing identifier set on document ${out?.documentId ?? '?'}: ${set}.${healedLine} Future matching events will attach here too (body mentions alone will not).` }] }
3343
3355
  },
3344
3356
  )
3345
3357
 
@@ -3464,13 +3476,21 @@ function renderNudge(payload) {
3464
3476
  'route_record',
3465
3477
  {
3466
3478
  title: 'Route a record to the pages it belongs on',
3467
- description: 'Attach a pending record to the pages you judge correct — the point of the whole triage path. Use this when you have real context on what the work was; that judgment is better than any rule the webhook could run. Attachments are additive: existing deterministic homes (routing identifiers, your profile) stay. Pass park=true instead when you have looked and there is genuinely no good home — parking beats attaching to a page that merely shares a word.',
3479
+ description: 'Attach a pending record to the pages you judge correct — the point of the whole triage path. Use this when you have real context on what the work was; that judgment is better than any rule the webhook could run. Attachments are additive: existing deterministic homes (routing identifiers, your profile) stay. Pass park=true instead when you have looked and there is genuinely no good home — parking beats attaching to a page that merely shares a word. \u26a0 ADR-0051: a record does not finish until somebody looked for its OTHER homes. A call that would end the record without adding a page it did not already have \u2014 a park, or a route naming only pages already attached \u2014 is REFUSED once, with instructions. Name the other pages, or go look, or say you looked.',
3468
3480
  inputSchema: {
3469
3481
  recordId: z.string().describe('record id from pending_records'),
3470
3482
  documentIds: z.array(z.string()).optional().describe('pages to attach. Accepts EITHER a brain_documents id (from a pending_records sweep) OR the `ref:` that read_page prints for a page — they are different columns and read_page only ever gives you the second, which is why this used to fail as no_documents on ids that looked perfectly valid. A ref resolves to that node\'s current page, accessible copy first. Anything that resolves to no page in this brain is named back to you rather than silently dropped.'),
3471
3483
  reason: z.string().describe('why these pages — recorded with the attachment'),
3472
3484
  park: z.boolean().optional().describe('no good home exists; leave it alone rather than guessing'),
3473
3485
  tier: z.number().int().optional().describe('2 when this came from the cheap sweep rather than your own context'),
3486
+ searchedForOtherHomes: z.boolean().optional().describe(
3487
+ 'ADR-0051. "I looked for other homes and there are genuinely none." Only consulted when the call '
3488
+ + 'would otherwise finish the record without adding a page it did not already have — so a route that '
3489
+ + 'attaches something new never needs it, and a park always does. '
3490
+ + '⚠ DO NOT SEND THIS ON THE FIRST CALL AS A MATTER OF COURSE. It is the answer AFTER looking, not a '
3491
+ + 'way past the question. The gate exists because a session-start instruction to route converted at '
3492
+ + '11% over 21 days; sending this reflexively rebuilds exactly the failure it was measured against.',
3493
+ ),
3474
3494
  identifierDispositions: z.array(z.any()).optional().describe(
3475
3495
  'ADR-0044. REQUIRED when this record carries identifiers no page claims — the refusal names exactly which. '
3476
3496
  + 'One entry per unresolved identifier, and this is a DIFFERENT question from documentIds. '
@@ -3489,13 +3509,13 @@ function renderNudge(payload) {
3489
3509
  ),
3490
3510
  },
3491
3511
  },
3492
- async ({ recordId, documentIds, reason, park, tier, identifierDispositions }) => {
3512
+ async ({ recordId, documentIds, reason, park, tier, identifierDispositions, searchedForOtherHomes }) => {
3493
3513
  let res
3494
3514
  try {
3495
3515
  res = await fetchCortex(`${BASE}/api/brain/triage`, {
3496
3516
  method: 'POST',
3497
3517
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3498
- body: JSON.stringify({ action: park ? 'park' : 'route', recordId, documentIds, reason, tier, identifierDispositions }),
3518
+ body: JSON.stringify({ action: park ? 'park' : 'route', recordId, documentIds, reason, tier, identifierDispositions, searchedForOtherHomes }),
3499
3519
  })
3500
3520
  } catch (e) {
3501
3521
  return toolError(`Could not route record: ${e.message}`)
@@ -3527,6 +3547,27 @@ function renderNudge(payload) {
3527
3547
  }
3528
3548
  return toolError(lines.join('\n'))
3529
3549
  }
3550
+ // ADR-0051 refusal. Rendered as INSTRUCTIONS for the same reason the ADR-0044 one above is: the
3551
+ // agent is one call from succeeding and the whole point of the gate is the question it asks. A
3552
+ // bare "other_homes_unchecked" would read as a malfunction and get retried verbatim.
3553
+ if (!res.ok && out?.error === 'other_homes_unchecked') {
3554
+ return toolError([
3555
+ park
3556
+ ? 'Not parked yet — before a record is filed as having no home, look for its other homes.'
3557
+ : 'Not routed yet — every page you named is already attached, so this call would finish the record without adding anything.',
3558
+ '',
3559
+ 'What OTHER pages does this record belong on? A record is usually about more than the page an',
3560
+ 'identifier matched: the project it concerns, the person it is about, the decision it bears on.',
3561
+ '',
3562
+ ' know some already → add them to documentIds and call again',
3563
+ ' none come to mind → go look first. grep or read for pages this could belong to;',
3564
+ ' you know the namespace better than any matcher does',
3565
+ ' looked, found none → call again with searchedForOtherHomes: true',
3566
+ '',
3567
+ 'Parking is still a complete answer, and a wrong attachment is still worse than none. The gate',
3568
+ 'asks that you looked, not that you found something.',
3569
+ ].join('\n'))
3570
+ }
3530
3571
  if (!res.ok) return toolError(`Could not route record: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
3531
3572
  if (park) return { content: [{ type: 'text', text: 'Parked. It stays visible and unrouted rather than badly attached.' }] }
3532
3573
  return { content: [{ type: 'text', text: `Routed — attached to ${out?.attached?.length ?? 0} page(s). Recorded as a session judgment, not a rule match.` }] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.134",
3
+ "version": "0.9.136",
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": {