@agimon-ai/doompi-hook 0.0.1-alpha.87 → 0.0.1-alpha.89

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 (39) hide show
  1. package/README.md +112 -54
  2. package/dist/authoring.cjs +10 -0
  3. package/dist/authoring.cjs.map +1 -0
  4. package/dist/authoring.d.mts +7 -0
  5. package/dist/authoring.d.mts.map +1 -0
  6. package/dist/authoring.mjs +9 -0
  7. package/dist/authoring.mjs.map +1 -0
  8. package/dist/extensions/pi.cjs +104 -227
  9. package/dist/extensions/pi.cjs.map +1 -1
  10. package/dist/extensions/pi.d.mts +1 -1
  11. package/dist/extensions/pi.d.mts.map +1 -1
  12. package/dist/extensions/pi.mjs +94 -217
  13. package/dist/extensions/pi.mjs.map +1 -1
  14. package/dist/extensions/server.cjs +157 -38
  15. package/dist/extensions/server.cjs.map +1 -1
  16. package/dist/extensions/server.d.mts.map +1 -1
  17. package/dist/extensions/server.mjs +159 -40
  18. package/dist/extensions/server.mjs.map +1 -1
  19. package/dist/hookModule-c2GtLnm1.d.mts +54 -0
  20. package/dist/hookModule-c2GtLnm1.d.mts.map +1 -0
  21. package/dist/{hooks-GyYjEZKZ.mjs → hookModules-DAFDGPUC.mjs} +257 -8
  22. package/dist/hookModules-DAFDGPUC.mjs.map +1 -0
  23. package/dist/{hooks-BeDKiWFr.cjs → hookModules-hfDQUKiA.cjs} +264 -7
  24. package/dist/hookModules-hfDQUKiA.cjs.map +1 -0
  25. package/dist/hookRuntime-BLFf_6_E.cjs +435 -0
  26. package/dist/hookRuntime-BLFf_6_E.cjs.map +1 -0
  27. package/dist/hookRuntime-D27m9ndl.mjs +346 -0
  28. package/dist/hookRuntime-D27m9ndl.mjs.map +1 -0
  29. package/dist/{index-CcfteHKn.d.mts → index-Dhd9UC9g.d.mts} +79 -11
  30. package/dist/index-Dhd9UC9g.d.mts.map +1 -0
  31. package/dist/index.cjs +22 -21
  32. package/dist/index.d.mts +3 -2
  33. package/dist/index.mjs +2 -2
  34. package/llms.txt +2 -2
  35. package/package.json +12 -7
  36. package/src/prompts/doompi-author-hook/SKILL.md +97 -20
  37. package/dist/hooks-BeDKiWFr.cjs.map +0 -1
  38. package/dist/hooks-GyYjEZKZ.mjs.map +0 -1
  39. package/dist/index-CcfteHKn.d.mts.map +0 -1
package/README.md CHANGED
@@ -1,11 +1,7 @@
1
1
  # @agimon-ai/doompi-hook
2
2
 
3
- Claude Code-compatible repository and plugin hooks for
4
- [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) sessions.
5
-
6
- A **hook** is a shell command the repository runs at a point in the session: before a tool call,
7
- after one, at session start, when the agent settles, and at session end. Commands are declared in
8
- `.doom/hooks.yaml`, grouped so a mode can turn a whole family of them on or off.
3
+ Repository, personal, and Claude Code-compatible plugin hooks for
4
+ [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) sessions. Registry rows can run shell commands or compiled TypeScript/JavaScript modules.
9
5
 
10
6
  > **Alpha:** hook configuration and runtime contracts may change between releases.
11
7
 
@@ -13,9 +9,9 @@ after one, at session start, when the agent settles, and at session end. Command
13
9
 
14
10
  - Node.js 22.19.0 or newer
15
11
  - Pi 1.0.0
16
- - `/bin/bash`
12
+ - `/bin/bash` for command hooks
17
13
 
18
- Configure hooks in `.doom/hooks.yaml`:
14
+ ## Registry and activation
19
15
 
