@theronap/agnoclast-mcp 0.9.257 → 0.9.259
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/filing_obligations.mjs +38 -3
- package/lib/intake_batch_render.mjs +12 -0
- package/lib/locked_tools.mjs +1 -1
- package/lib/server.mjs +75 -15
- package/package.json +1 -1
|
@@ -74,16 +74,42 @@ export function renderFilingObligations(o) {
|
|
|
74
74
|
}
|
|
75
75
|
if (o.write?.undated) lines.push(`⚠ ${o.write.undated} of them has no date, so nothing will raise it by time.`)
|
|
76
76
|
if (o.write?.unhomed) lines.push(`⚠ ${o.write.unhomed} could not be placed: the record is on no page.`)
|
|
77
|
-
|
|
77
|
+
// 2026-10-23: the server reports EACH candidate. Name every one it will not write as sent, and why.
|
|
78
|
+
const notAsSent = renderObligationItems(o.items, { onlyNotProposed: true })
|
|
79
|
+
if (notAsSent.length) {
|
|
80
|
+
lines.push(...notAsSent)
|
|
81
|
+
} else if (Array.isArray(o.unverified) && o.unverified.length) {
|
|
78
82
|
lines.push(
|
|
79
83
|
`⚠ NOT written — not in this text word for word: ${o.unverified.map((e) => `"${e}"`).join('; ')}. ` +
|
|
80
84
|
"Quote the text's own sentence (propose_obligation with this record id).",
|
|
81
85
|
)
|
|
82
86
|
}
|
|
83
|
-
if (o.malformed) lines.push(`⚠ ${o.malformed} malformed obligation(s) were dropped.`)
|
|
87
|
+
if (o.malformed && !Array.isArray(o.items)) lines.push(`⚠ ${o.malformed} malformed obligation(s) were dropped.`)
|
|
88
|
+
if (o.write?.kept) lines.push(`${o.write.kept} matched an obligation the person already acted on — left as they left it, not written again.`)
|
|
84
89
|
return lines
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
/**
|
|
93
|
+
* PURE. One line per obligation the server judged (`items`, server 2026-10-23): proposed, deduped (and against
|
|
94
|
+
* what), or rejected (and why). `onlyNotProposed` keeps just the ones that will NOT be written as sent. Empty
|
|
95
|
+
* for a server that sends no `items`, so an older server never reads as "nothing dropped".
|
|
96
|
+
*/
|
|
97
|
+
export function renderObligationItems(items, { onlyNotProposed = false } = {}) {
|
|
98
|
+
if (!Array.isArray(items) || items.length === 0) return []
|
|
99
|
+
const shown = onlyNotProposed ? items.filter((x) => x?.outcome !== 'proposed') : items
|
|
100
|
+
if (shown.length === 0) return []
|
|
101
|
+
const label = (x) => `obligations[${x.index}]${x.subject ? ` "${x.subject}"` : ''}`
|
|
102
|
+
const line = (x) => x.outcome === 'proposed'
|
|
103
|
+
? ` · ${label(x)} — proposed`
|
|
104
|
+
: x.outcome === 'deduped'
|
|
105
|
+
? ` · ${label(x)} — deduped: same sentence as ${x.against ?? 'another in this filing'} (one proposal, not two)`
|
|
106
|
+
: ` ⚠ ${label(x)} — REJECTED, not written: ${x.reason ?? 'refused'}`
|
|
107
|
+
const p = items.filter((x) => x?.outcome === 'proposed').length
|
|
108
|
+
const d = items.filter((x) => x?.outcome === 'deduped').length
|
|
109
|
+
const r = items.filter((x) => x?.outcome === 'rejected').length
|
|
110
|
+
return [`Obligations sent: ${items.length} — ${p} proposed, ${d} deduped, ${r} rejected:`, ...shown.map(line)]
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
/** PURE. The same, folded onto one line for a batch's per-unit detail. */
|
|
88
114
|
export function filingObligationsBrief(o) {
|
|
89
115
|
if (!o || typeof o !== 'object') return ''
|
|
@@ -91,6 +117,15 @@ export function filingObligationsBrief(o) {
|
|
|
91
117
|
const bits = []
|
|
92
118
|
if (o.sent === 0) bits.push('asks nothing')
|
|
93
119
|
else if (o.write?.proposed) bits.push(`${o.write.proposed} obligation(s) proposed`)
|
|
94
|
-
if (Array.isArray(o.
|
|
120
|
+
if (Array.isArray(o.items)) {
|
|
121
|
+
// 2026-10-23: name each one not written as sent, so a batch never hides which was dropped.
|
|
122
|
+
for (const x of o.items) {
|
|
123
|
+
if (x?.outcome === 'deduped') bits.push(`obligations[${x.index}] deduped (same sentence as ${x.against ?? 'another'})`)
|
|
124
|
+
if (x?.outcome === 'rejected') bits.push(`⚠ obligations[${x.index}] rejected: ${x.reason ?? 'refused'}`)
|
|
125
|
+
}
|
|
126
|
+
} else if (Array.isArray(o.unverified) && o.unverified.length) {
|
|
127
|
+
bits.push(`⚠ ${o.unverified.length} quote(s) not in the text, not written`)
|
|
128
|
+
}
|
|
129
|
+
if (o.write?.kept) bits.push(`${o.write.kept} already acted on, left as is`)
|
|
95
130
|
return bits.length ? ` · ${bits.join(', ')}` : ''
|
|
96
131
|
}
|
|
@@ -24,3 +24,15 @@ export function renderConversationOffer(x, why = '', looked = '') {
|
|
|
24
24
|
return ` - 📱 ${x.conversation.count} texts in one conversation · newest ${x.hoursAgo}h ago · ${ids}${why}${looked}\n` +
|
|
25
25
|
' decide them together: `intake_look` the newest, `intake_claim` them all, then file them in one `intake_materialize` (intakeItemIds) — one record each'
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A batch PREVIEW (2026-10-23), per id: until then a preview with intakeItemIds covered only the first id, so
|
|
30
|
+
* the other units' obligations were never checked and nothing said so. Each unit is previewed on its own call
|
|
31
|
+
* with its own obligations; `line` is that unit's rendered preview. Nothing is written by any of them.
|
|
32
|
+
*/
|
|
33
|
+
export function renderPreviewBatch(results) {
|
|
34
|
+
const refuse = results.filter((r) => !r.ok || r.wouldApply === false)
|
|
35
|
+
return `PREVIEW of a batch of ${results.length} — nothing was written. ` +
|
|
36
|
+
`${results.length - refuse.length} would file as sent, ${refuse.length} would not.\n` +
|
|
37
|
+
results.map((r) => `\n── ${r.id}\n${r.ok ? r.line : `✗ preview failed — ${r.detail}`}`).join('\n')
|
|
38
|
+
}
|
package/lib/locked_tools.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export const REGULAR_ONLY_TOOLS = [
|
|
|
22
22
|
'list_records', 'read_record', 'timeline_pull', 'record_status', 'set_record_notes', 'set_record_privacy',
|
|
23
23
|
'relabel_record_people', 'supersede_record', 'move_record', 'contain_record', 'pending_records', 'claim_record',
|
|
24
24
|
'route_record', 'unroute_record', 'request_file', 'get_file', 'create_custom_source', 'search_spam_email',
|
|
25
|
-
'gmail_modify', 'draft_email_reply', 'request_send_email', 'send_imessage', '
|
|
25
|
+
'gmail_modify', 'draft_email_reply', 'request_send_email', 'send_imessage', 'add_to_brain',
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
/** What a locked tool answers when the check itself could not be made. Nothing was run. */
|
package/lib/server.mjs
CHANGED
|
@@ -24,9 +24,9 @@ import { spawnObligationWorker } from './obligations_worker.mjs'
|
|
|
24
24
|
import { renderObligations, renderPageObligations, tierReads, who } from './obligations_render.mjs'
|
|
25
25
|
import { renderLeaveQuestion, renderLeft, renderRemovalQuestion, renderRemoved } from './member_remove_render.mjs'
|
|
26
26
|
import { renderSettings } from './settings_render.mjs'
|
|
27
|
-
import { renderConversationOffer, renderMaterializeBatch } from './intake_batch_render.mjs'
|
|
27
|
+
import { renderConversationOffer, renderMaterializeBatch, renderPreviewBatch } from './intake_batch_render.mjs'
|
|
28
28
|
import { intakeIdFrom, requireIntakeId, INTAKE_ID_ALIAS_NOTE } from './intake_id_alias.mjs'
|
|
29
|
-
import { obligationsForUnits, renderFilingObligations, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
|
|
29
|
+
import { obligationsForUnits, renderFilingObligations, renderObligationItems, filingObligationsBrief, OBLIGATIONS_UNANSWERED_TEXT } from './filing_obligations.mjs'
|
|
30
30
|
import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './record_render.mjs'
|
|
31
31
|
import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
|
|
32
32
|
import { formatSeriesRefusal, formatSeriesUpdated } from './series_render.mjs'
|
|
@@ -183,7 +183,26 @@ export function renderIntakePreviewLine(body) {
|
|
|
183
183
|
const pages = Array.isArray(p.pages) ? p.pages.length : 0
|
|
184
184
|
const g = p.governing
|
|
185
185
|
const gov = g ? `${g.title || g.kind} (${g.tier})${p.governingJudged ? ', your choice' : ''}` : 'none'
|
|
186
|
-
|
|
186
|
+
const lines = [`PREVIEW — nothing was written. Would attach ${pages} page(s); governing: ${gov}; born ${p.tier ?? 'unknown'}.`]
|
|
187
|
+
// 2026-10-23: whether apply would take THIS body, and what it would do with each answer sent. Every line
|
|
188
|
+
// below is printed only when the server sent its field, so an older server renders exactly as before.
|
|
189
|
+
if (body?.wouldApply === false && Array.isArray(body.applyWouldRefuse)) {
|
|
190
|
+
for (const r of body.applyWouldRefuse) lines.push(`⚠ apply would REFUSE this call: ${r?.error}${r?.detail ? ` — ${r.detail}` : ''}`)
|
|
191
|
+
} else if (body?.wouldApply === true) {
|
|
192
|
+
lines.push('Apply would accept this call as sent.')
|
|
193
|
+
}
|
|
194
|
+
const fd = body?.folderDecisions
|
|
195
|
+
if (fd && Array.isArray(fd.answers)) {
|
|
196
|
+
const ok = fd.answers.filter((a) => a.verdict === 'accepted')
|
|
197
|
+
const bad = fd.answers.filter((a) => a.verdict === 'rejected')
|
|
198
|
+
const open = Array.isArray(fd.open) ? fd.open : []
|
|
199
|
+
lines.push(`Folder answers: ${ok.length} accepted, ${bad.length} rejected; ${open.length} question(s) still open.`)
|
|
200
|
+
for (const a of ok) lines.push(` ✓ folderDecisions[${a.index}] ${a.action} \`${a.identifier}\`${a.page ? ` onto "${a.page.title}" (${a.page.tier})` : ''} — answers ${(a.answers ?? []).length}`)
|
|
201
|
+
for (const a of bad) lines.push(` ✗ folderDecisions[${a.index}] ${a.action ?? '?'} \`${a.identifier ?? '?'}\` — ${a.error}${a.detail ? `: ${a.detail}` : ''}`)
|
|
202
|
+
for (const q of open) lines.push(` ? still open: \`${q.identifier}\``)
|
|
203
|
+
}
|
|
204
|
+
lines.push(...renderObligationItems(body?.obligations?.items))
|
|
205
|
+
return lines.join('\n')
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
// TOP-OF-PAGE NOTES — ADR-0065's size note, and ADR-0064 §3's "Also visible to you" callout.
|
|
@@ -1206,12 +1225,13 @@ function renderNudge(payload) {
|
|
|
1206
1225
|
description:
|
|
1207
1226
|
'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only. Requires x-cortex-session-key (set automatically by this MCP server). Aged work never blocks a claim: the response reports cleanupDueCount and how far behind the oldest unit is, and servicing it is expected but always your call. Claiming is a commitment to process — hand back anything you will not finish with intake_release, or intake_defer if it is the owner\'s decision to make. ' +
|
|
1208
1227
|
'YOU ARE THE JUDGE of every unit you claim (ADR-0062): nothing is published until you materialize, and materializing is when it becomes visible. Each unit carries a `proposal` — the brain its signals point to and, for that brain, the pages it would attach to, the page that would govern it, and the `tier` it would be born with — computed without writing anything. Decide the brain, the pages (confirm, drop or add), which page should govern (the page whose reasons for restricting its audience fit the content, not a person page), and obligations. Pages only tighten a tier; if a record should be LOOSER than `tierReason.baseline`, that is the owner\'s call — intake_defer with the question. ' +
|
|
1209
|
-
'
|
|
1228
|
+
'DEFAULT VIEW IS "compact" where the server enables it (otherwise the server answers with the full response, whatever you ask): the judge rules, your brains, the lease and the cleanup count come ONCE per response in `rules`/`brains`, not on every unit; the proposal is flattened onto each unit — `brain`, the proposed brain\'s `pages` as one line each, `governing`, `tier` (and `baseline` when it differs) — or `candidates` when no brain is proposed; each unit carries its full content, and up front what intake_materialize would refuse it for (`materialize.otherHomes`, `materialize.folderQuestions`). Read `rules` before judging. Pass view "full" for the per-unit `proposal` object with every brain\'s placement.',
|
|
1210
1229
|
inputSchema: {
|
|
1211
1230
|
claimKind: z.enum(['relevance', 'cleanup']).optional().describe('default relevance'),
|
|
1212
1231
|
view: z.enum(['full', 'compact']).optional().describe(
|
|
1213
|
-
'default "
|
|
1214
|
-
'brain\'s placement. Honoured only where the server enables it; otherwise the response is the full one.'
|
|
1232
|
+
'default "compact": the rules once per response instead of on every unit, and only the proposed ' +
|
|
1233
|
+
'brain\'s placement. Honoured only where the server enables it; otherwise the response is the full one. ' +
|
|
1234
|
+
'"full" asks for the full per-unit `proposal` (every brain\'s placement, the judge text on each unit).',
|
|
1215
1235
|
),
|
|
1216
1236
|
limit: z.number().optional().describe('max items (default 10). Ignored when intakeItemIds is given.'),
|
|
1217
1237
|
source: z.string().optional().describe('claim only the oldest units of ONE source — e.g. "imessage", "meeting", "email", "github", "claude-code". Reaches texts without first claiming every older GitHub/session unit ahead of them. Ignored when intakeItemIds is given.'),
|
|
@@ -1259,8 +1279,11 @@ function renderNudge(payload) {
|
|
|
1259
1279
|
// claim into a no-op.
|
|
1260
1280
|
...(intakeItemIds ? { intakeItemIds } : {}),
|
|
1261
1281
|
...(source ? { source } : {}),
|
|
1262
|
-
//
|
|
1263
|
-
|
|
1282
|
+
// Compact by DEFAULT (Theron, 2026-10-23, after a watched real-filing trial: 26 units, 25 filed
|
|
1283
|
+
// correctly, 0 refusals, ~3.9K vs ~10K chars per unit). Always sent, so an explicit "full" is
|
|
1284
|
+
// honoured. The server still gates compact on RELEASE_FLAGS.compact_claim — an account outside
|
|
1285
|
+
// that cohort gets the full response even though this asks for compact.
|
|
1286
|
+
view: view ?? 'compact',
|
|
1264
1287
|
}),
|
|
1265
1288
|
})
|
|
1266
1289
|
if (!res.ok) {
|
|
@@ -1304,12 +1327,12 @@ function renderNudge(payload) {
|
|
|
1304
1327
|
{
|
|
1305
1328
|
title: 'Materialize a private intake item',
|
|
1306
1329
|
description:
|
|
1307
|
-
'Atomically publish a claimed intake item into one brain — the disclosure moment (ADR-0062). Deterministic identifier homes
|
|
1308
|
-
'Name the governing page with governingPage (one of documentIds, never a person page)
|
|
1309
|
-
'Pass mode "preview"
|
|
1310
|
-
'⚠ ADR-0051 — BEFORE you file, look for this unit\'s OTHER homes: the pages it is about beyond what an identifier matched (the design
|
|
1311
|
-
'⚠ A meeting or lecture TRANSCRIPT files with NOTES in `summary
|
|
1312
|
-
'⚠ ADR-0077: a GitHub commit or PR does not finish until every changed AREA FOLDER has an answer
|
|
1330
|
+
'Atomically publish a claimed intake item into one brain — the disclosure moment (ADR-0062). Deterministic identifier homes there are always attached; documentIds add pages. The record is born at the strictest of its source baseline, the producer\'s ask and every attached page; a source floor (iMessage, claude-code) pins it. ' +
|
|
1331
|
+
'Name the governing page with governingPage (one of documentIds, never a person page), else the ranking picks it. ' +
|
|
1332
|
+
'Pass mode "preview" when your choice differs from the proposal: it returns the pages, governing page and tier this exact call would produce, whether apply would accept it (folderDecisions and obligations included), and writes nothing. Never writes private intake into search/history before this call. ' +
|
|
1333
|
+
'⚠ ADR-0051 — BEFORE you file, look for this unit\'s OTHER homes: the pages it is about beyond what an identifier matched (the design, project, person or decision). grep or read for them and name them in documentIds. A call naming nothing beyond the deterministic homes still FILES on those only, and says so — add pages later with route_record. ' +
|
|
1334
|
+
'⚠ A meeting or lecture TRANSCRIPT files with NOTES in `summary`: gist, decisions, action items, dates, names spelled right — else REFUSED; the transcript is kept unchanged. ' +
|
|
1335
|
+
'⚠ ADR-0077: a GitHub commit or PR does not finish until every changed AREA FOLDER has an answer; unanswered, the call is REFUSED with the open questions. Answer each in folderDecisions (a page to claim it onto, or decline "never") and call again. A "never" on a folder also silences every unclaimed subfolder beneath it, so use it only when nothing under that folder deserves its own page. An earlier "not_yet" (`earlier`) no longer answers a GitHub folder question: claim the folder onto a page about it, or decline "never". A file directly in a grab-bag folder (e.g. db/supabase/) is never asked about.',
|
|
1313
1336
|
inputSchema: {
|
|
1314
1337
|
intakeItemId: z.string().optional().describe('intake item uuid (required — intake_item_id is accepted too)'),
|
|
1315
1338
|
intake_item_id: z.string().optional().describe(INTAKE_ID_ALIAS_NOTE),
|
|
@@ -1318,7 +1341,7 @@ function renderNudge(payload) {
|
|
|
1318
1341
|
'Any units you hold a live claim on may be batched, even from separate intake_claim calls — except that texts from ' +
|
|
1319
1342
|
'DIFFERENT text conversations never share a batch. Claim them with `intake_claim`, then pass ' +
|
|
1320
1343
|
'the first as intakeItemId and the rest here. title/summary/dedupeKey are ignored in a batch (each unit keeps its ' +
|
|
1321
|
-
'own), and a preview covers
|
|
1344
|
+
'own), and a preview covers every id, each with its own obligations. Each id is reported separately; a failure never hides behind a success.',
|
|
1322
1345
|
),
|
|
1323
1346
|
orgId: z.string().describe('destination brain org uuid'),
|
|
1324
1347
|
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'),
|
|
@@ -1382,6 +1405,43 @@ function renderNudge(payload) {
|
|
|
1382
1405
|
const ob = obligationsForUnits(batch, args.obligations)
|
|
1383
1406
|
if (ob.errors.length) return toolError(`Nothing was filed — fix these obligations first:\n${ob.errors.map((e) => ` · ${e}`).join('\n')}`)
|
|
1384
1407
|
const obligationsFor = (id) => (ob.byUnit ? { obligations: ob.byUnit.get(id) ?? [] } : {})
|
|
1408
|
+
// A BATCH PREVIEW (2026-10-23): every id, each on its own preview call with ITS OWN obligations — the
|
|
1409
|
+
// same body the batch apply below sends, so what it reports is what apply would do. Until then a preview
|
|
1410
|
+
// covered intakeItemId only, and the other units' obligations were silently never checked.
|
|
1411
|
+
if (batch.length > 1 && args.mode === 'preview') {
|
|
1412
|
+
const results = []
|
|
1413
|
+
for (const id of batch) {
|
|
1414
|
+
try {
|
|
1415
|
+
const r = await fetchCortex(`${BASE}${intakeMaterializePath('preview')}`, {
|
|
1416
|
+
method: 'POST',
|
|
1417
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1418
|
+
body: JSON.stringify({
|
|
1419
|
+
intakeItemId: id,
|
|
1420
|
+
orgId: args.orgId,
|
|
1421
|
+
documentIds: args.documentIds ?? [],
|
|
1422
|
+
record: { source: args.source, record_type: args.recordType },
|
|
1423
|
+
attachmentMeta: (args.documentIds ?? []).map(() => ({ origin: args.origin ?? 'llm' })),
|
|
1424
|
+
...(args.governingPage ? { governingDocumentId: args.governingPage } : {}),
|
|
1425
|
+
...(args.searchedForOtherHomes ? { searchedForOtherHomes: true } : {}),
|
|
1426
|
+
sameBatchAs: batch[0],
|
|
1427
|
+
...obligationsFor(id),
|
|
1428
|
+
}),
|
|
1429
|
+
})
|
|
1430
|
+
const o = await r.json().catch(() => null)
|
|
1431
|
+
if (r.status === 404 && !o?.error) {
|
|
1432
|
+
results.push({ id, ok: false, detail: 'this server cannot preview a materialization yet — nothing was written' })
|
|
1433
|
+
} else if (!r.ok) {
|
|
1434
|
+
results.push({ id, ok: false, detail: `${o?.error ?? `HTTP ${r.status}`}${o?.hint ? ` — ${o.hint}` : o?.detail ? ` — ${o.detail}` : ''}` })
|
|
1435
|
+
} else {
|
|
1436
|
+
results.push({ id, ok: true, wouldApply: o?.wouldApply, line: renderIntakePreviewLine(o), body: o })
|
|
1437
|
+
}
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
results.push({ id, ok: false, detail: e.message })
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const notes = ob.notes.length ? `\n${ob.notes.map((n) => `⚠ ${n}`).join('\n')}` : ''
|
|
1443
|
+
return { content: [{ type: 'text', text: `${renderPreviewBatch(results)}${notes}\n\n${JSON.stringify(results.map((r) => r.body ?? { id: r.id, error: r.detail }), null, 2)}` }] }
|
|
1444
|
+
}
|
|
1385
1445
|
if (batch.length > 1 && args.mode !== 'preview') {
|
|
1386
1446
|
const results = []
|
|
1387
1447
|
for (const id of batch) {
|
package/package.json
CHANGED