@theronap/agnoclast-mcp 0.9.150 → 0.9.151

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 +35 -13
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -78,6 +78,23 @@ export const sectionCurrencyStamp = (s, day) => {
78
78
  // invisible on exactly the sections that were healthy.
79
79
  export const renderSection = (s, day) => `### ${s.heading}${sectionCurrencyStamp(s, day)}\n${s.body}`
80
80
 
81
+ // TOP-OF-PAGE NOTES — ADR-0065's size note, and ADR-0064 §3's "Also visible to you" callout.
82
+ //
83
+ // ⚠ PRINTED VERBATIM, NEVER COMPOSED HERE. The server decides whether a note fires and what it says
84
+ // (`headerNotes` on /api/brain/page), so changing a threshold or a sentence is a deploy, not an npm release
85
+ // plus a desktop pin bump. An older server sends no field, and this prints nothing.
86
+ //
87
+ // ⚠ AT THE TOP, NOT IN THE FOOTER. Claude Code saves an over-cap tool result to a file and the agent reads
88
+ // it from offset 0, so the footer of a huge page is the part nobody reaches — and these notes exist for
89
+ // exactly those pages. Shared by read_page and project_status so the two cannot drift, which is how every
90
+ // earlier fix to this file's page rendering landed on one surface and not the other.
91
+ export const renderHeaderNotes = (notes) => {
92
+ const lines = (Array.isArray(notes) ? notes : [])
93
+ .filter((n) => typeof n === 'string' && n.trim() !== '')
94
+ .map((n) => n.replace(/\s*\n\s*/g, ' ').trim())
95
+ return lines.length ? `\n\n${lines.join('\n')}` : ''
96
+ }
97
+
81
98
  // gate5_status's transport half, pulled out of the tool handler so it can be exercised directly rather
82
99
  // than only string-matched (gate5_status_transport.drift.test.mjs). Takes the raw fetch Response from
83
100
  // /api/gates/5/status; the registerTool callback's only remaining job is fetching it. Same three
@@ -1348,7 +1365,7 @@ function renderNudge(payload) {
1348
1365
  return [head, secs].filter(Boolean).join('\n')
1349
1366
  })
1350
1367
  const brainTag = matches.length > 1 ? ` · brain: ${m.brain}` : ''
1351
- return `# ${m.title ?? key} (authored page${brainTag})\n\n${blocks.join('\n\n---\n\n')}`
1368
+ return `# ${m.title ?? key} (authored page${brainTag})${renderHeaderNotes(m.headerNotes)}\n\n${blocks.join('\n\n---\n\n')}`
1352
1369
  }
1353
1370
  return { content: [{ type: 'text', text: matches.map(renderMatch).join('\n\n═══\n\n') }] }
1354
1371
  }
@@ -1579,7 +1596,7 @@ function renderNudge(payload) {
1579
1596
  // Always emitted, not only in the multi-brain case: the ref is what makes the brain question
1580
1597
  // moot, so withholding it until brains collide is exactly backwards.
1581
1598
  const refLine = m.ref ? `\nref: ${m.ref}` : ''
1582
- return `# ${m.title ?? name} (full authored page${brainTag})${refLine}\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}`
1599
+ return `# ${m.title ?? name} (full authored page${brainTag})${refLine}${renderHeaderNotes(m.headerNotes)}\n\n${blocks.join('\n\n---\n\n')}\n\n${footer}`
1583
1600
  }
1584
1601
  // ── THE PRE-CLAIM NUDGE ───────────────────────────────────────────────────────────────────
1585
1602
  //