20
16
  ```yaml
21
17
  # .doom/hooks.yaml
@@ -26,7 +22,9 @@ groups:
26
22
  - event: PreToolUse
27
23
  pi:
28
24
  matcher: Bash
29
- command: .doom/hooks/guard-destructive-commands.sh
25
+ module: .doom/hooks/guard.ts
26
+ timeout: 10
27
+ order: 0
30
28
  workflow:
31
29
  hooks:
32
30
  - event: Stop
@@ -35,15 +33,9 @@ groups:
35
33
  skipInSubagent: true
36
34
  ```
37
35
 
38
- A group marked `core: true` always loads. Every other group loads only when the active mode selects
39
- it. The same document is read from the global `.doom` directory and from the repository. A
40
- repository group replaces the global group of the same ID outright.
41
-
42
- ## Activation
36
+ Each `pi` binding declares exactly one of `command` or `module`. Module paths resolve relative to the declaring repository root or personal `~/.pi/.doom` root, not the directory containing `hooks.yaml`. Personal configuration is `~/.pi/.doom/hooks.yaml`; repository configuration is `<repo>/.doom/hooks.yaml`. A repository group replaces the personal group of the same ID completely.
43
37
 
44
- The distribution activates this package by default through the `default.packages` list written by
45
- `doompi init` and `dpi init` in `.doom/modes.yaml`. Keep it there, or declare it in a selected layer,
46
- for hooks to run. Layers only declare the `hookGroups` to select; they do not define the hook commands:
38
+ The distribution activates this package by default through the `default.packages` list written by `doompi init` or `dpi init`. Keep it there, or declare it in a selected layer. Layers only declare the `hookGroups` to select; they do not define hook implementations:
47
39
 
48
40
  ```yaml
49
41
  # .doom/modes.yaml
@@ -52,53 +44,123 @@ layers:
52
44
  hookGroups: [safety, workflow]
53
45
  ```
54
46
 
55
- A session with no selected `hookGroups` still runs groups marked `core: true`; other groups remain
56
- inactive.
47
+ `core: true` always loads. Unset `hookGroups` means all groups; explicit `hookGroups: []` means core groups only. An explicit nonempty list selects those groups plus core. Lower `order` values run first, with declaration order breaking ties. Registry commands and modules interleave in that order, followed by plugin commands. An explicit denial ends the chain.
48
+
49
+ `matcher` is a regular expression over Claude tool names such as `Bash` and `Write`. Modules receive native Pi events and tool names such as `bash`, not translated command payloads. `skipInSubagent: true` excludes a registry row from child tool dispatch. Use it for parent-owned work; it does not enable child lifecycle dispatch.
50
+
51
+ ## Compiled module authoring
52
+
53
+ Use the dependency-free public authoring subpath, not the extension/runtime entry:
54
+
55
+ ```ts
56
+ import { defineDoomHook } from '@agimon-ai/doompi-hook/authoring';
57
+
58
+ export default defineDoomHook({
59
+ setup(ctx) {
60
+ let calls = 0;
61
+ return {
62
+ tool_call(event, ctx) {
63
+ calls += 1;
64
+ if (event.toolName === 'bash' && String(event.input.command).includes('rm -rf')) {
65
+ return { block: true, reason: 'Use a non-destructive command.' };
66
+ }
67
+ },
68
+ async dispose() {
69
+ // Release resources owned by this session's setup.
70
+ },
71
+ };
72
+ },
73
+ });
74
+ ```
75
+
76
+ `defineDoomHook` checks the contract while preserving inference. The default export must have `setup(ctx)`, returning handlers synchronously or asynchronously. Only handlers selected by registry rows run:
77
+
78
+ | Registry event | Native module handler | Result |
79
+ | -------------- | --------------------------- | ------------------------------------------------ |
80
+ | `SessionStart` | `session_start(event, ctx)` | `void` |
81
+ | `PreToolUse` | `tool_call(event, ctx)` | `void` or `{ block?: boolean, reason?: string }` |
82
+ | `PostToolUse` | `tool_result(event, ctx)` | `void` or `{ content?, details?, isError? }` |
83
+ | `Stop` | `agent_settled(event, ctx)` | `void`, cannot send messages |
84
+
85
+ Optional `dispose()` returns void or a promise. There is no module `SessionEnd` handler. Tool-call handlers may mutate `event.input`, which must remain a JSON object. Accepted mutations flow to later rows and the tool. Tool-result patches accept text/image content, JSON-compatible details, and a boolean error flag. Failed, invalid, or late invocations cannot publish input mutations or result patches. Lifecycle handlers cannot return decisions.
86
+
87
+ ### Full HookContext contract
88
+
89
+ The authoring subpath exports `HookContext`, `HookHandlers`, `HookModule`, `HookModuleDescriptor`, `AgentSettledEvent`, `Awaitable`, and `JsonValue` types.
90
+
91
+ ```ts
92
+ interface HookContext {
93
+ readonly sessionId: string;
94
+ readonly parentSessionId?: string;
95
+ readonly agent?: string;
96
+ readonly isSubagent: boolean;
97
+ readonly cwd: string;
98
+ readonly repoRoot: string;
99
+ readonly model?: { readonly provider: string; readonly id: string };
100
+ readonly signal: AbortSignal;
101
+ sendMessage(text: string, delivery: 'steer' | 'followUp'): Promise<void>;
102
+ appendCustomEntry(type: string, data: JsonValue): Promise<void>;
103
+ }
104
+ ```
105
+
106
+ `cwd` is the session working directory; `repoRoot` is its resolved repository root. Optional identity/model fields may be absent, including model in native children. Await context effects. Message delivery uses host prompt admission; admission failures propagate, they are not converted to advisory hook failures. `appendCustomEntry` records JSON-compatible data in session history.
57
107
 
