heedvane 0.0.0-stage → 0.2.0

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.
Files changed (98) hide show
  1. package/README.md +62 -2
  2. package/dist/apply.d.ts +104 -0
  3. package/dist/apply.js +388 -0
  4. package/dist/backend/client.d.ts +40 -0
  5. package/dist/backend/client.js +32 -0
  6. package/dist/backend/credentials.d.ts +15 -0
  7. package/dist/backend/credentials.js +76 -0
  8. package/dist/backend/login.d.ts +33 -0
  9. package/dist/backend/login.js +265 -0
  10. package/dist/backend/repo-remote.d.ts +15 -0
  11. package/dist/backend/repo-remote.js +61 -0
  12. package/dist/backend/sign-in-change.d.ts +12 -0
  13. package/dist/backend/sign-in-change.js +72 -0
  14. package/dist/best-effort.d.ts +4 -0
  15. package/dist/best-effort.js +25 -0
  16. package/dist/cli.d.ts +29 -0
  17. package/dist/cli.js +666 -0
  18. package/dist/clients/definitions.d.ts +113 -0
  19. package/dist/clients/definitions.js +427 -0
  20. package/dist/clients/hermes-plugin.d.ts +33 -0
  21. package/dist/clients/hermes-plugin.js +247 -0
  22. package/dist/commands/heedvane.d.ts +15 -0
  23. package/dist/commands/heedvane.js +74 -0
  24. package/dist/commands/memory.d.ts +10 -0
  25. package/dist/commands/memory.js +106 -0
  26. package/dist/config-doc.d.ts +97 -0
  27. package/dist/config-doc.js +465 -0
  28. package/dist/dispatchers/posix.d.ts +41 -0
  29. package/dist/dispatchers/posix.js +208 -0
  30. package/dist/dispatchers/snippets.d.ts +6 -0
  31. package/dist/dispatchers/snippets.js +39 -0
  32. package/dist/dispatchers/windows.d.ts +15 -0
  33. package/dist/dispatchers/windows.js +50 -0
  34. package/dist/doctor.d.ts +37 -0
  35. package/dist/doctor.js +303 -0
  36. package/dist/events/contract.d.ts +54 -0
  37. package/dist/events/contract.js +87 -0
  38. package/dist/events/normalize.d.ts +36 -0
  39. package/dist/events/normalize.js +143 -0
  40. package/dist/guards.d.ts +18 -0
  41. package/dist/guards.js +28 -0
  42. package/dist/heed-file.d.ts +78 -0
  43. package/dist/heed-file.js +316 -0
  44. package/dist/installer.d.ts +47 -0
  45. package/dist/installer.js +174 -0
  46. package/dist/listener/background.d.ts +8 -0
  47. package/dist/listener/background.js +36 -0
  48. package/dist/listener/endpoint.d.ts +26 -0
  49. package/dist/listener/endpoint.js +125 -0
  50. package/dist/listener/forward.d.ts +36 -0
  51. package/dist/listener/forward.js +257 -0
  52. package/dist/listener/serve.d.ts +2 -0
  53. package/dist/listener/serve.js +155 -0
  54. package/dist/listener/server.d.ts +48 -0
  55. package/dist/listener/server.js +251 -0
  56. package/dist/listener/spool.d.ts +28 -0
  57. package/dist/listener/spool.js +60 -0
  58. package/dist/mcp-server.d.ts +9 -0
  59. package/dist/mcp-server.js +187 -0
  60. package/dist/mcp.d.ts +57 -0
  61. package/dist/mcp.js +83 -0
  62. package/dist/memory/private-memory.d.ts +12 -0
  63. package/dist/memory/private-memory.js +54 -0
  64. package/dist/memory/table-of-contents.d.ts +10 -0
  65. package/dist/memory/table-of-contents.js +80 -0
  66. package/dist/memory/team-memory.d.ts +60 -0
  67. package/dist/memory/team-memory.js +54 -0
  68. package/dist/plan.d.ts +77 -0
  69. package/dist/plan.js +288 -0
  70. package/dist/render.d.ts +32 -0
  71. package/dist/render.js +132 -0
  72. package/dist/reply/harness-output.d.ts +57 -0
  73. package/dist/reply/harness-output.js +88 -0
  74. package/dist/reply/reply.d.ts +65 -0
  75. package/dist/reply/reply.js +169 -0
  76. package/dist/reply/verdict.d.ts +28 -0
  77. package/dist/reply/verdict.js +17 -0
  78. package/dist/skill/bundle.d.ts +23 -0
  79. package/dist/skill/bundle.js +102 -0
  80. package/dist/skill/content.d.ts +16 -0
  81. package/dist/skill/content.js +182 -0
  82. package/dist/skills/approved.d.ts +31 -0
  83. package/dist/skills/approved.js +123 -0
  84. package/dist/skills/check.d.ts +21 -0
  85. package/dist/skills/check.js +83 -0
  86. package/dist/state.d.ts +51 -0
  87. package/dist/state.js +118 -0
  88. package/dist/steering/rules.d.ts +49 -0
  89. package/dist/steering/rules.js +165 -0
  90. package/dist/steering/steer.d.ts +29 -0
  91. package/dist/steering/steer.js +64 -0
  92. package/dist/steering/violations.d.ts +27 -0
  93. package/dist/steering/violations.js +24 -0
  94. package/dist/uninstall.d.ts +23 -0
  95. package/dist/uninstall.js +159 -0
  96. package/package.json +35 -4
  97. package/skills/heedvane-hooks/SKILL.md +87 -0
  98. package/skills/heedvane-hooks/references/events.md +69 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `npx heedvane uninstall` — marker-verified removal. Only entries whose
