@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.
@@ -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 hook pair: detect incoming-record offers (PostToolUse) and surface them (--emit, UserPromptSubmit)\n` +
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
- // PostToolUse hook: surface the arrivals offer to the PERSON. The ⚡ block is appended to TOOL
225
- // RESULTS, which only the agent reads — and an audit on 2026-09-21 found the agent acting on it
226
- // once in a day. Pure text extraction from the payload it is handed: no network, no token, always
227
- // exit 0. It reports; it never blocks a tool call.
228
- // Two halves, one subcommand. `--emit` is the UserPromptSubmit side (drain + print); without it
229
- // this is the PostToolUse side (detect + spool, prints nothing). Neither can do the job alone:
230
- // PostToolUse sees tool output but its stdout is not injected; UserPromptSubmit is injected but
231
- // never sees tool output. See lib/arrivals_notice.mjs's header.
232
- const mod = await import('../lib/arrivals_notice.mjs')
233
- process.exitCode = rest.includes('--emit') ? await mod.runArrivalsDrain() : await mod.runArrivalsNotice()
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
@@ -1,11 +1,10 @@
1
- // PostToolUse hook — make the arrivals offer visible to the PERSON, not just the agent.
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 `⚡ ARRIVED WHILE YOU WERE WORKING` block and the
4
- // per-turn delta line are appended to TOOL RESULTS, which only the agent reads. Measured on
5
- // 2026-09-21 against a single session's own record: the block fired repeatedly through the day and
6
- // the agent acted on it ONCE, and only because the user asked it to prove the mechanism existed. An
7
- // audit of the four records it named found one that plainly belonged to that session's own work and
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: this reports, it never blocks a tool call, and it never
17
- // fails one. A notifier that can break the thing it observes gets disabled, and then it observes
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
- // 🔴 TWO EVENTS, BECAUSE NEITHER ONE CAN DO THIS ALONE. Measured 2026-09-21, after shipping a
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
- // PostToolUse has the arrivals block — but its stdout is NOT injected into the conversation
24
- // UserPromptSubmit IS injected (proven) — but never receives tool output at all
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
- // The first version wired only PostToolUse and printed to stdout. It ran perfectly: correct parse,
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
- // So: PostToolUse DETECTS and spools. UserPromptSubmit DRAINS and surfaces. Arrivals appear at the
33
- // top of the person's next turn, which is when they are actually looking at the screen.
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
- // ⚠ NO NETWORK, NO TOKEN, NO DECRYPT. Everything printed is already in the payload the hook was
36
- // handed. That keeps it fast enough to run after every tool call and means it cannot leak anything
37
- // the agent was not already shown.
38
- import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs'
39
- import { homedir } from 'node:os'
40
- import { join } from 'node:path'
41
-
42
- const MATCHED = '⚡ ARRIVED WHILE YOU WERE WORKING'
43
- const DELTA_RE = /⚡ (\d+) new arrivals? in your pile/
44
- // The listing lines look like: - Title (email · 5.1h ago · id <uuid>) — …
45
- const ITEM_RE = /^\s*-\s+(.*?)\s+\((\S+)\s+·\s+([^·]+?)\s+·\s+id\s+([0-9a-f-]{36})\)/gmu
46
-
47
- function readStdin() {
48
- try { return readFileSync(0, 'utf8') } catch { return '' }
49
- }
50
-
51
- /** Every string in the payload, so this does not depend on which key holds the tool's text. */
52
- function* strings(v, depth = 0) {
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
- * Pull the arrivals offer out of a PostToolUse payload.
52
+ * Render both turn-time signals for a person skimming their terminal.
61
53
  *
62
- * Exported for tests: the parsing is the part that can silently rot when the server changes the
63
- * block's wording, and a hook that stops matching fails SILENTLY — it just prints nothing forever,
64
- * which is indistinguishable from "nothing arrived".
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 extractArrivals(payloadText) {
67
- let blob = payloadText
68
- try {
69
- const parsed = JSON.parse(payloadText)
70
- blob = [...strings(parsed)].join('\n')
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
- const items = []
80
- if (blob.includes(MATCHED)) {
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 (!delta && items.length === 0) return null
89
- return { delta, items }
90
- }
91
-
92
- /** Ids already announced, so the same arrival is not re-reported on every tool call until claimed. */
93
- function seenPath(sessionKey) {
94
- const dir = join(homedir(), '.cortex', 'arrivals-announced')
95
- try { mkdirSync(dir, { recursive: true }) } catch { /* best effort */ }
96
- return join(dir, `${(sessionKey || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`)
97
- }
98
-
99
- function loadSeen(p) {
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 (arrivals.items.length > 6) lines.push(` … and ${arrivals.items.length - 6} more`)
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: DRAIN the spool and print. This is the half the person actually sees.
88
+ * UserPromptSubmit: ask, then print. This is the half the person actually sees.
185
89
  *
186
- * ⚠ CLEAR THE SPOOL BEFORE PRINTING FAILS, not after. A crash between print and clear would repeat
187
- * the same arrivals on every subsequent prompt forever, and a notifier that nags is one that gets
188
- * removed. Losing one notice is recoverable — the record is still unclaimed and will be offered
189
- * again by the server.
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 raw = readStdin()
194
- let sessionKey = ''
195
- try { sessionKey = String(JSON.parse(raw)?.session_id ?? '') } catch { /* fine */ }
196
-
197
- const sp = spoolPath(sessionKey)
198
- let pending = null
199
- try { pending = JSON.parse(readFileSync(sp, 'utf8')) } catch { return 0 }
200
- try { unlinkSync(sp) } catch { /* best effort */ }
201
-
202
- if (!pending || (!pending.delta && !(pending.items ?? []).length)) return 0
203
- process.stdout.write(formatNotice({ delta: pending.delta ?? 0, items: pending.items ?? [] }) + '\n')
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
  }
@@ -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
- // 🔴 THE ARRIVALS OFFER WAS INVISIBLE TO THE PERSON. `⚡ ARRIVED WHILE YOU WERE WORKING` and the
111
- // per-turn delta are appended to TOOL RESULTS, which only the agent reads. Measured 2026-09-21
112
- // against one session's own record: the block fired repeatedly through the day, the agent acted on
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
- // ⚠ SO THIS IS WIRED AS SOFTWARE, NOT LEFT TO INSTRUCTION. Asking the agent to relay arrivals is
118
- // asking it to be reliable at exactly the thing it was just measured failing at; a hook takes the
119
- // agent out of the delivery path entirely.
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
- // ⚠ Matcher is '' (every tool). The block only appears on three surfaces, so the filtering is
122
- // server-side and this stays a cheap no-op elsewhere: it reads its stdin, finds no marker, exits 0.
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.162'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.176",
3
+ "version": "0.9.177",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {