@theronap/agnoclast-mcp 0.9.176 → 0.9.177
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 +23 -11
- package/lib/arrivals_notice.mjs +79 -174
- package/lib/editors/claude.mjs +28 -21
- package/package.json +1 -1
package/bin/cortex-mcp.mjs
CHANGED
|
@@ -112,7 +112,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
112
112
|
` docs-scan detect new/changed local docs pending Agnoclast authoring (used by /cortex-author-docs)\n` +
|
|
113
113
|
` graphify-sync [path] [--brain <name-or-id>] rebuild the local code graph + log a timeline event\n` +
|
|
114
114
|
` snapshot-context save the exact startup context Agnoclast served to a local snapshot\n` +
|
|
115
|
-
` arrivals-notice
|
|
115
|
+
` arrivals-notice --emit: surface incoming-record offers to you (UserPromptSubmit hook)\n` +
|
|
116
116
|
` hydrate UserPromptSubmit hook — inject query-centered context on the first substantive turn\n` +
|
|
117
117
|
` migrate-key [--dry-run] move the MCP config key to its new name, allow rules first\n` +
|
|
118
118
|
` with-token -- <cmd> run <cmd> with your token in its environment (for cron/launchd wrappers)\n` +
|
|
@@ -221,16 +221,28 @@ if (cmd === 'login') {
|
|
|
221
221
|
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
222
222
|
await closeFetch()
|
|
223
223
|
} else if (cmd === 'arrivals-notice') {
|
|
224
|
-
//
|
|
225
|
-
// RESULTS, which only the agent reads — and an audit on 2026-09-21 found the agent acting on
|
|
226
|
-
// once in a day.
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
// UserPromptSubmit hook: surface the arrivals offer to the PERSON. The signals are appended to
|
|
225
|
+
// TOOL RESULTS, which only the agent reads — and an audit on 2026-09-21 found the agent acting on
|
|
226
|
+
// it once in a day. One cheap authenticated GET, fail-open, always exit 0. It reports; it never
|
|
227
|
+
// blocks a prompt.
|
|
228
|
+
// ONE half now. `--emit` (UserPromptSubmit) ASKS THE SERVER for the two turn-time signals and
|
|
229
|
+
// prints them. The PostToolUse scraper + spool it replaced could never surface anything before the
|
|
230
|
+
// next UserPromptSubmit anyway, because PostToolUse stdout is not injected — so this delivers at
|
|
231
|
+
// the same moment, from the source. See lib/arrivals_notice.mjs's header.
|
|
232
|
+
// ⚠ WITHOUT --emit THIS IS A DELIBERATE NO-OP, not a fallthrough. That was the PostToolUse half,
|
|
233
|
+
// retired 2026-09-25 along with the spool. A seat whose settings.json still has that hook must do
|
|
234
|
+
// NOTHING here: the live path CONSUMES a watermark, so letting a stale PostToolUse call reach it
|
|
235
|
+
// would announce arrivals into a stdout that is never injected and leave the next prompt — the one
|
|
236
|
+
// the person actually reads — correctly reporting nothing. Deleting a hook from one machine's
|
|
237
|
+
// settings does not delete it from anyone else's, so the old invocation has to stay harmless.
|
|
238
|
+
if (rest.includes('--emit')) {
|
|
239
|
+
const mod = await import('../lib/arrivals_notice.mjs')
|
|
240
|
+
process.exitCode = await mod.runArrivalsDrain()
|
|
241
|
+
const { closeFetch } = await import('../lib/diagnose.mjs')
|
|
242
|
+
await closeFetch()
|
|
243
|
+
} else {
|
|
244
|
+
process.exitCode = 0
|
|
245
|
+
}
|
|
234
246
|
} else if (cmd === 'hydrate') {
|
|
235
247
|
// UserPromptSubmit hook (① discovery): hydrate the model with query-centered Agnoclast context on the
|
|
236
248
|
// FIRST substantive turn, before it answers — then never again this session (topic-shift refresh stays
|
package/lib/arrivals_notice.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// UserPromptSubmit hook — make the arrivals offer visible to the PERSON, not just the agent.
|
|
2
2
|
//
|
|
3
|
-
// 🔴 WHY THIS EXISTS, and it is not a nicety. The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// had simply been let pass.
|
|
3
|
+
// 🔴 WHY THIS EXISTS, and it is not a nicety. The per-turn delta line and the settling feed are
|
|
4
|
+
// appended to TOOL RESULTS, which only the agent reads. Measured on 2026-09-21 against a single
|
|
5
|
+
// session's own record: the block fired repeatedly through the day and the agent acted on it ONCE,
|
|
6
|
+
// and only because the user asked it to prove the mechanism existed. An audit of the four records
|
|
7
|
+
// it named found one that plainly belonged to that session's own work and had simply been let pass.
|
|
9
8
|
//
|
|
10
9
|
// The failure is structural rather than a lapse: the offer arrives MID-TASK, inside a result the
|
|
11
10
|
// agent requested for an unrelated reason, and nothing forces a decision. A signal that only the
|
|
@@ -13,196 +12,102 @@
|
|
|
13
12
|
// coverage. So this does not ask the agent to be more diligent; it removes the agent from the
|
|
14
13
|
// delivery path.
|
|
15
14
|
//
|
|
16
|
-
// ⚠ IT IS NOT A GATE. Exit is always 0
|
|
17
|
-
//
|
|
18
|
-
// nothing.
|
|
15
|
+
// ⚠ IT IS NOT A GATE. Exit is always 0. A notifier that can break the thing it observes gets
|
|
16
|
+
// disabled, and then it observes nothing.
|
|
19
17
|
//
|
|
20
|
-
// 🔴
|
|
21
|
-
// version that could not work:
|
|
18
|
+
// 🔴 IT ASKS THE SERVER. IT USED TO SCRAPE ITS OWN CONVERSATION. (Rewritten 2026-09-25.)
|
|
22
19
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
20
|
+
// The previous design was a two-hook round trip: PostToolUse read the rendered English off tool
|
|
21
|
+
// results with two regexes, spooled the matches to ~/.cortex/arrivals-spool, and this half drained
|
|
22
|
+
// the spool on the next prompt. The server had computed both numbers and written that text itself —
|
|
23
|
+
// the scraper existed only to read them back out of its own output.
|
|
25
24
|
//
|
|
26
|
-
//
|
|
27
|
-
// correct ids, state file written seconds after the block appeared. And NOBODY SAW ANY OF IT —
|
|
28
|
-
// which is precisely the failure this file exists to fix, reproduced by the fix. The assumption came
|
|
29
|
-
// from seeing SessionStart and UserPromptSubmit output every turn and generalising to all hooks;
|
|
30
|
-
// those two are context-INJECTING events by design.
|
|
25
|
+
// It failed in both directions:
|
|
31
26
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
27
|
+
// MISSED — the signal rode on Agnoclast TOOL RESULTS, so a turn of bash and edits produced none,
|
|
28
|
+
// and nothing arrived no matter how much had actually arrived.
|
|
29
|
+
// INVENTED — any text containing the pattern was scraped as if it were a signal. This agent's own
|
|
30
|
+
// Bash output, while quoting the regex to debug it, was ingested as arrivals.
|
|
34
31
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (depth > 8 || v == null) return
|
|
54
|
-
if (typeof v === 'string') { yield v; return }
|
|
55
|
-
if (Array.isArray(v)) { for (const x of v) yield* strings(x, depth + 1); return }
|
|
56
|
-
if (typeof v === 'object') { for (const x of Object.values(v)) yield* strings(x, depth + 1) }
|
|
57
|
-
}
|
|
32
|
+
// A parser whose input is the conversation it is printing into cannot distinguish the world from
|
|
33
|
+
// its own echo. So it now asks the one party that knows.
|
|
34
|
+
//
|
|
35
|
+
// ⚠ AND IT COSTS NOTHING IN TIMING, which is the part that looks like a regression and is not.
|
|
36
|
+
// PostToolUse stdout is NOT injected into the conversation — that is measured, and it is why the
|
|
37
|
+
// spool existed at all. The spool could never surface anything before the next UserPromptSubmit.
|
|
38
|
+
// Asking at that same moment delivers at the identical time, from the source, with no state on disk.
|
|
39
|
+
//
|
|
40
|
+
// ⚠ THE NETWORK CALL IS NEW, AND DELIBERATE. The old header promised NO NETWORK, NO TOKEN, because
|
|
41
|
+
// it ran after EVERY TOOL CALL. That event is deleted. This runs once per prompt — the same budget
|
|
42
|
+
// SessionStart already spends — and fails open to silence.
|
|
43
|
+
//
|
|
44
|
+
// ⚠ THE SERVER OWNS THE DEDUPE NOW. arrivalDelta advances session_arrival_watermarks, announcing an
|
|
45
|
+
// arrival to a session exactly once, which is what the local `seen` files were approximating with
|
|
46
|
+
// filesystem state that no other party could see. A consequence worth stating: fetching CONSUMES
|
|
47
|
+
// the announcement, so this prints whatever it gets. Losing one notice is recoverable — the record
|
|
48
|
+
// is still unclaimed and the server will offer it again.
|
|
49
|
+
import { fetchCortex, resolveBase, resolveTokenSource } from './diagnose.mjs'
|
|
58
50
|
|
|
59
51
|
/**
|
|
60
|
-
*
|
|
52
|
+
* Render both turn-time signals for a person skimming their terminal.
|
|
61
53
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
54
|
+
* Deliberately shallow: names, ages and ids, never page contents. The settling half is owner-scoped
|
|
55
|
+
* at the query (private_intake_items joined to the caller's own account), so there is nobody to leak
|
|
56
|
+
* to — but this is a hook that prints unprompted, and a notice that dumps content trains its reader
|
|
57
|
+
* to skip it. Exported for the tests.
|
|
65
58
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} catch {
|
|
72
|
-
// Not JSON — scan the raw text. Fail open toward reporting rather than staying silent.
|
|
73
|
-
}
|
|
74
|
-
if (!blob) return null
|
|
75
|
-
|
|
76
|
-
const deltaMatch = blob.match(DELTA_RE)
|
|
77
|
-
const delta = deltaMatch ? Number(deltaMatch[1]) : 0
|
|
59
|
+
export function formatNotice(signals) {
|
|
60
|
+
const lines = []
|
|
61
|
+
const offers = signals?.settling?.offers ?? []
|
|
62
|
+
const total = Number(signals?.settling?.total ?? offers.length)
|
|
63
|
+
const delta = Number(signals?.arrival?.count ?? 0)
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Only the section AFTER the marker, so an unrelated bulleted list earlier in a page read is
|
|
82
|
-
// not mistaken for arrivals.
|
|
83
|
-
const section = blob.slice(blob.indexOf(MATCHED))
|
|
84
|
-
for (const m of section.matchAll(ITEM_RE)) {
|
|
85
|
-
items.push({ title: m[1].trim(), source: m[2], age: m[3].trim(), id: m[4] })
|
|
86
|
-
}
|
|
65
|
+
if (delta) {
|
|
66
|
+
lines.push(`⚡ ${delta} new arrival${delta === 1 ? '' : 's'} in the pile since this session was last told.`)
|
|
87
67
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
try { return new Set(JSON.parse(readFileSync(p, 'utf8'))) } catch { return new Set() }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function saveSeen(p, set) {
|
|
104
|
-
// Bounded: a long session must not grow this file without limit.
|
|
105
|
-
try { writeFileSync(p, JSON.stringify([...set].slice(-500))) } catch { /* best effort */ }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function formatNotice(arrivals) {
|
|
109
|
-
const lines = []
|
|
110
|
-
if (arrivals.items.length) {
|
|
111
|
-
lines.push(`⚡ ${arrivals.items.length} record${arrivals.items.length === 1 ? '' : 's'} offered to this session:`)
|
|
112
|
-
for (const it of arrivals.items.slice(0, 6)) {
|
|
113
|
-
lines.push(` · ${it.title.slice(0, 92)} (${it.source} · ${it.age})`)
|
|
114
|
-
lines.push(` ${it.id}`)
|
|
68
|
+
if (offers.length) {
|
|
69
|
+
lines.push(`⚡ ${total} arrival${total === 1 ? '' : 's'} asking THIS session to decide:`)
|
|
70
|
+
for (const o of offers.slice(0, 6)) {
|
|
71
|
+
// via_reason is carried through verbatim rather than flattened to "matched": an agent — or a
|
|
72
|
+
// person — who learns the stated reason is unreliable stops reading the reason at all.
|
|
73
|
+
const why = o.via_reason === 'matched'
|
|
74
|
+
? 'matches your work'
|
|
75
|
+
: o.via_reason === 'declined'
|
|
76
|
+
? 'every other matching session declined it'
|
|
77
|
+
: 'nothing matched it anywhere'
|
|
78
|
+
lines.push(` · ${o.source} · ${o.hoursAgo}h ago — ${why}`)
|
|
79
|
+
lines.push(` ${o.intakeItemId}`)
|
|
115
80
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
if (arrivals.delta) {
|
|
119
|
-
lines.push(`⚡ ${arrivals.delta} new arrival${arrivals.delta === 1 ? '' : 's'} in the pile since this session was last told.`)
|
|
81
|
+
if (total > offers.length) lines.push(` … and ${total - offers.length} more`)
|
|
82
|
+
lines.push(' (Claim + route them, or say they are not yours — they stay unclaimed until someone decides.)')
|
|
120
83
|
}
|
|
121
|
-
// Said plainly, because the whole point is that the person can tell whether anything happened.
|
|
122
|
-
if (arrivals.items.length) lines.push(` (Claim + route them, or say they are not yours — they stay unclaimed until someone decides.)`)
|
|
123
84
|
return lines.join('\n')
|
|
124
85
|
}
|
|
125
86
|
|
|
126
|
-
/** The handoff between the two hook events. One file per session, drained on the next prompt. */
|
|
127
|
-
function spoolPath(sessionKey) {
|
|
128
|
-
const dir = join(homedir(), '.cortex', 'arrivals-spool')
|
|
129
|
-
try { mkdirSync(dir, { recursive: true }) } catch { /* best effort */ }
|
|
130
|
-
return join(dir, `${(sessionKey || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* PostToolUse: detect, de-duplicate, SPOOL. Prints nothing — its stdout goes nowhere a person reads.
|
|
135
|
-
*
|
|
136
|
-
* ⚠ The spool ACCUMULATES across tool calls rather than overwriting. Several tool calls commonly
|
|
137
|
-
* land between two prompts, and only the first carries a given arrival; overwriting would mean the
|
|
138
|
-
* last tool call before the prompt decides what the person sees, which is usually nothing.
|
|
139
|
-
*/
|
|
140
|
-
export async function runArrivalsNotice() {
|
|
141
|
-
try {
|
|
142
|
-
const raw = readStdin()
|
|
143
|
-
if (!raw) return 0
|
|
144
|
-
|
|
145
|
-
let sessionKey = ''
|
|
146
|
-
try { sessionKey = String(JSON.parse(raw)?.session_id ?? '') } catch { /* fine */ }
|
|
147
|
-
|
|
148
|
-
const arrivals = extractArrivals(raw)
|
|
149
|
-
if (!arrivals) return 0
|
|
150
|
-
|
|
151
|
-
const p = seenPath(sessionKey)
|
|
152
|
-
const seen = loadSeen(p)
|
|
153
|
-
const fresh = arrivals.items.filter((it) => !seen.has(it.id))
|
|
154
|
-
|
|
155
|
-
// Nothing new to say. The delta line is server-side watermarked and already fires once per
|
|
156
|
-
// change, so it is reported whenever present.
|
|
157
|
-
if (fresh.length === 0 && !arrivals.delta) return 0
|
|
158
|
-
|
|
159
|
-
// Merge into whatever is already spooled for this session.
|
|
160
|
-
const sp = spoolPath(sessionKey)
|
|
161
|
-
let pending = { delta: 0, items: [] }
|
|
162
|
-
try { pending = JSON.parse(readFileSync(sp, 'utf8')) } catch { /* first one this turn */ }
|
|
163
|
-
const byId = new Map((pending.items ?? []).map((it) => [it.id, it]))
|
|
164
|
-
for (const it of fresh) byId.set(it.id, it)
|
|
165
|
-
try {
|
|
166
|
-
writeFileSync(sp, JSON.stringify({
|
|
167
|
-
// The server watermarks the delta, so the freshest sighting is the right one — not a sum,
|
|
168
|
-
// which would double-count the same pile across two tool calls in one turn.
|
|
169
|
-
delta: arrivals.delta || pending.delta || 0,
|
|
170
|
-
items: [...byId.values()].slice(-40),
|
|
171
|
-
}))
|
|
172
|
-
} catch { /* best effort */ }
|
|
173
|
-
|
|
174
|
-
for (const it of fresh) seen.add(it.id)
|
|
175
|
-
saveSeen(p, seen)
|
|
176
|
-
return 0
|
|
177
|
-
} catch {
|
|
178
|
-
// ⚠ ALWAYS 0. This observes; it must never be the reason a tool call failed.
|
|
179
|
-
return 0
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
87
|
/**
|
|
184
|
-
* UserPromptSubmit:
|
|
88
|
+
* UserPromptSubmit: ask, then print. This is the half the person actually sees.
|
|
185
89
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
90
|
+
* The session key is not read here: bin/cortex-mcp.mjs stamps it for every CONTEXT_HOOK_COMMAND, so
|
|
91
|
+
* fetchCortex already carries x-cortex-session-key. That also means this no longer touches stdin —
|
|
92
|
+
* which matters, because `hydrate` runs on the SAME event and consumes stdin itself, and two hooks
|
|
93
|
+
* racing to drain one pipe is a bug waiting on a scheduler.
|
|
190
94
|
*/
|
|
191
95
|
export async function runArrivalsDrain() {
|
|
192
96
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
process.stdout.write(
|
|
97
|
+
const { token } = resolveTokenSource()
|
|
98
|
+
if (!token) return 0
|
|
99
|
+
const base = resolveBase(process.env.CORTEX_URL)
|
|
100
|
+
const res = await fetchCortex(`${base}/api/session-signals`, {
|
|
101
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
102
|
+
})
|
|
103
|
+
if (!res?.ok) return 0
|
|
104
|
+
const signals = await res.json().catch(() => null)
|
|
105
|
+
if (!signals) return 0
|
|
106
|
+
const out = formatNotice(signals)
|
|
107
|
+
if (out) process.stdout.write(out + '\n')
|
|
204
108
|
return 0
|
|
205
109
|
} catch {
|
|
110
|
+
// Fail open and silent: a prompt must never be blocked or slowed by its own notifier.
|
|
206
111
|
return 0
|
|
207
112
|
}
|
|
208
113
|
}
|
package/lib/editors/claude.mjs
CHANGED
|
@@ -105,22 +105,33 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
105
105
|
}
|
|
106
106
|
sgrp.hooks.push({ type: 'command', command: snapshotCmd })
|
|
107
107
|
|
|
108
|
-
// PostToolUse — arrivals-notice.
|
|
108
|
+
// PostToolUse — arrivals-notice: RETIRED 2026-09-25. This block now REMOVES it and installs nothing.
|
|
109
109
|
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// it once, and an audit of the records it named found one belonging to that session's own work
|
|
114
|
-
// that had simply been let pass. A signal only the agent sees, and reliably scrolls past, LOOKS
|
|
115
|
-
// like coverage while providing none.
|
|
110
|
+
// It existed because the arrivals signals are appended to TOOL RESULTS, which only the agent reads,
|
|
111
|
+
// and an audit on 2026-09-21 found the agent acting on them once in a day. The hook scraped those
|
|
112
|
+
// rendered results with two regexes and spooled the matches for the next prompt to drain.
|
|
116
113
|
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// agent
|
|
114
|
+
// Deleted because the scraper could not tell the world from its own echo. It MISSED every turn that
|
|
115
|
+
// called no Agnoclast tool (the signal rides on tool results), and it INVENTED arrivals from any
|
|
116
|
+
// text containing the pattern — including an agent's own Bash output quoting the regex to debug it.
|
|
117
|
+
// The UserPromptSubmit half below now ASKS THE SERVER, which is the only party that knows.
|
|
120
118
|
//
|
|
121
|
-
// ⚠
|
|
122
|
-
//
|
|
119
|
+
// ⚠ AND IT COST NOTHING TO DELETE. PostToolUse stdout is not injected into the conversation — the
|
|
120
|
+
// measured fact that forced the spool to exist — so this hook could never surface anything before
|
|
121
|
+
// the next UserPromptSubmit. Asking there delivers at the identical moment.
|
|
122
|
+
//
|
|
123
|
+
// ⚠ THE FILTER STAYS, AND IS NOW THE POINT. Seats wired before today still have this hook. Left in
|
|
124
|
+
// place it would call a subcommand that is a deliberate no-op — harmless only because bin makes it
|
|
125
|
+
// one. Re-running setup RETIRES it, which is the only reach this package has into an existing
|
|
126
|
+
// settings.json.
|
|
123
127
|
s.hooks.PostToolUse = Array.isArray(s.hooks.PostToolUse) ? s.hooks.PostToolUse : []
|
|
128
|
+
for (const pg of s.hooks.PostToolUse) {
|
|
129
|
+
if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !isManagedSubcommand(h.command, 'arrivals-notice'))
|
|
130
|
+
}
|
|
131
|
+
// Drop any group this emptied, so retirement does not leave a bare {matcher:'',hooks:[]} behind.
|
|
132
|
+
s.hooks.PostToolUse = s.hooks.PostToolUse.filter((g) => (g.hooks ?? []).length)
|
|
133
|
+
if (!s.hooks.PostToolUse.length) delete s.hooks.PostToolUse
|
|
134
|
+
|
|
124
135
|
// 🔴 THIS HOOK DOES NOT FOLLOW `spec`, AND THE REASON IS A DEFECT THIS FILE SHIPPED ONCE.
|
|
125
136
|
//
|
|
126
137
|
// `spec` pins @stable, which is what desktop-wired seats deliberately follow. But a hook wired at
|
|
@@ -134,19 +145,15 @@ export function mergeClaudeSettings(existing, spec) {
|
|
|
134
145
|
// WIRED AT A SPEC THAT CANNOT EXECUTE IT. A version floor is the narrowest way to hold that, and
|
|
135
146
|
// npx resolves a semver range (verified 2026-09-21 against a real invocation, not assumed).
|
|
136
147
|
//
|
|
148
|
+
// ⚠ FLOOR RAISED TO 0.9.177 ON 2026-09-25, and it is load-bearing rather than tidy. Below it,
|
|
149
|
+
// `--emit` drains a spool that nothing writes any more: it would exit 0, print nothing, and look
|
|
150
|
+
// exactly like a quiet queue. The floor is what makes that impossible instead of silent.
|
|
151
|
+
//
|
|
137
152
|
// ⚠ And it uses the CANONICAL name. Every other command here carries `cortex-mcp` for
|
|
138
153
|
// compatibility with frozen launchd/cron scripts on unreachable machines (ADR-0033) — but a
|
|
139
154
|
// BRAND-NEW hook has no frozen scripts to be compatible with, so there is no reason to mint more
|
|
140
155
|
// references to the retiring name. New wiring uses the name the product actually has.
|
|
141
|
-
const ARRIVALS_MIN_VERSION = '0.9.
|
|
142
|
-
const arrivalsCmd = markCommand(`npx -y @theronap/agnoclast-mcp@^${ARRIVALS_MIN_VERSION} arrivals-notice`)
|
|
143
|
-
for (const pg of s.hooks.PostToolUse) {
|
|
144
|
-
if (Array.isArray(pg.hooks)) pg.hooks = pg.hooks.filter((h) => !isManagedSubcommand(h.command, 'arrivals-notice'))
|
|
145
|
-
}
|
|
146
|
-
let pgrp = s.hooks.PostToolUse.find((g) => (g.matcher ?? '') === '')
|
|
147
|
-
if (!pgrp) { pgrp = { matcher: '', hooks: [] }; s.hooks.PostToolUse.push(pgrp) }
|
|
148
|
-
pgrp.hooks = pgrp.hooks ?? []
|
|
149
|
-
pgrp.hooks.push({ type: 'command', command: arrivalsCmd })
|
|
156
|
+
const ARRIVALS_MIN_VERSION = '0.9.177'
|
|
150
157
|
|
|
151
158
|
// UserPromptSubmit — hydrate (① discovery). Makes the FIRST context load of a session
|
|
152
159
|
// non-discretionary: without it, hydration is a skill the agent may simply forget, and an agent
|
package/package.json
CHANGED