@theronap/agnoclast-mcp 0.9.253 → 0.9.255

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.
@@ -0,0 +1,31 @@
1
+ // ONE INTAKE UNIT ID, TWO SPELLINGS (2026-10-22).
2
+ //
3
+ // `not_mine` took `intake_item_id` while `intake_release` (and every other intake verb) took
4
+ // `intakeItemId`. The schemas strip an unknown key, so the wrong spelling arrived as "missing" — one
5
+ // session made 607 failing calls on 10-21/10-22 guessing between the two. Every intake verb now takes
6
+ // BOTH spellings; when both are given they must agree, never silently pick one.
7
+
8
+ /**
9
+ * PURE. The intake unit id from either spelling, or an error string.
10
+ * `required: false` lets a verb that also takes another kind of id (not_mine's record_id) accept neither.
11
+ */
12
+ export function intakeIdFrom(args, { required = true } = {}) {
13
+ const camel = typeof args?.intakeItemId === 'string' ? args.intakeItemId.trim() : ''
14
+ const snake = typeof args?.intake_item_id === 'string' ? args.intake_item_id.trim() : ''
15
+ if (camel && snake && camel !== snake) {
16
+ return { error: `intakeItemId (${camel}) and intake_item_id (${snake}) disagree — pass one of them.` }
17
+ }
18
+ const id = camel || snake
19
+ if (!id && required) return { error: 'intakeItemId is required (intake_item_id is accepted too).' }
20
+ return { id: id || null }
21
+ }
22
+
23
+ /** intakeIdFrom for an expression-bodied handler: the id, or a thrown Error the SDK returns as a tool error. */
24
+ export function requireIntakeId(args) {
25
+ const r = intakeIdFrom(args)
26
+ if (r.error) throw new Error(r.error)
27
+ return r.id
28
+ }
29
+
30
+ /** The zod description for the alias field, so every verb says the same thing. */
31
+ export const INTAKE_ID_ALIAS_NOTE = 'same as intakeItemId — either spelling is accepted'
@@ -5,7 +5,7 @@
5
5
  // that web/lib/engine/release.test.ts pins to the server's registry.
6
6
 
7
7
  /** Tools any release flag gates — hidden when the server cannot be asked. Mirror of allGatedTools(). */
8
- export const GATED_FALLBACK = ['my_messages', 'read_conversation', 'send_message', 'set_obligation_owed_to']
8
+ export const GATED_FALLBACK = ['my_messages', 'read_conversation', 'send_message', 'set_manager']
9
9
 
10
10
  export const RELEASE_FETCH_TIMEOUT_MS = 2500
11
11
 
