@theronap/cortex-mcp 0.9.77 → 0.9.78
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/bin/cortex-mcp.mjs +21 -5
- package/lib/diagnose.mjs +9 -2
- package/lib/doctor.mjs +14 -1
- package/lib/editors/claude.mjs +23 -10
- package/lib/server.mjs +47 -0
- package/lib/setup.mjs +3 -2
- package/lib/use_brain.mjs +82 -0
- package/package.json +1 -1
- package/lib/precompact.mjs +0 -16
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -49,6 +49,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
49
49
|
` uninstall remove ALL Agnoclast wiring (MCP, hooks, skills, launchd, cron). --dry-run to preview, --purge to also wipe ~/.cortex + npx cache\n` +
|
|
50
50
|
` doctor live health check — confirm your token works (no restart needed)\n` +
|
|
51
51
|
` status one-line connected/not-connected check (used by the SessionStart hook)\n` +
|
|
52
|
+
` use-brain [<brain>] where your session captures are saved — no arg shows the current setting\n` +
|
|
52
53
|
` skills install/repair the managed Agnoclast skills — bundled + org-published (also wired by setup)\n` +
|
|
53
54
|
` skills push <file> publish a SKILL.md to your org (owner/manager/admin)\n` +
|
|
54
55
|
` docs-scan detect new/changed local docs pending Agnoclast authoring (used by /cortex-author-docs)\n` +
|
|
@@ -177,12 +178,27 @@ if (cmd === 'login') {
|
|
|
177
178
|
// The cortex-author-docs skill authors the pending docs into wiki pages. No network.
|
|
178
179
|
const { runDocsScan } = await import('../lib/docs_scan.mjs')
|
|
179
180
|
process.exitCode = await runDocsScan(rest)
|
|
181
|
+
} else if (cmd === 'use-brain') {
|
|
182
|
+
// Set (or show) which brain this machine's unattended session captures land in. Registered here,
|
|
183
|
+
// BEFORE the default branch — an unrecognised subcommand falls through to "start the MCP server",
|
|
184
|
+
// which is how `connect-calendar` once silently became the file-watching daemon on a pilot user's
|
|
185
|
+
// machine and reported that daemon's errors instead of its own.
|
|
186
|
+
const { runUseBrain } = await import('../lib/use_brain.mjs')
|
|
187
|
+
process.exitCode = await runUseBrain(rest)
|
|
188
|
+
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
189
|
+
await closeFetch()
|
|
180
190
|
} else if (cmd === 'precompact') {
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
// RETIRED — deliberately kept as a silent no-op, do not delete yet.
|
|
192
|
+
//
|
|
193
|
+
// The reminder it used to print never reached a model: PreCompact takes a blocking `decision` and has
|
|
194
|
+
// no additionalContext channel, so its stdout went nowhere (verified 2026-08-10 against 7,232 local
|
|
195
|
+
// transcripts — see the PreCompact block in lib/editors/claude.mjs for the measurement and control).
|
|
196
|
+
//
|
|
197
|
+
// Why this branch survives the removal: seats installed before this release still have
|
|
198
|
+
// `PreCompact: npx -y @theronap/cortex-mcp@latest precompact` in ~/.claude/settings.json, and @latest
|
|
199
|
+
// resolves to THIS build. Deleting the branch would turn a harmless no-op into an unknown-command
|
|
200
|
+
// error on every compaction for anyone who has not re-run install. mergeClaudeSettings unwires them
|
|
201
|
+
// on their next install/repair; drop this branch a release after that has had time to propagate.
|
|
186
202
|
} else {
|
|
187
203
|
// Default: run the MCP server (stays alive; never exits).
|
|
188
204
|
const { runServer } = await import('../lib/server.mjs')
|
package/lib/diagnose.mjs
CHANGED
|
@@ -235,12 +235,19 @@ export async function checkToken(token, base) {
|
|
|
235
235
|
return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
|
|
236
236
|
}
|
|
237
237
|
let projectCount
|
|
238
|
+
let captureNotice = null
|
|
238
239
|
try {
|
|
239
|
-
const
|
|
240
|
+
const parsed = JSON.parse(body)
|
|
241
|
+
const ctx = parsed.context ?? ''
|
|
240
242
|
const m = ctx.match(/## Projects \((\d+)\)/)
|
|
241
243
|
if (m) projectCount = Number(m[1])
|
|
244
|
+
// The server's "your work is not landing" advisory. Optional by design: an older server does not
|
|
245
|
+
// send it and this must stay a health check, so a missing field is simply no notice.
|
|
246
|
+
if (parsed.captureNotice && typeof parsed.captureNotice.message === 'string') {
|
|
247
|
+
captureNotice = parsed.captureNotice
|
|
248
|
+
}
|
|
242
249
|
} catch { /* context shape changed — non-fatal for a health check */ }
|
|
243
|
-
return { ok: true, status: 200, projectCount, requestId }
|
|
250
|
+
return { ok: true, status: 200, projectCount, requestId, captureNotice }
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
// Probe the org-skills surface. SEPARATE FROM checkToken ON PURPOSE.
|
package/lib/doctor.mjs
CHANGED
|
@@ -11,7 +11,10 @@ import { checkToken, checkSkills, resolveBase } from './diagnose.mjs'
|
|
|
11
11
|
|
|
12
12
|
// Token resolution: env first, then the Claude config the setup command wrote (so `doctor`
|
|
13
13
|
// works the moment after `setup`, before any restart). Returns { token, source }.
|
|
14
|
-
|
|
14
|
+
// Exported so `use-brain` resolves the token EXACTLY as doctor/status do. There is already a second,
|
|
15
|
+
// subtly different copy of this in context_log.mjs (returns a bare token, not {token, source}); a
|
|
16
|
+
// third copy is how a machine ends up "connected" to one command and "no token found" to another.
|
|
17
|
+
export function resolveToken() {
|
|
15
18
|
if (process.env.CORTEX_TOKEN) return { token: process.env.CORTEX_TOKEN, source: 'CORTEX_TOKEN env' }
|
|
16
19
|
const claudeJson = join(homedir(), '.claude.json')
|
|
17
20
|
if (existsSync(claudeJson)) {
|
|
@@ -39,6 +42,16 @@ export async function runStatus() {
|
|
|
39
42
|
try {
|
|
40
43
|
const r = await checkToken(token, base)
|
|
41
44
|
if (r.ok) {
|
|
45
|
+
// ⚠ THE NOTICE REPLACES THE HAPPY LINE RATHER THAN FOLLOWING IT.
|
|
46
|
+
// "connected — sessions on this machine are captured to your org" was printed truthfully to
|
|
47
|
+
// three people whose sessions were, at that moment, landing in NO brain: connected is a fact
|
|
48
|
+
// about the TOKEN, and every reader takes it as a fact about their WORK. Printing both would
|
|
49
|
+
// leave the reassurance that caused four days of silent loss sitting directly above the
|
|
50
|
+
// warning that contradicts it.
|
|
51
|
+
if (r.captureNotice?.message) {
|
|
52
|
+
out(`Agnoclast: ⚠ ${r.captureNotice.message}`)
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
42
55
|
const n = typeof r.projectCount === 'number' ? ` · ${r.projectCount} project${r.projectCount === 1 ? '' : 's'} visible` : ''
|
|
43
56
|
out(`Agnoclast: connected — sessions on this machine are captured to your org${n}.`)
|
|
44
57
|
} else {
|
package/lib/editors/claude.mjs
CHANGED
|
@@ -41,7 +41,8 @@ export const CORTEX_ALLOWED_TOOLS = [
|
|
|
41
41
|
/** Merge Agnoclast's Claude Code hooks into a ~/.claude/settings.json object. Pure + idempotent: drops
|
|
42
42
|
* any prior cortex entry (old token/path/version) from each hook array before appending the current
|
|
43
43
|
* one — capture (Stop), status + skills-repair + snapshot-context (SessionStart), hydrate
|
|
44
|
-
* (UserPromptSubmit)
|
|
44
|
+
* (UserPromptSubmit). Also UNWIRES the retired PreCompact reminder from seats that still carry it.
|
|
45
|
+
* Commands carry NO inline token (each subcommand
|
|
45
46
|
* self-resolves it). Mirrors setup.mjs
|
|
46
47
|
* step 2 exactly; foreign hooks are never touched. Also merges the CORTEX_ALLOWED_TOOLS permission
|
|
47
48
|
* allowlist — additive-only: a user's own allow entries (even extra mcp__cortex__* ones) are never
|
|
@@ -105,16 +106,28 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
105
106
|
hgrp.hooks = hgrp.hooks ?? []
|
|
106
107
|
hgrp.hooks.push({ type: 'command', command: hydrateCmd })
|
|
107
108
|
|
|
108
|
-
// PreCompact —
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
// PreCompact — REMOVED, and this block is the migration that unwires existing seats.
|
|
110
|
+
//
|
|
111
|
+
// The "author now" reminder wrote to stdout on the assumption the harness surfaced it as context for
|
|
112
|
+
// the next turn. It does not: PreCompact accepts a blocking `decision` and nothing else — it has no
|
|
113
|
+
// additionalContext channel — so the reminder never reached a model. Measured 2026-08-10 across 7,232
|
|
114
|
+
// local transcripts: the reminder text appears 5 times, every one of them a tool_result from someone
|
|
115
|
+
// READING the file, assistant prose about it, or a compaction summary that absorbed such prose. Zero
|
|
116
|
+
// injections, against >=13 transcripts that demonstrably compacted. The control is what makes that
|
|
117
|
+
// conclusive rather than merely absent: SessionStart's hook output, on a channel that IS injected,
|
|
118
|
+
// appears in 912 transcripts of the same corpus.
|
|
119
|
+
//
|
|
120
|
+
// So: filter, never append. Every install/repair strips the stale entry from seats that already have
|
|
121
|
+
// it, which is why this runs unconditionally instead of shipping as a separate migration.
|
|
122
|
+
if (Array.isArray(s.hooks.PreCompact)) {
|
|
123
|
+
for (const pg of s.hooks.PreCompact) {
|
|
124
|
+
if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !/cortex-mcp(@[^ ]*)? precompact/.test(h.command ?? ''))
|
|
125
|
+
}
|
|
126
|
+
// Drop groups we just emptied, then the key itself if no foreign hook remains — a bare
|
|
127
|
+
// `PreCompact: []` reads as "cortex wires this event" to the next person to open settings.json.
|
|
128
|
+
s.hooks.PreCompact = s.hooks.PreCompact.filter((pg) => (pg.hooks ?? []).length > 0)
|
|
129
|
+
if (s.hooks.PreCompact.length === 0) delete s.hooks.PreCompact
|
|
113
130
|
}
|
|
114
|
-
let pgrp = s.hooks.PreCompact.find((g) => (g.matcher ?? '') === '')
|
|
115
|
-
if (!pgrp) { pgrp = { matcher: '', hooks: [] }; s.hooks.PreCompact.push(pgrp) }
|
|
116
|
-
pgrp.hooks = pgrp.hooks ?? []
|
|
117
|
-
pgrp.hooks.push({ type: 'command', command: precompactCmd })
|
|
118
131
|
|
|
119
132
|
// Permissions — pre-authorize the read + authoring core so a page update never stalls on a
|
|
120
133
|
// permission prompt. Append-missing only (no filter-and-rebuild like the hooks above): removals
|
package/lib/server.mjs
CHANGED
|
@@ -137,6 +137,53 @@ export async function runServer(version) {
|
|
|
137
137
|
},
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
+
// Where this machine's unattended session captures land. The server half shipped 2026-08-09 with no
|
|
141
|
+
// client surface at all, so the only way to set it was a hand-written authenticated HTTP call —
|
|
142
|
+
// which meant three real users whose sessions were silently being held could not fix it themselves.
|
|
143
|
+
// This is the surface that makes it answerable in conversation: "put my sessions in TTO".
|
|
144
|
+
server.registerTool(
|
|
145
|
+
'set_capture_brain',
|
|
146
|
+
{
|
|
147
|
+
title: 'Choose where your session captures are saved',
|
|
148
|
+
description:
|
|
149
|
+
'Set which brain THIS PERSON\'s unattended session captures (the automatic end-of-session record) land in, or read the current setting by omitting `brain`. WHEN TO USE: whenever the user says their sessions are not being saved, asks where their work is going, or a session-start notice says captures are being HELD. WHY IT IS NEEDED: with more than one brain, a capture that names no brain cannot be routed and is held outside every brain — correct, but invisible, so it accumulates silently. This is per-PERSON and applies to their own captures only; it cannot be set for someone else. ⚠ PASS THE ORG ID when the user has two brains with the SAME NAME (e.g. two called "Personal") — a name matching more than one is REFUSED rather than guessed, and the error lists the ids to choose from. Setting this does NOT file already-held captures; those keep their original dates and may belong in different brains, so sort them deliberately rather than dumping them into the new default.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
brain: z.string().optional().describe('the brain name or org id (from my_brains) where this person\'s session captures should land. Omit to read the current setting instead of changing it.'),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
async ({ brain }) => {
|
|
155
|
+
const url = `${BASE}/api/brain/capture-default`
|
|
156
|
+
if (!brain?.trim()) {
|
|
157
|
+
const res = await fetchCortex(url, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
const body = await res.text()
|
|
160
|
+
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
161
|
+
}
|
|
162
|
+
const { defaults } = await res.json()
|
|
163
|
+
if (!defaults?.length) {
|
|
164
|
+
return { content: [{ type: 'text', text: 'No capture brain is set. If you belong to more than one brain, your session captures are being HELD outside every brain until you set one. Call this tool again with `brain` to fix it.' }] }
|
|
165
|
+
}
|
|
166
|
+
const lines = defaults.map((d) => `${d.sourceType} captures land in "${d.orgName}" (${d.orgId}), set ${String(d.updatedAt).slice(0, 10)}`)
|
|
167
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
168
|
+
}
|
|
169
|
+
const res = await fetchCortex(url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ sourceType: 'claude-code', brain: brain.trim() }),
|
|
173
|
+
})
|
|
174
|
+
const body = await res.text()
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
// Prefer the server's own error: a 409 lists the org ids of an ambiguous name, which IS the
|
|
177
|
+
// remedy, and a generic classification would throw that away.
|
|
178
|
+
let msg
|
|
179
|
+
try { msg = JSON.parse(body).error } catch { /* fall through to the classified message */ }
|
|
180
|
+
throw new Error(msg ?? classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
181
|
+
}
|
|
182
|
+
const j = JSON.parse(body)
|
|
183
|
+
return { content: [{ type: 'text', text: `✓ Your Claude Code sessions now land in "${j.brain}" (${j.orgId}). Sessions captured before now are still held and keep their original dates — file those deliberately, they may not all belong in this brain.` }] }
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
140
187
|
// T8: cortex-log's authoritative writer. The skill composes a curated summary, then calls this to
|
|
141
188
|
// persist it AS the durable record (capture_source='skill'). The ingest conflict guard ensures the
|
|
142
189
|
// auto-capture hook never clobbers it. Pass the SAME sessionId the hook uses so the two dedupe onto
|
package/lib/setup.mjs
CHANGED
|
@@ -128,8 +128,9 @@ export async function runSetup(argv, version) {
|
|
|
128
128
|
process.exit(1)
|
|
129
129
|
}
|
|
130
130
|
const bak = backup(settingsJson)
|
|
131
|
-
// Capture (Stop) + status/skills/snapshot (SessionStart) +
|
|
132
|
-
// merge — the same pure function the `cortex install` Claude adapter uses.
|
|
131
|
+
// Capture (Stop) + status/skills/snapshot (SessionStart) + hydrate (UserPromptSubmit), idempotent
|
|
132
|
+
// merge — the same pure function the `cortex install` Claude adapter uses. Also unwires the retired
|
|
133
|
+
// PreCompact reminder from seats that still carry it.
|
|
133
134
|
s = mergeClaudeSettings(s, spec)
|
|
134
135
|
|
|
135
136
|
ensureDir(settingsJson)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { fetchCortex, classify, resolveBase } from './diagnose.mjs'
|
|
2
|
+
import { resolveToken } from './doctor.mjs'
|
|
3
|
+
|
|
4
|
+
// `use-brain` — set (or show) which brain this machine's unattended session captures land in.
|
|
5
|
+
//
|
|
6
|
+
// WHY A SUBCOMMAND EXISTS AT ALL. The server side of this shipped 2026-08-09 with NO client surface:
|
|
7
|
+
// no CLI, no MCP tool, no console setting. The only way to set a capture default was a raw
|
|
8
|
+
// authenticated HTTP call, which meant the only people who could fix a broken capture were the ones
|
|
9
|
+
// who could hand-write a curl with a bearer token. Three real users needed it; one of them is not
|
|
10
|
+
// technical. A fix only its author can operate is not a fix.
|
|
11
|
+
//
|
|
12
|
+
// Pairs with the SessionStart notice: the notice tells you captures are being held and names this
|
|
13
|
+
// command, so the loop from "something is wrong" to "it is fixed" is one paste with no docs.
|
|
14
|
+
|
|
15
|
+
function out(m) { process.stdout.write(m + '\n') }
|
|
16
|
+
|
|
17
|
+
export async function runUseBrain(args) {
|
|
18
|
+
const base = resolveBase(process.env.CORTEX_URL)
|
|
19
|
+
const { token } = resolveToken()
|
|
20
|
+
if (!token) {
|
|
21
|
+
out('Agnoclast: no token found. Run: npx -y @theronap/cortex-mcp setup <token>')
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Everything after the subcommand is the brain, joined — so an unquoted multi-word name still
|
|
26
|
+
// works. `use-brain Real estate` is what a person actually types; refusing it over a missing pair
|
|
27
|
+
// of quotes would be the same species of unhelpfulness this command exists to remove.
|
|
28
|
+
const wanted = (args ?? []).filter((a) => !a.startsWith('--')).join(' ').trim()
|
|
29
|
+
const url = `${base}/api/brain/capture-default`
|
|
30
|
+
|
|
31
|
+
if (!wanted) {
|
|
32
|
+
// No argument: report the current state rather than erroring. "What is it set to?" is a fair
|
|
33
|
+
// question and the answer is one GET away.
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetchCortex(url, { headers: { Authorization: `Bearer ${token}` } })
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body = await res.text()
|
|
38
|
+
out(`Agnoclast: could not read your capture settings — ${classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message}`)
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
const { defaults } = await res.json()
|
|
42
|
+
if (!defaults?.length) {
|
|
43
|
+
out('Agnoclast: no capture brain set. Your unattended session captures land in a brain only if')
|
|
44
|
+
out(' you belong to exactly one; otherwise they are HELD outside every brain until you set this.')
|
|
45
|
+
out(' Set one: npx -y @theronap/cortex-mcp use-brain "<brain name or org id>"')
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
for (const d of defaults) out(`Agnoclast: ${d.sourceType} captures land in "${d.orgName}" (${d.orgId})`)
|
|
49
|
+
return 0
|
|
50
|
+
} catch (e) {
|
|
51
|
+
out(`Agnoclast: could not reach the server (${e?.message ?? String(e)})`)
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetchCortex(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ sourceType: 'claude-code', brain: wanted }),
|
|
61
|
+
})
|
|
62
|
+
const body = await res.text()
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
let msg = classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message
|
|
65
|
+
// The server's own error text is better than a generic one here: a 404 names the brain that was
|
|
66
|
+
// not found, and a 409 lists the org ids of an ambiguous name — which is the whole remedy.
|
|
67
|
+
try { const j = JSON.parse(body); if (j.error) msg = j.error } catch { /* keep the classified message */ }
|
|
68
|
+
out(`Agnoclast: ${msg}`)
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
const j = JSON.parse(body)
|
|
72
|
+
out(`Agnoclast: ✓ your Claude Code sessions now land in "${j.brain}".`)
|
|
73
|
+
// Say plainly what this does NOT do. The setter's own server-side note makes the same point,
|
|
74
|
+
// because "I fixed it" reading as "and the backlog is handled" is how held records stay held.
|
|
75
|
+
out(' Sessions captured BEFORE now are still held — they keep their original dates until sorted.')
|
|
76
|
+
out(' Ask your assistant to file them (they may not all belong in the same brain).')
|
|
77
|
+
return 0
|
|
78
|
+
} catch (e) {
|
|
79
|
+
out(`Agnoclast: could not reach the server (${e?.message ?? String(e)})`)
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
CHANGED
package/lib/precompact.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// PreCompact "author now" reminder (③ live wiki authoring, [[cortex-wiki-authoring-spec]] D2).
|
|
2
|
-
//
|
|
3
|
-
// Wired by setup.mjs as a PreCompact hook. A hook CANNOT force a model turn — it can only inject text
|
|
4
|
-
// the model sees on its next turn (best-effort). So this prints a reminder to sweep understanding into
|
|
5
|
-
// the wiki BEFORE compaction discards the session's hot mental model. The HARD backstop is the /log
|
|
6
|
-
// skill (cortex-log step 5); this catches the in-session compaction that would otherwise lose the magic.
|
|
7
|
-
//
|
|
8
|
-
// Output goes to stdout, which the Claude Code harness surfaces as additional context for the next turn.
|
|
9
|
-
export function runPrecompactReminder() {
|
|
10
|
-
process.stdout.write(
|
|
11
|
-
'Agnoclast: context is about to compact. If your understanding of any node (the project(s) you worked ' +
|
|
12
|
-
'on, people you coordinated with, or yourself) advanced this session, AUTHOR it into the wiki NOW ' +
|
|
13
|
-
'before it is lost: call `authoring_context` then `author` for each. This is a synthesis of your ' +
|
|
14
|
-
'compiled understanding with inline [[links]], not a transcript dump. Skip nodes you did not advance.\n',
|
|
15
|
-
)
|
|
16
|
-
}
|