@@ -1867,15 +1884,15 @@ function renderNudge(payload) {
1867
1884
  'split_page',
1868
1885
  {
1869
1886
  title: 'Move sections onto a new child page',
1870
- description: 'SPLIT a page: move whole sections onto a NEW page, leaving the original in place. Use it when a page has grown past what can be read in one turn a correctness problem, not just a cost one, because an agent that cannot read the whole authority answers from part of it (a head page and its governing page disagreed about a gate status for a week that way). Measured across 480 pages: 5.2% of them hold a third of all authored text, so this targets the tail, not routine hygiene. It does NOT make pages go wrong less often — corrections scale roughly linearly with size — it changes what each one COSTS: fixing a claim on the 165k-char page means reading ~42,000 tokens; on a 20k child, ~5,000. WHAT IT NEVER DOES, each for a measured reason: never retires the original (a split is 1→2 and `superseded_by` holds one successor, and the original keeps receiving traffic with no narrower match); never moves governance (attaching a record to the child does not change its tier); never rewrites inbound [[links]] (the splitter cannot know which half a link meant — the reader following it does); never links the child FROM the source, because where that link goes is prose. GUARDS: the child is created BEFORE the source is trimmed, so a mid-way failure duplicates sections rather than losing them; `headings` must match the STORED heading exactly, INCLUDING any `· as of <date>` suffix (the rendered page shows a second `as of` stamp that is not part of it); moving every section is refused as a rename; and a STRICTER child is refused, because access is the union of attachments capped by the governing page the child would look private while its evidence stayed readable. The child copies the parent tier and grants and gets an `In short` section. Reversible via `page_history` + `rollback_page`.',
1887
+ description: 'SPLIT a page: move whole sections onto a NEW page, leaving the original in place. Use it when a page has outgrown one read — an agent that cannot read the whole authority answers from part of it. It targets the tail (5.2% of pages hold a third of all authored text), not routine hygiene, and it does not make pages go wrong less often: it makes each fix cheaper (~42,000 tokens to read a 165k-char page, ~5,000 for a 20k child). It NEVER retires the original, moves governance, rewrites inbound [[links]] (the reader following one knows which half it meant), or links the child FROM the source. EVERY REFUSAL IS DECIDED BEFORE ANYTHING IS WRITTEN (ADR-0064 §7): you need EDIT access to the version you read (edit_forbidden); pages with several tier versions are paused (multi_version_page); `headings` must match the STORED heading exactly, INCLUDING any `· as of <date>` suffix, and a heading two sections share is refused (ambiguous_heading); moving every section is refused as a rename; `new_title` must not name any existing or archived page (title_taken); a STRICTER child is refused, and a LOOSER one needs the source page\'s owner (widening_requires_owner); oversize sections and identity identifiers are refused (too_large, identity_claim). The child keeps the source page\'s OWNER, tier and grants, so the same people can read it, and gets an `In short` section. If a split stops after the child exists (source_trim_failed), finish with `merge_sections` (`from` only) for each heading in `remaining` — never re-run split_page. Reversible via `page_history` + `rollback_page`.',
1871
1888
  inputSchema: {
1872
1889
  name: z.string().describe('the exact page name to split, as read_page shows it'),
1873
- headings: z.array(z.string()).min(1).describe('the headings of the sections to MOVE, matched EXACTLY against the stored heading — include any `· as of <date>` suffix. Everything not listed stays on the original.'),
1874
- new_title: z.string().describe('the title of the new child page. It must not already exist in this brain.'),
1890
+ headings: z.array(z.string()).min(1).describe('the headings of the sections to MOVE, matched EXACTLY against the stored heading — include any `· as of <date>` suffix. A heading two sections share is refused: rename one with rename_section first. Everything not listed stays on the original.'),
1891
+ new_title: z.string().describe('the title of the new child page. It must not name ANY existing page in this brain, or an archived project: a split never adopts or revives one.'),
1875
1892
  base_version: z.string().describe('the `version` read_page prints for the SOURCE page (64-hex). REQUIRED — it is the concurrency check AND how the right brain is resolved.'),
1876
1893
  reason: z.string().describe('WHY you are splitting, in one short phrase — recorded in page_history. Say what outgrew the page.'),
1877
- owner: z.string().optional().describe('user id to own the child. Defaults to you, and the response says so when it doesworth setting deliberately, because ownership transfer has no reachable path once an owner is deactivated.'),
1878
- tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('omit to COPY the source page tier, which is almost always right. A LOOSER tier is a deliberate widening; a STRICTER one is refused, since a split cannot tighten access.'),
1894
+ owner: z.string().optional().describe('omit to keep the SOURCE page\'s owner, so the child has exactly the same audiencealmost always right. Naming a different user is an ownership change: it needs the page owner, or someone above them who can read the page, and must be an active member of this brain.'),
1895
+ tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('omit to COPY the source page tier, which is almost always right. A LOOSER tier widens who can read the moved text, so only the source page\'s owner may choose it; a STRICTER one is refused, since a split cannot tighten access.'),
1879
1896
  },
1880
1897
  },
1881
1898
  async ({ name, headings, new_title, base_version, reason, owner, tier }) => {
@@ -1895,20 +1912,25 @@ function renderNudge(payload) {
1895
1912
  const out = await res.json().catch(() => null)
1896
1913
  if (!res.ok) {
1897
1914
  // `child_created` + `remaining` is the one failure worth reading carefully: it means the split
1898
- // is HALF DONE and nothing was lost. Surface it first so the caller finishes rather than retries
1899
- // from the top, which would 409 on the now-taken title and look like a different problem.
1915
+ // is HALF DONE and nothing was lost. Surface it first so the caller FINISHES with merge_sections
1916
+ // (`from` only) rather than re-running split_page, which refuses the now-taken title (ADR-0064 §7.4).
1900
1917
  const extra = [
1901
1918
  out?.child_created ? `the child "${out.child_created}" EXISTS and holds every moved section — nothing was lost` : '',
1902
- Array.isArray(out?.remaining) ? `still on BOTH pages, re-run for these: ${out.remaining.join(' · ')}` : '',
1919
+ Array.isArray(out?.remaining) ? `still on BOTH pages — finish with merge_sections (\`from\` only) for each, do NOT re-run split_page: ${out.remaining.join(' · ')}` : '',
1903
1920
  Array.isArray(out?.detail) ? `detail: ${out.detail.join(' · ')}` : '',
1904
1921
  out?.message ?? '',
1905
1922
  ].filter(Boolean).join('\n')
1906
1923
  const hint = out?.hint ? `\n${out.hint}` : ''
1907
1924
  return toolError(`Could not split "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
1908
1925
  }
1909
- const ownerNote = out.owner_defaulted
1910
- ? '\n⚠ The child is owned by you because no `owner` was given. Set one deliberately if it should belong to someone else — transfer is unreachable once an owner is deactivated.'
1911
- : ''
1926
+ // ADR-0064 §7.5: an omitted owner now keeps the SOURCE page's owner, and the server's `note` says so.
1927
+ // Only a NAMED owner changes the audience, so that is the one worth a warning. `owner_defaulted` is
1928
+ // still read for a server older than #1041, where it meant the child fell to the caller.
1929
+ const ownerNote = out.owner_changed
1930
+ ? '\n⚠ The child is owned by the member you named, which changes who can read it if it is scoped or confidential.'
1931
+ : out.owner_defaulted
1932
+ ? '\n⚠ The child is owned by you because no `owner` was given. Set one deliberately if it should belong to someone else — transfer is unreachable once an owner is deactivated.'
1933
+ : ''
1912
1934
  const grants = out.grants_copied ? ` ${out.grants_copied} access grant(s) copied.` : ''
1913
1935
  return { content: [{ type: 'text', text: `Split "${name}" (${out.brain} · ${out.tier} tier) → new page "${out.child}". Moved: ${out.moved.join(' · ')}.${grants} Child version: ${out.version}\n${out.note}\nNEXT: ${out.next}${ownerNote}\nReversible: \`page_history "${name}"\` then \`rollback_page\` restores the source; the child can be retired with set_page_validity.` }] }
1914
1936
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.150",
3
+ "version": "0.9.151",
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": {