3
+ * serialized form carries our identity are removed; everything else is left
4
+ * byte-identical. heed.yaml is NEVER touched (the user's file) unless
5
+ * --purge-config is passed, and even then it is deleted, not rewritten.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { rm } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { supportedClients } from "./clients/definitions.js";
11
+ import { hermesPluginOwned } from "./clients/hermes-plugin.js";
12
+ import { applyEntry } from "./apply.js";
13
+ import { isOurHookGroup, parseDoc } from "./config-doc.js";
14
+ import { isHeedvaneEntry } from "./mcp.js";
15
+ import { hooksTarget, isClientDetected } from "./plan.js";
16
+ import { skillInstallDir } from "./skill/bundle.js";
17
+ import { statePath } from "./state.js";
18
+ import { unlinkOptional } from "./best-effort.js";
19
+ /** Hook events carrying one of our groups, with the user's own groups that must survive. */
20
+ function ourHookEvents(doc) {
21
+ const hooks = doc["hooks"];
22
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks))
23
+ return [];
24
+ return Object.entries(hooks)
25
+ .filter(([, groups]) => Array.isArray(groups) && groups.some(isOurHookGroup))
26
+ .map(([event, groups]) => ({ event, theirs: groups.filter((group) => !isOurHookGroup(group)) }));
27
+ }
28
+ function ourMcpEntryNames(doc, containerKeys) {
29
+ const names = [];
30
+ for (const key of containerKeys) {
31
+ const c = doc[key];
32
+ if (c === null || typeof c !== "object" || Array.isArray(c))
33
+ continue;
34
+ for (const [k, v] of Object.entries(c)) {
35
+ if (isHeedvaneEntry(k, v))
36
+ names.push(`${key}::${k}`);
37
+ }
38
+ }
39
+ return names;
40
+ }
41
+ /** The doc's parsed value, or null when the config is absent or unparseable — nothing marker-verified to remove. */
42
+ function parseClientDoc(path, format) {
43
+ try {
44
+ return parseDoc(readFileSync(path, "utf8"), format).value;
45
+ }
46
+ catch {
47
+ return null; // absent or unparseable — nothing marker-verified to remove
48
+ }
49
+ }
50
+ /**
51
+ * Remove our hook groups from every event of one parsed config except those in `keep`, leaving the
52
+ * user's own groups. Setup passes the events it installs, so a key an earlier version wrote under a
53
+ * name the harness skips (Gemini's and Cursor's snake_case keys) does not linger.
54
+ */
55
+ async function removeOurHookGroups(client, target, scope) {
56
+ const items = [];
57
+ for (const { event, theirs } of ourHookEvents(scope.doc)) {
58
+ if (scope.keep.has(event))
59
+ continue;
60
+ // Writing no groups of ours keeps the user's own (entryValue merges); drop the key only when ours was all it held.
61
+ const res = theirs.length > 0
62
+ ? await applyEntry(target, { containerKey: "hooks", entries: { [event]: [] } })
63
+ : await applyEntry(target, { containerKey: "hooks", remove: event });
64
+ items.push({ clientId: client.id, name: `${event} hook`, path: target.path, status: res.status === "update" ? "removed" : "failed", detail: res.reason });
65
+ }
66
+ return items;
67
+ }
68
+ /** Setup's half: our hook groups under any event this client no longer installs, removed from its hooks file. */
69
+ export async function removeStaleHookGroups(client, target) {
70
+ const doc = parseClientDoc(target.path, target.format);
71
+ return doc === null ? [] : removeOurHookGroups(client, target, { doc, keep: new Set(client.events) });
72
+ }
73
+ /** When hooksDeferredTo skips a client whose hooks an earlier run installed, remove our hook groups so events do not fire twice. */
74
+ export async function removeDeferredHookGroups(client) {
75
+ const target = hooksTarget(client);
76
+ if (target === undefined)
77
+ return [];
78
+ const doc = parseClientDoc(target.path, target.format);
79
+ return doc === null ? [] : removeOurHookGroups(client, target, { doc, keep: new Set() });
80
+ }
81
+ /** Remove every marker-verified entry of one config file; one item per removal. */
82
+ async function removeMarkedEntries(client, target, doc) {
83
+ // Both kinds can live in the same file (claude settings.json carries
84
+ // hooks AND mcpServers) — remove ours from every container.
85
+ const items = await removeOurHookGroups(client, target, { doc, keep: new Set() });
86
+ for (const qualified of ourMcpEntryNames(doc, [client.mcpServersKey, "mcp_servers", "mcp"])) {
87
+ const [containerKey, entryName] = qualified.split("::");
88
+ const res = await applyEntry(target, { containerKey, remove: entryName });
89
+ items.push({ clientId: client.id, name: `MCP ${entryName}`, path: target.path, status: res.status === "update" ? "removed" : "failed", detail: res.reason });
90
+ }
91
+ return items;
92
+ }
93
+ /** One non-hermes client's config files: parse each and remove our marked entries. */
94
+ async function uninstallClientConfigs(client, items) {
95
+ for (const target of client.configs) {
96
+ if (target.format === "yaml")
97
+ continue;
98
+ const doc = parseClientDoc(target.path, target.format);
99
+ if (doc === null)
100
+ continue;
101
+ items.push(...(await removeMarkedEntries(client, { path: target.path, format: target.format }, doc)));
102
+ }
103
+ }
104
+ export async function uninstall(input) {
105
+ const { home, env } = input;
106
+ const items = [];
107
+ const scope = input.agents && input.agents.length > 0 ? new Set(input.agents) : null;
108
+ for (const client of supportedClients({ home, platform: process.platform, env })) {
109
+ if (scope && !scope.has(client.id))
110
+ continue;
111
+ if (!input.agents || input.agents.length === 0) {
112
+ // Default scope: detected clients only. Explicit list may force.
113
+ if (!existsMarker(client))
114
+ continue;
115
+ }
116
+ if (client.id !== "hermes") {
117
+ await uninstallClientConfigs(client, items);
118
+ }
119
+ else {
120
+ items.push(await uninstallHermesPlugin(home));
121
+ }
122
+ // Agent-hook dispatcher scripts owned by this client.
123
+ const script = join(home, ".heed", "agent-hooks", `${client.id}-hook.sh`);
124
+ await unlinkOptional(script);
125
+ }
126
+ // Skill dir + machine state (never heed.yaml) are shared by every agent: a scoped uninstall keeps them.
127
+ items.push(...(await removeSharedFiles(home, scope !== null)));
128
+ if (input.purgeConfig) {
129
+ await unlinkOptional(join(home, ".heed", "heed.yaml"));
130
+ items.push({ clientId: "*", name: "heed.yaml (purged)", path: join(home, ".heed", "heed.yaml"), status: "removed" });
131
+ }
132
+ return items;
133
+ }
134
+ /** The Hermes plugin dir, removed only when its plugin.yaml declares our plugin. */
135
+ async function uninstallHermesPlugin(home) {
136
+ const pluginDir = join(home, ".hermes", "plugins", "heedvane-hooks");
137
+ const item = { clientId: "hermes", name: "hermes plugin", path: pluginDir };
138
+ if (await hermesPluginOwned(home)) {
139
+ await rm(pluginDir, { recursive: true, force: true });
140
+ return { ...item, status: "removed" };
141
+ }
142
+ return existsSync(pluginDir) ? { ...item, status: "skipped", detail: "not ours — left untouched" } : { ...item, status: "absent" };
143
+ }
144
+ /** The skill bundle and state.json every agent shares: a scoped uninstall keeps them. */
145
+ async function removeSharedFiles(home, scoped) {
146
+ const skillDir = skillInstallDir(join(home, ".agents", "skills"));
147
+ if (scoped) {
148
+ return [{ clientId: "*", name: "skill bundle and state.json", path: skillDir, status: "skipped", detail: "shared by other agents — run uninstall without --agent to remove" }];
149
+ }
150
+ await rm(skillDir, { recursive: true, force: true });
151
+ await unlinkOptional(statePath(join(home, ".heed")));
152
+ return [
153
+ { clientId: "*", name: "skill bundle", path: skillDir, status: "removed" },
154
+ { clientId: "*", name: "state.json", path: statePath(join(home, ".heed")), status: "removed" },
155
+ ];
156
+ }
157
+ function existsMarker(client) {
158
+ return isClientDetected(client);
159
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "heedvane",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Heedvane for coding agents: one command installs steering, guardrails, team memory and skill checks into Claude Code, Codex, Cursor, Gemini CLI and more.",
5
+ "private": false,
6
+ "type": "module",
7
+ "bin": {
8
+ "heedvane": "dist/cli.js",
9
+ "heedvane-hook": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "skills"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "scripts": {
19
+ "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --import ./tests/register-ts-resolver.mjs --test \"tests/**/*.test.ts\"",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit",
21
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
22
+ "build": "tsc -p tsconfig.json",
23
+ "prepack": "tsc -p tsconfig.json"
24
+ },
25
+ "dependencies": {
26
+ "@clack/prompts": "^1.8.1",
27
+ "jsonc-parser": "^3.3.1",
28
+ "smol-toml": "^1.9.1",
29
+ "write-file-atomic": "^5.0.1",
30
+ "yaml": "^2.9.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22",
34
+ "@types/write-file-atomic": "^4.0.3",
35
+ "typescript": "^5.9.3"
36
+ }
37
+ }
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: heedvane-hooks
3
+ description: Use when a user asks about heedvane hooks status, events, or how their coding agents are monitored — reports live hook installation state and the canonical event contract.
4
+ ---
5
+
6
+ # heedvane-hooks
7
+
8
+ ## Overview
9
+
10
+ Heedvane hooks give coding agents (Claude Code, Codex, Gemini CLI, Cursor, Warp, OpenCode, Hermes) a shared steering/guardrail/analysis channel without touching their internals. Three strictly separated layers:
11
+
12
+ 1. **Hook contract** — a harness-neutral event taxonomy (`pre_tool_use`, `post_tool_use`, …), one JSON envelope, one normalizer. See `references/events.md`.
13
+ 2. **Per-agent adapters** — native hook config injection where the harness has hooks (Claude, Codex, Gemini, Cursor); MCP registration as the universal fallback (Warp has no native hooks — policy arrives via MCP); an honest skip where neither exists.
14
+ 3. **Setup mechanism** — `heedvane-hook` CLI: `init` / `setup` / `doctor` / `uninstall` with backup → atomic write → read-back verify. Human intent lives in `~/.heed/heed.yaml`; machine state in `~/.heed/state.json`.
15
+
16
+ Which sessions run the hooks follows `heed.yaml` `gating.mode`: with `always` (the default) every session is governed, with `pane` only sessions launched with `HEED_SESSION` set. The dispatcher script (for Hermes, its plugin) decides, from the mode `setup` bakes into it, and always exits 0. Observability never blocks the agent.
17
+
18
+ ## When to Use
19
+
20
+ - "Is my heedvane install healthy?" / "why aren't hooks firing?"
21
+ - "What events does heedvane capture?" / "what shape is the payload?"
22
+ - "Which of my agents are monitored?"
23
+ - After the user edits `~/.heed/heed.yaml` and wants the change applied/verified.
24
+
25
+ Do **not** use this skill to author policies — an organization's policies are defined and assigned on the Heedvane server, never in `heed.yaml`.
26
+
27
+ ## Checking status
28
+
29
+ 1. Run the doctor (scoped, exit-code-carrying):
30
+
31
+ ```sh
32
+ npx heedvane doctor --json
33
+ ```
34
+
35
+ Or unscoped plain text: `npx heedvane doctor`. Every failing check carries a `remediation` string — surface it verbatim.
36
+
37
+ 2. Read the control file — it is the single source of truth for intent:
38
+
39
+ ```sh
40
+ cat ~/.heed/heed.yaml
41
+ ```
42
+
43
+ `agents: auto` manages every detected agent; an explicit list filters. `install.hooks/skill/mcp` toggles channels per agent.
44
+
45
+ 3. **Never edit agent configs by hand.** `~/.claude/settings.json`, `~/.codex/hooks.json`, etc. are compiled outputs. Edit `heed.yaml`, then re-run:
46
+
47
+ ```sh
48
+ npx heedvane setup
49
+ ```
50
+
51
+ 4. Machine state (installed digests, endpoint, protocol version) is `~/.heed/state.json` — CLI-owned, never hand-edit.
52
+
53
+ ## Event contract summary
54
+
55
+ - Canonical events (v1, snake_case): `session_start`, `session_end`, `user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `post_tool_use_failure`, `permission_request`, `stop`, `stop_failure`, `subagent_start`, `subagent_stop`, `notification`, `pre_compact`. Unknown harness events pass through as `unknown_event` (never dropped).
56
+ - Envelope: `{ "v": 1, "event", "agent", "session", "pane", "receivedAt", "payload": { … } }` — strings capped at 8 KB, nodes at 500.
57
+ - Per-harness mapping (Claude's PascalCase `:class` names → canonical; Codex/Gemini are already snake_case) is tabled in `references/events.md`. Read that file before reasoning about any specific payload.
58
+
59
+ ## Steering, approved skills and memory
60
+
61
+ With the listener running (`npx heedvane listener start`) and a sign-in (`npx heedvane login`; CI and headless machines: `npx heedvane auth org` with the key on stdin), the listener answers each hook with a verdict instead of only recording it:
62
+
63
+ - **Steering:** every rule of the organization that matches is enforced: a tool call is denied or a stop is blocked, with the rule's message as the reason. A session's stops are blocked at most four times in a row; the next one ends the turn so the person sees why, and no later stop of that session is blocked. No hook waits on Heedvane. Command and path rules are synced to the listener at each session start and checked locally, so they hold even when Heedvane is slow or down. Heedvane judges every event in the background, and a rule it finds violated blocks the agent's next tool call or stop.
64
+ - **Organization instructions:** the organization's non-negotiable rules are added as context at session start or with each prompt. They cannot be turned off, and the rules named with them enforce them.
65
+ - **Approved skills:** `npx heedvane skills sync` installs the organization's approved skills into `~/.agents/skills` and `~/.claude/skills` and caches the list in `~/.heed/approved-skills.json`. Using any other skill (the Skill tool, or reading its `SKILL.md`) is denied; writing a skill is not. `heed.yaml` `heedvane.skills` sets `enforce`, `strict` (also compare file hashes) and `whenUnknown` (what to do before the first sync or while the listener is down).
66
+ - **Private memory** (local only, never sent to Heedvane): at session start the hook adds `~/.heed/memory/MEMORY.md` (every repository) and `~/.heed/memory/<owner>/<name>.md` (this repository) as context, so every agent sees them. To save a private memory, append a short line to the right file.
67
+
68
+ - **Team memory:** at session start the hook adds a table of contents of this repository's team memory (folders and titles, never the text). Browse and read it with `npx heedvane memory list [folder]`, `npx heedvane memory show <id>` and `npx heedvane memory search "<question>" [--folder <folder>]`.
69
+
70
+ Backend failures never block the agent: without Heedvane's answer, the locally checked rules decide, and every enforcement is reported to Heedvane.
71
+
72
+ ## Common Pitfalls
73
+
74
+ - **Do not rewrite `~/.claude/settings.json` (or any agent config) manually while hooks are installed.** Hand edits fight the compiler; the next `setup` will either back up your change or report the file invalid. Edit `heed.yaml` instead.
75
+ - **Spool files are diagnostics, not policy.** When the listener is unreachable, hook payloads spool to `~/.heed/spool/` (5 MB cap, 7-day TTL) so nothing is lost — but spool presence is not enforcement, and clearing it loses observability history only.
76
+ - Warp gets policies **via MCP only** — that is the designed happy path, not an error to fix.
77
+ - If `doctor` reports `invalid` on a config, the file does not parse; heedvane never overwrites malformed configs. Fix or move the file aside yourself, then re-run.
78
+ - Re-running `setup` after no changes reports "already current" — idempotence is a feature, not a stalled run.
79
+
80
+ ## Verification Checklist
81
+
82
+ - [ ] `npx heedvane doctor` exits 0 and every check line shows ok
83
+ - [ ] `~/.heed/heed.yaml` parses and says `schemaVersion: v1`
84
+ - [ ] `~/.heed/state.json` skill digest matches the installed skill
85
+ - [ ] A new agent session (launched with `HEED_SESSION` set, or any session with `gating.mode: always`) produces events in the listener log or `~/.heed/spool/`
86
+ - [ ] Backups exist: `<config>.heedvane.backup` next to every patched config
87
+ - [ ] `/heedvane-status` in a fresh agent session reports the same agents as `doctor`
@@ -0,0 +1,69 @@
1
+ # Heedvane event contract (v1)
2
+
3
+ Harness-neutral taxonomy + JSON envelope. One normalizer maps every
4
+ per-source payload into the canonical shape; unknown events pass through
5
+ as `unknown_event` rather than being dropped.
6
+
7
+ ## Canonical events (v1)
8
+
9
+ | Event | Fires when |
10
+ | --- | --- |
11
+ | `session_start` | A managed agent session begins |
12
+ | `session_end` | The session ends (clean or crash) |
13
+ | `user_prompt_submit` | The user submits a prompt |
14
+ | `pre_tool_use` | Before a tool call executes |
15
+ | `post_tool_use` | After a tool call returns |
16
+ | `post_tool_use_failure` | A tool call raises/errors |
17
+ | `permission_request` | The agent asks for approval |
18
+ | `stop` | The agent turn completes |
19
+ | `stop_failure` | The turn aborts with an error |
20
+ | `subagent_start` | A subagent/task tool spawns |
21
+ | `subagent_stop` | The subagent finishes |
22
+ | `notification` | Harness notification (idle, attention, updates) |
23
+ | `pre_compact` | Before the harness compacts the conversation |
24
+
25
+ ## Envelope
26
+
27
+ ```json
28
+ {
29
+ "v": 1,
30
+ "event": "pre_tool_use",
31
+ "agent": "claude",
32
+ "session": "sess_01HQ…",
33
+ "pane": "iterm:tab-3",
34
+ "receivedAt": "2026-10-10T12:00:00.000Z",
35
+ "payload": { "tool": "Bash", "input": { "command": "ls" } }
36
+ }
37
+ ```
38
+
39
+ Caps (Orca's numbers): strings ≤ 8 KB, ≤ 500 nodes per payload; oversized
40
+ fields are truncated at ingest, never dropped.
41
+
42
+ ## Per-harness mapping
43
+
44
+ | Canonical | Claude Code / Codex | Gemini CLI | Cursor | Hermes plugin |
45
+ | --- | --- | --- | --- | --- |
46
+ | `session_start` | `SessionStart` | `SessionStart` | `sessionStart` | `on_session_start` |
47
+ | `session_end` | `SessionEnd` | `SessionEnd` | `sessionEnd` | `on_session_finalize` |
48
+ | `user_prompt_submit` | `UserPromptSubmit` | `BeforeAgent` | `beforeSubmitPrompt` | `pre_llm_call` |
49
+ | `pre_tool_use` | `PreToolUse` | `BeforeTool` | `preToolUse` | `pre_tool_call` |
50
+ | `post_tool_use` | `PostToolUse` | `AfterTool` | `postToolUse` | `post_tool_call` |
51
+ | `post_tool_use_failure` | `PostToolUseFailure` | — | — | — |
52
+ | `permission_request` | `PermissionRequest` | — | — | `pre_approval_request` |
53
+ | `stop` | `Stop` | `AfterAgent` | `stop` | — |
54
+ | `stop_failure` | `StopFailure` | — | — | — |
55
+ | `subagent_start` | `SubagentStart` | — | — | — |
56
+ | `subagent_stop` | `SubagentStop` | — | `subagentStop` | — |
57
+ | `notification` | `Notification` | `Notification` | — | — |
58
+ | `pre_compact` | `PreCompact` | `PreCompress` | — | — |
59
+
60
+ Hermes' `on_session_end` fires once per turn despite its name, so it passes
61
+ through as `unknown_event`, as does `post_approval_response`. Cursor's session
62
+ is its `conversation_id`.
63
+
64
+ Warp emits no native events at all — it has no hooks. Policy for Warp
65
+ arrives via MCP tool calls; nothing in this table applies to it.
66
+
67
+ Payload keys per source keep their harness names inside `payload` (e.g.
68
+ Claude's `tool_input`, Codex's `tool_input`) after normalization of the
69
+ envelope itself; the normalizer never invents fields.