@theronap/cortex-mcp 0.9.91 → 0.9.93
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/red_link_triage.mjs +8 -1
- package/lib/server.mjs +155 -11
- package/package.json +1 -1
package/lib/red_link_triage.mjs
CHANGED
|
@@ -21,7 +21,14 @@ export function renderTriage(t, name) {
|
|
|
21
21
|
: ''
|
|
22
22
|
const aliasHint = `if it's really an existing page under another title, \`grep "${name}"\` to find it, then \`alias_page name="${name}" target_name="<that page>"\``
|
|
23
23
|
if (t.demoted) {
|
|
24
|
-
|
|
24
|
+
// KWA-28 — `red_link_targets` is a graph-side object and must carry an as-of. The server ALREADY
|
|
25
|
+
// sends `updatedAt` on this arm (the retirement's own timestamp) and this renderer was dropping it,
|
|
26
|
+
// so "deliberately retired" read as timeless. WHEN it was retired is the load-bearing fact here:
|
|
27
|
+
// the whole point of the arm is to stop an agent authoring over a human decision, and a decision
|
|
28
|
+
// from yesterday and one from eight months ago warrant different confidence about whether it still
|
|
29
|
+
// holds. Explicitly undated rather than silent when the server predates the field.
|
|
30
|
+
const when = t.updatedAt ? ` on ${String(t.updatedAt).slice(0, 10)}` : ' (retirement date not recorded)'
|
|
31
|
+
return `\n\n[[${name}]] EXISTS but is marked ${t.validity ?? 'superseded'} — it was authored and then deliberately retired${when}, so read_page (which serves only current pages) will not show it. It is NOT missing.${refs} Do NOT author over it: that would silently overwrite a decision someone made on purpose. Read it with \`page_history "${name}"\` then \`read_page "${name}"\` with a version. If it genuinely should be live again, revive it deliberately with \`set_page_validity\`.`
|
|
25
32
|
}
|
|
26
33
|
if (t.category === 'node') {
|
|
27
34
|
return `\n\n[[${name}]] is a wanted page — a ${t.isPerson ? 'person' : 'node'} exists but has no page yet.${refs} Either author it now with \`author\`, or ${aliasHint}.`
|
package/lib/server.mjs
CHANGED
|
@@ -358,14 +358,20 @@ export async function runServer(version) {
|
|
|
358
358
|
m = await fetchCortex(`${BASE}/api/intake/materialize`, {
|
|
359
359
|
method: 'POST',
|
|
360
360
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
361
|
+
// NESTED under `record`, and snake_case inside it. The route reads the record body
|
|
362
|
+
// from `body.record` and ignores top-level title/summary entirely — a flat body still
|
|
363
|
+
// answers `ok` with a recordId, having written a record with no content. Four such
|
|
364
|
+
// records exist in the corpus from exactly this mistake, and this call was the fifth
|
|
365
|
+
// until 2026-08-15: the close-out reported success while changing nothing.
|
|
361
366
|
body: JSON.stringify({
|
|
362
367
|
intakeItemId: j.intakeItemId,
|
|
363
368
|
orgId,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
+
record: {
|
|
370
|
+
title: seg.title,
|
|
371
|
+
summary: seg.summary,
|
|
372
|
+
source: 'claude-code',
|
|
373
|
+
record_type: 'ai_session',
|
|
374
|
+
},
|
|
369
375
|
}),
|
|
370
376
|
})
|
|
371
377
|
} catch (e) {
|
|
@@ -455,6 +461,36 @@ export async function runServer(version) {
|
|
|
455
461
|
},
|
|
456
462
|
)
|
|
457
463
|
|
|
464
|
+
server.registerTool(
|
|
465
|
+
'gate2_status',
|
|
466
|
+
{
|
|
467
|
+
title: 'Gate 2 edit-accountability monitor',
|
|
468
|
+
description: 'Read the aggregate-only Gate 2 status for this brain — whether every edit records WHICH SESSION made it. It counts only the REPAIRED write paths (absorb, retier, replace) since the 2026-08-17 fix, because the author path was never broken and would certify a repair it never exercised. Operator and migration writes are excluded: they legitimately have no session, and imputing one would violate the 0093 don\'t-impute rule. It never exposes page titles, refs, reasons or session keys. "regressed" means a repaired path lost its session attribution again and the gate must NOT be closed; "machine_evidence_ready" means enough ordinary-work evidence has accumulated to request a human closure decision.',
|
|
469
|
+
inputSchema: {
|
|
470
|
+
days: z.number().optional().describe('rolling window in days (1-90, default 14)'),
|
|
471
|
+
brain: z.string().optional().describe('brain name or org id when you belong to more than one brain; omit for a sole brain'),
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
async ({ days, brain }) => {
|
|
475
|
+
const qs = new URLSearchParams()
|
|
476
|
+
if (days != null) qs.set('days', String(days))
|
|
477
|
+
if (brain) qs.set('brain', brain)
|
|
478
|
+
const suffix = qs.size ? `?${qs}` : ''
|
|
479
|
+
const res = await fetchCortex(`${BASE}/api/gates/2/status${suffix}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
480
|
+
if (!res.ok) {
|
|
481
|
+
const body = await res.text()
|
|
482
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
483
|
+
}
|
|
484
|
+
const status = await res.json()
|
|
485
|
+
const text = status.status === 'regressed'
|
|
486
|
+
? `\u26a0 Gate 2 has REGRESSED: ${status.unattributedMcpRevisions} MCP-authored revisions since ${status.since} carry NO session (${status.unattributedRepairedRevisions} of them on the repaired absorb/retier/replace paths). Attribution is being dropped again — do not close this gate; find the writer.`
|
|
487
|
+
: status.machineEvidenceReady
|
|
488
|
+
? `Gate 2 machine evidence is ready: ${status.repairedPathSessions} distinct sessions exercised the repaired write paths across ${status.repairedPathRevisions} revisions since ${status.since}, and NONE lost its session. A human should confirm these were ordinary work before closing the gate.`
|
|
489
|
+
: `Gate 2 is still collecting evidence: ${status.repairedPathSessions}/${status.requiredRepairedPathSessions} distinct sessions have exercised the repaired write paths (absorb/retier/replace) since ${status.since}, across ${status.repairedPathRevisions} revisions, 0 unattributed. No gate decision has been made.`
|
|
490
|
+
return { content: [{ type: 'text', text }] }
|
|
491
|
+
},
|
|
492
|
+
)
|
|
493
|
+
|
|
458
494
|
server.registerTool(
|
|
459
495
|
'session_context',
|
|
460
496
|
{
|
|
@@ -1049,6 +1085,14 @@ export async function runServer(version) {
|
|
|
1049
1085
|
}
|
|
1050
1086
|
lines.push(`· ${out.events.visibleCount} tagged timeline event${out.events.visibleCount === 1 ? '' : 's'} visible to you${out.events.recent.length ? ':' : expand ? '.' : ' — pass expand: true to list recent ones.'}`)
|
|
1051
1087
|
for (const e of out.events.recent) lines.push(` - ${String(e.occurredAt).slice(0, 10)} · ${e.source} · ${e.summary}`)
|
|
1088
|
+
// KWA-28 — an identifier node is VIRTUAL: derived per read, zero stored rows, so there is no
|
|
1089
|
+
// as_of to fetch. The item says exactly what to do in that case: "where the object is derived
|
|
1090
|
+
// per-read, stamp the read itself." Note this as-of means something DIFFERENT from every
|
|
1091
|
+
// other one in gate 3 — "this answer was computed now", not "this claim was true then" — and
|
|
1092
|
+
// the wording says so, because collapsing the two under one word is how a resolution that is
|
|
1093
|
+
// merely FRESH gets read as a claim that is VERIFIED. The home and the count are both live
|
|
1094
|
+
// computations over data that can change between two reads a minute apart.
|
|
1095
|
+
lines.push(`· Resolved ${new Date().toISOString().slice(0, 16).replace('T', ' ')}Z — this node is derived per read (no stored row), so this is when the answer was COMPUTED, not when anything was verified.`)
|
|
1052
1096
|
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
1053
1097
|
}
|
|
1054
1098
|
if (r.status === 400) {
|
|
@@ -1438,7 +1482,13 @@ export async function runServer(version) {
|
|
|
1438
1482
|
}
|
|
1439
1483
|
const red = Array.isArray(out.redLinks) && out.redLinks.length
|
|
1440
1484
|
? `\nRed-links now on this page: ${out.redLinks.map((r) => `[[${r}]]`).join(', ')}` : ''
|
|
1441
|
-
|
|
1485
|
+
// KWA-26 — same advisory flag as `author`, on the path that actually gets used. Absent (not
|
|
1486
|
+
// false) from an older server means "no verdict computed", so say nothing rather than imply the
|
|
1487
|
+
// section is dated — the 0093 don't-impute rule.
|
|
1488
|
+
const undatedNote = out?.undated === true
|
|
1489
|
+
? `\n⚠ This section now carries no explicit calendar date. A reader can see WHEN the text was written but not when the claim was TRUE. If it asserts a status, add the date inline — you still hold the context. The edit already landed; this is advisory.`
|
|
1490
|
+
: ''
|
|
1491
|
+
return { content: [{ type: 'text', text: `Edited "${name}" (${out.brain} · ${out.tier} tier) § ${out.heading}. Only that passage changed; every other section is byte-identical. New version: ${out.version}${red}${undatedNote}` }] }
|
|
1442
1492
|
},
|
|
1443
1493
|
)
|
|
1444
1494
|
|
|
@@ -1718,15 +1768,34 @@ export async function runServer(version) {
|
|
|
1718
1768
|
'list_brain_pages',
|
|
1719
1769
|
{
|
|
1720
1770
|
title: 'List every authored page in one brain',
|
|
1721
|
-
description: '
|
|
1771
|
+
description: 'QUERY the authored pages in ONE brain, by its org id (from my_brains) — filter by owner, recency, tier, kind or name, and sort. Returns one row per node with its kind, validity, tier(s), owner(s), last-updated date and content hash. This is the structural counterpart to `grep`: use it when the question is a FILTER-AND-SORT ("what has X written this week", "which pages are confidential", "what is stale") and grep when you need to match WORDS inside page text. ⚠ `validity` defaults to `current`, so superseded pages are EXCLUDED unless you ask for them — a stale page presented as live is a failure this system keeps hitting. Also use it to VERIFY a brain-to-brain migration: list BOTH brains, diff the page sets, compare freshness before retiring any original. You can only list a brain you are a member of, and within it you see only the pages you are cleared to read — a confidential page owned by someone else is not listed, not even by title, and that holds for every filter combination including `owner`.',
|
|
1722
1772
|
inputSchema: {
|
|
1723
1773
|
org_id: z.string().describe('the org id of the brain to enumerate (from my_brains)'),
|
|
1774
|
+
owner: z.string().optional().describe('only pages owned by this person — a user id or an EXACT display name. A name matching no member of the brain is an error, never a silently empty list. Owner applies to scoped/confidential pages; accessible pages have no owner.'),
|
|
1775
|
+
updated_within_days: z.number().optional().describe('only pages touched in the last N days'),
|
|
1776
|
+
tier: z.enum(['accessible', 'scoped', 'confidential']).optional().describe('only nodes that have a row at this tier. The row still reports every tier you can see, so a multi-tier page does not come back describing itself as single-tier.'),
|
|
1777
|
+
kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('only nodes of this kind'),
|
|
1778
|
+
validity: z.enum(['current', 'superseded', 'all']).optional().describe("default 'current'. Pass 'all' for the pre-2026-08 behavior, which mixed superseded pages in with nothing marking them."),
|
|
1779
|
+
name_contains: z.string().optional().describe('case-insensitive substring match on the node name or page title'),
|
|
1780
|
+
sort: z.enum(['recent', 'name']).optional().describe("default 'recent' (newest first). Ties break deterministically, so repeat calls are stable."),
|
|
1781
|
+
limit: z.number().optional().describe('default 50, capped at 500'),
|
|
1724
1782
|
},
|
|
1725
1783
|
},
|
|
1726
|
-
async ({ org_id }) => {
|
|
1784
|
+
async ({ org_id, owner, updated_within_days, tier, kind, validity, name_contains, sort, limit }) => {
|
|
1785
|
+
// Only send params the caller actually set: an omitted filter and an empty one are different
|
|
1786
|
+
// requests, and the route validates enums strictly rather than ignoring unknown values.
|
|
1787
|
+
const qs = new URLSearchParams({ orgId: org_id })
|
|
1788
|
+
if (owner) qs.set('owner', owner)
|
|
1789
|
+
if (updated_within_days != null) qs.set('updated_within_days', String(updated_within_days))
|
|
1790
|
+
if (tier) qs.set('tier', tier)
|
|
1791
|
+
if (kind) qs.set('kind', kind)
|
|
1792
|
+
if (validity) qs.set('validity', validity)
|
|
1793
|
+
if (name_contains) qs.set('name_contains', name_contains)
|
|
1794
|
+
if (sort) qs.set('sort', sort)
|
|
1795
|
+
if (limit != null) qs.set('limit', String(limit))
|
|
1727
1796
|
let res
|
|
1728
1797
|
try {
|
|
1729
|
-
res = await fetchCortex(`${BASE}/api/brains/pages
|
|
1798
|
+
res = await fetchCortex(`${BASE}/api/brains/pages?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1730
1799
|
} catch (e) {
|
|
1731
1800
|
return toolError(`Could not list pages: ${e.message}`)
|
|
1732
1801
|
}
|
|
@@ -2185,6 +2254,62 @@ export async function runServer(version) {
|
|
|
2185
2254
|
},
|
|
2186
2255
|
)
|
|
2187
2256
|
|
|
2257
|
+
server.registerTool(
|
|
2258
|
+
'set_governing_page',
|
|
2259
|
+
{
|
|
2260
|
+
title: 'Choose which attached page sets a record\'s tier',
|
|
2261
|
+
description:
|
|
2262
|
+
'Move a record\'s GOVERNING page — the one attached page whose tier the record takes. A record can sit on several pages, but exactly one of them decides how visible it is; the others confer access without authority (ADR-0027). Use this when a record is on the right pages but the WRONG one is deciding its tier — most often a record governed by your own user node when it plainly belongs to a project. The page must already be attached: run route_record first if it is not, because attaching is a relevance judgement and this is not. It applies immediately in BOTH directions, tightening or widening, because you asking for it IS the human confirmation a widening requires — so read the tier you are moving to before you move. Every move is recorded as a session judgment and is the signal the placement heuristics are calibrated against, which is why the reason matters.',
|
|
2263
|
+
inputSchema: {
|
|
2264
|
+
recordId: z.string().describe('record id (from pending_records or my_records)'),
|
|
2265
|
+
documentId: z.string().describe('document id of the ATTACHED page that should govern the tier'),
|
|
2266
|
+
reason: z.string().describe('why this page should set the tier — recorded, and read as calibration signal'),
|
|
2267
|
+
},
|
|
2268
|
+
},
|
|
2269
|
+
async ({ recordId, documentId, reason }) => {
|
|
2270
|
+
let res
|
|
2271
|
+
try {
|
|
2272
|
+
res = await fetchCortex(`${BASE}/api/brain/triage`, {
|
|
2273
|
+
method: 'POST',
|
|
2274
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2275
|
+
body: JSON.stringify({ action: 'regovern', recordId, documentId, reason }),
|
|
2276
|
+
})
|
|
2277
|
+
} catch (e) {
|
|
2278
|
+
return toolError(`Could not set the governing page: ${e.message}`)
|
|
2279
|
+
}
|
|
2280
|
+
const out = await res.json().catch(() => null)
|
|
2281
|
+
if (!res.ok) {
|
|
2282
|
+
// A guard rail, not a fault — say what to do instead of naming the code.
|
|
2283
|
+
if (out?.error === 'not_attached') {
|
|
2284
|
+
return toolError(
|
|
2285
|
+
'That page is not attached to this record, so it cannot govern it. Attach it first with route_record.',
|
|
2286
|
+
)
|
|
2287
|
+
}
|
|
2288
|
+
return toolError(
|
|
2289
|
+
`Could not set the governing page: ${out?.error ?? res.status}${out?.detail ? ` — ${out.detail}` : ''}`,
|
|
2290
|
+
)
|
|
2291
|
+
}
|
|
2292
|
+
if (out?.reaffirmed) {
|
|
2293
|
+
return {
|
|
2294
|
+
content: [{
|
|
2295
|
+
type: 'text',
|
|
2296
|
+
text: `That page already governed this record; recorded your confirmation. Tier: ${out?.toPrivacy ?? 'unchanged'}.`,
|
|
2297
|
+
}],
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const moved =
|
|
2301
|
+
out?.fromPrivacy && out?.toPrivacy && out.fromPrivacy !== out.toPrivacy
|
|
2302
|
+
? `Tier ${out.fromPrivacy} -> ${out.toPrivacy} (${out?.direction}).`
|
|
2303
|
+
: `Tier unchanged (${out?.toPrivacy ?? 'unknown'}).`
|
|
2304
|
+
return {
|
|
2305
|
+
content: [{
|
|
2306
|
+
type: 'text',
|
|
2307
|
+
text: `Governing page moved. ${moved} Recorded as a session judgment${out?.correctedAuto ? ' and counted as a correction to the placement heuristics' : ''}.`,
|
|
2308
|
+
}],
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
)
|
|
2312
|
+
|
|
2188
2313
|
server.registerTool(
|
|
2189
2314
|
'snooze_red_link',
|
|
2190
2315
|
{
|
|
@@ -2696,7 +2821,7 @@ export async function runServer(version) {
|
|
|
2696
2821
|
{
|
|
2697
2822
|
title: 'Author a wiki node (live, while it is hot)',
|
|
2698
2823
|
description:
|
|
2699
|
-
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this — an update resolves its brain from the page itself. If you hold more than one brain, the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide — the pointer is stale out-of-band state that knows nothing about what you are writing. So call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` up front; that turns a refused round trip into a single call. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate, and that is not a filing error you can quietly fix later.',
|
|
2824
|
+
'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. ⚠ EVERY STATUS CLAIM CARRIES AN EXPLICIT INLINE DATE (KWA-26): a sentence asserting what IS or IS NOT true right now — "X is live", "Y is not merged", "Z is blocked" — must say WHEN, in the prose, the way PRD items do. Section-level stamps are NOT enough: they record when the TEXT was written, so a section authored today can carry a six-week-old status claim and still read as current — exactly what made KWA-24 and TML-18 wrong. The response names any section that landed undated so you can fix it in-turn; it never blocks the write. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this — an update resolves its brain from the page itself. If you hold more than one brain, the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide — the pointer is stale out-of-band state that knows nothing about what you are writing. So call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` up front; that turns a refused round trip into a single call. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate, and that is not a filing error you can quietly fix later.',
|
|
2700
2825
|
inputSchema: {
|
|
2701
2826
|
kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
|
|
2702
2827
|
name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Agnoclast" or "Theron Peterson"'),
|
|
@@ -2772,8 +2897,27 @@ export async function runServer(version) {
|
|
|
2772
2897
|
` If that is not the tier you meant, read_page and check which copy you just changed —` +
|
|
2773
2898
|
` a page can exist at several tiers and they drift apart independently.`
|
|
2774
2899
|
: ''
|
|
2900
|
+
// KWA-26 — the advisory undated flag. The server has computed `undatedSections` since #558 and
|
|
2901
|
+
// the route has returned it ever since; NOTHING PRINTED IT, so the one consumer the item names
|
|
2902
|
+
// never saw it: "the author path returns a flag naming undated status claims SO THE AGENT FIXES
|
|
2903
|
+
// THEM IN-TURN." A flag the agent cannot see does not exist. Same shape as tierCorrections
|
|
2904
|
+
// directly above — computed, returned, and silently dropped at the client — and the fourth
|
|
2905
|
+
// instance of it in this subsystem.
|
|
2906
|
+
//
|
|
2907
|
+
// Advisory by DESIGN, not by omission: the write has already landed by the time this prints
|
|
2908
|
+
// (2026-07-28, option (b) "make this blocking" was weighed and rejected — rejecting the write
|
|
2909
|
+
// would lose the session's understanding, the more expensive failure). So this is phrased as
|
|
2910
|
+
// work the agent can do NOW, while the context is still hot, which is the only moment the fix
|
|
2911
|
+
// is cheap.
|
|
2912
|
+
const undated = Array.isArray(out?.undatedSections) && out.undatedSections.length
|
|
2913
|
+
? `\n⚠ Undated (${out.undatedSections.length}): ${out.undatedSections.map((h) => `"${h}"`).join(', ')}.` +
|
|
2914
|
+
` These landed with no explicit calendar date in the heading or body, so a reader cannot tell` +
|
|
2915
|
+
` WHEN the claim was true — only when the text was last written. If any of them assert a` +
|
|
2916
|
+
` STATUS ("X is live", "Y is not merged"), add the date inline with edit_page while you still` +
|
|
2917
|
+
` hold the context. The write already landed; this is advisory.`
|
|
2918
|
+
: ''
|
|
2775
2919
|
const verb = out?.created ? 'Created + authored' : 'Authored'
|
|
2776
|
-
const note = out?.built ? `${verb} "${name}" (${out.built} tier${out.built === 1 ? '' : 's'}). Links: ${blue} resolved, ${retired} retired, ${red} red.${corrections}${retiredList}${redList}${stamps}`
|
|
2920
|
+
const note = out?.built ? `${verb} "${name}" (${out.built} tier${out.built === 1 ? '' : 's'}). Links: ${blue} resolved, ${retired} retired, ${red} red.${corrections}${retiredList}${redList}${stamps}${undated}`
|
|
2777
2921
|
: `No change to "${name}"${out?.skipped?.length ? ` (${out.skipped.join(', ')})` : ''}.${corrections}`
|
|
2778
2922
|
return { content: [{ type: 'text', text: note }] }
|
|
2779
2923
|
},
|