This commit is contained in:
783
docs/superpowers/specs/2026-06-01-wave23-hook-stubs-design.md
Normal file
783
docs/superpowers/specs/2026-06-01-wave23-hook-stubs-design.md
Normal file
@@ -0,0 +1,783 @@
|
||||
# Wave 2/3 hive-mind Hook Ports — Design Specification
|
||||
|
||||
**Date:** 2026-06-01
|
||||
**Status:** Approved design, pre-plan (implementation-ready; no code written yet)
|
||||
**Reference package (FROZEN):** `packages/hive-mind-hooks-claude-code` (Wave 1, shipped)
|
||||
**Shared dependency (reused as-is):** `@waggle/hive-mind-shim-core` (`packages/hive-mind-shim-core`)
|
||||
**Author:** Design synthesis over the approved decision set + source-verified Tier B facts (Hermes + OpenClaw real repos)
|
||||
|
||||
> This spec expands the approved design verbatim. The approved scope, the three locked decisions
|
||||
> (D1/D2/D3), and the per-package plan are not relitigated here — they are made implementation-ready
|
||||
> and the source-verified Tier B facts are folded in. Where verification *contradicted* an assumption,
|
||||
> the contradiction is surfaced explicitly in §5 and §9 (and reflected in the structured `designImpact`
|
||||
> fields), never papered over.
|
||||
|
||||
---
|
||||
|
||||
## 1. Summary
|
||||
|
||||
### What
|
||||
|
||||
Build **five** new hive-mind hook packages, composed over **one** new shared package:
|
||||
|
||||
| New package | Tier | Reuse model |
|
||||
|---|---|---|
|
||||
| `@waggle/hive-mind-hooks-codex` | A | CC-clone JSON installer; second reference shape |
|
||||
| `@waggle/hive-mind-hooks-codex-desktop` | A | Thin re-export of the codex installer (shared `~/.codex/`) |
|
||||
| `@waggle/hive-mind-hooks-cursor` | A | JSON installer with field renames + degraded events |
|
||||
| `@waggle/hive-mind-hooks-hermes` | B | Bespoke YAML codec; no PreCompact event |
|
||||
| `@waggle/hive-mind-hooks-openclaw` | B | Bespoke JSON5 config + in-process TS handlers |
|
||||
| `@waggle/hive-mind-hooks-core` | — | NEW shared package; the 5 above consume it |
|
||||
|
||||
**Excluded:** `claude-desktop` (MCP-bridge category mismatch — no hook surface, on-demand/partial capture
|
||||
only). It is **deferred** and out of scope for this spec; the only claude-desktop work in scope is a TODO
|
||||
marker on its known-wrong `tool-detection.ts` pointer (see §6.3).
|
||||
|
||||
### Why
|
||||
|
||||
The Wave 1 claude-code hook package proves the silent-capture pattern (every-turn deterministic capture of
|
||||
SessionStart / UserPromptSubmit / Stop / PreCompact into hive-mind frames via `hive-mind-cli`). Five more AI
|
||||
tools have a real lifecycle-hook surface; porting the pattern widens the free-forever memory+harvest moat
|
||||
(CLAUDE.md §1 "Moat strategy") across the AI-tool ecosystem. All five packages are part of the hive-mind OSS
|
||||
split (CLAUDE.md §7.5) and ship publicly via subtree-split — so they must be generic, shell only to
|
||||
`hive-mind-cli`, and carry no proprietary/KVARK logic.
|
||||
|
||||
The Wave 1 package (`packages/hive-mind-hooks-claude-code`) is **FROZEN** — it is the reference shape; do NOT
|
||||
modify it. The accepted tradeoff (D3) is that its reversible-install logic is duplicated by the new
|
||||
`hooks-core` rather than retrofitted into CC.
|
||||
|
||||
### Shape of the work
|
||||
|
||||
The per-tool work is almost entirely confined to three concerns — **config paths**, **config codec /
|
||||
register-shape**, and **event-name mapping** — composed over shared primitives in `hooks-core`. The four
|
||||
lifecycle handler *bodies* (recall+inject, save-temporary-frame, summarize+save-important-frame,
|
||||
compact_memory) are parameterized by a per-tool `EventAdapter` and reuse shim-core's `runHook` + `CliBridge`
|
||||
unchanged for the stdin-JSON/exit-0 tools (codex, codex-desktop, cursor, hermes). OpenClaw is the exception:
|
||||
its handlers are in-process TypeScript, so it cannot reuse `runHook` as-is and needs a thin in-process wrapper
|
||||
(§5.5).
|
||||
|
||||
---
|
||||
|
||||
## 2. Decision Log
|
||||
|
||||
### D1 — Scope = Tier A + Tier B (the 5 packages)
|
||||
|
||||
**Decision:** Build `hive-mind-hooks-{codex, codex-desktop, cursor, hermes, openclaw}`.
|
||||
**Rationale:** All five have a real, deterministic lifecycle-hook surface and reuse ≥80% of the Wave 1
|
||||
package (shim-core + handler bodies + the runHook contract, except OpenClaw's handler model). Claude Desktop
|
||||
is excluded because it has *no* hook API — it is an MCP-server registration yielding voluntary,
|
||||
model-initiated, on-demand capture (strictly weaker than every-turn deterministic capture), a different
|
||||
deliverable that must not block the four real hook ports and carries a separate PM package-vs-fold decision.
|
||||
|
||||
### D2 — Tier B (hermes, openclaw) = source-verify the public repos FIRST
|
||||
|
||||
**Decision:** Before committing Tier B estimates, read the real Hermes and OpenClaw repositories to confirm
|
||||
config format, event names, handler model, fail-open mechanism, and external-CLI-invocation feasibility.
|
||||
**Rationale:** The feasibility brief rated Tier B *medium* confidence — key specifics (Hermes shell-hook
|
||||
`VALID_HOOKS` allow-list; OpenClaw `handler.ts` event type, `execFileAsync`-under-loop reliability,
|
||||
`bootstrapFiles` injection sanction) were docs-only, not source-read. **Status: this verification is DONE.**
|
||||
Results are folded into §5.4 (Hermes) and §5.5 (OpenClaw) with `confidenceAfterVerify: high` for both. The
|
||||
verification **confirmed** the Hermes design and **required two corrections** to the OpenClaw design (handler
|
||||
model + one event mismap) — see §5.5 and §9.
|
||||
|
||||
### D3 — Architecture = new shared `@waggle/hive-mind-hooks-core`; CC left as-is
|
||||
|
||||
**Decision:** Create a new shared package `@waggle/hive-mind-hooks-core` exporting the reversible-install
|
||||
primitives, the shared lifecycle handler bodies (parameterized by an `EventAdapter`), and a `jsonRegister`
|
||||
helper. The five new packages consume it. The Wave 1 claude-code package is NOT refactored to consume it.
|
||||
**Rationale:** Extracting shared logic into `hooks-core` keeps the five new packages thin (paths + codec +
|
||||
register-shape + bin + tests) and DRY, and keeps the OSS subtree-split clean (one shared package, not five
|
||||
copies). **Accepted tradeoff:** the reversible-install logic now exists in *two* places — the frozen CC
|
||||
package and `hooks-core`. This duplication is deliberate; retrofitting CC is explicitly out of scope and
|
||||
risks regressing a shipped, regression-locked package.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture — `@waggle/hive-mind-hooks-core`
|
||||
|
||||
`hooks-core` is a tool-agnostic library package (no `bin`; consumed by the five tool packages). It mirrors the
|
||||
naming and idioms of the Wave 1 CC modules (`install.ts`, `uninstall.ts`, `verify.ts`, `paths.ts`,
|
||||
`settings-merger.ts`, `hooks/_shared.ts`) so the per-package code reads like the reference. It depends on
|
||||
`@waggle/hive-mind-shim-core` and re-uses its `runHook`, `CliBridge`, `createCliBridge`, `encodeFrame`,
|
||||
`summarizeTurn`, `classifyImportance`, `maybeEmitDiscovery`, and `createLogger` (the same surface the CC hooks
|
||||
import today — see `packages/hive-mind-shim-core/src/index.ts`).
|
||||
|
||||
### 3.0 Package metadata
|
||||
|
||||
- `name: "@waggle/hive-mind-hooks-core"`, `version: "0.1.0"`, `type: "module"`, `license: "Apache-2.0"`,
|
||||
`main: "dist/index.js"`, `types: "dist/index.d.ts"`, `engines.node >= 20`, `publishConfig.access: "public"`.
|
||||
- `dependencies`: `@waggle/hive-mind-shim-core: "*"`, plus a YAML parser for the hermes codec and a JSON5
|
||||
parser for the openclaw codec **(see §9 OQ-3 — whether the YAML/JSON5 deps live in `hooks-core` or only in
|
||||
the hermes/openclaw consumer packages is an open question; default: keep codec-specific deps out of
|
||||
`hooks-core` and in the consumers, so `hooks-core` stays codec-agnostic and the JSON tools pull no YAML
|
||||
dep)**.
|
||||
- `peerDependencies`: `@waggle/hive-mind-cli: "*"` (optional, as in CC) — handlers shell to it at runtime.
|
||||
- No `bin`. Exports a barrel (`.`) plus per-primitive subpath exports if needed by consumers.
|
||||
|
||||
### 3.1 Reversible-install primitives (`install-core.ts` / `paths-core.ts`)
|
||||
|
||||
These generalize the Wave 1 CC logic. The CC reference assumes the config file **must pre-exist** and throws
|
||||
if absent (`install.ts` lines 85–90). `hooks-core` must add a **create-if-missing** mode and the matching
|
||||
uninstall semantics (delete-if-we-created vs restore-backup-if-existed), because codex/cursor/hermes config
|
||||
files are optional/standalone and may not exist on a fresh machine.
|
||||
|
||||
```ts
|
||||
// backupByteIdentical — write a byte-identical timestamped backup of an existing file.
|
||||
// Returns the backup path. No-op marker when the source did not exist (createdByUs case).
|
||||
export function backupPathFor(configPath: string, isoTimestamp: string): string;
|
||||
// → `${configPath}.hive-mind-backup.${iso.replace(/[:.]/g, '-')}` (mirrors CC paths.ts:86)
|
||||
|
||||
export async function backupByteIdentical(
|
||||
configPath: string,
|
||||
isoTimestamp: string,
|
||||
): Promise<{ backupPath: string | null; preExisted: boolean }>;
|
||||
// preExisted=false → no backup written; caller records createdByUs=true in the pointer.
|
||||
// preExisted=true → backup written with the exact original bytes (CC install.ts:104-106 idiom).
|
||||
|
||||
// Pointer file — records what we did so uninstall is exact. Superset of CC's pointer.
|
||||
export interface InstallPointer {
|
||||
version: string;
|
||||
installed_at: string;
|
||||
config_path: string;
|
||||
settings_backup: string | null; // null ⇔ created_by_us=true
|
||||
created_by_us: boolean; // NEW vs CC: true if the config file did not pre-exist
|
||||
hooks_dir: string | null; // null for in-process tools (openclaw)
|
||||
installed_hooks: readonly string[];
|
||||
cli_path: string | null;
|
||||
extra?: Record<string, unknown>; // per-tool: e.g. openclaw hook dir names, hermes registered event keys
|
||||
}
|
||||
export async function writePointer(pointerPath: string, pointer: InstallPointer): Promise<void>;
|
||||
export async function readPointer(pointerPath: string): Promise<InstallPointer>; // throws if malformed/absent
|
||||
|
||||
// restoreFromBackup — round-trip-verified restore. Mirrors CC uninstall.ts:71-89:
|
||||
// write backup bytes over configPath, re-read, assert SHA-256 / byte equality, refuse to delete
|
||||
// the backup unless the readback matches.
|
||||
export async function restoreFromBackup(args: {
|
||||
configPath: string;
|
||||
pointer: InstallPointer;
|
||||
cleanupBackup?: boolean; // default true
|
||||
}): Promise<{ restoredFrom: string | null; createdRemoved: boolean; backupRemoved: boolean }>;
|
||||
// created_by_us=true → DELETE the config file we created (never orphan it); restoredFrom=null.
|
||||
// created_by_us=false → restore the backup byte-identically (CC behavior).
|
||||
|
||||
// normalizeCliPath — reject embedded double-quotes (they break `--cli-path "<value>"` quoting).
|
||||
// Lifted verbatim from CC install.ts:142-154.
|
||||
export function normalizeCliPath(input: string | undefined): string | undefined;
|
||||
```
|
||||
|
||||
**Create-if-missing + uninstall semantics (precise):**
|
||||
|
||||
1. **Install, config pre-existed:** write byte-identical backup → merge/register hive entries → write merged
|
||||
config → pointer with `created_by_us=false`, `settings_backup=<backup>`.
|
||||
2. **Install, config absent:** create a minimal valid skeleton (per-tool: `{version:1,hooks:{}}` for cursor,
|
||||
`{hooks:{}}` for codex, `{}` for hermes YAML, JSON5 `{hooks:{internal:{...}}}` for openclaw) containing
|
||||
*only* hive entries → pointer with `created_by_us=true`, `settings_backup=null`.
|
||||
3. **Uninstall, `created_by_us=false`:** restore backup byte-identically (round-trip verified), delete
|
||||
backup + pointer.
|
||||
4. **Uninstall, `created_by_us=true`:** delete the config file we created, delete pointer. **Never orphan a
|
||||
file we created and never leave a backup behind.**
|
||||
5. **Re-install (upgrade) idempotency:** if a pointer already exists, treat as upgrade — replace the
|
||||
marker-tagged hive entries in place (mirrors CC `mergeHiveHooks` replace-in-place, settings-merger.ts:85-93)
|
||||
rather than duplicating, and keep the original backup so uninstall still restores the true pre-install
|
||||
state.
|
||||
|
||||
### 3.2 `EventAdapter` interface (`event-adapter.ts`)
|
||||
|
||||
The four lifecycle handler bodies are tool-agnostic and parameterized by a per-tool `EventAdapter` — field
|
||||
extractors (which incoming payload keys hold the cwd / prompt / response / session id / parent) plus the
|
||||
event-name map. This generalizes what the CC handlers already do inline via `pickStringFromObject` multi-key
|
||||
fallbacks (e.g. `stop.ts:37-42` reads `response | assistant_message | transcript`).
|
||||
|
||||
```ts
|
||||
export type Lifecycle = 'session-start' | 'user-prompt-submit' | 'stop' | 'pre-compact';
|
||||
|
||||
export interface EventAdapter {
|
||||
/** Tool id used for HookEvent.source + logger names. */
|
||||
readonly source: ShimSource; // 'codex' | 'cursor' | 'hermes' | 'openclaw' | ...
|
||||
/** Map our canonical lifecycle name → the tool's native event key. undefined ⇒ event not supported. */
|
||||
readonly eventName: Record<Lifecycle, string | undefined>;
|
||||
/** Field extractors over the opaque incoming payload (returns undefined when absent). */
|
||||
extractCwd(payload: unknown): string | undefined;
|
||||
extractSessionId(payload: unknown): string | undefined;
|
||||
extractPrompt(payload: unknown): string | undefined; // UserPromptSubmit
|
||||
extractResponse(payload: unknown, ctx: { readFile?: (p: string) => Promise<string> }): Promise<string | undefined> | string | undefined;
|
||||
// async because Cursor delivers the turn via base.transcript_path (read off disk), not inline.
|
||||
extractParent(payload: unknown): string | undefined; // Stop parent frame id, if any
|
||||
/** Per-tool shape of the SessionStart inject response (some tools differ). undefined ⇒ no inject. */
|
||||
formatInject?(additionalContext: string): unknown;
|
||||
}
|
||||
```
|
||||
|
||||
`ShimSource` is the existing shim-core union (`hook-event-types.ts`); it already includes `'claude-code'`,
|
||||
`'cursor'`, `'openclaw'` (per the signal-emitter doc comment). **Verify the union includes `'codex'` and
|
||||
`'hermes'`; if not, extend it in shim-core** (small additive change to the OSS shim-core — acceptable; see
|
||||
§9 OQ-2).
|
||||
|
||||
### 3.3 Shared handler bodies (`handlers-core.ts`)
|
||||
|
||||
Four factory functions return shim-core `HookHandler` objects parameterized by an `EventAdapter`. Each mirrors
|
||||
the corresponding CC hook body (`hooks/session-start.ts`, `user-prompt-submit.ts`, `stop.ts`, `pre-compact.ts`)
|
||||
but reads fields through the adapter instead of hardcoded key lists.
|
||||
|
||||
```ts
|
||||
export function makeSessionStartHandler(a: EventAdapter, opts?: { recallLimit?: number }): HookHandler<...>;
|
||||
// run: bridge.recallMemory('', { limit, scope: 'personal' }) → format hits → a.formatInject(text)
|
||||
// (default formatInject = CC's { hookSpecificOutput: { hookEventName, additionalContext } }).
|
||||
|
||||
export function makeUserPromptSubmitHandler(a: EventAdapter): HookHandler<...>;
|
||||
// run: encodeFrame({eventType:'user-prompt-submit', source:a.source, ...}, {importance:'temporary'})
|
||||
// → bridge.saveMemory(frame). No stdout. (mirrors user-prompt-submit.ts)
|
||||
|
||||
export function makeStopHandler(a: EventAdapter, opts?: { summaryBudgetChars?: number }): HookHandler<...>;
|
||||
// run: summarizeTurn(response) → classifyImportance → encodeFrame(importance:'important'|'critical', parent?)
|
||||
// → bridge.saveMemory → opt-in maybeEmitDiscovery on WAGGLE_SIGNAL_EMIT (mirrors stop.ts:46-108).
|
||||
|
||||
export function makePreCompactHandler(a: EventAdapter): HookHandler<...>;
|
||||
// run: bridge.cleanupFrames() (default mode 'compact'). (mirrors pre-compact.ts)
|
||||
```
|
||||
|
||||
These compose shim-core unchanged: `summarizeTurn`, `classifyImportance`, `encodeFrame`, `maybeEmitDiscovery`,
|
||||
`CliBridge.{recallMemory,saveMemory,cleanupFrames}`. The WAGGLE_SIGNAL_EMIT opt-in behavior (off by default,
|
||||
fail-open) is preserved exactly as in CC `stop.ts` so OSS consumers see no behavior change.
|
||||
|
||||
### 3.4 How `runHook` / `CliBridge` are composed
|
||||
|
||||
For the four stdin-JSON/exit-0 tools (codex, codex-desktop, cursor, hermes), each per-tool hook script is a
|
||||
thin entrypoint identical in shape to CC's `hooks/session-start.ts` etc.:
|
||||
|
||||
```ts
|
||||
// e.g. packages/hive-mind-hooks-codex/src/hooks/session-start.ts
|
||||
import { runHook } from '@waggle/hive-mind-shim-core'; // exact CC import
|
||||
import { makeSessionStartHandler } from '@waggle/hive-mind-hooks-core';
|
||||
import { codexAdapter } from '../adapter.js';
|
||||
export async function runSessionStart(opts = {}) {
|
||||
return runHook(makeSessionStartHandler(codexAdapter), { name: 'session-start', ...opts });
|
||||
}
|
||||
// + the CC isMain self-invoke guard verbatim (session-start.ts:91-102)
|
||||
```
|
||||
|
||||
`runHook` (shim-core `hooks/_shared` equivalent — actually re-exported from shim-core; the CC `_shared.ts`
|
||||
wraps it) already provides: stdin read with timeout, `safeJsonParse`, `--cli-path` argv parsing
|
||||
(`parseHookArgs`), `createCliBridge` wiring, **exit-0-always fail-open**, and stdout JSON emission. The
|
||||
per-tool packages reuse it as-is; `hooks-core` only supplies the adapter + handler bodies.
|
||||
|
||||
> Note on `_shared.ts`: the CC `hooks/_shared.ts` is itself a thin wrapper that re-exports/wraps shim-core's
|
||||
> `createCliBridge` + `createLogger` and defines `runHook`, `parseHookArgs`, `readStdinAsString`,
|
||||
> `safeJsonParse`, `pickStringFromObject`. To keep the new packages DRY, `hooks-core` should export an
|
||||
> equivalent `hook-shared.ts` (lifted from CC `_shared.ts`, which is not frozen-by-reference since it's a copy,
|
||||
> not the CC package — but to be safe, re-author it in `hooks-core` rather than import across package
|
||||
> boundaries). The five packages import `runHook`/`parseHookArgs` from `hooks-core`, not from the frozen CC
|
||||
> package.
|
||||
|
||||
### 3.5 `jsonRegister` helper (`json-register.ts`)
|
||||
|
||||
Generalizes CC's `mergeHiveHooks` (settings-merger.ts:72-101) for any JSON-config tool whose event keys map to
|
||||
**arrays of hook groups**. Additive merge + marker tag + dedup/replace-in-place.
|
||||
|
||||
```ts
|
||||
export const HIVE_MIND_MARKER_BASE = '@hive-mind'; // per-tool suffix appended, e.g. '@hive-mind/codex-hooks'
|
||||
|
||||
export interface JsonRegisterSpec {
|
||||
/** Top-level object key holding the per-event map (e.g. 'hooks'). */
|
||||
hooksKey: string;
|
||||
/** Canonical lifecycle → tool event-key map (from the EventAdapter.eventName). */
|
||||
eventName: Record<Lifecycle, string | undefined>;
|
||||
/** Builds the tool-shaped group object for one hook entry (codex uses {matcher,hooks:[...]}; cursor uses {command,type,timeout}). */
|
||||
buildGroup(lifecycle: Lifecycle, command: string, timeout: number): Record<string, unknown>;
|
||||
/** Reads the marker off a group to detect our own entries for replace/remove. */
|
||||
isHiveGroup(group: unknown): boolean;
|
||||
/** Optional wrapper to ensure skeleton (e.g. cursor needs {version:1}). */
|
||||
ensureSkeleton?(root: Record<string, unknown>): Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function jsonRegister(
|
||||
config: Record<string, unknown> | undefined,
|
||||
entries: readonly { lifecycle: Lifecycle; command: string; timeout: number }[],
|
||||
spec: JsonRegisterSpec,
|
||||
): Record<string, unknown>; // returns a NEW object; never mutates input (CC immutability contract)
|
||||
|
||||
export function jsonUnregister(config, spec): Record<string, unknown>; // strips marker-tagged groups
|
||||
export function hasHiveEntries(config, spec): boolean;
|
||||
```
|
||||
|
||||
`jsonRegister`/`jsonUnregister` preserve all non-hive entries verbatim (preserves the user's existing hooks,
|
||||
exactly as CC's merge preserves `gsd-context-monitor.js`), and dedup our own marker-tagged group by
|
||||
`(eventKey, command)` so re-install upgrades in place. Used by codex, codex-desktop (via codex), and cursor.
|
||||
Hermes (YAML) and OpenClaw (JSON5 + dirs) do NOT use `jsonRegister` — they have bespoke codecs (§5.4, §5.5).
|
||||
|
||||
---
|
||||
|
||||
## 4. Lifecycle → tool-event mapping (canonical reference)
|
||||
|
||||
The four canonical lifecycle actions map to native tool events as follows. "—" = no native event (degraded;
|
||||
documented per §6.1). All five packages prove the two invariants in §7.2.
|
||||
|
||||
| Lifecycle (canonical) | claude-code (ref) | codex | codex-desktop | cursor | hermes | openclaw |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **SessionStart** (recall+inject) | `SessionStart` | `SessionStart` | `SessionStart` | `sessionStart` | `on_session_start` (observe) **+** `pre_llm_call` `is_first_turn` (inject) | `agent:bootstrap` (mutate `bootstrapFiles`) |
|
||||
| **UserPromptSubmit** (save temp) | `UserPromptSubmit` | `UserPromptSubmit` | `UserPromptSubmit` | `beforeSubmitPrompt` (save-only) | `pre_llm_call` | `message:received` |
|
||||
| **Stop** (summarize+save) | `Stop` | `Stop` | `Stop` | `stop` (turn via `transcript_path`) | `post_llm_call` | `message:sent` (0..N/turn — debounce) |
|
||||
| **PreCompact** (compact_memory) | `PreCompact` | `PreCompact` | `PreCompact` (+`PostCompact` bonus) | `preCompact` (observational) | **— (none)** | `session:compact:before` (match `event.action==='compact:before'`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-Package Designs
|
||||
|
||||
### 5.1 `@waggle/hive-mind-hooks-codex` (Tier A — second reference shape)
|
||||
|
||||
- **Config surface:** standalone `~/.codex/hooks.json` (JSON). Do NOT touch `~/.codex/config.toml` (keeps us
|
||||
out of the user's TOML and away from protected `notify`/`profile`/`model_providers` keys). Pointer:
|
||||
`~/.codex/hive-mind-install.json`.
|
||||
- **Codec:** plain JSON (`JSON.parse`/`JSON.stringify(_, null, 2) + '\n'`, exactly CC install.ts:116).
|
||||
- **Register/merge shape:** `jsonRegister` with `hooksKey='hooks'`, group shape
|
||||
`{ matcher, hooks: [{ type:'command', command, timeout }], _hiveMindShim }`. `matcher='startup|resume|clear|compact'`
|
||||
for SessionStart, `''` (or omitted) elsewhere. Hooks across config layers are additive; the marker is kept
|
||||
for byte-identical reversible uninstall, not for correctness.
|
||||
- **Event map:** SessionStart→`SessionStart`, UserPromptSubmit→`UserPromptSubmit`, Stop→`Stop`,
|
||||
PreCompact→`PreCompact` (a deliberate CC clone — `HOOK_EVENT_BY_BASENAME` is effectively identical to CC).
|
||||
- **Adapter field extractors:** reuse CC's snake_case keys (`prompt`, `session_id`, `cwd`) plus codex
|
||||
additions: Stop reads `last_assistant_message` (added to the response fallback list); PreCompact reads
|
||||
`trigger` (`manual|auto`); `HookEvent.source='codex'`.
|
||||
- **Degraded/missing events:** none — all four native and field-compatible.
|
||||
- **Install UX (non-silent steps):** non-managed Codex hooks require a **one-time `/hooks` trust step** (trust
|
||||
keyed by hook hash) before they execute. The installer MUST print: "Run `/hooks` in Codex once to trust the
|
||||
hive-mind hooks." Also `verify` must surface admin lockdown `allow_managed_hooks_only = true`
|
||||
(`requirements.toml`) which suppresses user hooks so install doesn't silently no-op.
|
||||
- **Uninstall:** `created_by_us` aware — delete `hooks.json` if we created it, else restore byte-identical
|
||||
backup (§3.1).
|
||||
- **Reuses from core:** `jsonRegister`, all reversible-install primitives, all four handler bodies, the
|
||||
`runHook` entrypoint shape, `--cli-path` Windows quoting (verbatim from CC `hookCommandFor`, paths.ts:74-84).
|
||||
- **Implements locally:** `paths.ts` (codex paths), `adapter.ts` (codex EventAdapter + JsonRegisterSpec),
|
||||
4 hook entrypoints, bin (`codex-hooks`).
|
||||
- **Blocker to note:** field-name casing confirmed from docs, not a live payload — add codex keys as
|
||||
*fallbacks* in the adapter rather than assuming. Minimum codex version shipping stable hooks is unpinned
|
||||
("stable as of May 2026") — `verify` should probe dynamically; a too-old codex silently ignores hooks.json.
|
||||
|
||||
### 5.2 `@waggle/hive-mind-hooks-codex-desktop` (Tier A — thin re-export)
|
||||
|
||||
- **Config surface:** SHARES `~/.codex/hooks.json` with codex CLI — there is no separate codex-desktop config
|
||||
root. Same pointer file `~/.codex/hive-mind-install.json`.
|
||||
- **Codec / register / events / adapter:** identical to codex — this package is a **thin re-export of the
|
||||
codex installer** (`export * from '@waggle/hive-mind-hooks-codex'` plus a bin that delegates). It exists as a
|
||||
distinct package so the dependency graph + subtree-split see the package boundary and so the launcher's
|
||||
`hookPackageFor('codex-desktop')` resolves to `@waggle/hive-mind-hooks-codex-desktop`.
|
||||
- **In-scope fix — `tool-detection.ts` codex-desktop pointer:** `HOOK_POINTER_BY_TOOL['codex-desktop']` in
|
||||
`packages/agent/src/tool-detection.ts` (line 194) is currently `'.config/Codex/hive-mind-install.json'`,
|
||||
which is wrong — codex-desktop shares `~/.codex/`. **Fix to `'.codex/hive-mind-install.json'`** so the
|
||||
"is-it-installed" probe reads the real pointer. (Prefer config-presence detection at `~/.codex/` over the
|
||||
speculative binary-path guesses; the binary candidate paths in detection are admitted guesses for the
|
||||
unreleased app — out of scope to fully rework here, but the pointer fix is in scope.)
|
||||
- **Install UX:** same `/hooks` trust note as codex. Add a note that the desktop App honoring hooks is
|
||||
doc-asserted but needs runtime verification on an actual App install (CLI definitely fires).
|
||||
- **Reuses from core / codex:** everything, transitively via the codex package.
|
||||
- **Implements locally:** package manifest + thin re-export + bin + the `tool-detection.ts` pointer fix + a
|
||||
parity test that asserts it installs into the same `~/.codex/hooks.json` as codex.
|
||||
|
||||
### 5.3 `@waggle/hive-mind-hooks-cursor` (Tier A — JSON with field renames + degraded events)
|
||||
|
||||
- **Config surface:** `~/.cursor/hooks.json` (JSON) — a **separate file** from Cursor's `settings.json`
|
||||
(editor prefs). Pointer: `~/.cursor/hive-mind-install.json` (already present in `HOOK_POINTER_BY_TOOL`).
|
||||
- **Codec:** plain JSON. Skeleton when absent: `{ "version": 1, "hooks": {} }` (`ensureSkeleton` in the
|
||||
`JsonRegisterSpec` adds `version:1`).
|
||||
- **Register/merge shape:** `jsonRegister` with group shape `{ command, type:'command', timeout, _hiveMindShim }`
|
||||
in `hooks.<event>` arrays (note: cursor uses a flat group, not codex's `{matcher,hooks:[]}` wrapper —
|
||||
`buildGroup` differs).
|
||||
- **Event map (field renames):** SessionStart→`sessionStart`, UserPromptSubmit→`beforeSubmitPrompt`,
|
||||
Stop→`stop`, PreCompact→`preCompact`.
|
||||
- **Degraded/missing events (MUST be documented in README per §6.1):**
|
||||
- `beforeSubmitPrompt` is **SAVE-ONLY** — it cannot inject context (stdout only `{continue, user_message}`).
|
||||
Acceptable: hive-mind's UserPromptSubmit only persists. Adapter `formatInject` is undefined for this event.
|
||||
- `preCompact` is **observational only** — it cannot block/reorder, so "run compact_memory BEFORE host
|
||||
truncates" is best-effort, not guaranteed-before. `compact_memory` runs fire-and-forget.
|
||||
- SessionStart `sessionStart` returns `{ additional_context, env }` — adapter `formatInject` returns
|
||||
`{ additional_context: text }` (a rename of CC's `hookSpecificOutput.additionalContext`).
|
||||
- Stop reads the completed turn via base field `transcript_path` (NOT inline) — `extractResponse` is async,
|
||||
reads the file defensively (tolerate null when transcripts are disabled; do not assume JSONL format), and
|
||||
fails open.
|
||||
- **Install UX:** editing `hooks.json` likely needs a **Cursor restart** for the hooks to take effect (reload
|
||||
semantics unverified across 1.7.x) — installer MUST print "Restart Cursor for hive-mind hooks to take
|
||||
effect." Windows `.cmd`-shim exec problem applies — thread `--cli-path` to the compiled
|
||||
`hive-mind-cli dist/index.js`.
|
||||
- **Uninstall:** `created_by_us` aware (cursor `hooks.json` may not exist on fresh install).
|
||||
- **Reuses from core:** `jsonRegister`, reversible-install primitives, three handler bodies as-is; Stop handler
|
||||
via an adapter whose `extractResponse` reads `transcript_path`.
|
||||
- **Implements locally:** `paths.ts`, `adapter.ts` (renames + transcript reader), 4 entrypoints, bin.
|
||||
|
||||
### 5.4 `@waggle/hive-mind-hooks-hermes` (Tier B — YAML; **source-verified, confidence: high**)
|
||||
|
||||
**Verification status:** source-verified against `NousResearch/hermes-agent` (MIT, Python, default branch
|
||||
`main`, pushed 2026-05-31). The verification **CONFIRMS the approved design** with two clarifications that do
|
||||
not break it (folded in below). `confidenceAfterVerify: high`.
|
||||
|
||||
- **Config surface:** `~/.hermes/config.yaml` (the path `hermes_cli/config.py get_config_path` resolves to;
|
||||
the `cli-config.yaml.example` ships the same `hooks:` block). The relevant system is **Shell hooks** — a
|
||||
top-level `hooks:` block in `config.yaml`. Pointer: `~/.hermes/hive-mind-install.json`.
|
||||
|
||||
> **CRITICAL gotcha (folded from verified facts):** Hermes has **THREE** hook systems sharing the name
|
||||
> "hooks". The design targets the **SHELL-HOOKS** system (`config.yaml` `hooks:` block, shell-out via
|
||||
> subprocess). Do NOT confuse it with (a) the directory-based **Gateway hooks**
|
||||
> (`~/.hermes/hooks/<name>/{HOOK.yaml,handler.py}`, colon-style event names `session:start`, gateway-only,
|
||||
> in-process Python) or (b) the in-process **Plugin hooks** (`ctx.register_hook`). Only the shell-hooks
|
||||
> system is in scope. (Source: `agent/shell_hooks.py`.)
|
||||
|
||||
- **Codec:** bespoke YAML. Add a YAML parser dep (e.g. `yaml`) to the hermes consumer package. **YAML
|
||||
round-trip is NOT byte-identical** for re-serialized output (comments/ordering lost), so reversibility relies
|
||||
on the **literal byte-identical backup** written by `backupByteIdentical` (the merged config is what we
|
||||
*write*; uninstall restores the *original bytes*, not a re-serialized merge). This is fully compatible with
|
||||
the §3.1 primitives.
|
||||
- **Register/merge shape:** additive, marker-tagged merge of hive entries into the YAML `hooks:` block,
|
||||
preserving the user's existing hook entries. Each event key → list of
|
||||
`{ command, timeout?, matcher? }`. Our entries set `command` to the per-event hook script invocation
|
||||
(`node "<dist>/hooks/<event>.js" --cli-path "..."` — same Windows-safe quoting as CC) and `timeout` (default
|
||||
60, hard cap 300). The marker is carried as a sentinel comment or a recognizable command prefix so
|
||||
`jsonUnregister`-equivalent YAML logic can strip exactly our entries on re-install/upgrade.
|
||||
- **Event map (verified exact strings — snake_case):**
|
||||
- **SessionStart → SPLIT** (as the design assumed): `on_session_start` (observer-only; return value ignored;
|
||||
fires once per NEW session, `conversation_loop.py` ~L294) registers the observe/no-op side, **and**
|
||||
`pre_llm_call` with `is_first_turn=true` (stdout `{"context":"..."}` is appended to the user message — NOT
|
||||
the system prompt, to preserve prefix cache; `conversation_loop.py` L687-721) carries the recall+inject.
|
||||
Docs explicitly state Claude Code's `UserPromptSubmit` maps to `pre_llm_call`. So the SessionStart adapter
|
||||
registers **two** event keys.
|
||||
- **UserPromptSubmit → `pre_llm_call`** (fires once/turn before the tool loop; `extra` carries
|
||||
`user_message`, `conversation_history`, `is_first_turn`). Fire-and-forget save.
|
||||
- **Stop → `post_llm_call`** (fires once/turn after the loop completes, only if `final_response` and not
|
||||
interrupted; `extra` carries `assistant_response`; `conversation_loop.py` L4566-4583). This is the reliable
|
||||
turn-end signal for a single-shot CLI run. **Note:** `on_session_finalize` is gateway-path only (fires at
|
||||
`/new`/`/reset`/expiry boundaries) — do NOT assume it fires on every CLI invocation; use `post_llm_call` as
|
||||
the dependable Stop analogue (verified).
|
||||
- **PreCompact → NONE.** **CONFIRMED ABSENT** by source read: no compaction hook in `VALID_HOOKS`; no
|
||||
`compact`/`precompact`/`pre_compact` token anywhere in `plugins.py` or `shell_hooks.py`; the only
|
||||
compaction source (`agent/conversation_compression.py`) emits no hook. The design's "NO PreCompact"
|
||||
assumption is correct — there is genuinely nothing to hook. The hermes adapter's
|
||||
`eventName['pre-compact'] = undefined`; document the gap, do NOT invent (optionally approximate via
|
||||
opportunistic `compact_memory` from the Stop handler — see §9 OQ-4).
|
||||
- **Handler model (CONFIRMED shell-command):** each firing spawns the configured `command` as a real OS
|
||||
subprocess — `argv = shlex.split(os.path.expanduser(command))`,
|
||||
`subprocess.run(argv, input=stdin_json, capture_output=True, timeout=..., text=True, shell=False)`. The JSON
|
||||
payload is piped to stdin; stdout is read back as optional JSON. This is **structurally identical** to the
|
||||
shim-core `runHook` stdin-JSON contract, so the four handler bodies + `runHook` reuse as-is.
|
||||
- **External CLI invocation (CONFIRMED — idiomatic):** the `command` is ANY executable via
|
||||
`shlex.split + shell=False`; docs list languages as "Any (Bash, Python, Go binary, …)". A hook command of
|
||||
`node "<dist>/hooks/stop.js"` (which itself shells `hive-mind-cli` via `CliBridge`) works directly — the
|
||||
event JSON arrives on stdin and stdout JSON is read back. **Two real constraints, neither blocking:**
|
||||
(1) `shell=False` means no pipes/redirection in the command string itself — wrap multi-step logic in the JS
|
||||
hook script (we already do). (2) **FIRST-USE CONSENT ALLOW-LIST** — each unique `(event, command)` string
|
||||
must be approved once. Under any **non-TTY / headless launch** (the Waggle launcher), the hook registers
|
||||
ONLY if one of `--accept-hooks`, `HERMES_ACCEPT_HOOKS=1`, or `hooks_auto_accept: true` is set — otherwise it
|
||||
silently stays unregistered with a warning.
|
||||
- **Fail-open mechanism (CONFIRMED, multi-layer):** config parsing warn-and-skips malformed entries (never
|
||||
raises); the subprocess layer catches Timeout/FileNotFound/Permission/Exception and returns None; non-zero
|
||||
exit is logged but stdout is still parsed; the dispatcher wraps each callback in try/except; every runtime
|
||||
firing site wraps `invoke_hook` in try/except. **Behavioral difference to fold in:** unlike Claude Code there
|
||||
is **NO exit-code-2 / special-exit contract** — control flow (block/inject) is expressed purely via
|
||||
**stdout JSON** (`{"action":"block"}` or `{"context":"..."}`; two block-shapes accepted). Our hooks are
|
||||
capture-only (no block, no inject except SessionStart context), so they just emit `{}`/context and the
|
||||
exit-0-always shim contract is fully compatible.
|
||||
- **Install UX (non-silent steps):**
|
||||
- The installer/launcher MUST set `HERMES_ACCEPT_HOOKS=1` (or write `hooks_auto_accept: true` into the
|
||||
config) under headless/gateway launch, or the hive-mind hooks **silently never register**. Surface this
|
||||
clearly. (Allowlist keys on the exact command STRING, not a script hash — editing the target script is
|
||||
silently trusted; only `hermes hooks doctor` surfaces mtime drift.)
|
||||
- `matcher:` is honored ONLY for `pre_tool_call`/`post_tool_call` — on our lifecycle events it is stripped
|
||||
with a warning; do not set it.
|
||||
- **Cohort plumbing (in scope per design):** add `'hermes'` to `HOOKS_COHORT` in
|
||||
`packages/agent/src/tool-launcher.ts` (currently `['claude-code']`, line 62) once the package ships a real
|
||||
bin, so the launcher routes hook install/verify/uninstall for hermes. **Note:** add each Tier-A/B tool to
|
||||
`HOOKS_COHORT` as its package ships a bin (codex, codex-desktop, cursor too) — the design singled out hermes
|
||||
because the feasibility brief flagged it as excluded, but the cohort gate applies to every newly-real
|
||||
package (see §9 OQ-1).
|
||||
- **Reuses from core:** all four-minus-PreCompact handler bodies, `runHook`, reversible-install primitives.
|
||||
- **Implements locally:** `paths.ts`, bespoke YAML `settings-merger`/codec, `adapter.ts` (hermes field
|
||||
extractors + 2-key SessionStart registration), 3 entrypoints (no pre-compact), bin, the `HOOKS_COHORT` edit.
|
||||
- **Re-pin note:** the hermes repo moves fast (verified against HEAD/`main`, pushed 2026-05-31); **re-pin to a
|
||||
commit SHA before relying on the cited line numbers** during implementation.
|
||||
|
||||
### 5.5 `@waggle/hive-mind-hooks-openclaw` (Tier B — JSON5 + in-process TS; **source-verified, two design corrections**)
|
||||
|
||||
**Verification status:** source-verified against `openclaw/openclaw`. The verification **REQUIRES CHANGES** —
|
||||
two material corrections to the approved design's event/handler assumptions (folded in below; also surfaced in
|
||||
§9). `confidenceAfterVerify: high`. The corrections do NOT change the package's scope; they change the
|
||||
host↔handler glue and one event name.
|
||||
|
||||
- **Config surface:** `~/.openclaw/openclaw.json` (**JSON5** — strict `JSON.parse` first, then `JSON5.parse`
|
||||
fallback; supports `$include` merges + `${ENV}` substitution). Hooks are configured under the
|
||||
`hooks.internal.*` tree **inside this one file**; the `hooks/<name>/{HOOK.md,handler.ts}` files are the hook
|
||||
*implementation* (discovered from directories), separate from config. Pointer:
|
||||
`~/.openclaw/hive-mind-install.json`.
|
||||
- **Codec:** bespoke JSON5. **Naive `JSON.parse`/`JSON.stringify` destroys user comments + trailing commas.**
|
||||
Mitigation: write the hive **hook directory** (`~/.openclaw/hooks/hive-mind/{HOOK.md,handler.ts}` — or a
|
||||
managed dir) and touch `openclaw.json` minimally (add our entry under `hooks.internal.entries` and/or
|
||||
`hooks.internal.load.extraDirs`); rely on the literal byte-identical backup for uninstall rather than
|
||||
re-serialization fidelity (compatible with §3.1).
|
||||
- **Register/merge shape:** discovery-based. Install writes a hook directory with `HOOK.md`
|
||||
(frontmatter declaring `metadata.openclaw.events[]`) + `handler.ts` (default export), and patches
|
||||
`hooks.internal.enabled=true` + an `entries["hive-mind"]={enabled:true, env:{...}}` (or `extraDirs`). **Hooks
|
||||
are OFF until opted in** — set `hooks.internal.enabled=true` and/or run `openclaw hooks enable hive-mind`.
|
||||
Record the created dir + config keys in the pointer's `extra` so uninstall removes exactly what we added.
|
||||
- **Event map (VERIFIED — match on `(type, action)` pair, NOT the joined string):**
|
||||
- **SessionStart → `agent:bootstrap`** — `event.context.bootstrapFiles` is a **MUTABLE array**; the handler
|
||||
pushes recalled frames onto it before bootstrap files are injected into the system prompt
|
||||
(`applyBootstrapHookOverrides` reads it back). This is the sanctioned injection seam (≈ CC
|
||||
`additionalContext`). Only recognized basenames load (`AGENTS.md`/`MEMORY.md`/etc.). CONFIRMED.
|
||||
- **UserPromptSubmit → `message:received`** (inbound message from any channel; `event.context` =
|
||||
`{from, content, channelId, ...}`; replyable). The true user-prompt analog. (`command:new` is the
|
||||
`/new`-reset analog, not generic prompt submit.) CONFIRMED.
|
||||
- **Stop → `message:sent`** — fires **ONCE PER OUTBOUND PAYLOAD DELIVERED** (`deliver.ts:1044`), i.e.
|
||||
**0..N per turn**. This is the design's "needs debounce" case; it is **NON-replyable** (pushed
|
||||
`event.messages[]` are ignored). The consumer must debounce/dedupe (e.g. "last `message:sent` of a turn"
|
||||
heuristic or a short timer). CONFIRMED.
|
||||
- **PreCompact → `session:compact:before`** — **but the runtime `event.action` is `'compact:before'`, NOT
|
||||
`'session:compact:before'`** (the `session:` prefix appears only in the `HOOK.md` `events[]` array). A
|
||||
handler matching the full joined string will silently never fire. Match `event.action === 'compact:before'`.
|
||||
CONFIRMED + corrected.
|
||||
|
||||
**CORRECTION 1 — `before_agent_finalize` is the WRONG system.** The approved design listed
|
||||
`before_agent_finalize` alongside the internal events, but it is a **typed PLUGIN hook**
|
||||
(`src/plugins/hook-types.ts` `PluginHookName` union, registered via `api.on('before_agent_finalize', …)`),
|
||||
NOT an internal `HOOK.md`/`handler.ts` event. The internal-hooks event union is exactly
|
||||
`command | session | agent | gateway | message` — it has **no agent-finalize event**, and `command:stop` is
|
||||
explicitly documented as cancellation/command-lifecycle, **not** a finalization gate. **Resolution:** do NOT
|
||||
use `before_agent_finalize`. Use `message:sent` (with debounce) as the Stop analog. If a true finalization
|
||||
gate is ever required, it must be built as an OpenClaw **plugin** (`api.on`), a different subsystem — out of
|
||||
scope for this hook package. (Surfaced in §9.)
|
||||
|
||||
- **Handler model (VERIFIED — in-process TypeScript, NOT stdin/exit-0):** a hook is a directory
|
||||
`hooks/<name>/{HOOK.md, handler.ts}`. The gateway dynamically `import()`s `handler.ts`, grabs the default
|
||||
export, and registers it as an `InternalHookHandler`. Signature:
|
||||
`(event: InternalHookEvent) => Promise<void> | void`, where `InternalHookEvent =
|
||||
{ type, action, sessionKey, context, timestamp, messages }`. Handlers run **inside the gateway Node process**
|
||||
and share its event loop.
|
||||
|
||||
**CORRECTION 2 — shim-core `runHook` is NOT reusable as-is for OpenClaw.** `runHook` is a stdin-in /
|
||||
exit-0-out subprocess model; OpenClaw handlers are in-process functions. **Resolution:** OpenClaw needs a
|
||||
thin **in-process wrapper** — a `handler.ts` whose default export receives the `InternalHookEvent`, maps
|
||||
`event.context` → the payload shape the shared handler bodies expect, then invokes the shared logic. The
|
||||
shared *logic* (recall+inject via mutating `bootstrapFiles`; save-temp-frame; summarize+save;
|
||||
`compact_memory`) still reuses shim-core's `CliBridge`/`encodeFrame`/`summarizeTurn`/`classifyImportance` —
|
||||
only the host↔handler glue changes from stdin/exit to in-process call + `execFile`. So `hooks-core` exports
|
||||
an **openclaw-specific in-process handler factory** (distinct from the `runHook`-based entrypoints the JSON
|
||||
tools use). The four shared handler *bodies* (§3.3) are authored to take an already-extracted payload, so
|
||||
both the `runHook` path and the in-process path can drive them.
|
||||
|
||||
- **External CLI invocation (VERIFIED — fully supported, idiomatic):** a handler imports `node:child_process`
|
||||
and shells out — `execFile('hive-mind-cli', [...])` (promisified + awaited) is the sanctioned pattern (the
|
||||
`gateway:pre-restart` example in `docs/automation/hooks.md` uses exactly `execFileAsync`). So the
|
||||
external-CLI-invocation assumption **HOLDS** — a Waggle handler can `execFile('hive-mind-cli', …)` (or reuse
|
||||
`CliBridge`, which already spawns it). For reliability under the event loop: use the async form and `await`
|
||||
it (the host already awaits + try/catches each handler); for non-blocking emit, use
|
||||
`fireAndForgetBoundedHook` (bounded 16 concurrent, 2s timeout) or just don't await. **No sandbox/allow-list
|
||||
restricts `child_process` from a hook** — managed/workspace hooks are "trusted local code" (the loader logs a
|
||||
trust warning).
|
||||
- **Fail-open mechanism (VERIFIED — try/catch, NOT exit-0):** `triggerInternalHook` wraps each handler in
|
||||
try/catch (logs `Hook error [type:action]`, runs the next handler, the agent flow is unaffected). There is
|
||||
**no exit code** — handlers are JS functions, so "exit 0" does not apply. **Resolution:** the openclaw
|
||||
fail-open contract is "the default-exported async handler must not throw" — wrap the handler body in
|
||||
try/catch and return on error (as every bundled handler does), and do NOT block synchronously (return a
|
||||
promise; let the host await it).
|
||||
- **Degraded/missing events (document in README per §6.1):** Stop is 0..N/turn and non-replyable (debounce
|
||||
required); no single per-turn "agent finished one reply" internal event.
|
||||
- **`handler.ts` loadability caveat (VERIFIED):** `handler.ts` is loaded via dynamic `import()` of the file
|
||||
URL — a user-dropped raw `.ts` in `~/.openclaw/hooks/` relies on the gateway having a TS loader (tsx/bundled).
|
||||
**Resolution:** ship a **compiled `.js`** handler (with matching filename) rather than a raw `.ts`, OR verify
|
||||
the target install's TS loader. Default: ship compiled `.js` from our `dist/` and reference it (this is
|
||||
consistent with how every other package ships compiled hook scripts). (Surfaced in §9 OQ-5.)
|
||||
- **Provenance / dedup story (design-mandated):** OpenClaw can drive claude-code/codex as **backends** — if
|
||||
those backends also have hive-mind hooks installed, the gateway-layer capture double-counts the same
|
||||
conversation. The package MUST stamp a provenance marker on frames it saves (e.g. `source` metadata
|
||||
`openclaw-gateway` + the channel/session id) and the design notes it "can drive CC/codex as backends," so a
|
||||
dedup heuristic (skip frames whose content hashes match a backend-captured frame within a short window) is
|
||||
needed before shipping. (Open question on the exact dedup mechanism — §9 OQ-6.)
|
||||
- **Reuses from core:** the four shared handler *bodies* (§3.3) and shim-core (`CliBridge`, `encodeFrame`,
|
||||
`summarizeTurn`, `classifyImportance`), the reversible-install primitives, the openclaw in-process handler
|
||||
factory.
|
||||
- **Implements locally:** `paths.ts` (`~/.openclaw/` + hook dir), bespoke JSON5-minimal-touch codec, the
|
||||
`HOOK.md` + compiled `handler.ts` template, the in-process event→payload mapping (incl. `bootstrapFiles`
|
||||
mutation for SessionStart and `compact:before` action matching), debounce for `message:sent`, provenance
|
||||
stamping, bin, `HOOKS_COHORT` edit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cross-Cutting Concerns
|
||||
|
||||
### 6.1 Degraded / missing-event documentation policy
|
||||
|
||||
Every package's README MUST document its degraded or missing events explicitly and MUST NOT imply hook parity
|
||||
with claude-code. Required disclosures:
|
||||
|
||||
- **cursor:** `beforeSubmitPrompt` save-only (no inject); `preCompact` observational only (best-effort, not
|
||||
guaranteed-before-truncation); Stop turn read via `transcript_path` (null when transcripts disabled).
|
||||
- **hermes:** **no PreCompact event at all** (confirmed absent — only 3 hooks ship); SessionStart is split
|
||||
across two events; block/inject is via stdout-JSON not exit codes.
|
||||
- **openclaw:** Stop is `message:sent` 0..N/turn (debounced, non-replyable); no agent-finalize internal event;
|
||||
`before_agent_finalize` is a different (plugin) subsystem.
|
||||
- **codex / codex-desktop:** full parity (no degraded events) — but note the one-time `/hooks` trust step is
|
||||
required for hooks to execute (not a degraded event, an install-UX step).
|
||||
|
||||
A short "Capture fidelity" table in each README (events supported / degraded / absent) is the canonical format.
|
||||
|
||||
### 6.2 Non-silent install UX
|
||||
|
||||
Installers MUST **print** any non-silent manual step (do not assume silent success like CC):
|
||||
|
||||
- **codex / codex-desktop:** "Run `/hooks` in Codex once to trust the hive-mind hooks." `verify` surfaces
|
||||
`allow_managed_hooks_only` lockdown.
|
||||
- **cursor:** "Restart Cursor for hive-mind hooks to take effect."
|
||||
- **hermes:** "Headless/gateway runs require `HERMES_ACCEPT_HOOKS=1` (or `hooks_auto_accept: true`) or the
|
||||
hooks will not register." The launcher should set this env under headless launch.
|
||||
- **openclaw:** "Run `openclaw hooks enable hive-mind` (or set `hooks.internal.enabled=true`) to activate."
|
||||
|
||||
### 6.3 `tool-detection.ts` fixes
|
||||
|
||||
In `packages/agent/src/tool-detection.ts`, `HOOK_POINTER_BY_TOOL` (lines 189-197):
|
||||
|
||||
- **In scope — fix codex-desktop:** change `'codex-desktop': '.config/Codex/hive-mind-install.json'` →
|
||||
`'.codex/hive-mind-install.json'` (codex-desktop shares `~/.codex/`). Ship with the codex-desktop package.
|
||||
- **Out of scope — claude-desktop TODO:** `'claude-desktop': '.config/Claude/hive-mind-install.json'` is also
|
||||
wrong (not a real Claude Desktop config dir on any platform; the real config is mac
|
||||
`~/Library/Application Support/Claude/` / Win `%APPDATA%\Claude\`). **Leave a `// TODO(claude-desktop):`
|
||||
comment** pointing at the deferred MCP-bridge work; do NOT fix it now (claude-desktop is excluded from this
|
||||
spec's scope). Do not modify the speculative codex-desktop binary candidate paths beyond the pointer fix
|
||||
unless trivially co-located.
|
||||
|
||||
### 6.4 OSS / subtree-split
|
||||
|
||||
All five packages are part of the hive-mind OSS split (CLAUDE.md §7.5) — the public mirror
|
||||
(`marolinik/hive-mind`) is generated from this monorepo via `git subtree split`, so whatever lands is
|
||||
byte-identical in the mirror with no cross-repo drift to police. Implications:
|
||||
|
||||
- **No proprietary/KVARK-gated logic** in any of these packages — they only shell to `hive-mind-cli` (the
|
||||
subtree-split filter already excludes `vault.ts`/`evolution-runs.ts`/`execution-traces.ts`/
|
||||
`improvement-signals.ts`/`compliance/**`; these packages have no such concerns — keep it that way).
|
||||
- **`@waggle/hive-mind-shim-core` and the new `@waggle/hive-mind-hooks-core` are the shared, reused-as-is
|
||||
dependencies** and also live in the OSS split — reuse them (do not copy) to keep the mirror DRY.
|
||||
- Author everything in `packages/hive-mind-hooks-*` + `packages/hive-mind-hooks-core`; the mirror follows
|
||||
automatically. Do NOT resurrect the deprecated dual-repo sync workflows (CLAUDE.md §7.5).
|
||||
- **Keep `hooks-core` in the OSS allowlist:** ensure `scripts/oss-subtree-split.sh` includes the new package
|
||||
in the export set (open item — §9 OQ-7).
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 Per-package parity with the CC reference suite
|
||||
|
||||
Each of the five packages mirrors the CC reference test layout (the CC package has install/uninstall/verify/
|
||||
paths/settings-merger tests + per-hook handler tests). Required suites per package:
|
||||
|
||||
- **paths tests:** correct config path, pointer path, hooks-dir resolution, Windows-safe backup path
|
||||
(`:`/`.` → `-`), Windows `--cli-path` quoting.
|
||||
- **register/merge tests:** additive merge preserves existing user entries verbatim; marker-tagged dedup /
|
||||
replace-in-place on re-install; immutability (input config never mutated — mirrors CC settings-merger
|
||||
contract).
|
||||
- **install tests:** config pre-existed → byte-identical backup + merged config + pointer with
|
||||
`created_by_us=false`; config absent → skeleton created + pointer `created_by_us=true`, no backup.
|
||||
- **uninstall tests:** `created_by_us=false` → byte-identical restore (round-trip verified), backup+pointer
|
||||
removed; `created_by_us=true` → config file deleted (no orphan), pointer removed. (Per-tool: hermes/openclaw
|
||||
prove literal-backup restore since codec round-trip is lossy.)
|
||||
- **verify tests:** entries present + point at live hook scripts + `hive-mind-cli --help` probe (CC verify.ts
|
||||
shape); plus per-tool surfacing (codex `allow_managed_hooks_only`; hermes consent/registration state).
|
||||
- **per-hook handler tests:** each lifecycle handler given a representative tool payload → asserts the right
|
||||
`CliBridge` call (recallMemory / saveMemory with correct importance+scope / cleanupFrames) with an injected
|
||||
mock bridge (the CC `runHook` test hooks: `readStdin`/`writeStdout`/`exit`/`bridge` overrides). OpenClaw
|
||||
handler tests drive the in-process handler with a synthetic `InternalHookEvent` (incl. `bootstrapFiles`
|
||||
mutation assertion + `compact:before` action match + `message:sent` debounce).
|
||||
|
||||
### 7.2 `hooks-core` unit tests
|
||||
|
||||
`hooks-core` carries its own unit suite for the shared primitives independent of any tool: `backupByteIdentical`
|
||||
(pre-existed vs absent), `restoreFromBackup` (both branches + round-trip-failure refusal), pointer
|
||||
read/write/malformed, `normalizeCliPath` (double-quote rejection), `jsonRegister`/`jsonUnregister`/`hasHiveEntries`
|
||||
(additive + dedup + immutability), each `make*Handler` factory against a mock bridge + mock adapter, and the
|
||||
openclaw in-process handler factory.
|
||||
|
||||
### 7.3 The two invariants every package MUST prove
|
||||
|
||||
1. **Hooks fail open (always exit 0 / never throw to host).** For the four stdin-JSON tools: an injected
|
||||
bridge error / malformed payload still results in `exit(0)` (CC `runHook` contract). For openclaw: an
|
||||
injected handler-body error is swallowed by the handler's own try/catch and the returned promise resolves
|
||||
(never rejects) — proving the host's per-handler try/catch is not relied on as the only safety net.
|
||||
2. **Byte-identical reversibility.** Uninstall restores the pre-install config to SHA-256-identical state when
|
||||
the config pre-existed (round-trip-verified, mirroring CC uninstall.ts), OR removes exactly the file(s) we
|
||||
created when it did not (no orphans, no leftover backup). Test by snapshotting the config bytes before
|
||||
install and asserting equality (or absence) after uninstall, for both the pre-existed and absent cases.
|
||||
|
||||
---
|
||||
|
||||
## 8. Build Order + Tier B Handling
|
||||
|
||||
**Locked build order:**
|
||||
|
||||
```
|
||||
hooks-core
|
||||
→ codex (Tier A; becomes the 2nd reference shape: CC-clone + create-if-missing)
|
||||
→ codex-desktop (Tier A; thin re-export over codex + the tool-detection.ts pointer fix)
|
||||
→ cursor (Tier A; JSON + field renames + degraded events)
|
||||
→ [source-verify spike: hermes + openclaw] ← DONE in this workflow (D2); results in §5.4/§5.5
|
||||
→ hermes (Tier B; YAML codec, no PreCompact, HOOKS_COHORT add)
|
||||
→ openclaw (Tier B; JSON5 + in-process TS handlers, in-process factory, debounce, provenance)
|
||||
```
|
||||
|
||||
**Tier B handling (D2):** the source-verify spike is complete (this workflow). Both repos resolved
|
||||
(`NousResearch/hermes-agent` MIT; `openclaw/openclaw`); `confidenceAfterVerify: high` for both. Hermes
|
||||
**confirmed** the design (build as designed, 3 hooks, YAML codec). OpenClaw **required two corrections**
|
||||
(in-process handler model instead of `runHook`; `before_agent_finalize` removed in favor of debounced
|
||||
`message:sent`) — both folded into §5.5; neither changes scope, only the host↔handler glue and one event name.
|
||||
Therefore no second spike is needed before implementation; the remaining Tier B unknowns are the small
|
||||
open questions in §9 (dedup mechanism, compiled-handler loadability), resolvable during implementation.
|
||||
|
||||
`hooks-core` is built and unit-tested first (it has no tool dependency). Each tool package is built →
|
||||
tested → and its `HOOKS_COHORT` entry added (so the launcher only offers a hook action once the bin is real),
|
||||
then the next package. Re-pin the hermes/openclaw repo SHAs before relying on the cited line numbers.
|
||||
|
||||
---
|
||||
|
||||
## 9. Risks + Open Questions
|
||||
|
||||
**Design impact from verification:** the Tier B verification (D2) produced **one design-impacting change** —
|
||||
OpenClaw's handler model is in-process TypeScript (not `runHook` stdin/exit-0) and `before_agent_finalize` was
|
||||
a wrong-system event. Both are resolved within the approved scope and architecture (the in-process wrapper +
|
||||
debounced `message:sent` substitution are folded into §5.5) — the *scope* (5 packages + 1 core, openclaw =
|
||||
bespoke JSON5 + in-process TS, Stop debounced) was already correct in the approved design, so this is a
|
||||
mechanism clarification, not a scope or architecture change. It does **not** require human re-approval, but it
|
||||
IS recorded here for visibility.
|
||||
|
||||
**Risks:**
|
||||
|
||||
1. **Tier B repos move fast** — hermes verified against `main` pushed 2026-05-31; re-pin both to commit SHAs
|
||||
before implementation relies on line numbers.
|
||||
2. **Create-if-missing is a behavioral fork from the frozen CC reference** (CC hard-throws if config absent).
|
||||
Easy to get the uninstall side subtly wrong (orphaned files). The §3.1 primitives + §7.2 invariant-2 tests
|
||||
are the guard.
|
||||
3. **Config round-trip fidelity** — hermes YAML and openclaw JSON5 do not survive naive parse→stringify.
|
||||
Mitigated by literal byte-identical backups for uninstall (not diff-merge fidelity) and minimal-touch edits.
|
||||
4. **Hermes headless consent** — without `HERMES_ACCEPT_HOOKS=1` the hooks silently never register under the
|
||||
launcher. The launcher MUST set it; otherwise capture is silently zero.
|
||||
5. **OpenClaw double-counting** — gateway-layer capture can duplicate backend (CC/codex) capture; provenance
|
||||
stamping + a dedup heuristic are required before shipping.
|
||||
|
||||
**Open questions (need a human decision before / during writing-plans):**
|
||||
|
||||
- **OQ-1 — `HOOKS_COHORT` membership:** the design explicitly names hermes for `HOOKS_COHORT`. Confirm that
|
||||
codex, codex-desktop, and cursor are ALSO added to `HOOKS_COHORT` as each ships a real bin (the cohort gate
|
||||
applies to every newly-real package, not just hermes). Default assumption: yes, add each as it ships.
|
||||
- **OQ-2 — `ShimSource` union extension:** confirm `'codex'` and `'hermes'` are valid `ShimSource` values in
|
||||
shim-core (`hook-event-types.ts`); if absent, the spec assumes a small additive extension there. Confirm
|
||||
that additive shim-core edit is acceptable (it ships in the OSS mirror).
|
||||
- **OQ-3 — YAML/JSON5 dep placement:** keep the `yaml` (hermes) and `json5` (openclaw) parser deps in the
|
||||
consumer packages (default) vs in `hooks-core`. Default keeps `hooks-core` codec-agnostic and avoids forcing
|
||||
a YAML dep on the JSON tools.
|
||||
- **OQ-4 — hermes PreCompact approximation:** PreCompact is confirmed absent. Do we (a) ship hermes with no
|
||||
compaction maintenance, or (b) opportunistically call `compact_memory` from the hermes Stop handler? Default:
|
||||
document the gap; do not invent — leave (b) as an optional follow-up.
|
||||
- **OQ-5 — openclaw handler shipping format:** ship a compiled `.js` `handler.ts`-equivalent (default, robust)
|
||||
vs a raw `.ts` relying on the gateway's TS loader. Default: compiled `.js`. Confirm the `HOOK.md` `default`
|
||||
export resolution works against a compiled file on a real openclaw install (the one remaining
|
||||
needs-a-live-install validation).
|
||||
- **OQ-6 — openclaw dedup mechanism:** exact provenance/dedup story (content-hash window? backend-detection?)
|
||||
needs a decision before openclaw ships. Not blocking codex/cursor/hermes.
|
||||
- **OQ-7 — OSS subtree-split allowlist:** confirm `scripts/oss-subtree-split.sh` includes the new
|
||||
`hive-mind-hooks-core` (and the four new hook packages, if the script enumerates rather than globs) in the
|
||||
export set.
|
||||
|
||||
No TBD/placeholder remains unresolved outside this open-questions list; the spec is internally consistent and
|
||||
scoped to a single implementation plan (hooks-core + 5 packages, build order in §8).
|
||||
Reference in New Issue
Block a user