58
- ## What a hook sees and can say
108
+ Setup and row calls for the same module and session are serialized. A module referenced by multiple rows shares one setup instance in that session. Keep mutable state inside `setup`, not at module scope: imported modules may be shared across sessions, while setup closures are session-local. In-memory state is not durable across reloads. Explicitly record history entries when durability is needed.
59
109
 
60
- Each command is run through `/bin/bash -c` in the repository root, with the Claude Code payload for
61
- the event on stdin. The environment includes `CLAUDE_PROJECT_DIR`, `CODEX_REPO_ROOT`, and
62
- `ORIGINAL_REPO_PATH`. Every resolved hook also receives `CLAUDE_PLUGIN_ROOT`, set to the root that
63
- declared its repository, personal, or plugin configuration. Tool names in the payload and in
64
- `matcher` are Claude names (`Bash`, `Write`), not Pi's.
110
+ Setup receives a lifetime context; handlers receive an invocation context whose signal also reflects cancellation. Invocation effects are revoked after completion. Setup failure, setup/handler timeout, or cancellation quarantines the instance and revokes its context effects. Cooperate with `signal`; revocation cannot stop arbitrary JavaScript or undo already completed external effects.
65
111
 
66
- The last stdout line that starts with `{` is read as the hook's decision:
112
+ ## Command payloads and decisions
67
113
 
68
- | Event mapping | A decision can |
69
- | ------------------------------- | ------------------------------------------------------------------ |
70
- | `SessionStart` → session start | add `additionalContext` to the conversation |
71
- | `PreToolUse` → tool call | block the call with a reason, or steer it with `additionalContext` |
72
- | `PostToolUse` → tool result | append text to the result and mark it an error |
73
- | `Stop` → agent settled | nothing; the hook runs for its side effects |
74
- | `SessionEnd` → session shutdown | nothing; plugin hooks only |
114
+ Commands run through `/bin/bash -c` in the repository root with a Claude-compatible JSON payload on stdin. The environment includes `CLAUDE_PROJECT_DIR`, `CODEX_REPO_ROOT`, `ORIGINAL_REPO_PATH`, and `CLAUDE_PLUGIN_ROOT`. The last field names the root declaring the repository, personal, or plugin configuration. Native child tool payloads use the child's own `session_id` and include `parent_session_id` and `agent_type`.
75
115
 
76
- A hook that exits non-zero, times out, cannot be spawned, or prints unparseable JSON does not fail
77
- the turn. It is reported to the agent instead, because a guardrail that never ran is otherwise
78
- indistinguishable from one that passed.
116
+ The last stdout line beginning with `{` is parsed as the decision. Denial is `decision: "block"` or `hookSpecificOutput.permissionDecision: "deny"`; the reason is top-level `reason` or `hookSpecificOutput.reason`. Context is `hookSpecificOutput.additionalContext`.
79
117
 
