@theronap/agnoclast-mcp 0.9.169 → 0.9.170
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/server.mjs +151 -23
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1116,6 +1116,11 @@ function renderNudge(payload) {
|
|
|
1116
1116
|
if (preview && res.status === 404) {
|
|
1117
1117
|
return toolError('This server cannot preview a materialization yet — nothing was written. Materialize without mode, or wait for the server to update.')
|
|
1118
1118
|
}
|
|
1119
|
+
// Same redirect-not-dispatch rule as place_staged_record. A staged arrival needs no claim and
|
|
1120
|
+
// no unseal, so the fix is a different verb rather than a retry of this one.
|
|
1121
|
+
if (res.status === 404 && (await pileHolding(intakeItemId)) === 'staged') {
|
|
1122
|
+
return toolError(`${intakeItemId} is a STAGED arrival, not an intake unit — \`intake_materialize\` cannot act on it. Use \`place_staged_record\` with the pages it belongs to; staged arrivals take no lease, so there is nothing to claim or release. (\`staged_look\` reads it first, and carries the brain-ladder proposal.)`)
|
|
1123
|
+
}
|
|
1119
1124
|
return toolError(body)
|
|
1120
1125
|
}
|
|
1121
1126
|
const body = await res.json()
|
|
@@ -1303,33 +1308,13 @@ function renderNudge(payload) {
|
|
|
1303
1308
|
{
|
|
1304
1309
|
title: 'Read an intake unit without claiming it',
|
|
1305
1310
|
description:
|
|
1306
|
-
'Read a PENDING intake unit\'s FULL decrypted body without taking a lease
|
|
1311
|
+
'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.',
|
|
1307
1312
|
inputSchema: {
|
|
1308
1313
|
intakeItemId: z.string().describe('intake item uuid, from the settling feed (intake_changes) or intake_claim'),
|
|
1309
1314
|
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'),
|
|
1310
1315
|
},
|
|
1311
1316
|
},
|
|
1312
|
-
async ({ intakeItemId, workingIdentifiers }) =>
|
|
1313
|
-
let res
|
|
1314
|
-
try {
|
|
1315
|
-
res = await fetchCortex(`${BASE}/api/intake/look`, {
|
|
1316
|
-
method: 'POST',
|
|
1317
|
-
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1318
|
-
body: JSON.stringify({ intakeItemId, workingIdentifiers: workingIdentifiers ?? [] }),
|
|
1319
|
-
})
|
|
1320
|
-
} catch (e) {
|
|
1321
|
-
return toolError(`Could not look: ${e.message}`)
|
|
1322
|
-
}
|
|
1323
|
-
const out = await res.json().catch(() => null)
|
|
1324
|
-
if (!res.ok) {
|
|
1325
|
-
if (out?.error === 'not_lookable') {
|
|
1326
|
-
return toolError(`Not lookable: the unit is ${out?.state ?? 'resolved'} — resolved units are read as records, not looks.`)
|
|
1327
|
-
}
|
|
1328
|
-
if (out?.error === 'not_found') return toolError('No such intake unit in your account.')
|
|
1329
|
-
return toolError(`Could not look: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
|
|
1330
|
-
}
|
|
1331
|
-
return { content: [{ type: 'text', text: withAskNotice(JSON.stringify(out, null, 2)) }] }
|
|
1332
|
-
},
|
|
1317
|
+
async ({ intakeItemId, workingIdentifiers }) => renderIntakeLook(intakeItemId, workingIdentifiers ?? []),
|
|
1333
1318
|
)
|
|
1334
1319
|
|
|
1335
1320
|
server.registerTool(
|
|
@@ -2216,7 +2201,52 @@ function renderNudge(payload) {
|
|
|
2216
2201
|
stagedId: z.string().describe('staged arrival id, from `staged_records` or the session-start arrivals block'),
|
|
2217
2202
|
},
|
|
2218
2203
|
},
|
|
2219
|
-
async ({ stagedId }) =>
|
|
2204
|
+
async ({ stagedId }) => renderStagedLook(stagedId),
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2207
|
+
// ⭐ ONE LOOK VERB, WHICHEVER PILE THE ID CAME FROM (ADR-0066 §1, the interface half).
|
|
2208
|
+
//
|
|
2209
|
+
// The two arrival piles are different tables with different keys, different sealing and different
|
|
2210
|
+
// state models — collapsing the STORAGE is not on the table. What was actually costing decisions
|
|
2211
|
+
// is that an agent holding an id could not tell which pile it came from, and the wrong verb
|
|
2212
|
+
// answered "No such intake unit in your account." — a valid id, a real pending arrival, and an
|
|
2213
|
+
// error that reads as "this does not exist". This file already records the same confusion from the
|
|
2214
|
+
// other direction: "`intake_changes` returns ids from a different queue that `place` rejects.
|
|
2215
|
+
// Twice in twelve hours a staged arrival could only be placed because a human read an id off a
|
|
2216
|
+
// screen and pasted it."
|
|
2217
|
+
//
|
|
2218
|
+
// ⚠ READS DISPATCH, WRITES REDIRECT. Looking is free, non-exclusive and side-effect-free in both
|
|
2219
|
+
// piles, so resolving the id and answering the question the caller actually asked is pure gain.
|
|
2220
|
+
// The write verbs deliberately do NOT dispatch: `place_staged_record` and `intake_materialize`
|
|
2221
|
+
// have different semantics (one replays a body through ingest, one unseals and publishes), so
|
|
2222
|
+
// silently running the other one would be a different action than the caller requested. They name
|
|
2223
|
+
// the right verb and stop.
|
|
2224
|
+
function otherPileHint(id, pile) {
|
|
2225
|
+
return pile === 'staged'
|
|
2226
|
+
? `That id is a STAGED arrival, not an intake unit. Staged verbs: \`staged_look\`, \`place_staged_record\`, \`discard_staged\`.`
|
|
2227
|
+
: `That id is an INTAKE unit, not a staged arrival. Intake verbs: \`intake_look\`, \`intake_claim\`, \`intake_materialize\`, \`intake_discard\`, \`intake_defer\`.`
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/** Does the OTHER pile hold this id? Only ever asked after the named pile said not-found. */
|
|
2231
|
+
async function pileHolding(id) {
|
|
2232
|
+
try {
|
|
2233
|
+
const r = await fetchCortex(`${BASE}/api/staged/look?stagedId=${encodeURIComponent(id)}`, {
|
|
2234
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
2235
|
+
})
|
|
2236
|
+
if (r.ok) return 'staged'
|
|
2237
|
+
} catch { /* fall through — a probe must never fail the caller's own verb */ }
|
|
2238
|
+
try {
|
|
2239
|
+
const r = await fetchCortex(`${BASE}/api/intake/look`, {
|
|
2240
|
+
method: 'POST',
|
|
2241
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2242
|
+
body: JSON.stringify({ intakeItemId: id, workingIdentifiers: [] }),
|
|
2243
|
+
})
|
|
2244
|
+
if (r.ok) return 'intake'
|
|
2245
|
+
} catch { /* same */ }
|
|
2246
|
+
return null
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
async function renderStagedLook(stagedId, { crossedFrom = null } = {}) {
|
|
2220
2250
|
let res
|
|
2221
2251
|
try {
|
|
2222
2252
|
res = await fetchCortex(`${BASE}/api/staged/look?stagedId=${encodeURIComponent(stagedId)}`, {
|
|
@@ -2227,6 +2257,9 @@ function renderNudge(payload) {
|
|
|
2227
2257
|
}
|
|
2228
2258
|
const out = await res.json().catch(() => null)
|
|
2229
2259
|
if (res.status === 404) {
|
|
2260
|
+
if (!crossedFrom && (await pileHolding(stagedId)) === 'intake') {
|
|
2261
|
+
return renderIntakeLook(stagedId, [], { crossedFrom: 'staged' })
|
|
2262
|
+
}
|
|
2230
2263
|
return toolError(`No staged arrival ${stagedId} of yours. Staged rows are per-person and in no brain, so this means it is not yours or does not exist — the two are deliberately indistinguishable.`)
|
|
2231
2264
|
}
|
|
2232
2265
|
if (!res.ok) return toolError(`Could not read staged arrival ${stagedId}: ${out?.error ?? res.status}`)
|
|
@@ -2270,9 +2303,97 @@ function renderNudge(payload) {
|
|
|
2270
2303
|
(l.summary ? `\n summary ${l.summary}\n` : '') +
|
|
2271
2304
|
held +
|
|
2272
2305
|
ladder +
|
|
2306
|
+
(crossedFrom ? `\n\nℹ Resolved by id: you asked with an intake verb and this id is a STAGED arrival, so the staged look is above. ${otherPileHint(stagedId, 'staged')}` : '') +
|
|
2273
2307
|
`\n⚠ This is the ENVELOPE. The message body was never stored (bodyStored: ${l.bodyStored === true}), so you have seen who sent it and what it was called — not what it says. Judge accordingly, and do not report having read it.`,
|
|
2274
2308
|
}],
|
|
2275
2309
|
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
async function renderIntakeLook(intakeItemId, workingIdentifiers = [], { crossedFrom = null } = {}) {
|
|
2313
|
+
let res
|
|
2314
|
+
try {
|
|
2315
|
+
res = await fetchCortex(`${BASE}/api/intake/look`, {
|
|
2316
|
+
method: 'POST',
|
|
2317
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2318
|
+
body: JSON.stringify({ intakeItemId, workingIdentifiers: workingIdentifiers ?? [] }),
|
|
2319
|
+
})
|
|
2320
|
+
} catch (e) {
|
|
2321
|
+
return toolError(`Could not look: ${e.message}`)
|
|
2322
|
+
}
|
|
2323
|
+
const out = await res.json().catch(() => null)
|
|
2324
|
+
if (!res.ok) {
|
|
2325
|
+
if (out?.error === 'not_lookable') {
|
|
2326
|
+
return toolError(`Not lookable: the unit is ${out?.state ?? 'resolved'} — resolved units are read as records, not looks.`)
|
|
2327
|
+
}
|
|
2328
|
+
if (out?.error === 'not_found') {
|
|
2329
|
+
// ⚠ ONE HOP ONLY — `crossedFrom` stops the two renderers bouncing an id nobody holds back
|
|
2330
|
+
// and forth. A genuinely unknown id must cost two probes and then stop.
|
|
2331
|
+
if (!crossedFrom && (await pileHolding(intakeItemId)) === 'staged') {
|
|
2332
|
+
return renderStagedLook(intakeItemId, { crossedFrom: 'intake' })
|
|
2333
|
+
}
|
|
2334
|
+
return toolError('No such intake unit in your account, and no staged arrival of yours has that id either — so it is not yours, or it does not exist.')
|
|
2335
|
+
}
|
|
2336
|
+
return toolError(`Could not look: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
|
|
2337
|
+
}
|
|
2338
|
+
const note = crossedFrom
|
|
2339
|
+
? `ℹ Resolved by id: you asked with a staged verb and this id is an INTAKE unit, so the intake look is below. ${otherPileHint(intakeItemId, 'intake')}\n\n`
|
|
2340
|
+
: ''
|
|
2341
|
+
return { content: [{ type: 'text', text: withAskNotice(note + JSON.stringify(out, null, 2)) }] }
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// ⭐ THE THIRD UNIFORM AFFORDANCE (ADR-0066 §1). Intake has had `intake_defer` since ADR-0062;
|
|
2345
|
+
// staged had nothing, so an agent that correctly judged "this is not mine to decide" could only
|
|
2346
|
+
// walk away — and the queue could not tell that apart from "nobody has looked yet", which wants
|
|
2347
|
+
// the opposite response from the next reader.
|
|
2348
|
+
server.registerTool(
|
|
2349
|
+
'staged_defer',
|
|
2350
|
+
{
|
|
2351
|
+
title: 'Hand an owner-only decision about a staged arrival upward',
|
|
2352
|
+
description:
|
|
2353
|
+
'Record a question about a staged arrival that only its OWNER can answer, instead of leaving the arrival untouched. The staged twin of `intake_defer`. Use it when you have looked (`staged_look`) and the blocker is a decision rather than a lookup: which brain a message belongs to when several defensibly claim it (RUNG 4 says explicitly "do not pick"), or whether something is worth keeping at all. ' +
|
|
2354
|
+
'⚠ NOT A DISCARD AND NOT A PLACEMENT — nothing about the arrival changes. It stays `pending` and keeps its body; the question is what is new, and it lands in `intake_cleanup_status.openQuestions` where the owner answers it. So this is the SAFEST verb in the staged surface, and the right one whenever the alternative is guessing. ' +
|
|
2355
|
+
'⚠ ONE OPEN QUESTION PER ARRIVAL: deferring one that already has an open question returns that question rather than stacking a second. Two sessions hitting the same wall is evidence, not two questions. ' +
|
|
2356
|
+
'A question is REQUIRED — a deferral with no question marks the arrival as handled while telling the owner nothing, which is the walking-away this verb exists to replace.',
|
|
2357
|
+
inputSchema: {
|
|
2358
|
+
stagedId: z.string().describe('staged arrival id, from `staged_records`, `staged_look` or the session-start arrivals block'),
|
|
2359
|
+
question: z.string().describe('what the owner needs to decide, in their terms — name the arrival and the actual fork, e.g. "Elder Walker conference email — Personal or Known By Name? Both brains have pages that could claim it."'),
|
|
2360
|
+
options: z.array(z.string()).optional().describe('the concrete choices when the decision is a pick rather than an open question — e.g. ["Personal","Known By Name","Discard — nothing to record"]. Rendered with the question, so the owner can answer with a choice instead of prose.'),
|
|
2361
|
+
},
|
|
2362
|
+
},
|
|
2363
|
+
async ({ stagedId, question, options }) => {
|
|
2364
|
+
let res
|
|
2365
|
+
try {
|
|
2366
|
+
res = await fetchCortex(`${BASE}/api/staged/defer`, {
|
|
2367
|
+
method: 'POST',
|
|
2368
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2369
|
+
body: JSON.stringify({ stagedId, question, options }),
|
|
2370
|
+
})
|
|
2371
|
+
} catch (e) {
|
|
2372
|
+
return toolError(`Could not defer: ${e.message}`)
|
|
2373
|
+
}
|
|
2374
|
+
const out = await res.json().catch(() => null)
|
|
2375
|
+
if (!res.ok) {
|
|
2376
|
+
if (res.status === 404) {
|
|
2377
|
+
// Same cross-pile courtesy the look verbs give (#1175): an intake id here is the wrong
|
|
2378
|
+
// verb, not a missing arrival.
|
|
2379
|
+
if ((await pileHolding(stagedId)) === 'intake') {
|
|
2380
|
+
return toolError(`${stagedId} is an INTAKE unit, not a staged arrival — use \`intake_defer\` (it needs a claim first, unlike this verb).`)
|
|
2381
|
+
}
|
|
2382
|
+
return toolError(`No staged arrival ${stagedId} of yours. Staged rows are per-person, so this means it is not yours or does not exist.`)
|
|
2383
|
+
}
|
|
2384
|
+
if (out?.error === 'not_pending') {
|
|
2385
|
+
return toolError(`That arrival is already ${out?.detail ?? 'resolved'} — there is nothing left for the owner to decide. Deferring a placed or discarded arrival would queue a question about a decision already made.`)
|
|
2386
|
+
}
|
|
2387
|
+
return toolError(`Could not defer: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`)
|
|
2388
|
+
}
|
|
2389
|
+
return {
|
|
2390
|
+
content: [{
|
|
2391
|
+
type: 'text',
|
|
2392
|
+
text: out?.alreadyOpen
|
|
2393
|
+
? `That arrival already had an open question (${out.questionId}) — returned rather than stacked a second. The owner sees it in \`intake_cleanup_status.openQuestions\`; it has not been asked twice.`
|
|
2394
|
+
: `Deferred to the owner. The arrival is untouched and still pending; question ${out?.questionId} is queued in \`intake_cleanup_status.openQuestions\`. Nothing was placed or discarded.`,
|
|
2395
|
+
}],
|
|
2396
|
+
}
|
|
2276
2397
|
},
|
|
2277
2398
|
)
|
|
2278
2399
|
|
|
@@ -2971,6 +3092,13 @@ function renderNudge(payload) {
|
|
|
2971
3092
|
const out = await res.json().catch(() => null)
|
|
2972
3093
|
if (!res.ok && res.status !== 207) {
|
|
2973
3094
|
const detail = out?.detail ? `\n${out.detail}` : ''
|
|
3095
|
+
// ⚠ WRITES REDIRECT, THEY DO NOT DISPATCH. `place_staged_record` replays a stored body
|
|
3096
|
+
// through ingest; `intake_materialize` unseals and publishes. Running one because the caller
|
|
3097
|
+
// named the other would be a different action than they asked for — so name the verb and
|
|
3098
|
+
// stop. The look verbs dispatch instead, because looking is free and side-effect-free.
|
|
3099
|
+
if (res.status === 404 && ids.length === 1 && (await pileHolding(staged_id)) === 'intake') {
|
|
3100
|
+
return toolError(`${staged_id} is an INTAKE unit, not a staged arrival — \`place_staged_record\` cannot act on it. Claim it with \`intake_claim {intakeItemIds: ["${staged_id}"]}\` and then \`intake_materialize\`. (\`intake_look\` reads it first, free and without a lease.)`)
|
|
3101
|
+
}
|
|
2974
3102
|
return toolError(`Could not place ${staged_id}: ${out?.error ?? res.status}${detail}`)
|
|
2975
3103
|
}
|
|
2976
3104
|
// 207 and the no-record branch both mean the content LANDED and the attach did not. Report
|
package/package.json
CHANGED