package/lib/server.mjs CHANGED
@@ -25,6 +25,7 @@ import { renderObligations, renderPageObligations, tierReads, who } from './obli
25
25
  import { renderLeaveQuestion, renderLeft, renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs'
26
26
  import { renderSettings } from './settings_render.mjs'
27
27
  import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_render.mjs'
28
+ import { intakeIdFrom, requireIntakeId, INTAKE_ID_ALIAS_NOTE } from './intake_id_alias.mjs'
28
29
  import { obligationsForUnits, renderFilingObligations, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
29
30
  import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './record_render.mjs'
30
31
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
@@ -1221,6 +1222,7 @@ function renderNudge(payload) {
1221
1222
  'a single unit to claim — the same thing as intakeItemIds with one element. Accepted because ' +
1222
1223
  'every other intake verb spells it this way.',
1223
1224
  ),
1225
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1224
1226
  intakeItemIds: z.array(z.string()).optional().describe(
1225
1227
  'claim these specific units instead of the head of the queue (max 50). Without it you get FIFO, ' +
1226
1228
  'so reaching one known unit means claiming everything ahead of it. Ids come from intake_changes ' +
@@ -1232,9 +1234,11 @@ function renderNudge(payload) {
1232
1234
  ),
1233
1235
  },
1234
1236
  },
1235
- async ({ claimKind, limit, source, intakeItemId, intakeItemIds, leaseMinutes }) => {
1236
- // Merge the two spellings before anything else looks at them. Passing both is not an error —
1237
- // the union is what the caller plainly meant.
1237
+ async ({ claimKind, limit, source, intakeItemId, intakeItemIds, leaseMinutes, intake_item_id }) => {
1238
+ // Merge the spellings before anything else looks at them. Passing both is not an error —
1239
+ // the union is what the caller plainly meant. (intake_item_id: 2026-10-22, see intake_id_alias.mjs —
1240
+ // dropped by the schema it would have meant a FIFO claim of the head of the queue.)
1241
+ if (intake_item_id) intakeItemIds = [...(intakeItemIds ?? []), intake_item_id]
1238
1242
  const ids = [...new Set([...(intakeItemIds ?? []), ...(intakeItemId ? [intakeItemId] : [])])]
1239
1243
  intakeItemIds = ids.length ? ids : undefined
1240
1244
  const res = await fetchCortex(`${BASE}/api/intake/claim`, {
@@ -1294,19 +1298,21 @@ function renderNudge(payload) {
1294
1298
  'Atomically publish a claimed intake item into one brain — the disclosure moment (ADR-0062). Deterministic identifier homes in that brain are always attached; documentIds may add further pages. The record is born at the strictest of its source baseline, what the producer asked for, and every attached page; a source floor (iMessage, claude-code) pins it. ' +
1295
1299
  'Name the governing page with governingPage (one of documentIds, never a person page) — otherwise the ranking picks it. ' +
1296
1300
  'Pass mode "preview" first when your choice differs from the claim\'s proposal: it returns the pages, the governing page and the tier this exact call would produce, and writes nothing. Never writes private intake into search/history before this call. ' +
1297
- '⚠ ADR-0051: a unit does not finish until you looked for its OTHER homes. A call whose documentIds add nothing beyond the deterministic homes it gets anyway is REFUSED once — name the pages this unit is about (the design it implements, the project, the person), or go look, or say you looked with searchedForOtherHomes. ' +
1301
+ '⚠ ADR-0051 — BEFORE you file, look for this unit\'s OTHER homes: the pages it is about beyond what an identifier matched (the design it implements, the project, the person, the decision it bears on). grep or read for them, then name them in documentIds. A call that names nothing beyond the deterministic homes still FILES, on those homes only, and says so — add pages later with route_record. ' +
1298
1302
  '⚠ A meeting or lecture TRANSCRIPT files with NOTES in `summary` (Theron, 2026-10-08): the gist, decisions, action items and dates, names spelled right. Without them the call is REFUSED; the transcript itself is kept on the record unchanged. ' +
1299
1303
  '⚠ ADR-0077: a GitHub commit or PR does not finish until every changed path has an answer. If a changed file\'s deepest folder (or, in a grab-bag folder like web/lib/engine/ or db/supabase/, the file itself) has no claim and no decline, the call is REFUSED with the open questions; answer each in folderDecisions and call again.',
1300
1304
  inputSchema: {
1301
- intakeItemId: z.string().describe('intake item uuid'),
1305
+ intakeItemId: z.string().optional().describe('intake item uuid (required — intake_item_id is accepted too)'),
1306
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1302
1307
  intakeItemIds: z.array(z.string()).optional().describe(
1303
1308
  'MORE units to file exactly the same way, in one call — one record EACH, never merged (ADR-0076 §4.5). ' +
1304
- 'For a conversation\'s waiting texts that you decided together: claim them all with `intake_claim`, then pass ' +
1309
+ 'Any units you hold a live claim on may be batched, even from separate intake_claim calls — except that texts from ' +
1310
+ 'DIFFERENT text conversations never share a batch. Claim them with `intake_claim`, then pass ' +
1305
1311
  'the first as intakeItemId and the rest here. title/summary/dedupeKey are ignored in a batch (each unit keeps its ' +
1306
1312
  'own), and a preview covers intakeItemId only. Each id is reported separately; a failure never hides behind a success.',
1307
1313
  ),
1308
1314
  orgId: z.string().describe('destination brain org uuid'),
1309
- documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page `ref:` exactly as read_page shows it, or a brain_documents id (deterministic homes are merged automatically)'),
1315
+ documentIds: z.array(z.string()).optional().describe('additional pages in orgId — each a page TITLE exactly as read_page shows it, its `ref:`, or a brain_documents id (deterministic homes are merged automatically). A title that matches no page, or more than one, is refused with the candidates listed'),
1310
1316
  title: z.string().optional(),
1311
1317
  summary: z.string().optional().describe(
1312
1318
  'what future retrieval reads first. For a meeting or lecture TRANSCRIPT these are its NOTES, and they are required: ' +
@@ -1317,7 +1323,7 @@ function renderNudge(payload) {
1317
1323
  dedupeKey: z.string().optional(),
1318
1324
  origin: z.enum(['deterministic', 'llm', 'user', 'session']).optional(),
1319
1325
  governingPage: z.string().optional().describe(
1320
- 'the page that should GOVERN this record (ADR-0062) — a page `ref:` or document id, and one of documentIds. ' +
1326
+ 'the page that should GOVERN this record (ADR-0062) — a page title, `ref:` or document id, and one of documentIds. ' +
1321
1327
  'Choose the page whose reasons for restricting its audience fit the content; never a person page. It can only ' +
1322
1328
  'tighten the record, and the ranking will not move it. Omit to leave the choice to the ranking.',
1323
1329
  ),
@@ -1326,9 +1332,8 @@ function renderNudge(payload) {
1326
1332
  ),
1327
1333
  searchedForOtherHomes: z.boolean().optional().describe(
1328
1334
  'ADR-0051. "I looked for other homes and there are genuinely none." Only consulted when documentIds add no page ' +
1329
- 'beyond the deterministic homes. ⚠ DO NOT SEND THIS ON THE FIRST CALL AS A MATTER OF COURSE — it is the answer AFTER ' +
1330
- 'looking, not a way past the question. Measured 2026-09-25: 30 records filed through this verb in one afternoon with no ' +
1331
- 'judged page on any of them.',
1335
+ 'beyond the deterministic homes; it suppresses the "filed on its deterministic homes only" note. Send it only AFTER ' +
1336
+ 'looking — it is the answer to the question, not a way past it.',
1332
1337
  ),
1333
1338
  folderDecisions: z.array(z.object({
1334
1339
  identifier: z.string().describe('the open question\'s identifier, or a folder above it that its answerWith lists'),
@@ -1356,6 +1361,9 @@ function renderNudge(payload) {
1356
1361
  },
1357
1362
  },
1358
1363
  async (args) => {
1364
+ const unit = intakeIdFrom(args)
1365
+ if (unit.error) return toolError(unit.error)
1366
+ args = { ...args, intakeItemId: unit.id }
1359
1367
  // ADR-0076 §4.5 — A BATCH: one decision, N records. Theron, 2026-10-05: a conversation's waiting texts
1360
1368
  // are looked up once and handled together, but "it shouldn't file all of them as one record". The
1361
1369
  // loop lives here, as it does for place_staged_record's batch: the route stays one unit per call.
@@ -1380,8 +1388,8 @@ function renderNudge(payload) {
1380
1388
  attachmentMeta: (args.documentIds ?? []).map(() => ({ origin: args.origin ?? 'llm' })),
1381
1389
  ...(args.governingPage ? { governingDocumentId: args.governingPage } : {}),
1382
1390
  ...(args.searchedForOtherHomes ? { searchedForOtherHomes: true } : {}),
1383
- // One placement is a decision about ONE conversation: the server refuses any id whose
1384
- // batch differs from the first id's, so a stray id is never filed onto these pages.
1391
+ // The server refuses an id it cannot safely file with the first: a text from ANOTHER text
1392
+ // conversation, or a unit this session holds no live claim on (not_same_batch).
1385
1393
  sameBatchAs: batch[0],
1386
1394
  ...obligationsFor(id),
1387
1395
  }),
@@ -1389,8 +1397,11 @@ function renderNudge(payload) {
1389
1397
  const o = await r.json().catch(() => null)
1390
1398
  const why = o?.error === 'obligations_unanswered'
1391
1399
  ? 'not filed — a text needs `obligations` ([] when it asks nothing)'
1392
- : (o?.error ?? `HTTP ${r.status}`)
1393
- results.push({ id, ok: r.ok, detail: r.ok ? `${o?.recordId ?? 'filed'}${filingObligationsBrief(o?.obligations)}` : why })
1400
+ : o?.error === 'not_same_batch'
1401
+ ? `not filed — ${o?.hint ?? 'not in the same batch as the first id'}`
1402
+ : `${o?.error ?? `HTTP ${r.status}`}${o?.detail ? ` — ${o.detail}` : ''}`
1403
+ const homesNote = o?.otherHomes?.note ? ' (deterministic homes only — add pages with route_record)' : ''
1404
+ results.push({ id, ok: r.ok, detail: r.ok ? `${o?.recordId ?? 'filed'}${homesNote}${filingObligationsBrief(o?.obligations)}` : why })
1394
1405
  } catch (e) {
1395
1406
  results.push({ id, ok: false, detail: e.message })
1396
1407
  }
@@ -1484,6 +1495,8 @@ function renderNudge(payload) {
1484
1495
  // the ordinary case (observed 2026-09-03 on the first handoff record materialised). Silence is
1485
1496
  // correct here, and it also covers a server too old to send the field at all.
1486
1497
  const lines = []
1498
+ // ADR-0051, non-blocking since server 2026-10-22: filed, but on deterministic homes only.
1499
+ if (body?.otherHomes?.note) lines.push(`⚠ ${body.otherHomes.note}`)
1487
1500
  const governingLine = renderGoverningLine(body?.governing)
1488
1501
  if (governingLine) lines.push(governingLine)
1489
1502
  const sa = body?.sessionAttachments
@@ -1538,11 +1551,16 @@ function renderNudge(payload) {
1538
1551
  description:
1539
1552
  'The OTHER terminal outcome for a claimed intake unit: this is nothing, drop it. Use it for content that should never become a record — unsubscribe receipts, empty greetings, marketing blasts, a stranger\'s photo — instead of materializing junk into a brain because materialize was the only verb available. IRREVERSIBLE: the ciphertext and nonces are destroyed in the same transaction, and a content-free tombstone stops the connector re-delivering the unit. You must already hold the claim (discarding something you never read is refused), `reason` is required and is stored, and it is one item per call — a loop discarding a whole pile on one decision is a bulk job, not judgment.',
1540
1553
  inputSchema: {
1541
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1554
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1555
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1542
1556
  reason: z.string().describe('why this is nothing — recorded on the claim, and the only surviving trace of the decision'),
1543
1557
  },
1544
1558
  },
1545
- async ({ intakeItemId, reason }) => {
1559
+ async (args) => {
1560
+ const { reason } = args
1561
+ const unit = intakeIdFrom(args)
1562
+ if (unit.error) return toolError(unit.error)
1563
+ const intakeItemId = unit.id
1546
1564
  let res
1547
1565
  try {
1548
1566
  res = await fetchCortex(`${BASE}/api/intake/discard`, {
@@ -1577,12 +1595,17 @@ function renderNudge(payload) {
1577
1595
  description:
1578
1596
  'The THIRD terminal outcome, and the right one whenever the honest answer is "this is not mine to decide." Use it when a claimed unit is someone else\'s private content, when the destination brain is a real judgment call rather than a lookup, or when publishing and destroying are both wrong — a stranger\'s message thread, a photo you cannot place, an email whose brain depends on context only the owner has. Destroys NOTHING: the unit stays encrypted and intact, its state becomes `awaiting_user`, and your `question` is what the owner actually sees. Prefer this over letting a lease lapse — a lapsed lease says nothing, increments `attempts`, and hands the identical dead end to the next session. Requires the claim, one item per call.',
1579
1597
  inputSchema: {
1580
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1598
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1599
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1581
1600
  question: z.string().describe('what you need the owner to decide, in their words not yours — this is the entire message they get, so "which brain should this iMessage thread go to, if any?" beats "needs triage"'),
1582
1601
  options: z.array(z.string()).optional().describe('the concrete choices, when the decision is a pick rather than an open question — e.g. ["Personal","TTO","Discard — nothing to record"]. Stored on the question and rendered by intake_cleanup_status, so the owner can answer with a choice instead of prose.'),
1583
1602
  },
1584
1603
  },
1585
- async ({ intakeItemId, question, options }) => {
1604
+ async (args) => {
1605
+ const { question, options } = args
1606
+ const unit = intakeIdFrom(args)
1607
+ if (unit.error) return toolError(unit.error)
1608
+ const intakeItemId = unit.id
1586
1609
  let res
1587
1610
  try {
1588
1611
  res = await fetchCortex(`${BASE}/api/intake/defer`, {
@@ -1617,11 +1640,16 @@ function renderNudge(payload) {
1617
1640
  description:
1618
1641
  'Give a claimed unit back to the queue WITHOUT deciding anything. This is not a fourth outcome — materialize, discard and defer resolve a unit; release just ends your hold on it. Use it when you claimed more than you can act on, or when the unit turns out to belong to work you are not doing. Prefer it over letting the lease expire: a lapse and a crash are indistinguishable in the ledger, so silently timing out costs the pile the one signal that says a session looked and chose to pass. Use intake_defer instead when the unit needs the OWNER to decide — release puts it back in front of the next session, which will hit whatever wall you did.',
1619
1642
  inputSchema: {
1620
- intakeItemId: z.string().describe('intake item uuid from intake_claim'),
1643
+ intakeItemId: z.string().optional().describe('intake item uuid from intake_claim (required — intake_item_id is accepted too)'),
1644
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1621
1645
  reason: z.string().describe('why you are handing it back — "claimed too broadly", "not related to this session\'s work". Recorded on the claim, and the only thing distinguishing this from a lapsed lease'),
1622
1646
  },
1623
1647
  },
1624
- async ({ intakeItemId, reason }) => {
1648
+ async (args) => {
1649
+ const { reason } = args
1650
+ const unit = intakeIdFrom(args)
1651
+ if (unit.error) return toolError(unit.error)
1652
+ const intakeItemId = unit.id
1625
1653
  let res
1626
1654
  try {
1627
1655
  res = await fetchCortex(`${BASE}/api/intake/release`, {
@@ -1715,11 +1743,12 @@ function renderNudge(payload) {
1715
1743
  description:
1716
1744
  'Read a PENDING intake unit\'s FULL decrypted body AND its `proposal` — the brain its signals point to, the pages it would attach to, the page that would govern it, the tier it would be born with, and `unclaimedIdentifiers` — without taking a lease. A read RECEIPT, not a claim (ADR-0035). The proposal rides with the look because they answer one question together: what is this, and where would it go — so you can tell whether a unit is yours to judge BEFORE leasing it. Use it when the settling feed\'s headline + candidate identifiers cannot tell you whether the unit belongs to your current work; looking is non-exclusive (any number of sessions may inspect the same unit) and always logged, so look freely rather than guessing from the headline — diligence is subsidized here. After looking: claim it if it is yours to process, or simply move on — no release needed, you never held it. The response includes how many sessions have looked (`looks`/`distinctLookers`): several looks and no claim is a sign the unit needs the triage agent or the owner, not another look. Resolved units are refused — read those as records.',
1717
1745
  inputSchema: {
1718
- intakeItemId: z.string().describe('intake item uuid, from the settling feed (intake_changes) or intake_claim'),
1746
+ intakeItemId: z.string().optional().describe('intake item uuid, from the settling feed (intake_changes) or intake_claim (required — intake_item_id is accepted too)'),
1747
+ intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
1719
1748
  workingIdentifiers: z.array(z.string()).optional().describe('the identifiers your current work touches (repo:…, file:…, project:…) — stored on the receipt; feeds look→claim conversion analysis'),
1720
1749
  },
1721
1750
  },
1722
- async ({ intakeItemId, workingIdentifiers }) => renderIntakeLook(intakeItemId, workingIdentifiers ?? []),
1751
+ async ({ intakeItemId, intake_item_id, workingIdentifiers }) => renderIntakeLook(requireIntakeId({ intakeItemId, intake_item_id }), workingIdentifiers ?? []),
1723
1752
  )
1724
1753
 
1725
1754
  server.registerTool(
@@ -3347,6 +3376,33 @@ function renderNudge(payload) {
3347
3376
  },
3348
3377
  )
3349
3378
 
3379
+ // 2026-10-21. Change who a member reports to — people and AI agents (Theron: "It's got to be real").
3380
+ server.registerTool(
3381
+ 'set_manager',
3382
+ {
3383
+ title: 'Change who someone reports to',
3384
+ description: "Move a member of one of the person's brains — a person or an AI agent — to report to someone else (a person or an agent). ONLY when the person asked you to. Everyone below the member moves with them. This changes permissions: the new manager and everyone above them can see the member's scoped work, and what the manager gives them becomes an assignment instead of a request; the old chain loses both. Reversible: move them back the same way. Only an owner or admin of the brain can do it; nobody reports to themselves or to someone in their own line of reports, and owners stay at the top. Use an email address when a name could match two people.",
3385
+ inputSchema: {
3386
+ member: z.string().describe('who moves: their email address, or their name as it appears in the brain'),
3387
+ manager: z.string().describe('who they will report to: an email address or a name as it appears in the brain'),
3388
+ brain: z.string().optional().describe('which brain — required if the person belongs to more than one'),
3389
+ },
3390
+ },
3391
+ async ({ member, manager, brain }) => {
3392
+ let res
3393
+ try {
3394
+ res = await fetchCortex(`${BASE}/api/members/manager`, {
3395
+ method: 'POST',
3396
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3397
+ body: JSON.stringify({ member, manager, brain }),
3398
+ })
3399
+ } catch (e) { return toolError(`Could not reach Agnoclast: ${e.message}`) }
3400
+ const out = await res.json().catch(() => null)
3401
+ if (!res.ok) return toolError(`Not moved: ${out?.detail ?? out?.error ?? res.status}`)
3402
+ return { content: [{ type: 'text', text: out.summary }] }
3403
+ },
3404
+ )
3405
+
3350
3406
  // 2026-10-12. A member leaves a brain THEMSELVES — the remove_member path with the caller as the target.
3351
3407
  server.registerTool(
3352
3408
  'leave_brain',
@@ -4042,10 +4098,24 @@ function renderNudge(payload) {
4042
4098
  // a session had judged irrelevant was re-offered on every tool call — pilot B's 22-hour
4043
4099
  // re-offer defect (§11), recreated by the fix for the other half of the same problem.
4044
4100
  intake_item_id: z.string().optional().describe('the ARRIVAL id from the ⚡ ARRIVALS SETTLING block (not a record id)'),
4101
+ // 2026-10-22: every other intake verb spells it intakeItemId; one session made 607 failing calls
4102
+ // guessing between the two. Both are accepted — see intake_id_alias.mjs.
4103
+ intakeItemId: z.string().optional().describe('same as intake_item_id — either spelling is accepted'),
4104
+ recordId: z.string().optional().describe('same as record_id — either spelling is accepted'),
4045
4105
  reason: z.string().describe('WHY it is not yours — recorded, and the only thing that can later answer "why did nobody take this"'),
4046
4106
  },
4047
4107
  },
4048
- async ({ record_id, intake_item_id, reason }) => {
4108
+ async (args) => {
4109
+ const { reason } = args
4110
+ const unit = intakeIdFrom(args, { required: false })
4111
+ if (unit.error) return toolError(unit.error)
4112
+ const intake_item_id = unit.id
4113
+ const recCamel = typeof args.recordId === 'string' ? args.recordId.trim() : ''
4114
+ const recSnake = typeof args.record_id === 'string' ? args.record_id.trim() : ''
4115
+ if (recCamel && recSnake && recCamel !== recSnake) {
4116
+ return toolError(`recordId (${recCamel}) and record_id (${recSnake}) disagree — pass one of them.`)
4117
+ }
4118
+ const record_id = recCamel || recSnake || null
4049
4119
  // ⚠ REFUSE BOTH AND NEITHER EXPLICITLY. Tonight's other defect was a parameter accepted by the
4050
4120
  // caller, dropped by the schema, and replaced by a default that did something else entirely.
4051
4121
  // An ambiguous decline must fail loudly, never pick one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.253",
3
+ "version": "0.9.255",
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": {