@theronap/cortex-mcp 0.9.59 → 0.9.61
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 +14 -5
- package/lib/login.mjs +148 -0
- package/lib/server.mjs +76 -64
- package/package.json +1 -1
- package/skills/log/SKILL.md +18 -16
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -36,11 +36,13 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
36
36
|
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
37
37
|
process.stdout.write(
|
|
38
38
|
`cortex-mcp ${VERSION} — connect your AI assistant to Cortex\n\n` +
|
|
39
|
-
`Onboard (one command):\n` +
|
|
40
|
-
` npx -y @theronap/cortex-mcp
|
|
41
|
-
`This
|
|
42
|
-
`
|
|
39
|
+
`Onboard (one command, no token to copy):\n` +
|
|
40
|
+
` npx -y @theronap/cortex-mcp login\n\n` +
|
|
41
|
+
`This opens your browser, you click Approve, and it wires everything up. Restart\n` +
|
|
42
|
+
`Claude Code after, and your AI sees your Cortex context while your sessions flow\n` +
|
|
43
|
+
`into the org automatically.\n\n` +
|
|
43
44
|
`Subcommands:\n` +
|
|
45
|
+
` login [--label <name>] browser-approved sign-in — gets a token for you, then runs setup\n` +
|
|
44
46
|
` setup <token> wire MCP server + capture hook into ~/.claude config (single editor)\n` +
|
|
45
47
|
` install [<token>] [--editor auto|all|<id,...>] wire Cortex into EVERY detected editor + write the capability manifest\n` +
|
|
46
48
|
` repair re-run setup at the latest version using your existing token (no token needed)\n` +
|
|
@@ -68,7 +70,14 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
68
70
|
// A graceful drain lets libuv finish those handles first, so the assertion never fires. The MCP
|
|
69
71
|
// server (default branch) runs forever and never reaches an exit path. if/else so the CLI
|
|
70
72
|
// commands don't fall through into the server.
|
|
71
|
-
if (cmd === '
|
|
73
|
+
if (cmd === 'login') {
|
|
74
|
+
// Browser-approved sign-in: no token to copy. Ends by calling runSetup with the token it
|
|
75
|
+
// collected, so there is still exactly ONE code path that writes a credential to disk.
|
|
76
|
+
const { runLogin } = await import('../lib/login.mjs')
|
|
77
|
+
await runLogin(rest, VERSION)
|
|
78
|
+
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
79
|
+
await closeFetch()
|
|
80
|
+
} else if (cmd === 'setup') {
|
|
72
81
|
const { runSetup } = await import('../lib/setup.mjs')
|
|
73
82
|
await runSetup(rest, VERSION)
|
|
74
83
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { resolveBase, CANONICAL_BASE } from './diagnose.mjs'
|
|
4
|
+
|
|
5
|
+
// `cortex-mcp login` — get a token without the human copy-pasting one out of the web console.
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS EXISTS: the old path was log into the console, find /connect, copy a uuid, paste it into
|
|
8
|
+
// a terminal. That copy-paste is the step that failed on the first non-technical onboarding
|
|
9
|
+
// (2026-08-03), and it is the reason there could never be a real one-line install. Here the person
|
|
10
|
+
// clicks Approve in a browser and types nothing.
|
|
11
|
+
//
|
|
12
|
+
// THE FLOW (server side: /api/device/{start,approve,exchange}):
|
|
13
|
+
// 1. start — we generate nothing; the server issues a device_code (our secret) + a short
|
|
14
|
+
// user_code, and we open the pre-filled approval URL.
|
|
15
|
+
// 2. approve — happens in their browser, against their login. We never see their password and
|
|
16
|
+
// never handle a JWT.
|
|
17
|
+
// 3. exchange — we poll with the device_code; once a human has approved, the token is minted and
|
|
18
|
+
// returned exactly once. Then we hand straight off to the normal `setup` wiring.
|
|
19
|
+
//
|
|
20
|
+
// Deliberately a DEVICE flow rather than a loopback redirect. Loopback needs a local port and an
|
|
21
|
+
// open browser on the same machine, so it dies over SSH and on locked-down laptops. With the URL
|
|
22
|
+
// pre-filled, the device flow costs the user the same single click and works everywhere.
|
|
23
|
+
|
|
24
|
+
/** Open a URL in the user's default browser. Best-effort: never throws, never blocks the flow. */
|
|
25
|
+
function openBrowser(url) {
|
|
26
|
+
const cmd =
|
|
27
|
+
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
|
|
28
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url]
|
|
29
|
+
try {
|
|
30
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true })
|
|
31
|
+
// If the browser cannot be launched we still printed the URL, so this is genuinely non-fatal.
|
|
32
|
+
child.on('error', () => {})
|
|
33
|
+
child.unref()
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
41
|
+
|
|
42
|
+
/** A label the person will recognise on the approval screen. Machine name, not a user name. */
|
|
43
|
+
function deviceLabel() {
|
|
44
|
+
const host = os.hostname().replace(/\.local$/i, '')
|
|
45
|
+
return host || `${os.platform()} device`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runLogin(argv = [], version = 'dev') {
|
|
49
|
+
const base = resolveBase(process.env.CORTEX_URL) || CANONICAL_BASE
|
|
50
|
+
|
|
51
|
+
// --label lets a person running several machines tell them apart later; account_tokens.label
|
|
52
|
+
// carries it, so it survives long after the grant row is swept.
|
|
53
|
+
const labelFlag = argv.indexOf('--label')
|
|
54
|
+
const label = labelFlag !== -1 && argv[labelFlag + 1] ? argv[labelFlag + 1] : deviceLabel()
|
|
55
|
+
|
|
56
|
+
let start
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${base}/api/device/start`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ label }),
|
|
62
|
+
})
|
|
63
|
+
if (!res.ok) throw new Error(`server said ${res.status}`)
|
|
64
|
+
start = await res.json()
|
|
65
|
+
} catch (err) {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`cortex: could not reach ${base} to start login (${err.message}).\n` +
|
|
68
|
+
`Check your connection, then try again.\n`,
|
|
69
|
+
)
|
|
70
|
+
process.exitCode = 1
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const opened = openBrowser(start.verificationUriComplete)
|
|
75
|
+
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`\n Confirm this code in your browser: ${start.userCode}\n\n` +
|
|
78
|
+
(opened
|
|
79
|
+
? ` A browser should have opened. If not, go to:\n ${start.verificationUriComplete}\n\n`
|
|
80
|
+
: ` Open this in your browser:\n ${start.verificationUriComplete}\n\n`) +
|
|
81
|
+
` Waiting for you to approve it...\n`,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Poll until approved or the grant expires. The server sets the pace (and says slow_down if we
|
|
85
|
+
// are early), so the cadence stays server-controlled rather than hardcoded here.
|
|
86
|
+
let interval = (start.interval ?? 2) * 1000
|
|
87
|
+
const deadline = Date.now() + (start.expiresIn ?? 600) * 1000
|
|
88
|
+
let token = null
|
|
89
|
+
let activeOrgId = null
|
|
90
|
+
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
await sleep(interval)
|
|
93
|
+
let res
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(`${base}/api/device/exchange`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ deviceCode: start.deviceCode }),
|
|
99
|
+
})
|
|
100
|
+
} catch {
|
|
101
|
+
continue // transient network blip: keep waiting rather than failing a login mid-approval
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (res.status === 429) {
|
|
105
|
+
interval = Math.min(interval * 2, 10_000) // back off, do not give up
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (res.status === 410) {
|
|
109
|
+
process.stderr.write(`\ncortex: that took too long and the code expired. Run login again.\n`)
|
|
110
|
+
process.exitCode = 1
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 409 || res.status === 404) {
|
|
114
|
+
const body = await res.json().catch(() => ({}))
|
|
115
|
+
process.stderr.write(`\ncortex: ${body.error ?? 'this login is no longer valid'}. Run login again.\n`)
|
|
116
|
+
process.exitCode = 1
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok) continue
|
|
120
|
+
|
|
121
|
+
const body = await res.json().catch(() => ({}))
|
|
122
|
+
if (body.status === 'approved' && body.personalToken) {
|
|
123
|
+
token = body.personalToken
|
|
124
|
+
activeOrgId = body.activeOrgId ?? null
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
// 'pending' — the human has not clicked yet. Keep waiting quietly; a spinner that reprints
|
|
128
|
+
// every two seconds is noise on the one screen where the person is reading instructions.
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!token) {
|
|
132
|
+
process.stderr.write(`\ncortex: nobody approved that login before it expired. Run login again.\n`)
|
|
133
|
+
process.exitCode = 1
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.stdout.write(`\n Approved. Setting up...\n\n`)
|
|
138
|
+
|
|
139
|
+
// Hand the raw token straight to the existing installer. We never write it anywhere ourselves —
|
|
140
|
+
// runSetup owns every file that touches a credential, so there is exactly one code path that
|
|
141
|
+
// stores a token and one place to audit.
|
|
142
|
+
const { runSetup } = await import('./setup.mjs')
|
|
143
|
+
await runSetup([token], version)
|
|
144
|
+
|
|
145
|
+
if (activeOrgId) {
|
|
146
|
+
process.stdout.write(` New pages will be saved in the space you picked.\n`)
|
|
147
|
+
}
|
|
148
|
+
}
|
package/lib/server.mjs
CHANGED
|
@@ -28,6 +28,18 @@ async function redLinkTriage(BASE, TOKEN, name) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// MCP tool results carry an `isError` flag, and a failure that omits it is indistinguishable from a
|
|
32
|
+
// success at the protocol level — the caller just sees a normal result whose text happens to begin
|
|
33
|
+
// "Could not". This file had 41 tools, 64 failure returns and ZERO uses of isError, so nothing
|
|
34
|
+
// downstream could tell a rejected write from a completed one. Measured 2026-07-31: a walker over
|
|
35
|
+
// 397 transcripts scored 18 REJECTED `author` calls as successful writes for exactly this reason,
|
|
36
|
+
// and an agent skimming its own tool result is exposed the same way. Route every failure through
|
|
37
|
+
// here so the flag cannot be forgotten at a new call site.
|
|
38
|
+
//
|
|
39
|
+
// NAME: deliberately not `fail` — runServer already has a local `fail(verb, res)` for the
|
|
40
|
+
// file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
|
|
41
|
+
const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
|
|
42
|
+
|
|
31
43
|
// The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
|
|
32
44
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
33
45
|
|
|
@@ -406,12 +418,12 @@ export async function runServer(version) {
|
|
|
406
418
|
if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
|
|
407
419
|
if (!vr.ok) {
|
|
408
420
|
const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
|
|
409
|
-
return
|
|
421
|
+
return toolError(`Could not read version "${version}" of "${name}": ${d.message}`)
|
|
410
422
|
}
|
|
411
423
|
const v = await vr.json()
|
|
412
424
|
return { content: [{ type: 'text', text: `# ${name} — historical version (rev ${v.revNo} · ${v.op} · ${String(v.createdAt).slice(0, 10)} · ${v.tier})\nversion: ${v.version}\n\n${v.body}\n\n— This is a HISTORICAL snapshot, not the current page. \`read_page "${name}"\` (no version) shows what's live; \`rollback_page\` restores this one as a new version.` }] }
|
|
413
425
|
} catch (e) {
|
|
414
|
-
return
|
|
426
|
+
return toolError(`Could not read version "${version}" of "${name}": ${e.message}`)
|
|
415
427
|
}
|
|
416
428
|
}
|
|
417
429
|
// PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
|
|
@@ -422,7 +434,7 @@ export async function runServer(version) {
|
|
|
422
434
|
if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
|
|
423
435
|
if (!r.ok) {
|
|
424
436
|
const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
|
|
425
|
-
return
|
|
437
|
+
return toolError(`Could not read the timeline for "${name}": ${d.message}`)
|
|
426
438
|
}
|
|
427
439
|
const t = await r.json()
|
|
428
440
|
if (!t.identifiers?.length) {
|
|
@@ -435,7 +447,7 @@ export async function runServer(version) {
|
|
|
435
447
|
if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
|
|
436
448
|
return { content: [{ type: 'text', text: lines.join('\n') }] }
|
|
437
449
|
} catch (e) {
|
|
438
|
-
return
|
|
450
|
+
return toolError(`Could not read the timeline for "${name}": ${e.message}`)
|
|
439
451
|
}
|
|
440
452
|
}
|
|
441
453
|
// IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
|
|
@@ -474,14 +486,14 @@ export async function runServer(version) {
|
|
|
474
486
|
res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
|
|
475
487
|
{ headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
476
488
|
} catch (e) {
|
|
477
|
-
return
|
|
489
|
+
return toolError(`Could not read "${name}": ${e.message}`)
|
|
478
490
|
}
|
|
479
491
|
if (res.status === 404) {
|
|
480
492
|
return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
|
|
481
493
|
}
|
|
482
494
|
if (!res.ok) {
|
|
483
495
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
484
|
-
return
|
|
496
|
+
return toolError(`Could not read "${name}": ${d.message}`)
|
|
485
497
|
}
|
|
486
498
|
const page = await res.json()
|
|
487
499
|
// Multi-brain (decision 3A): /api/brain/page returns matches[] — one entry per brain that has a
|
|
@@ -559,12 +571,12 @@ export async function runServer(version) {
|
|
|
559
571
|
const qs = new URLSearchParams({ kind: k, key: name, ...(limit ? { limit: String(limit) } : {}) })
|
|
560
572
|
res = await fetchCortex(`${BASE}/api/brain/page-history?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
561
573
|
} catch (e) {
|
|
562
|
-
return
|
|
574
|
+
return toolError(`Could not read history for "${name}": ${e.message}`)
|
|
563
575
|
}
|
|
564
576
|
if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}". Pass kind (person/org) if it isn't a project.` }] }
|
|
565
577
|
if (!res.ok) {
|
|
566
578
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
567
|
-
return
|
|
579
|
+
return toolError(`Could not read history for "${name}": ${d.message}`)
|
|
568
580
|
}
|
|
569
581
|
const out = await res.json()
|
|
570
582
|
const revs = out.revisions ?? []
|
|
@@ -612,12 +624,12 @@ export async function runServer(version) {
|
|
|
612
624
|
})
|
|
613
625
|
res = await fetchCortex(`${BASE}/api/brain/page-diff?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
614
626
|
} catch (e) {
|
|
615
|
-
return
|
|
627
|
+
return toolError(`Could not diff "${name}": ${e.message}`)
|
|
616
628
|
}
|
|
617
629
|
if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}", or it has no version "${to}". Run \`page_history "${name}"\` to list its versions.` }] }
|
|
618
630
|
if (!res.ok) {
|
|
619
631
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
620
|
-
return
|
|
632
|
+
return toolError(`Could not diff "${name}": ${d.message}`)
|
|
621
633
|
}
|
|
622
634
|
const out = await res.json()
|
|
623
635
|
const d = out.diff
|
|
@@ -669,10 +681,10 @@ export async function runServer(version) {
|
|
|
669
681
|
body: JSON.stringify({ kind: k, name, to_version, ...(tier ? { tier } : {}), ...(reason ? { reason } : {}) }),
|
|
670
682
|
})
|
|
671
683
|
} catch (e) {
|
|
672
|
-
return
|
|
684
|
+
return toolError(`Could not roll back "${name}": ${e.message}`)
|
|
673
685
|
}
|
|
674
686
|
const out = await res.json().catch(() => null)
|
|
675
|
-
if (!res.ok) return
|
|
687
|
+
if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
|
|
676
688
|
return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
|
|
677
689
|
},
|
|
678
690
|
)
|
|
@@ -705,7 +717,7 @@ export async function runServer(version) {
|
|
|
705
717
|
}),
|
|
706
718
|
})
|
|
707
719
|
} catch (e) {
|
|
708
|
-
return
|
|
720
|
+
return toolError(`Could not rename the section: ${e.message}`)
|
|
709
721
|
}
|
|
710
722
|
const out = await res.json().catch(() => null)
|
|
711
723
|
if (!res.ok) {
|
|
@@ -718,7 +730,7 @@ export async function runServer(version) {
|
|
|
718
730
|
out?.currentVersion ? `current version: ${out.currentVersion}` : '',
|
|
719
731
|
].filter(Boolean).join(' · ')
|
|
720
732
|
const hint = out?.hint ? `\n${out.hint}` : ''
|
|
721
|
-
return
|
|
733
|
+
return toolError(`Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
|
|
722
734
|
}
|
|
723
735
|
return { content: [{ type: 'text', text: `Renamed on "${name}" (${out.brain} · ${out.tier} tier): "${out.from}" -> "${out.to}". The section kept its body, position and as-of date. New version: ${out.version}` }] }
|
|
724
736
|
},
|
|
@@ -753,7 +765,7 @@ export async function runServer(version) {
|
|
|
753
765
|
}),
|
|
754
766
|
})
|
|
755
767
|
} catch (e) {
|
|
756
|
-
return
|
|
768
|
+
return toolError(`Could not edit "${name}": ${e.message}`)
|
|
757
769
|
}
|
|
758
770
|
const out = await res.json().catch(() => null)
|
|
759
771
|
if (!res.ok) {
|
|
@@ -784,7 +796,7 @@ export async function runServer(version) {
|
|
|
784
796
|
Array.isArray(out?.concurrentRemoved) && out.concurrentRemoved.length ? `sections REMOVED elsewhere: ${out.concurrentRemoved.join(', ')}` : '',
|
|
785
797
|
Array.isArray(out?.concurrentTruncated) && out.concurrentTruncated.length ? `(truncated, see page_diff for all of: ${out.concurrentTruncated.join(', ')})` : '',
|
|
786
798
|
].filter(Boolean).join('\n')
|
|
787
|
-
return
|
|
799
|
+
return toolError(`Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}`)
|
|
788
800
|
}
|
|
789
801
|
const red = Array.isArray(out.redLinks) && out.redLinks.length
|
|
790
802
|
? `\nRed-links now on this page: ${out.redLinks.map((r) => `[[${r}]]`).join(', ')}` : ''
|
|
@@ -804,11 +816,11 @@ export async function runServer(version) {
|
|
|
804
816
|
try {
|
|
805
817
|
res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
806
818
|
} catch (e) {
|
|
807
|
-
return
|
|
819
|
+
return toolError(`Could not load writing style: ${e.message}`)
|
|
808
820
|
}
|
|
809
821
|
if (!res.ok) {
|
|
810
822
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
811
|
-
return
|
|
823
|
+
return toolError(`Could not load writing style: ${d.message}`)
|
|
812
824
|
}
|
|
813
825
|
const { style } = await res.json()
|
|
814
826
|
return { content: [{ type: 'text', text: style }] }
|
|
@@ -831,11 +843,11 @@ export async function runServer(version) {
|
|
|
831
843
|
body: JSON.stringify({ style_md }),
|
|
832
844
|
})
|
|
833
845
|
} catch (e) {
|
|
834
|
-
return
|
|
846
|
+
return toolError(`Could not save writing style: ${e.message}`)
|
|
835
847
|
}
|
|
836
848
|
if (!res.ok) {
|
|
837
849
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
838
|
-
return
|
|
850
|
+
return toolError(`Could not save writing style: ${d.message}`)
|
|
839
851
|
}
|
|
840
852
|
const r = await res.json()
|
|
841
853
|
return { content: [{ type: 'text', text: r.saved ? `Saved your writing-style profile (${r.chars} chars).` : 'Cleared your writing-style profile.' }] }
|
|
@@ -854,11 +866,11 @@ export async function runServer(version) {
|
|
|
854
866
|
try {
|
|
855
867
|
res = await fetchCortex(`${BASE}/api/brains`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
856
868
|
} catch (e) {
|
|
857
|
-
return
|
|
869
|
+
return toolError(`Could not list brains: ${e.message}`)
|
|
858
870
|
}
|
|
859
871
|
if (!res.ok) {
|
|
860
872
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
861
|
-
return
|
|
873
|
+
return toolError(`Could not list brains: ${d.message}`)
|
|
862
874
|
}
|
|
863
875
|
const { brains, activeIsExplicit, activeSource, sessionOrgId, accountOrgId } = await res.json()
|
|
864
876
|
if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
|
|
@@ -898,11 +910,11 @@ export async function runServer(version) {
|
|
|
898
910
|
try {
|
|
899
911
|
res = await fetchCortex(`${BASE}/api/brains/pages?orgId=${encodeURIComponent(org_id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
900
912
|
} catch (e) {
|
|
901
|
-
return
|
|
913
|
+
return toolError(`Could not list pages: ${e.message}`)
|
|
902
914
|
}
|
|
903
915
|
if (!res.ok) {
|
|
904
916
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
905
|
-
return
|
|
917
|
+
return toolError(`Could not list pages: ${d.message}`)
|
|
906
918
|
}
|
|
907
919
|
const { nodeCount, rowCount, pages } = await res.json()
|
|
908
920
|
if (!pages?.length) return { content: [{ type: 'text', text: `Brain ${org_id} has no authored pages.` }] }
|
|
@@ -939,11 +951,11 @@ export async function runServer(version) {
|
|
|
939
951
|
body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
|
|
940
952
|
})
|
|
941
953
|
} catch (e) {
|
|
942
|
-
return
|
|
954
|
+
return toolError(`Could not set active brain: ${e.message}`)
|
|
943
955
|
}
|
|
944
956
|
if (!res.ok) {
|
|
945
957
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
946
|
-
return
|
|
958
|
+
return toolError(`Could not set active brain: ${d.message}`)
|
|
947
959
|
}
|
|
948
960
|
const r = await res.json()
|
|
949
961
|
// Always say WHICH scope changed. The default is session-only, so a caller expecting the old
|
|
@@ -974,11 +986,11 @@ export async function runServer(version) {
|
|
|
974
986
|
body: JSON.stringify({ name }),
|
|
975
987
|
})
|
|
976
988
|
} catch (e) {
|
|
977
|
-
return
|
|
989
|
+
return toolError(`Could not create brain: ${e.message}`)
|
|
978
990
|
}
|
|
979
991
|
if (!res.ok) {
|
|
980
992
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
981
|
-
return
|
|
993
|
+
return toolError(`Could not create brain: ${d.message}`)
|
|
982
994
|
}
|
|
983
995
|
const r = await res.json()
|
|
984
996
|
return { content: [{ type: 'text', text: `Created brain "${name}" [${r.orgId}]. Reads already span it. Edits to pages in it route themselves; set_active_brain only if you want NEW pages to default here.` }] }
|
|
@@ -1007,11 +1019,11 @@ export async function runServer(version) {
|
|
|
1007
1019
|
try {
|
|
1008
1020
|
res = await fetchCortex(`${BASE}/api/records?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1009
1021
|
} catch (e) {
|
|
1010
|
-
return
|
|
1022
|
+
return toolError(`Could not list records: ${e.message}`)
|
|
1011
1023
|
}
|
|
1012
1024
|
if (!res.ok) {
|
|
1013
1025
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1014
|
-
return
|
|
1026
|
+
return toolError(`Could not list records: ${d.message}`)
|
|
1015
1027
|
}
|
|
1016
1028
|
const { text } = await res.json()
|
|
1017
1029
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1039,11 +1051,11 @@ export async function runServer(version) {
|
|
|
1039
1051
|
try {
|
|
1040
1052
|
res = await fetchCortex(`${BASE}/api/records/daily?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1041
1053
|
} catch (e) {
|
|
1042
|
-
return
|
|
1054
|
+
return toolError(`Could not build daily log: ${e.message}`)
|
|
1043
1055
|
}
|
|
1044
1056
|
if (!res.ok) {
|
|
1045
1057
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1046
|
-
return
|
|
1058
|
+
return toolError(`Could not build daily log: ${d.message}`)
|
|
1047
1059
|
}
|
|
1048
1060
|
const { text } = await res.json()
|
|
1049
1061
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1064,11 +1076,11 @@ export async function runServer(version) {
|
|
|
1064
1076
|
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
1065
1077
|
})
|
|
1066
1078
|
} catch (e) {
|
|
1067
|
-
return
|
|
1079
|
+
return toolError(`Could not list your sessions: ${e.message}`)
|
|
1068
1080
|
}
|
|
1069
1081
|
if (!res.ok) {
|
|
1070
1082
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1071
|
-
return
|
|
1083
|
+
return toolError(`Could not list your sessions: ${d.message}`)
|
|
1072
1084
|
}
|
|
1073
1085
|
const { text } = await res.json()
|
|
1074
1086
|
return { content: [{ type: 'text', text }] }
|
|
@@ -1094,11 +1106,11 @@ export async function runServer(version) {
|
|
|
1094
1106
|
body: JSON.stringify({ privacy }),
|
|
1095
1107
|
})
|
|
1096
1108
|
} catch (e) {
|
|
1097
|
-
return
|
|
1109
|
+
return toolError(`Could not set privacy: ${e.message}`)
|
|
1098
1110
|
}
|
|
1099
1111
|
if (!res.ok) {
|
|
1100
1112
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1101
|
-
return
|
|
1113
|
+
return toolError(`Could not set privacy: ${d.message}`)
|
|
1102
1114
|
}
|
|
1103
1115
|
const out = await res.json()
|
|
1104
1116
|
return { content: [{ type: 'text', text: `Done — record ${out.id} is now "${out.privacy}".` }] }
|
|
@@ -1127,11 +1139,11 @@ export async function runServer(version) {
|
|
|
1127
1139
|
body: JSON.stringify({ kind, name, validity, modality, superseded_by }),
|
|
1128
1140
|
})
|
|
1129
1141
|
} catch (e) {
|
|
1130
|
-
return
|
|
1142
|
+
return toolError(`Could not set validity: ${e.message}`)
|
|
1131
1143
|
}
|
|
1132
1144
|
if (!res.ok) {
|
|
1133
1145
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1134
|
-
return
|
|
1146
|
+
return toolError(`Could not set validity: ${d.message}`)
|
|
1135
1147
|
}
|
|
1136
1148
|
const out = await res.json()
|
|
1137
1149
|
return { content: [{ type: 'text', text: `Done — "${out.name}" is now ${out.validity}${out.modality ? ` / ${out.modality}` : ''} (${out.docs_updated} tier-doc(s) updated).` }] }
|
|
@@ -1158,10 +1170,10 @@ export async function runServer(version) {
|
|
|
1158
1170
|
body: JSON.stringify({ name, target_name, ...(target_kind ? { target_kind } : {}) }),
|
|
1159
1171
|
})
|
|
1160
1172
|
} catch (e) {
|
|
1161
|
-
return
|
|
1173
|
+
return toolError(`Could not alias: ${e.message}`)
|
|
1162
1174
|
}
|
|
1163
1175
|
const out = await res.json().catch(() => null)
|
|
1164
|
-
if (!res.ok) return
|
|
1176
|
+
if (!res.ok) return toolError(`Could not alias "${name}": ${out?.error ?? res.status}`)
|
|
1165
1177
|
if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
|
|
1166
1178
|
return { content: [{ type: 'text', text: `Done — [[${out.alias}]] now resolves to "${out.target}" (${out.target_kind}). It's out of the wanted-page backlog.` }] }
|
|
1167
1179
|
},
|
|
@@ -1186,10 +1198,10 @@ export async function runServer(version) {
|
|
|
1186
1198
|
body: JSON.stringify({ name, ...(days ? { days } : {}) }),
|
|
1187
1199
|
})
|
|
1188
1200
|
} catch (e) {
|
|
1189
|
-
return
|
|
1201
|
+
return toolError(`Could not snooze: ${e.message}`)
|
|
1190
1202
|
}
|
|
1191
1203
|
const out = await res.json().catch(() => null)
|
|
1192
|
-
if (!res.ok) return
|
|
1204
|
+
if (!res.ok) return toolError(`Could not snooze "${name}": ${out?.error ?? res.status}`)
|
|
1193
1205
|
if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
|
|
1194
1206
|
return { content: [{ type: 'text', text: `Snoozed "${out.name}" for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
|
|
1195
1207
|
},
|
|
@@ -1218,7 +1230,7 @@ export async function runServer(version) {
|
|
|
1218
1230
|
body: JSON.stringify({ kind, name, tier, ...(source_tier ? { source_tier } : {}), ...(absorb ? { absorb: true } : {}), ...(target_version ? { target_version } : {}) }),
|
|
1219
1231
|
})
|
|
1220
1232
|
} catch (e) {
|
|
1221
|
-
return
|
|
1233
|
+
return toolError(`Could not set page privacy: ${e.message}`)
|
|
1222
1234
|
}
|
|
1223
1235
|
const out = await res.json().catch(() => null)
|
|
1224
1236
|
if (!res.ok) {
|
|
@@ -1228,10 +1240,10 @@ export async function runServer(version) {
|
|
|
1228
1240
|
const extra = out.collision === 'readable' && out.blocking
|
|
1229
1241
|
? `\nYour ${out.source?.tier} page: ${out.source?.summary ?? out.source?.title}\nExisting ${out.blocking.tier} page (current version: ${out.blocking.version ?? 'none — this page predates content-hash tracking and cannot be re-authored via base_version; ask an admin about a backfill'}): ${out.blocking.summary ?? out.blocking.title}`
|
|
1230
1242
|
: ''
|
|
1231
|
-
return
|
|
1243
|
+
return toolError(`Could not set page privacy: ${out.error}${extra}`)
|
|
1232
1244
|
}
|
|
1233
1245
|
const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
|
|
1234
|
-
return
|
|
1246
|
+
return toolError(`Could not set page privacy: ${d.message}`)
|
|
1235
1247
|
}
|
|
1236
1248
|
const g = out.live_grants?.length
|
|
1237
1249
|
? ` Grants still active for: ${out.live_grants.map((x) => x.grantee_name ?? x.grantee_user_id).join(', ')}.`
|
|
@@ -1262,10 +1274,10 @@ export async function runServer(version) {
|
|
|
1262
1274
|
body: JSON.stringify({ kind, name, grantee, action, ...(tier ? { tier } : {}) }),
|
|
1263
1275
|
})
|
|
1264
1276
|
} catch (e) {
|
|
1265
|
-
return
|
|
1277
|
+
return toolError(`Could not ${action}: ${e.message}`)
|
|
1266
1278
|
}
|
|
1267
1279
|
const out = await res.json().catch(() => null)
|
|
1268
|
-
if (!res.ok) return
|
|
1280
|
+
if (!res.ok) return toolError(`Could not ${action}: ${out?.error ?? res.status}`)
|
|
1269
1281
|
const verb = { granted: 'now has access to', already_granted: 'already had access to', revoked: 'no longer has access to', not_granted: 'had no grant on' }[out.action]
|
|
1270
1282
|
return { content: [{ type: 'text', text: `Done — ${out.grantee} ${verb} the ${out.tier} page.` }] }
|
|
1271
1283
|
},
|
|
@@ -1288,10 +1300,10 @@ export async function runServer(version) {
|
|
|
1288
1300
|
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
1289
1301
|
})
|
|
1290
1302
|
} catch (e) {
|
|
1291
|
-
return
|
|
1303
|
+
return toolError(`Could not list grants: ${e.message}`)
|
|
1292
1304
|
}
|
|
1293
1305
|
const out = await res.json().catch(() => null)
|
|
1294
|
-
if (!res.ok) return
|
|
1306
|
+
if (!res.ok) return toolError(`Could not list grants: ${out?.error ?? res.status}`)
|
|
1295
1307
|
if (!out.grants?.length) return { content: [{ type: 'text', text: 'No grants on this page.' }] }
|
|
1296
1308
|
const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} variant (since ${String(g.created_at).slice(0, 10)})`)
|
|
1297
1309
|
return { content: [{ type: 'text', text: `Grants (${out.grants.length}):\n${lines.join('\n')}` }] }
|
|
@@ -1310,11 +1322,11 @@ export async function runServer(version) {
|
|
|
1310
1322
|
try {
|
|
1311
1323
|
res = await fetchCortex(`${BASE}/api/brain/retier-notices`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1312
1324
|
} catch (e) {
|
|
1313
|
-
return
|
|
1325
|
+
return toolError(`Could not list notices: ${e.message}`)
|
|
1314
1326
|
}
|
|
1315
1327
|
if (!res.ok) {
|
|
1316
1328
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1317
|
-
return
|
|
1329
|
+
return toolError(`Could not list notices: ${d.message}`)
|
|
1318
1330
|
}
|
|
1319
1331
|
const { notices } = await res.json()
|
|
1320
1332
|
if (!notices?.length) return { content: [{ type: 'text', text: 'No re-tier notices.' }] }
|
|
@@ -1336,11 +1348,11 @@ export async function runServer(version) {
|
|
|
1336
1348
|
try {
|
|
1337
1349
|
res = await fetchCortex(`${BASE}/api/brain/page-merge-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1338
1350
|
} catch (e) {
|
|
1339
|
-
return
|
|
1351
|
+
return toolError(`Could not list merge requests: ${e.message}`)
|
|
1340
1352
|
}
|
|
1341
1353
|
if (!res.ok) {
|
|
1342
1354
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1343
|
-
return
|
|
1355
|
+
return toolError(`Could not list merge requests: ${d.message}`)
|
|
1344
1356
|
}
|
|
1345
1357
|
const { requests } = await res.json()
|
|
1346
1358
|
if (!requests?.length) return { content: [{ type: 'text', text: 'No pending page merge requests.' }] }
|
|
@@ -1369,17 +1381,17 @@ export async function runServer(version) {
|
|
|
1369
1381
|
body: JSON.stringify({ id, decision }),
|
|
1370
1382
|
})
|
|
1371
1383
|
} catch (e) {
|
|
1372
|
-
return
|
|
1384
|
+
return toolError(`Could not decide: ${e.message}`)
|
|
1373
1385
|
}
|
|
1374
1386
|
const out = await res.json().catch(() => null)
|
|
1375
|
-
if (!res.ok) return
|
|
1387
|
+
if (!res.ok) return toolError(`Could not decide: ${out?.error ?? res.status}`)
|
|
1376
1388
|
return { content: [{ type: 'text', text: out.decision === 'denied' ? 'Denied — nothing moved.' : `Approved — removed variant(s): ${out.removed_variants.join(', ') || 'none remained'}. ${out.note}` }] }
|
|
1377
1389
|
},
|
|
1378
1390
|
)
|
|
1379
1391
|
|
|
1380
1392
|
// ── File requests: ask the owner for a record's FULL original (lives on their machine) ──
|
|
1381
1393
|
const fail = (verb, res) => async () =>
|
|
1382
|
-
(
|
|
1394
|
+
toolError(`Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}`)
|
|
1383
1395
|
|
|
1384
1396
|
server.registerTool(
|
|
1385
1397
|
'request_file',
|
|
@@ -1395,7 +1407,7 @@ export async function runServer(version) {
|
|
|
1395
1407
|
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1396
1408
|
body: JSON.stringify({ recordId: record_id }),
|
|
1397
1409
|
})
|
|
1398
|
-
} catch (e) { return
|
|
1410
|
+
} catch (e) { return toolError(`Could not request: ${e.message}`) }
|
|
1399
1411
|
if (res.status === 409) {
|
|
1400
1412
|
const j = await res.json().catch(() => ({}))
|
|
1401
1413
|
if (j.error === 'you_own_it') return { content: [{ type: 'text', text: `You own this record — open the original directly:\n${j.uri}` }] }
|
|
@@ -1417,7 +1429,7 @@ export async function runServer(version) {
|
|
|
1417
1429
|
async () => {
|
|
1418
1430
|
let res
|
|
1419
1431
|
try { res = await fetchCortex(`${BASE}/api/file-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
1420
|
-
catch (e) { return
|
|
1432
|
+
catch (e) { return toolError(`Could not load file requests: ${e.message}`) }
|
|
1421
1433
|
if (!res.ok) return (await fail('load file requests', res))()
|
|
1422
1434
|
const { mine, inbox } = await res.json()
|
|
1423
1435
|
const parts = []
|
|
@@ -1441,7 +1453,7 @@ export async function runServer(version) {
|
|
|
1441
1453
|
method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
1442
1454
|
body: JSON.stringify({ action: decision }),
|
|
1443
1455
|
})
|
|
1444
|
-
} catch (e) { return
|
|
1456
|
+
} catch (e) { return toolError(`Could not decide: ${e.message}`) }
|
|
1445
1457
|
if (!res.ok) return (await fail('decide', res))()
|
|
1446
1458
|
return { content: [{ type: 'text', text: `Request ${decision === 'approve' ? 'approved' : 'denied'}.` }] }
|
|
1447
1459
|
},
|
|
@@ -1457,7 +1469,7 @@ export async function runServer(version) {
|
|
|
1457
1469
|
async ({ request_id }) => {
|
|
1458
1470
|
let res
|
|
1459
1471
|
try { res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/download`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
|
|
1460
|
-
catch (e) { return
|
|
1472
|
+
catch (e) { return toolError(`Could not download: ${e.message}`) }
|
|
1461
1473
|
if (res.status === 409) return { content: [{ type: 'text', text: 'Not ready — the owner hasn\'t fulfilled this yet. Try again after they approve.' }] }
|
|
1462
1474
|
if (res.status === 410) return { content: [{ type: 'text', text: 'This file has expired (downloads are available for 7 days). Request it again.' }] }
|
|
1463
1475
|
if (!res.ok) return (await fail('download', res))()
|
|
@@ -1516,11 +1528,11 @@ export async function runServer(version) {
|
|
|
1516
1528
|
try {
|
|
1517
1529
|
res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
1518
1530
|
} catch (e) {
|
|
1519
|
-
return
|
|
1531
|
+
return toolError(`Could not fetch authoring context: ${e.message}`)
|
|
1520
1532
|
}
|
|
1521
1533
|
if (!res.ok) {
|
|
1522
1534
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1523
|
-
return
|
|
1535
|
+
return toolError(`Could not fetch authoring context: ${d.message}`)
|
|
1524
1536
|
}
|
|
1525
1537
|
const { connectionRules, namespace, retiredLinks } = await res.json()
|
|
1526
1538
|
const ns = Array.isArray(namespace) ? namespace : []
|
|
@@ -1571,11 +1583,11 @@ export async function runServer(version) {
|
|
|
1571
1583
|
body: JSON.stringify({ kind, name, pages, reason, change_kind }),
|
|
1572
1584
|
})
|
|
1573
1585
|
} catch (e) {
|
|
1574
|
-
return
|
|
1586
|
+
return toolError(`Could not author "${name}": ${e.message}`)
|
|
1575
1587
|
}
|
|
1576
1588
|
if (!res.ok) {
|
|
1577
1589
|
const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
|
|
1578
|
-
return
|
|
1590
|
+
return toolError(`Could not author "${name}": ${d.message}`)
|
|
1579
1591
|
}
|
|
1580
1592
|
const out = await res.json()
|
|
1581
1593
|
const blue = out?.links?.blue ?? 0
|
package/package.json
CHANGED
package/skills/log/SKILL.md
CHANGED
|
@@ -52,9 +52,8 @@ No arguments. Read the conversation context.
|
|
|
52
52
|
docs are pending, follow the `cortex-author-docs` skill (author each into its page, then
|
|
53
53
|
`docs-scan --mark`). Specs/plans written to disk this session must not die on disk — a spec IS
|
|
54
54
|
a page. If no roots are registered or nothing is pending, skip silently.
|
|
55
|
-
7. **Reconcile
|
|
56
|
-
|
|
57
|
-
write slips through. Before printing the Output:
|
|
55
|
+
7. **Reconcile the sweep (don't trust it).** Step 5 relies on your in-the-moment judgment of "what
|
|
56
|
+
advanced"; this step closes the loop so nothing is silently missed. Before printing the Output:
|
|
58
57
|
a. **Enumerate what you touched** — from the transcript, list the concrete entities this session
|
|
59
58
|
advanced: the project(s), notable files/artifacts, and the people you coordinated with. Derive
|
|
60
59
|
this checklist from what actually *happened*, not from what you remember authoring — the whole
|
|
@@ -62,18 +61,21 @@ No arguments. Read the conversation context.
|
|
|
62
61
|
b. **Assert one outcome per entity** — every item gets exactly `authored [[Page]]` **or**
|
|
63
62
|
`skipped — <reason>` (e.g. "no material change", "not a node", "already current"). Nothing may be
|
|
64
63
|
left unaccounted for. If an entity that genuinely advanced has neither, `author` it now (Step 5).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
Carry the tally into the Output.
|
|
65
|
+
|
|
66
|
+
> **A read-back verification sub-step lived here and was REMOVED 2026-07-31. Do not re-add it
|
|
67
|
+
> without new evidence.** It asked you to `read_page` every page you had just authored, to confirm
|
|
68
|
+
> the write landed. Measured over 397 local transcripts — 18 sessions that wrote a page,
|
|
69
|
+
> 2026-07-29→31 — **18 `author` calls were rejected and all 18 were retried to success: zero
|
|
70
|
+
> silently lost.** An end-of-session pass would have caught nothing, because the rejection reason
|
|
71
|
+
> arrives *in the tool result at the moment of the call*. Session end is the weakest place to
|
|
72
|
+
> verify a write; the result you already have in hand is the strongest.
|
|
73
|
+
>
|
|
74
|
+
> **What replaces it — at the moment of each write, not at the end:** a rejected `author` comes back
|
|
75
|
+
> as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Cortex API 409:
|
|
76
|
+
> <reason>` — and `No change to "<page>"` is a **200 OK that wrote nothing**. Neither is an error at
|
|
77
|
+
> the protocol level, so nothing will interrupt you. **Read the result text of every write; never
|
|
78
|
+
> skim it.** That inline read is where this step's value actually was.
|
|
77
79
|
|
|
78
80
|
## Output
|
|
79
81
|
|
|
@@ -88,7 +90,7 @@ After calling `log_session`, show a short structured summary:
|
|
|
88
90
|
**Coordinated with:** people involved
|
|
89
91
|
**Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
|
|
90
92
|
**Wiki authored:** [[Node A]], [[Node B]] — pages updated (or "— nothing advanced this session")
|
|
91
|
-
**Reconciled:** N touched → M authored, K skipped (reason each)
|
|
93
|
+
**Reconciled:** N touched → M authored, K skipped (reason each)
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
## Safety rules
|