80
- ## Timeouts
118
+ | Event | Command decision behavior |
119
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
120
+ | `SessionStart` | Adds additional context to the conversation |
121
+ | `PreToolUse` | Denial blocks the call. Additional context also blocks the current call with that text as its reason, allowing the agent to reconsider; it does not silently steer an executing tool |
122
+ | `PostToolUse` | Appends additional context or reason to the result; denial marks it as an error |
123
+ | `Stop` | Denial with a reason requests a follow-up turn, capped at five consecutive refusals |
124
+ | `SessionEnd` | Plugin commands only, run for side effects |
81
125
 
82
- `timeout` is in seconds and defaults to 10. A hook is spawned in its own process group and, when it
83
- expires, is sent `SIGTERM` and then `SIGKILL` two seconds later, so a stalled hook does not leave
84
- the processes it started behind.
126
+ Stop payloads include `stop_hook_active` when a prior refusal is active. After five consecutive refusals, a further refusal is reported without another follow-up turn. A stop without a refusal reason resets the count. This continuation contract belongs to commands, not module `agent_settled` handlers.
85
127
 
86
- ## Help guidance
128
+ ## Event ownership and children
87
129
 
88
- While the Help minor mode is active, this package contributes `doompi-author-hook`. The prompt
89
- covers `.doom/hooks.yaml`, group activation, commands, payloads, decisions, and verification, and
90
- is withdrawn when the package or Help provider unloads.
130
+ Terminal parents use Pi tool and lifecycle handlers. In headless parents, the Pi bridge owns lifecycle rows: `SessionStart`, `Stop`, and plugin-only `SessionEnd`. Server facets alone own headless tool dispatch. The bridge and server tool path share session-local module setup, so lifecycle and tool rows do not create separate instances. Session-start readiness precedes dependent turns/tools. Session-end runs once during shutdown before service disposal.
91
131
 
92
- ## Install
132
+ Native children, through either the headless or terminal adapter, run only `PreToolUse` and `PostToolUse`. Registry and plugin tool hooks run, excluding registry rows marked `skipInSubagent`. Children do not run `SessionStart`, `Stop`, or `SessionEnd`. Each child has its own module setup/state and pins its launch selection and module generation, even after the parent reloads or disposes.
93
133
 
94
- For standalone Pi installation:
134
+ ## Sync, reload, and failure handling
135
+
136
+ After changing a module or its imports, registry, or mode selection:
137
+
138
+ 1. Run `doompi sync` (or `dpi sync`). Sync compiles core modules plus the union of mode-selected groups; if any mode leaves selection unset, it compiles all groups.
139
+ 2. Run `doompi sync --check` to check drift.
140
+ 3. Use `/reload` or restart the terminal session to adopt the published generation. Reopen headless sessions, then exercise the intended events.
141
+
142
+ Module imports use compiled artifacts from a synchronized descriptor, never a live source fallback. The runtime pins the descriptor at binding creation, including cold lazy imports after configuration replacement. A compilation failure preserves the previous published generation. Sync alone does not replace modules in a running session.
143
+
144
+ `timeout` is a positive finite number of seconds, defaulting to 10. For modules, the invocation budget includes first import/setup and the handler. Module execution/setup errors, invalid results, and timeouts are advisory, reported as missed checks rather than automatically denying tools. Host readiness, cancellation, and context effect/admission failures are not advisory execution failures. A quarantined module does not resume checking later rows in that session.
145
+
146
+ Command nonzero exits, spawn failures, invalid JSON, and timeouts are likewise advisory. Timed-out command process groups receive `SIGTERM`, then `SIGKILL` two seconds later.
147
+
148
+ ## Trust and isolation limits
149
+
150
+ Hooks are executable trusted code. Compilation and generation pinning provide artifact consistency, not a sandbox or authorization boundary. Modules execute in the host Node.js process with its filesystem, network, and process privileges; commands inherit host privileges too. A synchronous infinite loop, `process.exit`, or out-of-memory failure cannot be contained in-process. Prefer `command:` when process isolation and hard termination matter. Session-local setup and serialized calls do not isolate module globals or external side effects. Context revocation only gates the exposed context effects. Do not load untrusted hooks or treat advisory checks as guaranteed enforcement.
151
+
152
+ ## Verification scope
153
+
154
+ Functional proof covers real Pi bridge exactly-once dispatch, shared lifecycle/tool setup, prompt-admission marker propagation, in-flight cancellation, native command rows, cold descriptor pinning, and both native child adapters retaining modules after parent disposal. Direct/MCP command-row proof uses explicitly mocked tool bodies. This does not establish full intercom plugin integration.
155
+
156
+ ## Help, install, and public API
157
+
158
+ Help minor mode contributes `doompi-author-hook` while this package and its Help provider are active.
95
159
 
