Repository navigation
Description
Relocated from dezren39/darwin-nix#81 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).
Parent: #173
Hook vocabulary (#181 collision 2): this issue carries the one hook vocabulary that script.* (shipped), #82's runtime registration and #112's exec lifecycle list adopt. The decision is being recorded by SURFACES under pkgs/mcpx/docs/decisions/ (in flight; linked here when it merges). Until then the phase table and hook-point set in Approach C are input to that record, not the decision.
Summary
mcpx already has hooks in two disconnected places: five script.* settings that splice TypeScript into a generated script wrapper, and an internal pool.Hooks struct the daemon wires to the event bus. Neither is what the notes ask for — a hook that a user declares in configuration, that fires on something the daemon noticed, and that runs anything: a command, a JS/TS module, a Python module, or a plugin.
The daemon already publishes everything worth hooking (internal/events/events.go). What is missing is a declarative subscriber table and a small set of executors. Build the hook table on top of the existing bus rather than adding call sites.
Original notes
- plugins
- config can set hooks for non-go/typescript(/python)
- config can set hooks to call any command,js/ts/py module
- go/ts(/py) can directly interact with app set own hooks, get/set vars, trigger any external command internally which can be triggered outside the app or in a script, call public and private functions of app, fork configure socket shutdown and independently interact (maybe this mode also supports still get/set vars transport types rpc anything they could do inside.
(The third bullet is the programmatic half — a plugin registering its own hooks at runtime — and is answered in the privileged-surface issue. This issue is the declarative half.)
Current state (what mcpx does today, with file refs)
Script hooks exist and are good, but they are script-shaped and TypeScript-only. internal/settings/registry.go:201-243 declares six settings — script.launcher, script.before, script.prefix, script.onSuccess, script.onError, script.suffix — all Repeatable, most of kind KindSource. KindSource means the value may be inline text, a path to a file, or a directory of files (internal/source/source.go), resolved with @text:/@file: prefixes or probed. The launcher then splices them at @-placeholders (internal/launcher/template.go, #7). onError is documented as "A hook, not a handler. The error is rethrown afterwards" (internal/settings/registry.go:234). Since #75 these apply to inline snippets as well as files.
So the configuration ergonomics of hooks are already solved: repeatable, inline-or-file, composable, with a search path. What they lack is anything but TypeScript, and anything but a script run.
Pool hooks exist and are internal. internal/daemon/hooks.go:24 InstallHooks attaches a pool.Hooks to every pool: OnMessage, OnProgress, OnListChanged, OnResourceUpdated, OnElicitationComplete, Elicit. Every one of these does the same thing — translate into an event and r.publish it. This is a Go-level extension point with no user-facing surface.
The bus is the hook source. 17 kinds today: 16 constants in internal/events/events.go:30-51 (tools.changed, resources.changed, prompts.changed, resource.updated, elicit.opened|answered|completed, sample.opened|answered, server.started|stopped, call.started|finished, server.log, progress, task.updated) plus catalog.changed, published ad hoc from internal/daemon/consumer.go:236 without a constant. Events carry Seq (monotonic, so a reconnecting subscriber replays losslessly), Kind, At, Server, Session, Trace, URI and a JSON Data. Filter matches kind prefixes — "elicit" matches every elicitation event, "*" matches all — plus session, server and URI. GET /v1/events is the SSE view with kinds, session, server, uri and since query parameters (internal/api/ops.go:156); subscriptions/listen is the MCP-legal filtered view of the same stream.
Two structural facts that constrain the design:
- The bus is fire-and-forget.
Publishfans out to subscribers; nothing waits for a subscriber, and nothing can veto. A hook that must change what happens (deny a call, rewrite arguments) cannot be a bus subscriber as the bus exists today. - The history is bounded (
New(keep int);events.history, default 1024 —internal/defaults/defaults.json:52, wired atinternal/daemon/server.go:165). A subscriber that reconnects past the history is told so (event: gap,server.go:838-842), but one that falls behind while connected is not: its 256-slot channel drops events and only bumps a counter nothing reads (events.go:177,:224-231), and a replay longer than 256 matching events is truncated the same way. A hook runner that is slow is therefore a correctness problem, not just a performance one. Gap semantics are Bump build/buck/preludes/buck2-prelude from3a02ea3to4371899#6's to define (Phase 3: sync, blob gate, and relays #181 collision 13); this issue consumes them.
Approaches
Approach A — least recommended: a hooks block per event kind that shells out inline and synchronously
"hooks": { "call.finished": "notify-send 'done'" }The obvious shape, and the one every tool from git to husky to opencode ships. It is wrong here specifically because of where it would have to be called from. The natural implementation is a call in Registry.publish or in the pool, which puts an arbitrary user command on the synchronous path of every tool call. One hook that spawns a 200ms process turns every call.finished into a 200ms tax, and a hook that hangs hangs the daemon; mcpx's own performance story (#79) is about shaving tens of milliseconds off exec, which a per-event fork+exec erases.
It also multiplies call sites. Each new hook point is a new line in the pool or the registry, and they drift — which is the exact failure the internal/api/ops*.go one-table design exists to prevent.
And it says nothing about how a hook receives the event. Argv? Environment? stdin JSON? Picking badly here is the thing that cannot be changed later.
If it ships at all, ship it as sugar over B, not as its own mechanism.
Approach B — middle: a declarative subscriber table over the existing bus, with three executors
Hooks are a config list. Each entry is a filter plus an action. The runner is one bus subscriber that owns a bounded worker pool; the daemon's event path does not change at all.
{
"hooks": [
{ "on": "server.stopped", "run": "./bin/page-me", "when": { "server": "chrome" } },
{ "on": "elicit", "module": "./hooks/route-questions.ts" },
{ "on": "call.finished", "module": "hooks/record.py", "async": true }
]
}Facts and opinions that belong in the design:
onreusesevents.Filterverbatim. Prefix matching is already there,"*"is already there, and the/v1/eventsquery parameters are already the same vocabulary (kinds,session,server,uri). A hook filter that is a different language from the event-stream filter is a bug factory. Anything beyond that — matching on the tool name insideData, for instance — is a JSON pointer predicate and can wait.- The event is delivered as JSON on stdin, with the flat fields also in the environment (
MCPX_EVENT_KIND,MCPX_EVENT_SERVER,MCPX_EVENT_SESSION,MCPX_EVENT_SEQ,MCPX_TRACE_IDS). stdin because the payload is arbitrary JSON and argv has a length limit; environment as well because the 90% hook is a one-liner that wants$MCPX_EVENT_SERVERwithoutjq. This is the decision from A that must be made once and never changed. - Three executors, and they should not be three code paths. A command (argv, or a string through
sh -c— pick one and be loud about which); a JS/TS module, which is the existing runner (internal/runner/runner.go, deno/bun/node) with the event as the argument, so a TS hook gets the generated typed client for free and can call other servers; and a Python module, which is a command with a runtime resolver in front of it (uv run/uvx/python3— uv 0.9.28 and Python 3.14.7 are present here). Python is not special: it is the proof the command executor generalises. MCPX_SOCKETin the hook's environment makes every hook a/v1client, which is what makes hooks useful rather than decorative — a hook can add a server, set a setting, answer an elicitation, or run a script.- Delivery semantics have to be stated. At-most-once, bounded queue per hook, drop with a warning when the queue is full, and a
seqwatermark per hook so a daemon restart can replay. Do not promise ordering across hooks; do promise ordering within one hook. - Timeout, and what a non-zero exit means. Follow the adapter's precedent (
internal/adapter/adapter.go): a non-zero exit is a result, not an error — record it, publish ahook.failedevent, carry on. A hook must never be able to fail a tool call it was only watching. - This is
settings, not an endpoint. It fits the registry as a repeatable structured setting; the existingKindSourcemachinery already gives inline-or-file, and apaths.hookslist would give a drop-in directory exactly likepaths.adapters.
What B does not do: it cannot influence anything. Every hook is an observer. For a large fraction of what people want hooks for — "block this tool call", "rewrite these arguments", "inject this into the context" — that is not enough.
Approach C — recommended: B, plus a declared phase per hook, with only observers on the bus
Everything in B, with one addition that changes what hooks are for: each hook declares a phase, and the phase decides whether it is a bus subscriber or an in-line interceptor.
| phase | when | blocking | may veto/rewrite | delivery |
|---|---|---|---|---|
observe (default) |
after the fact | no | no | bus subscriber, queued, replayable by seq |
before |
before the operation | yes, bounded | yes — deny, or return a replacement result | direct call, one per operation |
after |
before the result is returned | yes, bounded | yes — rewrite the result | direct call |
observe is B unchanged and costs the daemon nothing. before/after exist only at a small, enumerated set of points — the same kind of one-table discipline internal/api/ops.go uses for /v1: a table of hook points, each with a declared input and output schema, from which the documentation, the validation and the dispatch are all generated. Start with three points: call (a tools/call about to go upstream), exec (a script about to run), server.start. That is enough for the real uses and small enough to audit.
Why this is the recommendation:
- It is the only option where a hook can do the thing people actually want hooks for, without putting user code on the path of every event.
- The blocking set is bounded and declared, so the "every hook is a latency tax" failure of A is confined to hooks the user explicitly opted into a blocking phase for, with a mandatory timeout and a declared fail-open/fail-closed per hook.
before/afterare the same shape as the existingscript.before/script.onSuccess/script.onErrorsettings, which means the script hooks can eventually be re-expressed as hook-table entries with phasebefore/afteron pointexec— one hook concept instead of two. Do not do that migration in this issue; do make sure the vocabulary does not block it. Input for the decision record: the shippedscript.*list has five phases (before,prefix,onSuccess,onError,suffix;internal/settings/registry.go:212-242, run atinternal/runner/runner.go:190-205), and the shippedonErrorruns and then rethrows ("a hook, not a handler"), while Update module gopkg.in/go-jose/go-jose.v2 to v2.6.3 [SECURITY] - autoclosed #112'sonErrormay replace the result. The vocabulary has to name which of the two a hook is.- A plugin (sibling issue) becomes a hook target for free:
{ "on": "call", "phase": "before", "plugin": "guard", "tool": "check" }is a plugin tool call. That is the composition that makes the plugin system worth having, and it is why hooks and plugins should be designed in the same week even if they ship apart. - It answers "config can set hooks for non-go/typescript" directly: the executor set is command / JS-TS module / Python module / plugin tool, and none of them is Go. Go extension is by writing a plugin binary, not by being loaded into the daemon.
Cost, stated plainly: an enumerated hook-point table is real work, and a blocking hook point is a new class of daemon hang. Mitigate with a hard timeout (a setting with a low default), fail-open as the default, and a mcpx doctor check that reports slow and failing hooks — internal/cli/doctor.go already does exactly this for adapters and API specs.
Open questions
- Does a
before-phase hook get the arguments, and may it rewrite them? Rewriting is the powerful case and the one that makes debugging "why did this tool get called with that" miserable. Possibly: may deny with a reason, may not rewrite, until someone has a concrete need. - Fail-open or fail-closed by default for a
beforehook that times out? Fail-open is safer for availability and is the wrong default for anything guard-shaped. Probably per-hook with fail-open as the default. - Where do hook definitions live in the config chain? Bump build/buck/preludes/buck2-prelude from
3a02ea3to370cd4d#5 governs the chain; a project config that can declare a hook that runs a command is a notable escalation over a project config that can declare a server. - Should hooks be addressable from
/v1at all — list them, disable one, fire one for testing?mcpx hook ls/mcpx hook test <name> --event <file>are cheap and make the feature debuggable. Listing yes; a runtime-mutable hook table probably belongs to the privileged-plugin issue instead. - Interaction with Bump build/buck/preludes/buck2-prelude from
3a02ea3to4371899#6: if every event lands in an event store, a hook is a trigger on that table, and "replay hooks over history" becomes possible. Worth designing theseqwatermark so this stays open. The bus history bound: what isAnswered:keeptoday, and is it enough that a hook whose worker pool is saturated for a few seconds does not lose events?events.history, default 1024 (above). A saturated hook pool is gapped, not merely slow; what the hook is told is Bump build/buck/preludes/buck2-prelude from3a02ea3to4371899#6's gap contract (collision 13), which recommends one explicit gap marker for reconnect, live overflow and replay truncation.- Do hooks fire for events a plugin published? They should, and that implies plugins can publish to the bus, which is a privileged-surface question.
Ease · Impact · Related
Ease: M for the observer table and the three executors; L with phases and the hook-point table. Impact: high — hooks are what let someone extend mcpx without writing a server, and the bus that makes them cheap already exists.
Depends on / related: #6 (event store — the same event vocabulary; a hook is a live trigger, the store is the history; owns gap semantics, collision 13), #112 (exec lifecycle hooks — adopts this issue's vocabulary, collision 2), #82 (runtime hook registration — adopts it too), #80 (a plugin tool is a hook target), #83, #5 (config chain, and what a project-level config may declare; paths.config is still unread), #7 (script.* splicing and placeholders, the existing hook-shaped settings), #79 (per-event work on a latency-sensitive path), #172 (any new MCPX_EVENT_* variable must be declared), #77.
Security/auth deferred; see tracker.
Activity
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#81 (verbatim, oldest first):
@dezren39 · 2026-09-30T07:21:25Z
Integration pass. Placed in the DAG rooted at #181.
d956689to2901d28#80 chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to5f79ad1#82 chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to36464cf#83 chore(deps): bump prettier-plugin-toml from 1.0.0 to 2.0.1 #84@dezren39 · 2026-09-30T07:27:47Z
Integration pass -- cross-theme collision. Surfaced only when the themes were put side by side; the authors of these issues could not see each other's work. Recorded at the root in #181.
Four hook vocabularies, and no issue owns the reconciliation
script.*(shipped), #81's hook-point table, #82's runtime hook registration, and #112'sprePrompt/postPrompt/onErrorlist are four vocabularies for one concept. #112 sits in a different theme (#176) from #81/#82 (#173), so nobody currently owns the overlap. Reconciling them after the fact is a breaking config change -- design them as one.Also on: #82 #112. Nobody currently owns this.
@dezren39 · 2026-09-30T07:28:28Z
Integration pass -- cross-theme collision. Surfaced only when the themes were put side by side; the authors of these issues could not see each other's work. Recorded at the root in #181.
events.historyalready answers #81's open question, and changes its design#81 asks what
keepis today. It isevents.history, default 1024 (internal/defaults/defaults.json:52, wired atinternal/daemon/server.go:165). That means a saturated hook pool is gapped, not merely slow, which theseq-watermark design has to handle explicitly.Also on: #6. Nobody currently owns this.
@dezren39 · 2026-09-30T11:05:46Z
Collision ownership (issues pass, 2026-09-30, at 05c78b2). #181 listed these cross-theme collisions with no owner. Each now has one:
script.*shipped, #81, #82, #112)pkgs/mcpx/docs/decisions/(in flight; linked here when it merges)events.history(default 1024) already answers #81's open questionseqwatermark and an explicit gap marker); #81's hooks consume them.Where this issue is the owner, the decision is made here; where it is not, it adopts the owner's decision rather than making its own.
@dezren39 · 2026-09-30T12:12:08Z
Issues pass (2026-09-30, at 05c78b2).
Parent: #173and a line at the top. chore(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 #81 carries the hook vocabulary (Phase 3: sync, blob gate, and relays #181 collision 2), and SURFACES is recording the decision underpkgs/mcpx/docs/decisions/(in flight). The phase/hook-point design is input to that record. The vocabulary sections themselves were not changed.3a02ea3to4371899#6 (collision 13).keeptoday" is marked answered:events.history, default 1024.events.go:30-51) pluscatalog.changed(internal/daemon/consumer.go:236). Re-citedregistry.go:201-243/:234. Noted Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75:script.*phases now apply to snippets too.d956689to5f79ad1#82 (they adopt this vocabulary), Bump build/buck/preludes/buck2-prelude from3a02ea3to4371899#6 (gap owner), chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to2901d28#80, chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to36464cf#83 and Phase 2: membership, delegation, groups, and server config #172. Added thedesignlabel.@dezren39 · 2026-09-30T12:16:47Z
Issues pass (2026-09-30, architect follow-up). Added input for the hook-vocabulary decision record (collision 2): the shipped
script.*list has five phases (registry.go:212-242,runner.go:190-205), and itsonErrorrethrows while #112's may replace the result; the vocabulary has to name which a hook is. (From the AI-layer worker.)@dezren39 · 2026-09-30T16:58:13Z
Decided (SURFACES, 0001-hook-vocabulary, merged in #228). #181 collision 2 is settled: the shipped launcher phases are the one vocabulary.
What changes in this issue:
before,prefix,onSuccess,onError,suffix(shipped) plusobserve(new, the default: asynchronous, off the bus).afterbecomesonSuccess/onError(orsuffix);asyncbecomesphase: observe.api.Ops():script,call,server.start. The point this issue callsexecis renamedscript, so it visibly matchesscript.<phase>.{on, phase, action, effect}; actions aresource(only viascript.<phase>),run(an argv array, never a shell string),module,prompt,plugin.effecttakes the autonomy levels of 0002.hookssetting, not endpoints (Phase 3: sync, blob gate, and relays #181 recommendation 6). That needs a JSON setting kind ininternal/settings.events.historyis 1024 and a subscriber's buffer is 256, so a saturated hook pool is gapped (Phase 3: sync, blob gate, and relays #181 row 13).call.startedandtask.updatedare declared bus kinds nothing publishes; drop them from the hookable list until something does.@dezren39 · 2026-10-28T21:03:09Z
Triage verdict — confirmed, partially decided, unimplemented; mis-homed in this repo.
Verified against current main (2026-10-28):
dezren39/nix@8c42c2c, origin/main) has no mcpx Go source —pkgs/mcpx/andinternal/were deleted 2026-10-19 (lab-ops-hub container crawler accepts inet_aton IPv4 spellings (127.1, 2130706433, 0x7f.1) — loopback SSRF bypass in validate_crawl_url #468,planning/plans/mcpx-extraction.mdsteps 4–5); mcpx now lives solely indeveloping-today/codeatpkgs/mcpx/, consumed here as a pinned source-only flake input.developing-today/code@528886e, origin/main): the internal half this issue describes still exists exactly as described —pool.Hooksatpkgs/mcpx/internal/pool/pool.go:1069, wired to the bus byRegistry.InstallHooksinpkgs/mcpx/internal/daemon/hooks.go— and it is explicitly "not a user vocabulary; the source of bus events" (pkgs/mcpx/docs/decisions/0001-hook-vocabulary.md, Mapping table).git grep "hooks." pkgs/mcpx/internal/settings/registry.goat code HEAD returns no config-declared hook settings (only the unrelatedhooks.autonomyconsumer entry atinternal/settings/consumer.go:145); there is nohooks: [{on, phase, run|module|plugin}]declaration surface.run:must be an argv array (a string is refused/forbidden),phase: aftersplits intoonSuccess/onError, and several items are explicitly Deferred → chore(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 #81 (e.g.mcpx hook test,script.started/script.finishedbus kinds). So the design input landed, the feature did not.Route: no
small_fixpacket in the nix queue — implementing config hooks here would recreate the vendored mcpx copy the extraction removed, and a duplicate filing in code would violate the one-issue-per-change rule while code#309 owns the transfer. Keep open here until code#309 re-files it indeveloping-today/code(carrying decision 0001 as its vocabulary), then close here with that link.agent-note:v1 · agent: repo-queue-worker-shift-nix (lane A) · task 2064 · kind: triage · outcome: confirmed, decision 0001 landed in code, feature unimplemented; routed to #309 re-home; no nix follow-up packet
developing-today commentedon Oct 28, 2026
CODE-799
dezren39 commentedon Oct 29, 2026
Agent note — triage verdict (queue task 2765): NOT a duplicate; scope confirmed with corrections; implementation follow-up enqueued
Dedupe (checked 2026-10-29, code issues, all states): no existing issue covers config-declared hooks over the event bus. Named neighbours, none a duplicate: tracker #1885 (extensibility — plugins, hooks, tool sources, embedding) is the parent; #1873 is the programmatic/privileged half, explicitly split from this declarative half by this issue's own body; #1929 is the exec-lifecycle list (the #112 successor) which adopts this issue's vocabulary — a consumer, not a cover; #1870 (a plugin is an MCP server) defines one hook target type; #1842 (paths.config governs the config chain) owns the config-chain question this issue's open questions point at.
Scope vs current main (origin/main 42796be) — corrections to the issue's stated current state:
docs/decisions/0001-hook-vocabulary.md(dated 2026-09-30; covers chore(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 #81/chore(deps): bump build/buck/preludes/buck2-prelude fromd956689to5f79ad1#82/Update module gopkg.in/go-jose/go-jose.v2 to v2.6.3 [SECURITY] - autoclosed #112/Bump checkmarx/ast-github-action from 2.0.19 to 2.0.23 #111) is on main — the issue's "in flight; linked here when it merges" is stale. The follow-up builds to decision 0001, not to Approach C as raw input.call.startedno longer exists (declared but published by nothing; removed and recorded in decision 0001 — a hook table must not offer it),resource.watchingexists but is never on the bus, andevents.gonow declares 18 kind constants. The issue's "17 kinds" list needs the follow-up to re-derive the hookable set from currentinternal/events/events.go.script.*family), no declarative subscriber table, no executors —internal/daemon/hooks.go(pool.Hooks → bus) remains the only hook code. The bus, its prefixFilter,Seqreplay, andevents.history(default 1024) are all in place as the issue describes.Follow-up: implementation packet enqueued on this queue with acceptance: hookable event set re-derived from current events.go + decision 0001; declarative hook table in config (respecting #1842's config-chain ruling on what project config may declare); command / JS-TS module / Python module executors (no Go loading); hard timeout + fail-open default per-hook;
mcpx doctorreporting of slow/failing hooks; pkgs/mcpxgo test ./...green in CI.dezren39 commentedon Oct 29, 2026
Agent note — triage closed (queue task 2765)
30 remaining items
dezren39 commentedon Oct 30, 2026
🤖 Agent note · parked
TestEveryDocumentHasAChapterfails on clean main too — pre-existing, documented on the PR), then complete the task.queue/3097-1872-config-hooksdezren39 commentedon Oct 30, 2026
🤖 Agent note · parked
awaiting_ci, stillawaiting_ci; parked 20 minqueue/3097-1872-config-hooks, headeb4cabc6) is OPEN;basic: proxy port fallbacksandbasic: spec paritypass,basic: testis red with 4 package failures:internal/defaults,internal/settings(the two pre-existing doc-drift reds, tracked in mcpx docs: 13 pages missing from compendium.manifest.json — TestEveryDocumentHasAChapter red on main #2552 /main-brokenmain-broken: developing-today/code id @281d999 — basic: nix flake check (cargo-check) #2554/main-broken: developing-today/code mcpx @9f3c829 — basic: test #2534) plusinternal/hooks, which is this diff's own package — the job log does not carry the per-test output, so the failing assertion must be read from run 40997046260's job page or reproduced withcd pkgs/mcpx && go test ./internal/hooks/.has_any_findings:true,has_unresolved_findings:trueon the PR — findings are not yet dispositioned.go test ./internal/hooks/ateb4cabc6, fix the hooks failure, dispose the Codex findings with fix replies, push, rerun./internal/hooks/ ./internal/settings/ ./internal/defaults/, then merge if the only remaining reds are the two pre-existing doc-drift tests.Merge pull request #2541 from developing-today/queue/3097-1872-config…
dezren39 commentedon Oct 30, 2026
🤖 Agent note · parked
pkgs/mcpx.docs/decisions/0001-hook-vocabulary.md.dezren39 commentedon Oct 30, 2026
Agent note — resumed, verifying merged PR (queue task 3097, Lane D)
queue/3097-1872-config-hooks, single commit eb4cabc) is MERGED to main at 2026-10-30T04:31:13Z, merge commit 1aef00e;pkgs/mcpx/internal/hooksis present on main. mcpx: config-declared hooks over the event bus, running any command, JS/TS or Python module #1872 stayed open because the PR body said "Closes the declarative half", not an auto-close keyword.basic: testrun was red —internal/defaults+internal/settingsare the pre-existing doc-drift reds tracked in mcpx docs: 13 pages missing from compendium.manifest.json — TestEveryDocumentHasAChapter red on main #2552, butinternal/hooks(this PR's own new package) also FAILed in 0.159s; (2) the Codex review on mcpx: config-declared hooks over the event bus (#1872) #2541 (2026-10-30T03:27:58Z) left 4 findings (1 P1, 3 P2) with no disposition on the PR before merge.dezren39 commentedon Oct 30, 2026
Agent note — done, closing (queue task 3097, Lane D)
hooks/hooks.timeout), events.Bus subscriber with prefix Filter semantics, command + JS/TS + Python module executors, hard timeout with fail-open default,mcpx hook ls, and doctor checks. Hookable set matches current events.go (no call.started; resource.watching and hook.* excluded). The privileged/programmatic half stays in mcpx: the privileged plugin surface -- runtime hooks, variables, internal commands, detachment #1873, as scoped.go test ./internal/hooks/ -count=1on the laptop at the merged commit eb4cabc passes 11/11.basic: testrun was red and its Codex review (4 findings: 1 P1, 3 P2) undispositioned. The defaults/settings reds are pre-existing (mcpx docs: 13 pages missing from compendium.manifest.json — TestEveryDocumentHasAChapter red on main #2552); the CI-onlyinternal/hooksfailure (unreproducible on the laptop; per-test detail is not retained in the workflow log) and all 4 Codex findings are filed as mcpx hooks (#2541 follow-up): bound descendant processes on timeout, cap retained output, count bus-level drops, real interpreter readiness — plus CI-only internal/hooks failure #2573 with a queued fix task (3823) — the P1 is real: a hook spawning descendants is not bounded by the timeout today.