@theronap/agnoclast-mcp 0.9.170 → 0.9.172

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 +93 -1
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -2165,7 +2165,24 @@ function renderNudge(payload) {
2165
2165
  // managed to list these rows for weeks without making one of them actionable.
2166
2166
  lines.push(` ${a.stagedId}`)
2167
2167
  lines.push(` ${a.title ?? '(no subject)'} · ${a.sourceType} · ${a.occurredAt?.slice(0, 10) ?? ''}`)
2168
- if (a.blockingIdentifiers?.length) {
2168
+ // 🔴 A QUESTION IS ALREADY OUT — say so FIRST, before any "held because" reason.
2169
+ //
2170
+ // Caught by using the feature once on real data, 2026-09-24, minutes after shipping it:
2171
+ // `staged_defer` wrote the question, `intake_cleanup_status` showed it to the owner, and
2172
+ // this listing rendered the arrival IDENTICALLY to three nobody had ever looked at. So the
2173
+ // next session would judge it from scratch and hit the same wall — which is the exact
2174
+ // failure the defer verb was built to end.
2175
+ //
2176
+ // ⚠ AND IT WAS IN THE API RESPONSE THE WHOLE TIME. `listStagedArrivals` returns
2177
+ // `deferredQuestion`, with an engine test asserting it. The test passed. What was missing
2178
+ // was a reader — the eighth instance of that shape this week, written into the very change
2179
+ // whose stated purpose was avoiding it. An engine-level test cannot catch it, because the
2180
+ // engine is not the surface anyone reads.
2181
+ if (a.deferredQuestion) {
2182
+ const asked = String(a.deferredQuestion.askedAt ?? '').slice(0, 10)
2183
+ lines.push(` ⏸ ASKED THE OWNER${asked ? ` on ${asked}` : ''} — do not re-judge this one: "${a.deferredQuestion.prompt}"`)
2184
+ lines.push(' Still pending on purpose: it needs placing once they answer. Their answer is in `intake_cleanup_status.openQuestions`.')
2185
+ } else if (a.blockingIdentifiers?.length) {
2169
2186
  lines.push(` blocked by: ${a.blockingIdentifiers.join(', ')}`)
2170
2187
  } else {
2171
2188
  lines.push(' held because no brain could be determined (no unknown identifiers)')
@@ -4089,6 +4106,81 @@ function renderNudge(payload) {
4089
4106
  },
4090
4107
  )
4091
4108
 
4109
+ // ⭐ THE OTHER HALF OF set_routing_identifier (2026-09-24). A claim was ONE CALL to create and,
4110
+ // through this server, impossible to remove — so a wrong brain decision was permanent. That
4111
+ // contradicts the thing ADR-0066 is built on: filing is a judgment, and judgments get revised.
4112
+ //
4113
+ // ⚠ THE ENGINE AND THE ENDPOINT BOTH ALREADY EXISTED. `clearPageRoutingIdentifier` and the DELETE
4114
+ // on /api/brain/routing-identifiers have been there the whole time; only the agent-facing verb was
4115
+ // missing, so the capability was real and unreachable. Ninth instance of that shape this week.
4116
+ server.registerTool(
4117
+ 'unclaim_routing_identifier',
4118
+ {
4119
+ title: 'Remove a routing claim from a page',
4120
+ description:
4121
+ 'Remove an identifier claim from a page — the counterpart to `set_routing_identifier`. Use it when a claim sends mail to the wrong brain, when a page was claimed by mistake, or when TWO brains claim the same key and you want one of them to stop (that is the case where nothing routes at all, because a split stages rather than picking). ' +
4122
+ '⭐ THE RESPONSE TELLS YOU WHAT ROUTING DOES NOW, which is the reason to call it: removing a claim moves the identifier between "routes", "stages because nothing claims it" and "stages because several do", and the delete count alone cannot tell you which. Computed with the router\'s own logic. ' +
4123
+ '⚠ HISTORY IS NOT UNDONE BY DEFAULT. `set_routing_identifier` HEALS history — it attaches every record already carrying the identifier — but removing the claim leaves those attachments in place, and the response says how many. That asymmetry is deliberate: a claim is a ROUTING RULE, an attachment is a RECORD\'S PLACEMENT, and one such claim was measured accounting for 1,271 attachments on a single page. Pass `detach: true` only when you mean to unfile them too; it never touches a governing attachment, or one a person or an LLM made rather than this claim. ' +
4124
+ '⚠ REVERSIBLE BY CONSTRUCTION: `set_routing_identifier` puts it back in one call and re-heals history. No content is destroyed here, which is why this needs no dry run.',
4125
+ inputSchema: {
4126
+ kind: z.enum(['project', 'person', 'org', 'user']).describe('the page kind'),
4127
+ name: z.string().optional().describe('page title (or pass ref)'),
4128
+ ref: z.string().optional().describe('node ref from read_page — prefer over name when available'),
4129
+ brain: z.string().optional().describe('brain label when the title is ambiguous across brains — REQUIRED in practice when two brains hold a page of the same name, which is the usual reason you are here'),
4130
+ identifier: z.string().describe('the claim to remove, e.g. email:someone@example.com or repo:owner/name. See `my_routing_claims` for what is claimed where'),
4131
+ detach: z.boolean().optional().describe('also remove the attachments this claim made (default false). Leaves governing attachments and anything a person or LLM attached'),
4132
+ },
4133
+ },
4134
+ async ({ kind, name, ref, brain, identifier, detach }) => {
4135
+ if (!name && !ref) return toolError('Pass name or ref')
4136
+ let res
4137
+ try {
4138
+ res = await fetchCortex(`${BASE}/api/brain/routing-identifiers`, {
4139
+ method: 'DELETE',
4140
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
4141
+ body: JSON.stringify({ kind, name, ref, brain, identifier, detach: detach === true }),
4142
+ })
4143
+ } catch (e) {
4144
+ return toolError(`Could not remove the routing claim: ${e.message}`)
4145
+ }
4146
+ const out = await res.json().catch(() => null)
4147
+ if (!res.ok) {
4148
+ if (out?.candidates?.length) {
4149
+ return toolError(`That page title is ambiguous across brains — pass \`brain\`. Candidates: ${out.candidates.join(', ')}`)
4150
+ }
4151
+ return toolError(`Could not remove the routing claim: ${out?.error ?? res.status}`)
4152
+ }
4153
+ // A ZERO IS A VERDICT, NOT AN ERROR — the same convention my_routing_claims states. "No such
4154
+ // claim" is worth saying plainly, because the usual cause is that the claim is on a DIFFERENT
4155
+ // page (or in a different brain) than the caller assumed, and retrying the same call will not
4156
+ // find it either.
4157
+ if (!out?.removed) {
4158
+ return {
4159
+ content: [{
4160
+ type: 'text',
4161
+ text: `No claim on ${identifier} was found on that page, so nothing changed. That usually means the claim lives on a different page or in another brain — \`my_routing_claims ${identifier}\` shows every page that claims it.`,
4162
+ }],
4163
+ }
4164
+ }
4165
+ const left = typeof out?.attachmentsLeft === 'number' ? out.attachmentsLeft : null
4166
+ const historyLine = out?.detached
4167
+ ? `\n ${out.detached} attachment(s) this claim had made were also removed; governing attachments and anything a person or an LLM attached were left alone.`
4168
+ : left
4169
+ ? `\n ⚠ ${left} record(s) this claim previously attached to that page are STILL attached — removing the rule does not unfile history. Re-run with \`detach: true\` if they should come off too.`
4170
+ : ''
4171
+ return {
4172
+ content: [{
4173
+ type: 'text',
4174
+ text:
4175
+ `Removed the claim on ${out?.identifier ?? identifier}.\n` +
4176
+ ` ➜ ${out?.routing?.verdict ?? '(routing outcome unavailable)'}` +
4177
+ historyLine +
4178
+ `\n Reversible: \`set_routing_identifier\` puts it back and re-attaches history.`,
4179
+ }],
4180
+ }
4181
+ },
4182
+ )
4183
+
4092
4184
  // ── Gate 4 record triage, over timeline_claims ────────────────────────────────────────────
4093
4185
  // A connector event materializes in seconds and has no idea what the work WAS. The session that
4094
4186
  // did the work knows exactly, and arrives later. These three tools are that handoff: look at what
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.170",
3
+ "version": "0.9.172",
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": {