96
160
  ```bash
97
161
  pi install npm:@agimon-ai/doompi-hook
98
162
  ```
99
163
 
100
- ## Public API
101
-
102
164
  ```ts
103
165
  import { createBashHookRunner, hookExtension } from '@agimon-ai/doompi-hook';
104
166
  import type { HookDecision, HookEventName } from '@agimon-ai/doompi-hook';
@@ -113,14 +175,10 @@ pnpm test
113
175
  pnpm lint
114
176
  ```
115
177
 
178
+ Routed roots and named handlers live under `src/extensions/workspaces/sessions/(backend)/`. `services/hookRuntime/` owns readiness and session lifetimes. The package build generates Pi/server entries; `src/exports/` publishes reusable capabilities.
179
+
116
180
  Maintained by [Agimon](https://agimon.ai/about).
117
181
 
118
182
  ## License
119
183
 
120
184
  MIT
121
-
122
- ## Extension lifecycle and source layout
123
-
124
- `extensions/pi.ts` declares Config-dependent service bindings, typed controller events, and Help resources. The readiness gate in `services/hookRuntime/` keeps session-start hooks ahead of dependent events and rejects stale generations. Session-end hooks finish during `session_shutdown` before the shared helper disposes the service binding.
125
-
126
- `extensions/server.ts` declares the server hooks from `controllers/serverHooks.ts`. Controllers dispatch through `services/<name>/`; constants and shared types have their own roots. Flat `exports/` publishes reusable capabilities, and extension entry points are built directly.
@@ -0,0 +1,10 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/services/hookAuthoring/index.ts
3
+ /** Preserve the author's inferred module type while checking the hook contract. */
4
+ function defineDoomHook(module) {
5
+ return module;
6
+ }
7
+ //#endregion
8
+ exports.defineDoomHook = defineDoomHook;
9
+
10
+ //# sourceMappingURL=authoring.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authoring.cjs","names":[],"sources":["../src/services/hookAuthoring/index.ts"],"sourcesContent":["import type { HookModule } from '../../types/hookModule';\n\n/** Preserve the author's inferred module type while checking the hook contract. */\nexport function defineDoomHook<T extends HookModule>(module: T): T {\n return module;\n}\n"],"mappings":";;;AAGA,SAAgB,eAAqC,QAAc;CACjE,OAAO;AACT"}
@@ -0,0 +1,7 @@
1
+ import { a as HookModule, i as HookHandlers, l as JsonValue, n as Awaitable, o as HookModuleDescriptor, r as HookContext, t as AgentSettledEvent } from "./hookModule-c2GtLnm1.mjs";
2
+ //#region src/services/hookAuthoring/index.d.ts
3
+ /** Preserve the author's inferred module type while checking the hook contract. */
4
+ export declare function defineDoomHook<T extends HookModule>(module: T): T;
5
+ //#endregion
6
+ export type { AgentSettledEvent, Awaitable, HookContext, HookHandlers, HookModule, HookModuleDescriptor, JsonValue };
7
+ //# sourceMappingURL=authoring.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authoring.d.mts","names":[],"sources":["../src/services/hookAuthoring/index.ts"],"mappings":";;;wBAGgB,eAAe,UAAU,YAAY,QAAQ,IAAI"}
@@ -0,0 +1,9 @@
1
+ //#region src/services/hookAuthoring/index.ts
2
+ /** Preserve the author's inferred module type while checking the hook contract. */
3
+ function defineDoomHook(module) {
4
+ return module;
5
+ }
6
+ //#endregion
7
+ export { defineDoomHook };
8
+
9
+ //# sourceMappingURL=authoring.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authoring.mjs","names":[],"sources":["../src/services/hookAuthoring/index.ts"],"sourcesContent":["import type { HookModule } from '../../types/hookModule';\n\n/** Preserve the author's inferred module type while checking the hook contract. */\nexport function defineDoomHook<T extends HookModule>(module: T): T {\n return module;\n}\n"],"mappings":";;AAGA,SAAgB,eAAqC,QAAc;CACjE,OAAO;AACT"}