@theronap/agnoclast-mcp 0.9.198 → 0.9.199

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.
@@ -64,6 +64,8 @@ const MAX_YOURS = 25
64
64
  // session's context. One line, bounded, and quoted so it reads as the name of a thing.
65
65
  const named = (s) => `“${String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 120) || '(untitled)'}”`
66
66
  const mins = (n) => `${n} min`
67
+ // A person's name, as someone else typed it into a profile: one line, bounded, never quoted as a title.
68
+ const who = (s) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 60) || 'Someone'
67
69
 
68
70
  /**
69
71
  * ⏰ PRESSING — what is about to start or come due (2026-10-02).
@@ -99,6 +101,15 @@ export function formatPressing(pressing) {
99
101
  const more = count > names.length ? ` … and ${count - names.length} more` : ''
100
102
  lines.push(`⏰ Due today (${count}): ${names.join(' · ')}${more}`)
101
103
  }
104
+ // ADR-0073 §2.7 — a hand-off, said once to the one person it concerns. Names and notes are other
105
+ // people's text: quoted and bounded like every title above.
106
+ else if (p?.kind === 'asked') {
107
+ lines.push(`📥 ${who(p.from)} asks you to take: ${named(p.text)}${p.note ? ` — their note: ${named(p.note)}` : ''}. NOT yours until you accept — \`answer_obligation_request\` (${p.obligationId})`)
108
+ } else if (p?.kind === 'given') {
109
+ lines.push(`📥 ${who(p.from)} gave you: ${named(p.text)} — it is yours now. (${p.obligationId})`)
110
+ } else if (p?.kind === 'declined') {
111
+ lines.push(`↩ ${who(p.by)} DECLINED ${named(p.text)}: ${named(p.reason)}. It is still yours. (${p.obligationId})`)
112
+ }
102
113
  // ⚠ NO OVERDUE LINE, DELIBERATELY. Everything dated is raised once BEFORE it lapses; a daily
103
114
  // "Overdue (14)" could only repeat, and repeating finished work is how a reader learns to skip
104
115
  // the line that carries the meeting. The overdue pile is `my_obligations`, when someone asks.
@@ -118,7 +129,8 @@ export function pressingAcks(pressing) {
118
129
  const out = []
119
130
  for (const p of Array.isArray(pressing) ? pressing : []) {
120
131
  if (typeof p?.subject !== 'string' || typeof p?.bucket !== 'string') continue
121
- if (p.kind === 'event' || p.kind === 'due_soon' || (p.kind === 'due_today' && Number(p.count) > 0)) {
132
+ const handoff = p.kind === 'asked' || p.kind === 'given' || p.kind === 'declined'
133
+ if (p.kind === 'event' || p.kind === 'due_soon' || handoff || (p.kind === 'due_today' && Number(p.count) > 0)) {
122
134
  out.push({ subject: p.subject, bucket: p.bucket })
123
135
  }
124
136
  }
@@ -16,8 +16,47 @@
16
16
  // only at `overdueAfter` (the next local midnight). Without `overdueAfter`, the old rule — overdue at
17
17
  // dueAt — would call an all-day deadline OVERDUE from the first minute of its own day. A server older
18
18
  // than these fields sends neither, and gets exactly the old output.
19
- export function renderObligations(obs, now = Date.now()) {
20
- if (!obs?.length) return 'Nothing open.'
19
+ // ADR-0073 §2.7 — TWO THINGS THAT ARE NOT "WHAT I OWE" AND MUST NOT READ AS IF THEY WERE.
20
+ // requests asked of this person, waiting for their answer. Not theirs until they accept.
21
+ // asked what this person asked of others: still waiting, declined (with the reason), or taken.
22
+ // They get their own sections, after the list, so "N open" stays the count of what is actually owed.
23
+ // A server older than hand-offs sends neither key, and the output is exactly what it was.
24
+ const quoted = (s) => `“${String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 140)}”`
25
+ // A person's name is text someone typed into their own profile. One line, bounded — the same treatment
26
+ // as a subject, without the quotes. Exported: every obligation tool that prints a name uses it.
27
+ export const who = (s) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, 60) || 'someone'
28
+
29
+ export function renderHandoffs(extras) {
30
+ const requests = Array.isArray(extras?.requests) ? extras.requests : []
31
+ const asked = Array.isArray(extras?.asked) ? extras.asked : []
32
+ const lines = []
33
+ if (requests.length) {
34
+ lines.push(`ASKED OF YOU — ${requests.length} waiting for your answer. Not yours until you accept:`)
35
+ for (const r of requests) {
36
+ lines.push(`${r.obligationId}`)
37
+ lines.push(` ${who(r.from)} asks you to take: ${quoted(r.subject)}${r.dueAt ? ` — due ${String(r.dueAt).slice(0, 10)}` : ''}`)
38
+ if (r.note) lines.push(` their note: ${quoted(r.note)}`)
39
+ }
40
+ lines.push(' → answer_obligation_request (accept: true, or accept: false with a reason). Ask the person; do not answer for them.')
41
+ lines.push('')
42
+ }
43
+ const waiting = asked.filter((a) => a.state === 'pending')
44
+ const declined = asked.filter((a) => a.state === 'declined')
45
+ const taken = asked.filter((a) => a.state === 'accepted' || a.state === 'assigned')
46
+ if (waiting.length || declined.length || taken.length) {
47
+ lines.push('YOU ASKED:')
48
+ for (const a of declined) lines.push(` ✗ ${who(a.to)} DECLINED ${quoted(a.subject)} — ${quoted(a.response)}. It is still yours. (${a.obligationId})`)
49
+ for (const a of waiting) lines.push(` … waiting on ${who(a.to)}: ${quoted(a.subject)} — still yours until they accept. (${a.obligationId})`)
50
+ for (const a of taken.slice(0, 8)) lines.push(` ✓ ${who(a.to)} now owes ${quoted(a.subject)} (${a.obligationId})`)
51
+ if (taken.length > 8) lines.push(` … and ${taken.length - 8} more taken`)
52
+ lines.push('')
53
+ }
54
+ return lines
55
+ }
56
+
57
+ export function renderObligations(obs, now = Date.now(), extras = null) {
58
+ const handoffs = renderHandoffs(extras)
59
+ if (!obs?.length) return handoffs.length ? ['Nothing open of your own.', '', ...handoffs].join('\n') : 'Nothing open.'
21
60
  const proposed = obs.filter((o) => o.state === 'proposed').length
22
61
  // ADR-0073: every obligation lives on a page. `needsPage` is sent only by a server that knows the
23
62
  // rule, so against an older one this count is zero and the output is exactly what it was.
@@ -39,6 +78,9 @@ export function renderObligations(obs, now = Date.now()) {
39
78
  const pages = Array.isArray(o.pages) ? o.pages.filter((p) => p?.origin !== 'fallback') : []
40
79
  if (pages.length) lines.push(` on: ${pages.slice(0, 4).map((p) => `“${String(p.title ?? '').replace(/\s+/g, ' ').trim().slice(0, 60)}”`).join(' · ')}${pages.length > 4 ? ` … and ${pages.length - 4} more` : ''}`)
41
80
  if (o.needsPage === true) lines.push(' ⚠ on no page anyone chose — set_obligation_pages')
81
+ // Whose idea it was, and whether it is on its way to someone else. Both change what to DO with it.
82
+ if (o.askedBy) lines.push(` asked for by ${who(o.askedBy)}`)
83
+ if (o.offeredTo) lines.push(` offered to ${who(o.offeredTo)} — waiting for their answer; still yours until they accept`)
42
84
  // ⚠ EVIDENCE IS RENDERED AS A QUESTION, NEVER AS A VERDICT. The phrasing is the feature:
43
85
  // "is this done?" costs the reader a second, "you still owe this" about finished work costs
44
86
  // the channel its credibility.
@@ -50,6 +92,7 @@ export function renderObligations(obs, now = Date.now()) {
50
92
  }
51
93
  lines.push('')
52
94
  }
95
+ lines.push(...handoffs)
53
96
  return lines.join('\n')
54
97
  }
55
98
 
package/lib/server.mjs CHANGED
@@ -19,7 +19,7 @@ import { runCodeGraphQuery } from './code_graph_cli.mjs'
19
19
  import { repoFullNameFrom, workingFilesFrom } from './capture.mjs'
20
20
  import { findTranscript, createWorkedFilesTracker } from './working_files.mjs'
21
21
  import { spawnObligationWorker } from './obligations_worker.mjs'
22
- import { renderObligations } from './obligations_render.mjs'
22
+ import { renderObligations, who } from './obligations_render.mjs'
23
23
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
24
24
 
25
25
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
@@ -2910,7 +2910,7 @@ function renderNudge(payload) {
2910
2910
  const out = await res.json().catch(() => null)
2911
2911
  if (!res.ok) return toolError(`Could not list: ${out?.error ?? res.status}`)
2912
2912
  const obs = out?.obligations ?? []
2913
- return { content: [{ type: 'text', text: renderObligations(obs) }] }
2913
+ return { content: [{ type: 'text', text: renderObligations(obs, Date.now(), { requests: out?.requests, asked: out?.asked }) }] }
2914
2914
  },
2915
2915
  )
2916
2916
 
@@ -2922,19 +2922,21 @@ function renderNudge(payload) {
2922
2922
  inputSchema: {
2923
2923
  subject: z.string().describe('what is owed, in your own words'),
2924
2924
  pages: z.array(z.string()).min(1).describe('REQUIRED. The `ref:` of each page this obligation is about, exactly as read_page prints it. Read the page first; do not guess.'),
2925
+ for: z.string().optional().describe('ONLY when it is someone ELSE who should owe this: their email address, or their name as it appears in this brain. Omit for something the person owes themselves. You do not choose whether it is assigned or requested — the reporting line does.'),
2926
+ note: z.string().optional().describe('with `for`: a short note to the person it is given to'),
2925
2927
  due_at: z.string().optional().describe('ISO 8601. OMIT when there is no real deadline — never guess one.'),
2926
2928
  anchor_identifier: z.string().optional().describe('e.g. "email:ben@example.com" — records from them become evidence this may be done'),
2927
2929
  source_record_id: z.string().optional().describe('the record this came from, if any'),
2928
2930
  brain: z.string().optional().describe('required only if you belong to more than one brain'),
2929
2931
  },
2930
2932
  },
2931
- async ({ subject, pages, due_at, anchor_identifier, source_record_id, brain }) => {
2933
+ async ({ subject, pages, due_at, anchor_identifier, source_record_id, brain, for: giveTo, note: giveNote }) => {
2932
2934
  let res
2933
2935
  try {
2934
2936
  res = await fetchCortex(`${BASE}/api/obligations`, {
2935
2937
  method: 'POST',
2936
2938
  headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2937
- body: JSON.stringify({ subject, pages, dueAt: due_at, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain }),
2939
+ body: JSON.stringify({ subject, pages, dueAt: due_at, anchorIdentifier: anchor_identifier, sourceRecordId: source_record_id, brain, for: giveTo, note: giveNote }),
2938
2940
  })
2939
2941
  } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
2940
2942
  const out = await res.json().catch(() => null)
@@ -2948,10 +2950,76 @@ function renderNudge(payload) {
2948
2950
  ? `\nRecords carrying ${anchor_identifier} will be offered as evidence this is done. ${nudge}`
2949
2951
  : `\n${nudge}`
2950
2952
  const on = Number(out.pages) > 0 ? ` — on ${out.pages} page${out.pages === 1 ? '' : 's'}` : ''
2953
+ // Given to someone else: say plainly which of the two things happened. "Assigned" and "asked"
2954
+ // leave the obligation with different people, and the agent must not report one as the other.
2955
+ if (out.given) {
2956
+ const what = out.given.outcome === 'assigned'
2957
+ ? `It is now ${who(out.given.to)}'s — they are below you in your chain of command, so it was assigned outright. They are told once.`
2958
+ : `${who(out.given.to)} has been ASKED to take it. It stays YOURS until they accept; you are told if they decline.`
2959
+ return { content: [{ type: 'text', text: `Recorded: ${subject}${due_at ? ` (due ${due_at.slice(0, 10)})` : ' (no deadline)'}${on}\n${out.id}\n${what}` }] }
2960
+ }
2951
2961
  return { content: [{ type: 'text', text: `Recorded: ${subject}${due_at ? ` (due ${due_at.slice(0, 10)})` : ' (no deadline)'}${on}\n${out.id}${note}` }] }
2952
2962
  },
2953
2963
  )
2954
2964
 
2965
+ server.registerTool(
2966
+ 'give_obligation',
2967
+ {
2968
+ title: 'Give an obligation to someone',
2969
+ description: "Hand an existing obligation to another person in the same brain (ADR-0073). YOU DO NOT CHOOSE THE OUTCOME, and neither does the person asking: the reporting line does. Someone BELOW the giver in the giver's own chain of command gets it at once (assigned). Anyone else — above, a peer, or lower down on another team — is ASKED, and it stays with its current owner until they accept; if they decline it simply stays where it was. Report which of the two happened exactly as the result states it. Only its owner, or someone above its owner, can give it. Giving it to the caller themselves is taking it over, allowed only from above. Use the person's email address when a name could match two people.",
2970
+ inputSchema: {
2971
+ obligation_id: z.string().describe('from my_obligations'),
2972
+ to: z.string().describe("who should owe it: their email address, or their name as it appears in this brain"),
2973
+ note: z.string().optional().describe('a short note to them'),
2974
+ },
2975
+ },
2976
+ async ({ obligation_id, to, note }) => {
2977
+ let res
2978
+ try {
2979
+ res = await fetchCortex(`${BASE}/api/obligations/give`, {
2980
+ method: 'POST',
2981
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
2982
+ body: JSON.stringify({ obligationId: obligation_id, to, note }),
2983
+ })
2984
+ } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
2985
+ const out = await res.json().catch(() => null)
2986
+ if (!res.ok) return toolError(`Could not give it: ${out?.detail ?? out?.error ?? res.status}`)
2987
+ const text = out.outcome === 'assigned'
2988
+ ? `ASSIGNED: it is now ${who(out.to)}'s. They are below the giver in the chain of command, so it moved at once; they are told once.`
2989
+ : `ASKED: ${who(out.to)} has been asked to take it. It has NOT moved — it stays with its current owner until they accept, and the asker is told if they decline.`
2990
+ return { content: [{ type: 'text', text }] }
2991
+ },
2992
+ )
2993
+
2994
+ server.registerTool(
2995
+ 'answer_obligation_request',
2996
+ {
2997
+ title: 'Accept or decline an obligation someone asked you to take',
2998
+ description: "Answer a request listed under ASKED OF YOU in my_obligations (ADR-0073). Accepting makes the person its owner. Declining REQUIRES a reason — the person who asked is told it — and changes nothing else: the obligation stays with whoever had it. ⚠ This is the PERSON's decision. Ask them; never accept or decline on their behalf because it seems reasonable.",
2999
+ inputSchema: {
3000
+ obligation_id: z.string().describe('from the ASKED OF YOU section of my_obligations'),
3001
+ accept: z.boolean().describe('true = take it; false = decline'),
3002
+ reason: z.string().optional().describe("REQUIRED when declining — in the person's own words"),
3003
+ },
3004
+ },
3005
+ async ({ obligation_id, accept, reason }) => {
3006
+ let res
3007
+ try {
3008
+ res = await fetchCortex(`${BASE}/api/obligations/answer`, {
3009
+ method: 'POST',
3010
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
3011
+ body: JSON.stringify({ obligationId: obligation_id, accept, reason }),
3012
+ })
3013
+ } catch (e) { return toolError(`Could not reach obligations: ${e.message}`) }
3014
+ const out = await res.json().catch(() => null)
3015
+ if (!res.ok) return toolError(`Could not answer: ${out?.detail ?? out?.error ?? res.status}`)
3016
+ const asker = out.from ? who(out.from) : 'the person who asked'
3017
+ return { content: [{ type: 'text', text: out.outcome === 'accepted'
3018
+ ? `Accepted: it is yours now. ${asker} is not told separately — they see it under YOU ASKED.`
3019
+ : `Declined. Nothing moved: it stays with ${asker}, who is told your reason once.` }] }
3020
+ },
3021
+ )
3022
+
2955
3023
  server.registerTool(
2956
3024
  'set_obligation_pages',
2957
3025
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.198",
3
+ "version": "0.9.199",
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": {