@agimon-ai/doompi-hook 0.0.1-alpha.2 → 0.0.1-alpha.20

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.
package/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # @agimon-ai/doompi-hook
2
2
 
3
- Claude-Code-compatible repository and plugin hooks for
3
+ Claude Code-compatible repository and plugin hooks for
4
4
  [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) sessions.
5
5
 
6
6
  A **hook** is a shell command the repository runs at a point in the session: before a tool call,
7
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:
8
+ `.doom/hooks.yaml`, grouped so a mode can turn a whole family of them on or off.
9
+
10
+ > **Alpha:** hook configuration and runtime contracts may change between releases.
11
+
12
+ ## Requirements
13
+
14
+ - Node.js 22.19.0 or newer
15
+ - Pi 0.84.4
16
+ - `/bin/bash`
17
+
18
+ Configure hooks in `.doom/hooks.yaml`:
9
19
 
10
20
  ```yaml
11
21
  # .doom/hooks.yaml
@@ -25,9 +35,9 @@ groups:
25
35
  skipInSubagent: true
26
36
  ```
27
37
 
28
- A group marked `core: true` always loads. Every other group loads only when the active mode selected
29
- it. The same document is read from the global `.doom` directory and from the repository, and a
30
- repository group replaces the global group of the same id outright.
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.
31
41
 
32
42
  ## Activation
33
43
 
@@ -41,14 +51,16 @@ layers:
41
51
  hookGroups: [safety, workflow]
42
52
  ```
43
53
 
44
- A session with no selected `hookGroups` still runs groups marked `core: true`; other groups remain inactive.
54
+ A session with no selected `hookGroups` still runs groups marked `core: true`; other groups remain
55
+ inactive.
45
56
 
46
57
  ## What a hook sees and can say
47
58
 
48
59
  Each command is run through `/bin/bash -c` in the repository root, with the Claude Code payload for
49
- the event on stdin and `CLAUDE_PROJECT_DIR`, `CODEX_REPO_ROOT`, `ORIGINAL_REPO_PATH` and — for a
50
- hook declared by a plugin or a global config — `CLAUDE_PLUGIN_ROOT` in the environment. Tool names
51
- in the payload and in `matcher` are Claude names (`Bash`, `Write`), not Pi's.
60
+ the event on stdin. The environment includes `CLAUDE_PROJECT_DIR`, `CODEX_REPO_ROOT`, and
61
+ `ORIGINAL_REPO_PATH`. Every resolved hook also receives `CLAUDE_PLUGIN_ROOT`, set to the root that
62
+ declared its repository, personal, or plugin configuration. Tool names in the payload and in
63
+ `matcher` are Claude names (`Bash`, `Write`), not Pi's.
52
64
 
53
65
  The last stdout line that starts with `{` is read as the hook's decision:
54
66
 
@@ -70,7 +82,13 @@ indistinguishable from one that passed.
70
82
  expires, is sent `SIGTERM` and then `SIGKILL` two seconds later, so a stalled hook does not leave
71
83
  the processes it started behind.
72
84
 
73
- ## Installation
85
+ ## Help guidance
86
+
87
+ While the Help minor mode is active, this package contributes `doompi-author-hook`. The prompt
88
+ covers `.doom/hooks.yaml`, group activation, commands, payloads, decisions, and verification, and
89
+ is withdrawn when the package or Help provider unloads.
90
+
91
+ ## Install
74
92
 
75
93
  DoomPi already includes this package. For standalone Pi installation:
76
94
 
@@ -78,6 +96,24 @@ DoomPi already includes this package. For standalone Pi installation:
78
96
  pi install npm:@agimon-ai/doompi-hook
79
97
  ```
80
98
 
99
+ ## Public API
100
+
101
+ ```ts
102
+ import { createBashHookRunner, hookExtension } from '@agimon-ai/doompi-hook';
103
+ import type { HookDecision, HookEventName } from '@agimon-ai/doompi-hook';
104
+ ```
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ pnpm build
110
+ pnpm typecheck
111
+ pnpm test
112
+ pnpm lint
113
+ ```
114
+
115
+ Maintained by [Agimon](https://agimon.ai/about).
116
+
81
117
  ## License
82
118
 
83
119
  MIT
@@ -1,2 +1,2 @@
1
- const e=require("../hookDocuments.cjs"),t=require("../hookRunner.cjs"),n=require("../telemetry/logSinkTelemetry.cjs"),r=require("./hookHandlers.cjs");let i=require("@deepseek-ai/cordis");function a(r){let i=r.telemetry,a=()=>i??=n.createHookTelemetry();return{runner:r.runner??t.createBashHookRunner({telemetry:a()}),documents:r.documents??e.createHookDocumentReader({telemetry:a()})}}async function o(e,t={}){let n=a(t),o=new i.Context,s,c=()=>(s??=o.fiber.dispose(),s);try{o.effect(function*(){for(let t of r.registerHookHandlers(e,n))yield t},`@agimon-ai/doompi-hook`)}catch(e){throw await c(),e}e.on(`session_shutdown`,async(e,t)=>{if(s)return s;try{await r.runSessionEndHooks(n,t)}finally{await c()}})}exports.default=o,exports.hookExtension=o;
1
+ const e=require("../hookDocuments.cjs"),t=require("../hookRunner.cjs"),n=require("../telemetry/logSinkTelemetry.cjs"),r=require("./hookHandlers.cjs");let i=require("@agimon-ai/doompi-extension-contracts/readiness"),a=require("@agimon-ai/doompi-extension-contracts/cordis-host"),o=require("@agimon-ai/doompi-extension-contracts/config"),s=require("@agimon-ai/doompi-extension-contracts/help"),c=require("@agimon-ai/doompi-config/piContext");const l=`@agimon-ai/doompi-hook`;function u(r,i){let a=i.telemetry,o=()=>a??=n.createHookTelemetry();return{config:()=>(0,c.requireDoomConfigContext)(r),runner:i.runner??t.createBashHookRunner({telemetry:o()}),documents:i.documents??e.createHookDocumentReader({telemetry:o()})}}function d(e,t){let n=u(e,t),r=!0,a=0,o;return{session:n,readiness:{start(t,n){let s=++a,c=()=>r&&s===a,u=(0,i.readDoomReadinessCoordinator)(e);if(!u)return n(new AbortController().signal,c);let d=o,f=(async()=>{d?.coordinator===u&&await d.operation.catch(()=>void 0),c()&&await u.start(l,`${t.sessionManager.getSessionId()}:${s}`,async e=>(await n(e,c),{value:void 0})).wait()})();f.catch(()=>void 0),o={sessionManager:t.sessionManager,coordinator:u,operation:f}},async wait(e){let t=o;if(t){if(t.sessionManager!==e.sessionManager)throw Error(`Hook readiness belongs to a stale Pi session.`);if(await t.operation,!r||t!==o)throw Error(`Hook readiness belongs to a stale extension generation.`)}}},isCurrent:()=>r,dispose(){r=!1,a+=1,o=void 0}}}function f(e,{pi:t,options:n}){e.inject([s.DOOM_HELP_SERVICE],e=>{let t=(0,s.requireDoomHelpService)(e).register({source:l,moduleUrl:require("url").pathToFileURL(__filename).href,skills:[{name:`doompi-author-hook`,description:`Author DoomPi repository or plugin hooks. Use when creating or changing .doom/hooks.yaml, selecting hook groups from modes.yaml, writing hook commands, or adapting Claude Code hook payloads and decisions to DoomPi.`}]});return()=>t.dispose()});let i;e.inject([o.DOOM_CONFIG_SERVICE],e=>{let t=d(e,n);i=t,e.effect(()=>()=>{t.dispose(),i===t&&(i=void 0)},`${l}/config-runtime`)}),e.effect(function*(){for(let e of r.registerHookHandlers(t,()=>i))yield e;yield()=>{i=void 0}},l);let a;t.on(`session_shutdown`,(e,t)=>{let n=i;if(n?.isCurrent())return a??=r.runSessionEndHooks(n.session,t),a})}async function p(e,t={}){let n=await(0,a.connectDoomCordisHost)(e,l),r=n.root.plugin(f,{pi:e,options:t});try{await r}catch(e){try{await r.dispose()}finally{await n.dispose()}throw e}let i;e.on(`session_shutdown`,()=>i??=(async()=>{try{await r.dispose()}finally{await n.dispose()}})())}exports.default=p,exports.hookExtension=p;
2
2
  //# sourceMappingURL=extension.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.cjs","names":["createHookTelemetry","createBashHookRunner","createHookDocumentReader","Context","registerHookHandlers","runSessionEndHooks"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { HookDocumentReader, HookRunner } from '../../types/hooks.ts';\nimport type { HookTelemetry } from '../../types/telemetry.ts';\nimport { createHookDocumentReader } from '../hookDocuments.ts';\nimport { createBashHookRunner } from '../hookRunner.ts';\nimport { createHookTelemetry } from '../telemetry/logSinkTelemetry.ts';\nimport { type HookSession, registerHookHandlers, runSessionEndHooks } from './hookHandlers.ts';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-hook';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\n\nexport interface HookExtensionOptions {\n telemetry?: HookTelemetry;\n runner?: HookRunner;\n documents?: HookDocumentReader;\n}\n\nfunction hookSession(options: HookExtensionOptions): HookSession {\n // The log sink is only built if something actually reports to it, so a fully\n // stubbed session never touches the telemetry backend.\n let telemetry = options.telemetry;\n const requireTelemetry = (): HookTelemetry => (telemetry ??= createHookTelemetry());\n return {\n runner: options.runner ?? createBashHookRunner({ telemetry: requireTelemetry() }),\n documents: options.documents ?? createHookDocumentReader({ telemetry: requireTelemetry() }),\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse. Shutdown\n * runs the plugin SessionEnd hooks first: they need the session that dispose\n * ends.\n */\nexport async function hookExtension(pi: ExtensionAPI, options: HookExtensionOptions = {}): Promise<void> {\n const session = hookSession(options);\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n for (const disposeRegistration of registerHookHandlers(pi, session)) yield disposeRegistration;\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, async (_event, ctx) => {\n // Pi can fire session_shutdown more than once across a reload, and the\n // SessionEnd hooks are side effects that must not run twice.\n if (disposal) return disposal;\n try {\n await runSessionEndHooks(session, ctx);\n } finally {\n await dispose();\n }\n });\n}\n\nexport default hookExtension;\n"],"mappings":"2LAkBA,SAAS,EAAY,EAA4C,CAG/D,IAAI,EAAY,EAAQ,UAClB,MAAyC,IAAcA,EAAAA,oBAAoB,EACjF,MAAO,CACL,OAAQ,EAAQ,QAAUC,EAAAA,qBAAqB,CAAE,UAAW,EAAiB,CAAE,CAAC,EAChF,UAAW,EAAQ,WAAaC,EAAAA,yBAAyB,CAAE,UAAW,EAAiB,CAAE,CAAC,CAC5F,CACF,CAUA,eAAsB,EAAc,EAAkB,EAAgC,CAAC,EAAkB,CACvG,IAAM,EAAU,EAAY,CAAO,EAC7B,EAAS,IAAIC,EAAAA,QACf,EACE,OACJ,IAAa,EAAO,MAAM,QAAQ,EAC3B,GAGT,GAAI,CACF,EAAO,OAAO,WAAa,CACzB,IAAK,IAAM,KAAuBC,EAAAA,qBAAqB,EAAI,CAAO,EAAG,MAAM,CAC7E,EAAG,wBAAc,CACnB,OAAS,EAAO,CAEd,MADA,MAAM,EAAQ,EACR,CACR,CAEA,EAAG,GAAG,mBAAwB,MAAO,EAAQ,IAAQ,CAGnD,GAAI,EAAU,OAAO,EACrB,GAAI,CACF,MAAMC,EAAAA,mBAAmB,EAAS,CAAG,CACvC,QAAU,CACR,MAAM,EAAQ,CAChB,CACF,CAAC,CACH"}
1
+ {"version":3,"file":"extension.cjs","names":["createHookTelemetry","requireDoomConfigContext","createBashHookRunner","createHookDocumentReader","readDoomReadinessCoordinator","DOOM_HELP_SERVICE","requireDoomHelpService","DOOM_CONFIG_SERVICE","registerHookHandlers","runSessionEndHooks","connectDoomCordisHost"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import {\n type DoomReadinessCoordinator,\n readDoomReadinessCoordinator,\n} from '@agimon-ai/doompi-extension-contracts/readiness';\nimport { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport { DOOM_CONFIG_SERVICE } from '@agimon-ai/doompi-extension-contracts/config';\nimport { DOOM_HELP_SERVICE, requireDoomHelpService } from '@agimon-ai/doompi-extension-contracts/help';\nimport { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport type { HookDocumentReader, HookRunner } from '../../types/hooks.ts';\nimport type { HookTelemetry } from '../../types/telemetry.ts';\nimport { createHookDocumentReader } from '../hookDocuments.ts';\nimport { createBashHookRunner } from '../hookRunner.ts';\nimport { createHookTelemetry } from '../telemetry/logSinkTelemetry.ts';\nimport {\n type HookReadinessGate,\n type HookRuntime,\n type HookSession,\n registerHookHandlers,\n runSessionEndHooks,\n} from './hookHandlers.ts';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-hook';\n\nexport interface HookExtensionOptions {\n telemetry?: HookTelemetry;\n runner?: HookRunner;\n documents?: HookDocumentReader;\n}\n\nfunction hookSession(cordis: Context, options: HookExtensionOptions): HookSession {\n // The log sink is only built if something actually reports to it, so a fully\n // stubbed session never touches the telemetry backend.\n let telemetry = options.telemetry;\n const requireTelemetry = (): HookTelemetry => (telemetry ??= createHookTelemetry());\n return {\n config: () => requireDoomConfigContext(cordis),\n runner: options.runner ?? createBashHookRunner({ telemetry: requireTelemetry() }),\n documents: options.documents ?? createHookDocumentReader({ telemetry: requireTelemetry() }),\n };\n}\n\ninterface HookPluginConfig {\n readonly pi: ExtensionAPI;\n readonly options: HookExtensionOptions;\n}\n\ninterface HookRuntimeBinding extends HookRuntime {\n dispose(): void;\n}\n\nfunction createHookRuntime(cordis: Context, options: HookExtensionOptions): HookRuntimeBinding {\n const session = hookSession(cordis, options);\n let active = true;\n let generation = 0;\n let readiness:\n | {\n readonly sessionManager: object;\n readonly coordinator: DoomReadinessCoordinator;\n readonly operation: Promise<void>;\n }\n | undefined;\n const readinessGate: HookReadinessGate = {\n start(context, operation) {\n const ownGeneration = ++generation;\n const isCurrent = (): boolean => active && ownGeneration === generation;\n const coordinator = readDoomReadinessCoordinator(cordis);\n if (!coordinator) return operation(new AbortController().signal, isCurrent);\n\n const previous = readiness;\n const readinessOperation = (async (): Promise<void> => {\n if (previous?.coordinator === coordinator) await previous.operation.catch(() => undefined);\n if (!isCurrent()) return;\n const handle = coordinator.start(\n PACKAGE_SOURCE,\n `${context.sessionManager.getSessionId()}:${ownGeneration}`,\n async (signal) => {\n await operation(signal, isCurrent);\n return { value: undefined };\n },\n );\n await handle.wait();\n })();\n // Config's coordinator owns the single user-facing failure notification.\n void readinessOperation.catch(() => undefined);\n readiness = {\n sessionManager: context.sessionManager,\n coordinator,\n operation: readinessOperation,\n };\n return undefined;\n },\n async wait(context: ExtensionContext): Promise<void> {\n const current = readiness;\n if (!current) return;\n if (current.sessionManager !== context.sessionManager) {\n throw new Error('Hook readiness belongs to a stale Pi session.');\n }\n await current.operation;\n if (!active || current !== readiness) {\n throw new Error('Hook readiness belongs to a stale extension generation.');\n }\n },\n };\n return {\n session,\n readiness: readinessGate,\n isCurrent: () => active,\n dispose() {\n active = false;\n generation += 1;\n readiness = undefined;\n },\n };\n}\n\nfunction hookPlugin(cordis: Context, { pi, options }: HookPluginConfig): void {\n cordis.inject([DOOM_HELP_SERVICE], (helpContext) => {\n const contribution = requireDoomHelpService(helpContext).register({\n source: PACKAGE_SOURCE,\n moduleUrl: import.meta.url,\n skills: [\n {\n name: 'doompi-author-hook',\n description:\n 'Author DoomPi repository or plugin hooks. Use when creating or changing .doom/hooks.yaml, selecting hook groups from modes.yaml, writing hook commands, or adapting Claude Code hook payloads and decisions to DoomPi.',\n },\n ],\n });\n return () => contribution.dispose();\n });\n\n let runtime: HookRuntimeBinding | undefined;\n cordis.inject([DOOM_CONFIG_SERVICE], (configContext) => {\n const binding = createHookRuntime(configContext, options);\n runtime = binding;\n configContext.effect(\n () => () => {\n binding.dispose();\n if (runtime === binding) runtime = undefined;\n },\n `${PACKAGE_SOURCE}/config-runtime`,\n );\n });\n\n cordis.effect(function* () {\n for (const disposeRegistration of registerHookHandlers(pi, () => runtime)) yield disposeRegistration;\n yield () => {\n runtime = undefined;\n };\n }, PACKAGE_SOURCE);\n\n let shutdown: Promise<void> | undefined;\n pi.on('session_shutdown', (_event, ctx) => {\n const current = runtime;\n if (!current?.isCurrent()) return undefined;\n shutdown ??= runSessionEndHooks(current.session, ctx);\n return shutdown;\n });\n}\n\n/** The package's single standard Pi factory. */\nexport async function hookExtension(pi: ExtensionAPI, options: HookExtensionOptions = {}): Promise<void> {\n const connection = await connectDoomCordisHost(pi, PACKAGE_SOURCE);\n const fiber = connection.root.plugin(hookPlugin, { pi, options });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\nexport default hookExtension;\n"],"mappings":"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"}
@@ -7,14 +7,7 @@ interface HookExtensionOptions {
7
7
  runner?: HookRunner;
8
8
  documents?: HookDocumentReader;
9
9
  }
10
- /**
11
- * The package's single standard Pi factory, and the owner of the cordis Context.
12
- *
13
- * Pi can reload an extension in-process, so every registration is yielded to the
14
- * fiber and one dispose on session shutdown unwinds them in reverse. Shutdown
15
- * runs the plugin SessionEnd hooks first: they need the session that dispose
16
- * ends.
17
- */
10
+ /** The package's single standard Pi factory. */
18
11
  declare function hookExtension(pi: ExtensionAPI, options?: HookExtensionOptions): Promise<void>;
19
12
  //#endregion
20
13
  export { HookExtensionOptions, hookExtension as default, hookExtension };
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;UAYiB;EACf,YAAY;EACZ,SAAS;EACT,YAAY;;;;;;;;;;iBAsBQ,cAAc,IAAI,cAAc,UAAS,uBAA4B"}
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;UAyBiB;EACf,YAAY;EACZ,SAAS;EACT,YAAY;;;iBAuIQ,cAAc,IAAI,cAAc,UAAS,uBAA4B"}
@@ -7,14 +7,7 @@ interface HookExtensionOptions {
7
7
  runner?: HookRunner;
8
8
  documents?: HookDocumentReader;
9
9
  }
10
- /**
11
- * The package's single standard Pi factory, and the owner of the cordis Context.
12
- *
13
- * Pi can reload an extension in-process, so every registration is yielded to the
14
- * fiber and one dispose on session shutdown unwinds them in reverse. Shutdown
15
- * runs the plugin SessionEnd hooks first: they need the session that dispose
16
- * ends.
17
- */
10
+ /** The package's single standard Pi factory. */
18
11
  declare function hookExtension(pi: ExtensionAPI, options?: HookExtensionOptions): Promise<void>;
19
12
  //#endregion
20
13
  export { HookExtensionOptions, hookExtension as default, hookExtension };
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;UAYiB;EACf,YAAY;EACZ,SAAS;EACT,YAAY;;;;;;;;;;iBAsBQ,cAAc,IAAI,cAAc,UAAS,uBAA4B"}
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;UAyBiB;EACf,YAAY;EACZ,SAAS;EACT,YAAY;;;iBAuIQ,cAAc,IAAI,cAAc,UAAS,uBAA4B"}
@@ -1,2 +1,2 @@
1
- import{createHookDocumentReader as e}from"../hookDocuments.mjs";import{createBashHookRunner as t}from"../hookRunner.mjs";import{createHookTelemetry as n}from"../telemetry/logSinkTelemetry.mjs";import{registerHookHandlers as r,runSessionEndHooks as i}from"./hookHandlers.mjs";import{Context as a}from"@deepseek-ai/cordis";function o(r){let i=r.telemetry,a=()=>i??=n();return{runner:r.runner??t({telemetry:a()}),documents:r.documents??e({telemetry:a()})}}async function s(e,t={}){let n=o(t),s=new a,c,l=()=>(c??=s.fiber.dispose(),c);try{s.effect(function*(){for(let t of r(e,n))yield t},`@agimon-ai/doompi-hook`)}catch(e){throw await l(),e}e.on(`session_shutdown`,async(e,t)=>{if(c)return c;try{await i(n,t)}finally{await l()}})}export{s as default,s as hookExtension};
1
+ import{createHookDocumentReader as e}from"../hookDocuments.mjs";import{createBashHookRunner as t}from"../hookRunner.mjs";import{createHookTelemetry as n}from"../telemetry/logSinkTelemetry.mjs";import{registerHookHandlers as r,runSessionEndHooks as i}from"./hookHandlers.mjs";import{readDoomReadinessCoordinator as a}from"@agimon-ai/doompi-extension-contracts/readiness";import{connectDoomCordisHost as o}from"@agimon-ai/doompi-extension-contracts/cordis-host";import{DOOM_CONFIG_SERVICE as s}from"@agimon-ai/doompi-extension-contracts/config";import{DOOM_HELP_SERVICE as c,requireDoomHelpService as l}from"@agimon-ai/doompi-extension-contracts/help";import{requireDoomConfigContext as u}from"@agimon-ai/doompi-config/piContext";const d=`@agimon-ai/doompi-hook`;function f(r,i){let a=i.telemetry,o=()=>a??=n();return{config:()=>u(r),runner:i.runner??t({telemetry:o()}),documents:i.documents??e({telemetry:o()})}}function p(e,t){let n=f(e,t),r=!0,i=0,o;return{session:n,readiness:{start(t,n){let s=++i,c=()=>r&&s===i,l=a(e);if(!l)return n(new AbortController().signal,c);let u=o,f=(async()=>{u?.coordinator===l&&await u.operation.catch(()=>void 0),c()&&await l.start(d,`${t.sessionManager.getSessionId()}:${s}`,async e=>(await n(e,c),{value:void 0})).wait()})();f.catch(()=>void 0),o={sessionManager:t.sessionManager,coordinator:l,operation:f}},async wait(e){let t=o;if(t){if(t.sessionManager!==e.sessionManager)throw Error(`Hook readiness belongs to a stale Pi session.`);if(await t.operation,!r||t!==o)throw Error(`Hook readiness belongs to a stale extension generation.`)}}},isCurrent:()=>r,dispose(){r=!1,i+=1,o=void 0}}}function m(e,{pi:t,options:n}){e.inject([c],e=>{let t=l(e).register({source:d,moduleUrl:import.meta.url,skills:[{name:`doompi-author-hook`,description:`Author DoomPi repository or plugin hooks. Use when creating or changing .doom/hooks.yaml, selecting hook groups from modes.yaml, writing hook commands, or adapting Claude Code hook payloads and decisions to DoomPi.`}]});return()=>t.dispose()});let a;e.inject([s],e=>{let t=p(e,n);a=t,e.effect(()=>()=>{t.dispose(),a===t&&(a=void 0)},`${d}/config-runtime`)}),e.effect(function*(){for(let e of r(t,()=>a))yield e;yield()=>{a=void 0}},d);let o;t.on(`session_shutdown`,(e,t)=>{let n=a;if(n?.isCurrent())return o??=i(n.session,t),o})}async function h(e,t={}){let n=await o(e,d),r=n.root.plugin(m,{pi:e,options:t});try{await r}catch(e){try{await r.dispose()}finally{await n.dispose()}throw e}let i;e.on(`session_shutdown`,()=>i??=(async()=>{try{await r.dispose()}finally{await n.dispose()}})())}export{h as default,h as hookExtension};
2
2
  //# sourceMappingURL=extension.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { HookDocumentReader, HookRunner } from '../../types/hooks.ts';\nimport type { HookTelemetry } from '../../types/telemetry.ts';\nimport { createHookDocumentReader } from '../hookDocuments.ts';\nimport { createBashHookRunner } from '../hookRunner.ts';\nimport { createHookTelemetry } from '../telemetry/logSinkTelemetry.ts';\nimport { type HookSession, registerHookHandlers, runSessionEndHooks } from './hookHandlers.ts';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-hook';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\n\nexport interface HookExtensionOptions {\n telemetry?: HookTelemetry;\n runner?: HookRunner;\n documents?: HookDocumentReader;\n}\n\nfunction hookSession(options: HookExtensionOptions): HookSession {\n // The log sink is only built if something actually reports to it, so a fully\n // stubbed session never touches the telemetry backend.\n let telemetry = options.telemetry;\n const requireTelemetry = (): HookTelemetry => (telemetry ??= createHookTelemetry());\n return {\n runner: options.runner ?? createBashHookRunner({ telemetry: requireTelemetry() }),\n documents: options.documents ?? createHookDocumentReader({ telemetry: requireTelemetry() }),\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse. Shutdown\n * runs the plugin SessionEnd hooks first: they need the session that dispose\n * ends.\n */\nexport async function hookExtension(pi: ExtensionAPI, options: HookExtensionOptions = {}): Promise<void> {\n const session = hookSession(options);\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n for (const disposeRegistration of registerHookHandlers(pi, session)) yield disposeRegistration;\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, async (_event, ctx) => {\n // Pi can fire session_shutdown more than once across a reload, and the\n // SessionEnd hooks are side effects that must not run twice.\n if (disposal) return disposal;\n try {\n await runSessionEndHooks(session, ctx);\n } finally {\n await dispose();\n }\n });\n}\n\nexport default hookExtension;\n"],"mappings":"iUAkBA,SAAS,EAAY,EAA4C,CAG/D,IAAI,EAAY,EAAQ,UAClB,MAAyC,IAAc,EAAoB,EACjF,MAAO,CACL,OAAQ,EAAQ,QAAU,EAAqB,CAAE,UAAW,EAAiB,CAAE,CAAC,EAChF,UAAW,EAAQ,WAAa,EAAyB,CAAE,UAAW,EAAiB,CAAE,CAAC,CAC5F,CACF,CAUA,eAAsB,EAAc,EAAkB,EAAgC,CAAC,EAAkB,CACvG,IAAM,EAAU,EAAY,CAAO,EAC7B,EAAS,IAAI,EACf,EACE,OACJ,IAAa,EAAO,MAAM,QAAQ,EAC3B,GAGT,GAAI,CACF,EAAO,OAAO,WAAa,CACzB,IAAK,IAAM,KAAuB,EAAqB,EAAI,CAAO,EAAG,MAAM,CAC7E,EAAG,wBAAc,CACnB,OAAS,EAAO,CAEd,MADA,MAAM,EAAQ,EACR,CACR,CAEA,EAAG,GAAG,mBAAwB,MAAO,EAAQ,IAAQ,CAGnD,GAAI,EAAU,OAAO,EACrB,GAAI,CACF,MAAM,EAAmB,EAAS,CAAG,CACvC,QAAU,CACR,MAAM,EAAQ,CAChB,CACF,CAAC,CACH"}
1
+ {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import {\n type DoomReadinessCoordinator,\n readDoomReadinessCoordinator,\n} from '@agimon-ai/doompi-extension-contracts/readiness';\nimport { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport { DOOM_CONFIG_SERVICE } from '@agimon-ai/doompi-extension-contracts/config';\nimport { DOOM_HELP_SERVICE, requireDoomHelpService } from '@agimon-ai/doompi-extension-contracts/help';\nimport { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport type { HookDocumentReader, HookRunner } from '../../types/hooks.ts';\nimport type { HookTelemetry } from '../../types/telemetry.ts';\nimport { createHookDocumentReader } from '../hookDocuments.ts';\nimport { createBashHookRunner } from '../hookRunner.ts';\nimport { createHookTelemetry } from '../telemetry/logSinkTelemetry.ts';\nimport {\n type HookReadinessGate,\n type HookRuntime,\n type HookSession,\n registerHookHandlers,\n runSessionEndHooks,\n} from './hookHandlers.ts';\n\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-hook';\n\nexport interface HookExtensionOptions {\n telemetry?: HookTelemetry;\n runner?: HookRunner;\n documents?: HookDocumentReader;\n}\n\nfunction hookSession(cordis: Context, options: HookExtensionOptions): HookSession {\n // The log sink is only built if something actually reports to it, so a fully\n // stubbed session never touches the telemetry backend.\n let telemetry = options.telemetry;\n const requireTelemetry = (): HookTelemetry => (telemetry ??= createHookTelemetry());\n return {\n config: () => requireDoomConfigContext(cordis),\n runner: options.runner ?? createBashHookRunner({ telemetry: requireTelemetry() }),\n documents: options.documents ?? createHookDocumentReader({ telemetry: requireTelemetry() }),\n };\n}\n\ninterface HookPluginConfig {\n readonly pi: ExtensionAPI;\n readonly options: HookExtensionOptions;\n}\n\ninterface HookRuntimeBinding extends HookRuntime {\n dispose(): void;\n}\n\nfunction createHookRuntime(cordis: Context, options: HookExtensionOptions): HookRuntimeBinding {\n const session = hookSession(cordis, options);\n let active = true;\n let generation = 0;\n let readiness:\n | {\n readonly sessionManager: object;\n readonly coordinator: DoomReadinessCoordinator;\n readonly operation: Promise<void>;\n }\n | undefined;\n const readinessGate: HookReadinessGate = {\n start(context, operation) {\n const ownGeneration = ++generation;\n const isCurrent = (): boolean => active && ownGeneration === generation;\n const coordinator = readDoomReadinessCoordinator(cordis);\n if (!coordinator) return operation(new AbortController().signal, isCurrent);\n\n const previous = readiness;\n const readinessOperation = (async (): Promise<void> => {\n if (previous?.coordinator === coordinator) await previous.operation.catch(() => undefined);\n if (!isCurrent()) return;\n const handle = coordinator.start(\n PACKAGE_SOURCE,\n `${context.sessionManager.getSessionId()}:${ownGeneration}`,\n async (signal) => {\n await operation(signal, isCurrent);\n return { value: undefined };\n },\n );\n await handle.wait();\n })();\n // Config's coordinator owns the single user-facing failure notification.\n void readinessOperation.catch(() => undefined);\n readiness = {\n sessionManager: context.sessionManager,\n coordinator,\n operation: readinessOperation,\n };\n return undefined;\n },\n async wait(context: ExtensionContext): Promise<void> {\n const current = readiness;\n if (!current) return;\n if (current.sessionManager !== context.sessionManager) {\n throw new Error('Hook readiness belongs to a stale Pi session.');\n }\n await current.operation;\n if (!active || current !== readiness) {\n throw new Error('Hook readiness belongs to a stale extension generation.');\n }\n },\n };\n return {\n session,\n readiness: readinessGate,\n isCurrent: () => active,\n dispose() {\n active = false;\n generation += 1;\n readiness = undefined;\n },\n };\n}\n\nfunction hookPlugin(cordis: Context, { pi, options }: HookPluginConfig): void {\n cordis.inject([DOOM_HELP_SERVICE], (helpContext) => {\n const contribution = requireDoomHelpService(helpContext).register({\n source: PACKAGE_SOURCE,\n moduleUrl: import.meta.url,\n skills: [\n {\n name: 'doompi-author-hook',\n description:\n 'Author DoomPi repository or plugin hooks. Use when creating or changing .doom/hooks.yaml, selecting hook groups from modes.yaml, writing hook commands, or adapting Claude Code hook payloads and decisions to DoomPi.',\n },\n ],\n });\n return () => contribution.dispose();\n });\n\n let runtime: HookRuntimeBinding | undefined;\n cordis.inject([DOOM_CONFIG_SERVICE], (configContext) => {\n const binding = createHookRuntime(configContext, options);\n runtime = binding;\n configContext.effect(\n () => () => {\n binding.dispose();\n if (runtime === binding) runtime = undefined;\n },\n `${PACKAGE_SOURCE}/config-runtime`,\n );\n });\n\n cordis.effect(function* () {\n for (const disposeRegistration of registerHookHandlers(pi, () => runtime)) yield disposeRegistration;\n yield () => {\n runtime = undefined;\n };\n }, PACKAGE_SOURCE);\n\n let shutdown: Promise<void> | undefined;\n pi.on('session_shutdown', (_event, ctx) => {\n const current = runtime;\n if (!current?.isCurrent()) return undefined;\n shutdown ??= runSessionEndHooks(current.session, ctx);\n return shutdown;\n });\n}\n\n/** The package's single standard Pi factory. */\nexport async function hookExtension(pi: ExtensionAPI, options: HookExtensionOptions = {}): Promise<void> {\n const connection = await connectDoomCordisHost(pi, PACKAGE_SOURCE);\n const fiber = connection.root.plugin(hookPlugin, { pi, options });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n\nexport default hookExtension;\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- const e=require("../../services/hookRegistry.cjs"),t=require("../../services/hookDecisions.cjs"),n=require("../../services/hookPayload.cjs"),r=require("../../services/pluginHooks.cjs"),i=require("../../types/hooks.cjs");let a=require("@agimon-ai/doompi-config/piContext");const o=`repository-hooks`,s={deliverAs:`steer`};async function c(t,n,i,o){let s=(0,a.requireDoomConfigContext)(n).harness,c=s.root??n.cwd,[l,u]=await Promise.all([t.documents.registry(c),t.documents.plugins(s.pluginHooks)]),d=[...e.selectRegistryHooks(l.entries,{event:i,toolName:o,allowedGroups:s.hookGroups,inSubagent:!!process.env.PI_SUBAGENT_CHILD}),...r.selectPluginHooks(u.documents,i,o)];return{repoRoot:c,sessionId:n.sessionManager.getSessionId(),hooks:d,failures:[...l.failure?[l.failure]:[],...u.failures]}}async function l(e,t,n){let i=(0,a.requireDoomConfigContext)(t).harness,o=await e.documents.plugins(i.pluginHooks);return{repoRoot:i.root??t.cwd,sessionId:t.sessionManager.getSessionId(),hooks:r.selectPluginHooks(o.documents,n),failures:o.failures}}async function u(e,t,n,r,i,a){let o=[];try{for(let[s,c]of n.hooks.entries())t.hasUI&&t.ui.setStatus(i,`${a} (${s+1}/${n.hooks.length})...`),o.push(await e.runner.run(c.hook,r,{repoRoot:n.repoRoot,pluginRoot:c.root}))}finally{t.hasUI&&t.ui.setStatus(i,void 0)}return o}function d(e,n){return[...e.failures,...t.failuresFrom(n)]}function f(e,n){n.length!==0&&e.sendMessage({customType:`repository-hook-failure`,content:t.hookFailureMessage(n),display:!0},s)}function p(e,r){e.on(`session_start`,async(a,l)=>{let p=await c(r,l,i.HOOK_EVENT.sessionStart),m=await u(r,l,p,n.sessionHookPayload(p.sessionId,p.repoRoot),`${o}:${p.sessionId}:start`,`Running session-start hooks`);f(e,d(p,m));let h=t.additionalContextsFrom(t.decisionsFrom(m)).join(`
1
+ const e=require("../../services/hookRegistry.cjs"),t=require("../../services/hookDecisions.cjs"),n=require("../../services/hookPayload.cjs"),r=require("../../services/pluginHooks.cjs"),i=require("../../types/hooks.cjs"),a=`repository-hooks`,o={deliverAs:`steer`};async function s(t,n,i,a){let o=t.config().harness,s=o.root??n.cwd,[c,l]=await Promise.all([t.documents.registry(s),t.documents.plugins(o.pluginHooks)]),u=[...e.selectRegistryHooks(c.entries,{event:i,toolName:a,allowedGroups:o.hookGroups,inSubagent:!!process.env.PI_SUBAGENT_CHILD}),...r.selectPluginHooks(l.documents,i,a)];return{repoRoot:s,sessionId:n.sessionManager.getSessionId(),hooks:u,failures:[...c.failure?[c.failure]:[],...l.failures]}}async function c(e,t,n){let i=e.config().harness,a=await e.documents.plugins(i.pluginHooks);return{repoRoot:i.root??t.cwd,sessionId:t.sessionManager.getSessionId(),hooks:r.selectPluginHooks(a.documents,n),failures:a.failures}}async function l(e,t,n,r,i,a){let o=[];try{for(let[s,c]of n.hooks.entries())t.hasUI&&t.ui.setStatus(i,`${a} (${s+1}/${n.hooks.length})...`),o.push(await e.runner.run(c.hook,r,{repoRoot:n.repoRoot,pluginRoot:c.root}))}finally{t.hasUI&&t.ui.setStatus(i,void 0)}return o}function u(e,n){return[...e.failures,...t.failuresFrom(n)]}function d(e,n){n.length!==0&&e.sendMessage({customType:`repository-hook-failure`,content:t.hookFailureMessage(n),display:!0},o)}async function f(e,r,c,f,p=()=>!0){f?.throwIfAborted();let m=await s(r,c,i.HOOK_EVENT.sessionStart);f?.throwIfAborted();let h=await l(r,c,m,n.sessionHookPayload(m.sessionId,m.repoRoot),`${a}:${m.sessionId}:start`,`Running session-start hooks`);if(f?.throwIfAborted(),!p())return;d(e,u(m,h));let g=t.additionalContextsFrom(t.decisionsFrom(h)).join(`
2
2
 
3
- `);h&&e.sendMessage({customType:`repository-hook-context`,content:h,display:!0},s)})}function m(e,r){e.on(`tool_call`,async(a,s)=>{let l=await c(r,s,i.HOOK_EVENT.preToolUse,a.toolName),p=await u(r,s,l,n.toolHookPayload(a,i.HOOK_EVENT.preToolUse,l.repoRoot,l.sessionId),`${o}:${a.toolCallId}:pre`,`Running pre-tool hooks for ${a.toolName}`),m=t.decisionsFrom(p);f(e,d(l,p));let h=m.find(t.isDenied);if(h)return{block:!0,reason:t.decisionReason(h)??`Blocked by repository hook`};let g=t.additionalContextsFrom(m).join(`
3
+ `);g&&e.sendMessage({customType:`repository-hook-context`,content:g,display:!0},o)}function p(e,t){e.on(`session_start`,(n,r)=>{let i=t();if(!i?.isCurrent())return;let{readiness:a,session:o}=i;return a?a.start(r,(t,n)=>f(e,o,r,t,()=>i.isCurrent()&&n())):f(e,o,r)})}function m(e,t){e.on(`before_agent_start`,async(e,n)=>{let r=t();r?.isCurrent()&&await r.readiness?.wait(n)})}function h(e,r){e.on(`tool_call`,async(o,c)=>{let f=r();if(!f?.isCurrent()||(await f.readiness?.wait(c),!f.isCurrent()))return;let p=await s(f.session,c,i.HOOK_EVENT.preToolUse,o.toolName);if(!f.isCurrent())return;let m=n.toolHookPayload(o,i.HOOK_EVENT.preToolUse,p.repoRoot,p.sessionId),h=await l(f.session,c,p,m,`${a}:${o.toolCallId}:pre`,`Running pre-tool hooks for ${o.toolName}`);if(!f.isCurrent())return;let g=t.decisionsFrom(h);d(e,u(p,h));let _=g.find(t.isDenied);if(_)return{block:!0,reason:t.decisionReason(_)??`Blocked by repository hook`};let v=t.additionalContextsFrom(g).join(`
4
4
 
5
- `);return g?{block:!0,reason:g}:void 0})}function h(e,r){e.on(`tool_result`,async(e,a)=>{let s=await c(r,a,i.HOOK_EVENT.postToolUse,e.toolName),l=await u(r,a,s,n.toolHookPayload(e,i.HOOK_EVENT.postToolUse,s.repoRoot,s.sessionId),`${o}:${e.toolCallId}:post`,`Running post-tool hooks for ${e.toolName}`),f=t.decisionsFrom(l),p=d(s,l),m=t.toolResultMessages(f);if(p.length>0&&m.push(t.hookFailureMessage(p)),m.length!==0)return{content:[...e.content,{type:`text`,text:m.join(`
5
+ `);return v?{block:!0,reason:v}:void 0})}function g(e,r){e.on(`tool_result`,async(e,o)=>{let c=r();if(!c?.isCurrent()||(await c.readiness?.wait(o),!c.isCurrent()))return;let d=await s(c.session,o,i.HOOK_EVENT.postToolUse,e.toolName);if(!c.isCurrent())return;let f=n.toolHookPayload(e,i.HOOK_EVENT.postToolUse,d.repoRoot,d.sessionId),p=await l(c.session,o,d,f,`${a}:${e.toolCallId}:post`,`Running post-tool hooks for ${e.toolName}`);if(!c.isCurrent())return;let m=t.decisionsFrom(p),h=u(d,p),g=t.toolResultMessages(m);if(h.length>0&&g.push(t.hookFailureMessage(h)),g.length!==0)return{content:[...e.content,{type:`text`,text:g.join(`
6
6
 
7
- `)}],isError:e.isError||f.some(t.isDenied)}})}function g(e,t){e.on(`agent_settled`,async(e,r)=>{let a=await c(t,r,i.HOOK_EVENT.stop);await u(t,r,a,n.sessionHookPayload(a.sessionId,a.repoRoot),`${o}:${a.sessionId}:stop`,`Running stop hooks`)})}function _(e,t){return[p,m,h,g].map(n=>(n(e,t),()=>void 0))}async function v(e,t){let r=await l(e,t,i.HOOK_EVENT.sessionEnd);await u(e,t,r,n.sessionHookPayload(r.sessionId,r.repoRoot),`${o}:${r.sessionId}:end`,`Running session-end hooks`)}exports.registerHookHandlers=_,exports.runSessionEndHooks=v;
7
+ `)}],isError:e.isError||m.some(t.isDenied)}})}function _(e,t){e.on(`agent_settled`,async(e,r)=>{let o=t();if(!o?.isCurrent()||(await o.readiness?.wait(r),!o.isCurrent()))return;let c=await s(o.session,r,i.HOOK_EVENT.stop);o.isCurrent()&&await l(o.session,r,c,n.sessionHookPayload(c.sessionId,c.repoRoot),`${a}:${c.sessionId}:stop`,`Running stop hooks`)})}function v(e,t){return[p,m,h,g,_].map(n=>(n(e,t),()=>void 0))}async function y(e,t){let r=await c(e,t,i.HOOK_EVENT.sessionEnd);await l(e,t,r,n.sessionHookPayload(r.sessionId,r.repoRoot),`${a}:${r.sessionId}:end`,`Running session-end hooks`)}exports.registerHookHandlers=v,exports.runSessionEndHooks=y;
8
8
  //# sourceMappingURL=hookHandlers.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"hookHandlers.cjs","names":["requireDoomConfigContext","selectRegistryHooks","selectPluginHooks","failuresFrom","hookFailureMessage","HOOK_EVENT","sessionHookPayload","additionalContextsFrom","decisionsFrom","toolHookPayload","isDenied","decisionReason","toolResultMessages"],"sources":["../../../src/adapters/pi/hookHandlers.ts"],"sourcesContent":["import { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n additionalContextsFrom,\n decisionReason,\n decisionsFrom,\n failuresFrom,\n hookFailureMessage,\n isDenied,\n toolResultMessages,\n} from '../../services/hookDecisions.ts';\nimport { sessionHookPayload, toolHookPayload } from '../../services/hookPayload.ts';\nimport { selectRegistryHooks } from '../../services/hookRegistry.ts';\nimport { selectPluginHooks } from '../../services/pluginHooks.ts';\nimport {\n HOOK_EVENT,\n type HookDocumentReader,\n type HookFailure,\n type HookOutcome,\n type HookPayload,\n type HookRunner,\n type HookToolEvent,\n type ResolvedHook,\n} from '../../types/hooks.ts';\n\nconst STATUS_PREFIX = 'repository-hooks';\nconst FAILURE_MESSAGE_TYPE = 'repository-hook-failure';\nconst CONTEXT_MESSAGE_TYPE = 'repository-hook-context';\nconst STEER = { deliverAs: 'steer' } as const;\nconst BLOCKED_BY_HOOK = 'Blocked by repository hook';\nconst SUBAGENT_ENVIRONMENT_FLAG = 'PI_SUBAGENT_CHILD';\nconst CONTEXT_SEPARATOR = '\\n\\n';\n\n/** The two collaborators a dispatch needs, both replaceable in tests. */\nexport interface HookSession {\n runner: HookRunner;\n documents: HookDocumentReader;\n}\n\n/** Everything one dispatch resolved before it ran anything. */\ninterface HookDispatch {\n repoRoot: string;\n sessionId: string;\n hooks: ResolvedHook[];\n failures: HookFailure[];\n}\n\n/**\n * Registry rows plus plugin hooks for one event, in that order.\n *\n * Both sources are read on every dispatch because either can change mid-session:\n * /mode switches the selected groups and a plugin can rewrite its own config.\n */\nasync function resolveHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n toolName?: string,\n): Promise<HookDispatch> {\n const harness = requireDoomConfigContext(ctx).harness;\n const repoRoot = harness.root ?? ctx.cwd;\n const [registry, plugins] = await Promise.all([\n session.documents.registry(repoRoot),\n session.documents.plugins(harness.pluginHooks),\n ]);\n const hooks = [\n ...selectRegistryHooks(registry.entries, {\n event: eventName,\n toolName,\n allowedGroups: harness.hookGroups,\n inSubagent: Boolean(process.env[SUBAGENT_ENVIRONMENT_FLAG]),\n }),\n ...selectPluginHooks(plugins.documents, eventName, toolName),\n ];\n return {\n repoRoot,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks,\n failures: [...(registry.failure ? [registry.failure] : []), ...plugins.failures],\n };\n}\n\n/** Session end is plugin-only: the registry has no binding for it. */\nasync function resolvePluginHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n): Promise<HookDispatch> {\n const harness = requireDoomConfigContext(ctx).harness;\n const plugins = await session.documents.plugins(harness.pluginHooks);\n return {\n repoRoot: harness.root ?? ctx.cwd,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks: selectPluginHooks(plugins.documents, eventName),\n failures: plugins.failures,\n };\n}\n\nasync function runHooks(\n session: HookSession,\n ctx: ExtensionContext,\n dispatch: HookDispatch,\n payload: HookPayload,\n statusKey: string,\n statusLabel: string,\n): Promise<HookOutcome[]> {\n const outcomes: HookOutcome[] = [];\n try {\n for (const [index, resolved] of dispatch.hooks.entries()) {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, `${statusLabel} (${index + 1}/${dispatch.hooks.length})...`);\n outcomes.push(\n await session.runner.run(resolved.hook, payload, { repoRoot: dispatch.repoRoot, pluginRoot: resolved.root }),\n );\n }\n } finally {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, undefined);\n }\n return outcomes;\n}\n\nfunction allFailures(dispatch: HookDispatch, outcomes: ReadonlyArray<HookOutcome>): HookFailure[] {\n return [...dispatch.failures, ...failuresFrom(outcomes)];\n}\n\nfunction steerHookFailures(pi: ExtensionAPI, failures: ReadonlyArray<HookFailure>): void {\n if (failures.length === 0) return;\n pi.sendMessage({ customType: FAILURE_MESSAGE_TYPE, content: hookFailureMessage(failures), display: true }, STEER);\n}\n\nfunction registerSessionStart(pi: ExtensionAPI, session: HookSession): void {\n pi.on('session_start', async (_event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.sessionStart);\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:start`,\n 'Running session-start hooks',\n );\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const context = additionalContextsFrom(decisionsFrom(outcomes)).join(CONTEXT_SEPARATOR);\n if (context) pi.sendMessage({ customType: CONTEXT_MESSAGE_TYPE, content: context, display: true }, STEER);\n });\n}\n\nfunction registerToolCall(pi: ExtensionAPI, session: HookSession): void {\n pi.on('tool_call', async (event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.preToolUse, event.toolName);\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.preToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:pre`,\n `Running pre-tool hooks for ${event.toolName}`,\n );\n const decisions = decisionsFrom(outcomes);\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const denied = decisions.find(isDenied);\n if (denied) return { block: true, reason: decisionReason(denied) ?? BLOCKED_BY_HOOK };\n const context = additionalContextsFrom(decisions).join(CONTEXT_SEPARATOR);\n return context ? { block: true, reason: context } : undefined;\n });\n}\n\nfunction registerToolResult(pi: ExtensionAPI, session: HookSession): void {\n pi.on('tool_result', async (event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.postToolUse, event.toolName);\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.postToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:post`,\n `Running post-tool hooks for ${event.toolName}`,\n );\n const decisions = decisionsFrom(outcomes);\n const failures = allFailures(dispatch, outcomes);\n const messages = toolResultMessages(decisions);\n if (failures.length > 0) messages.push(hookFailureMessage(failures));\n if (messages.length === 0) return undefined;\n // The result is appended to rather than replaced, so the tool's own output\n // still reaches the model alongside whatever the hook had to say.\n return {\n content: [...event.content, { type: 'text' as const, text: messages.join(CONTEXT_SEPARATOR) }],\n isError: event.isError || decisions.some(isDenied),\n };\n });\n}\n\nfunction registerAgentSettled(pi: ExtensionAPI, session: HookSession): void {\n pi.on('agent_settled', async (_event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.stop);\n // skipInSubagent on the workflow-stop binding keeps subagents from closing\n // the parent's workflow step.\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:stop`,\n 'Running stop hooks',\n );\n });\n}\n\n/**\n * Registers the four in-session lifecycle events and returns one disposer each.\n *\n * Pi owns handler deregistration across a reload, so each disposer only has to\n * drop this package's own reference to the session.\n */\nexport function registerHookHandlers(pi: ExtensionAPI, session: HookSession): Array<() => void> {\n return [registerSessionStart, registerToolCall, registerToolResult, registerAgentSettled].map((register) => {\n register(pi, session);\n return () => undefined;\n });\n}\n\n/** Runs the plugin SessionEnd hooks while the session is still usable. */\nexport async function runSessionEndHooks(session: HookSession, ctx: ExtensionContext): Promise<void> {\n const dispatch = await resolvePluginHooks(session, ctx, HOOK_EVENT.sessionEnd);\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:end`,\n 'Running session-end hooks',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"hookHandlers.cjs","names":["selectRegistryHooks","selectPluginHooks","failuresFrom","hookFailureMessage","HOOK_EVENT","sessionHookPayload","additionalContextsFrom","decisionsFrom","toolHookPayload","isDenied","decisionReason","toolResultMessages"],"sources":["../../../src/adapters/pi/hookHandlers.ts"],"sourcesContent":["import type { DoomConfigContext } from '@agimon-ai/doompi-config/types';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n additionalContextsFrom,\n decisionReason,\n decisionsFrom,\n failuresFrom,\n hookFailureMessage,\n isDenied,\n toolResultMessages,\n} from '../../services/hookDecisions.ts';\nimport { sessionHookPayload, toolHookPayload } from '../../services/hookPayload.ts';\nimport { selectRegistryHooks } from '../../services/hookRegistry.ts';\nimport { selectPluginHooks } from '../../services/pluginHooks.ts';\nimport {\n HOOK_EVENT,\n type HookDocumentReader,\n type HookFailure,\n type HookOutcome,\n type HookPayload,\n type HookRunner,\n type HookToolEvent,\n type ResolvedHook,\n} from '../../types/hooks.ts';\n\nconst STATUS_PREFIX = 'repository-hooks';\nconst FAILURE_MESSAGE_TYPE = 'repository-hook-failure';\nconst CONTEXT_MESSAGE_TYPE = 'repository-hook-context';\nconst STEER = { deliverAs: 'steer' } as const;\nconst BLOCKED_BY_HOOK = 'Blocked by repository hook';\nconst SUBAGENT_ENVIRONMENT_FLAG = 'PI_SUBAGENT_CHILD';\nconst CONTEXT_SEPARATOR = '\\n\\n';\n\n/** The two collaborators a dispatch needs, both replaceable in tests. */\nexport interface HookSession {\n config(): DoomConfigContext;\n runner: HookRunner;\n documents: HookDocumentReader;\n}\n\n/** Package-scoped startup ordering supplied by the standard Pi adapter. */\nexport interface HookReadinessGate {\n start(\n context: ExtensionContext,\n operation: (signal: AbortSignal, isCurrent: () => boolean) => Promise<void>,\n ): void | Promise<void>;\n wait(context: ExtensionContext): Promise<void>;\n}\n\n/** One dependency-complete Hook runtime owned by a reactive Cordis fiber. */\nexport interface HookRuntime {\n readonly session: HookSession;\n readonly readiness?: HookReadinessGate;\n isCurrent(): boolean;\n}\n\nexport type HookRuntimeResolver = () => HookRuntime | undefined;\n\n/** Everything one dispatch resolved before it ran anything. */\ninterface HookDispatch {\n repoRoot: string;\n sessionId: string;\n hooks: ResolvedHook[];\n failures: HookFailure[];\n}\n\n/**\n * Registry rows plus plugin hooks for one event, in that order.\n *\n * Both sources are read on every dispatch because either can change mid-session:\n * /mode switches the selected groups and a plugin can rewrite its own config.\n */\nasync function resolveHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n toolName?: string,\n): Promise<HookDispatch> {\n const harness = session.config().harness;\n const repoRoot = harness.root ?? ctx.cwd;\n const [registry, plugins] = await Promise.all([\n session.documents.registry(repoRoot),\n session.documents.plugins(harness.pluginHooks),\n ]);\n const hooks = [\n ...selectRegistryHooks(registry.entries, {\n event: eventName,\n toolName,\n allowedGroups: harness.hookGroups,\n inSubagent: Boolean(process.env[SUBAGENT_ENVIRONMENT_FLAG]),\n }),\n ...selectPluginHooks(plugins.documents, eventName, toolName),\n ];\n return {\n repoRoot,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks,\n failures: [...(registry.failure ? [registry.failure] : []), ...plugins.failures],\n };\n}\n\n/** Session end is plugin-only: the registry has no binding for it. */\nasync function resolvePluginHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n): Promise<HookDispatch> {\n const harness = session.config().harness;\n const plugins = await session.documents.plugins(harness.pluginHooks);\n return {\n repoRoot: harness.root ?? ctx.cwd,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks: selectPluginHooks(plugins.documents, eventName),\n failures: plugins.failures,\n };\n}\n\nasync function runHooks(\n session: HookSession,\n ctx: ExtensionContext,\n dispatch: HookDispatch,\n payload: HookPayload,\n statusKey: string,\n statusLabel: string,\n): Promise<HookOutcome[]> {\n const outcomes: HookOutcome[] = [];\n try {\n for (const [index, resolved] of dispatch.hooks.entries()) {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, `${statusLabel} (${index + 1}/${dispatch.hooks.length})...`);\n outcomes.push(\n await session.runner.run(resolved.hook, payload, { repoRoot: dispatch.repoRoot, pluginRoot: resolved.root }),\n );\n }\n } finally {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, undefined);\n }\n return outcomes;\n}\n\nfunction allFailures(dispatch: HookDispatch, outcomes: ReadonlyArray<HookOutcome>): HookFailure[] {\n return [...dispatch.failures, ...failuresFrom(outcomes)];\n}\n\nfunction steerHookFailures(pi: ExtensionAPI, failures: ReadonlyArray<HookFailure>): void {\n if (failures.length === 0) return;\n pi.sendMessage({ customType: FAILURE_MESSAGE_TYPE, content: hookFailureMessage(failures), display: true }, STEER);\n}\n\nasync function runSessionStartHooks(\n pi: ExtensionAPI,\n session: HookSession,\n ctx: ExtensionContext,\n signal?: AbortSignal,\n isCurrent: () => boolean = () => true,\n): Promise<void> {\n signal?.throwIfAborted();\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.sessionStart);\n signal?.throwIfAborted();\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:start`,\n 'Running session-start hooks',\n );\n signal?.throwIfAborted();\n if (!isCurrent()) return;\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const context = additionalContextsFrom(decisionsFrom(outcomes)).join(CONTEXT_SEPARATOR);\n if (context) pi.sendMessage({ customType: CONTEXT_MESSAGE_TYPE, content: context, display: true }, STEER);\n}\n\nfunction registerSessionStart(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('session_start', (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n const { readiness, session } = runtime;\n if (readiness) {\n return readiness.start(ctx, (signal, isReady) =>\n runSessionStartHooks(pi, session, ctx, signal, () => runtime.isCurrent() && isReady()),\n );\n }\n return runSessionStartHooks(pi, session, ctx);\n });\n}\n\nfunction registerBeforeAgentStart(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('before_agent_start', async (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return;\n await runtime.readiness?.wait(ctx);\n });\n}\n\nfunction registerToolCall(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('tool_call', async (event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return undefined;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.preToolUse, event.toolName);\n if (!runtime.isCurrent()) return undefined;\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.preToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n runtime.session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:pre`,\n `Running pre-tool hooks for ${event.toolName}`,\n );\n if (!runtime.isCurrent()) return undefined;\n const decisions = decisionsFrom(outcomes);\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const denied = decisions.find(isDenied);\n if (denied) return { block: true, reason: decisionReason(denied) ?? BLOCKED_BY_HOOK };\n const context = additionalContextsFrom(decisions).join(CONTEXT_SEPARATOR);\n return context ? { block: true, reason: context } : undefined;\n });\n}\n\nfunction registerToolResult(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('tool_result', async (event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return undefined;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.postToolUse, event.toolName);\n if (!runtime.isCurrent()) return undefined;\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.postToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n runtime.session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:post`,\n `Running post-tool hooks for ${event.toolName}`,\n );\n if (!runtime.isCurrent()) return undefined;\n const decisions = decisionsFrom(outcomes);\n const failures = allFailures(dispatch, outcomes);\n const messages = toolResultMessages(decisions);\n if (failures.length > 0) messages.push(hookFailureMessage(failures));\n if (messages.length === 0) return undefined;\n // The result is appended to rather than replaced, so the tool's own output\n // still reaches the model alongside whatever the hook had to say.\n return {\n content: [...event.content, { type: 'text' as const, text: messages.join(CONTEXT_SEPARATOR) }],\n isError: event.isError || decisions.some(isDenied),\n };\n });\n}\n\nfunction registerAgentSettled(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('agent_settled', async (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.stop);\n if (!runtime.isCurrent()) return;\n // skipInSubagent on the workflow-stop binding keeps subagents from closing\n // the parent's workflow step.\n await runHooks(\n runtime.session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:stop`,\n 'Running stop hooks',\n );\n });\n}\n\n/**\n * Registers the four in-session lifecycle events and returns one disposer each.\n *\n * Pi owns handler deregistration across a reload, so each disposer only has to\n * drop this package's own reference to the session.\n */\nexport function registerHookHandlers(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): Array<() => void> {\n return [\n registerSessionStart,\n registerBeforeAgentStart,\n registerToolCall,\n registerToolResult,\n registerAgentSettled,\n ].map((register) => {\n register(pi, resolveRuntime);\n return () => undefined;\n });\n}\n\n/** Runs the plugin SessionEnd hooks while the session is still usable. */\nexport async function runSessionEndHooks(session: HookSession, ctx: ExtensionContext): Promise<void> {\n const dispatch = await resolvePluginHooks(session, ctx, HOOK_EVENT.sessionEnd);\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:end`,\n 'Running session-end hooks',\n );\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- import{selectRegistryHooks as e}from"../../services/hookRegistry.mjs";import{additionalContextsFrom as t,decisionReason as n,decisionsFrom as r,failuresFrom as i,hookFailureMessage as a,isDenied as o,toolResultMessages as s}from"../../services/hookDecisions.mjs";import{sessionHookPayload as c,toolHookPayload as l}from"../../services/hookPayload.mjs";import{selectPluginHooks as u}from"../../services/pluginHooks.mjs";import{HOOK_EVENT as d}from"../../types/hooks.mjs";import{requireDoomConfigContext as f}from"@agimon-ai/doompi-config/piContext";const p=`repository-hooks`,m={deliverAs:`steer`};async function h(t,n,r,i){let a=f(n).harness,o=a.root??n.cwd,[s,c]=await Promise.all([t.documents.registry(o),t.documents.plugins(a.pluginHooks)]),l=[...e(s.entries,{event:r,toolName:i,allowedGroups:a.hookGroups,inSubagent:!!process.env.PI_SUBAGENT_CHILD}),...u(c.documents,r,i)];return{repoRoot:o,sessionId:n.sessionManager.getSessionId(),hooks:l,failures:[...s.failure?[s.failure]:[],...c.failures]}}async function g(e,t,n){let r=f(t).harness,i=await e.documents.plugins(r.pluginHooks);return{repoRoot:r.root??t.cwd,sessionId:t.sessionManager.getSessionId(),hooks:u(i.documents,n),failures:i.failures}}async function _(e,t,n,r,i,a){let o=[];try{for(let[s,c]of n.hooks.entries())t.hasUI&&t.ui.setStatus(i,`${a} (${s+1}/${n.hooks.length})...`),o.push(await e.runner.run(c.hook,r,{repoRoot:n.repoRoot,pluginRoot:c.root}))}finally{t.hasUI&&t.ui.setStatus(i,void 0)}return o}function v(e,t){return[...e.failures,...i(t)]}function y(e,t){t.length!==0&&e.sendMessage({customType:`repository-hook-failure`,content:a(t),display:!0},m)}function b(e,n){e.on(`session_start`,async(i,a)=>{let o=await h(n,a,d.sessionStart),s=await _(n,a,o,c(o.sessionId,o.repoRoot),`${p}:${o.sessionId}:start`,`Running session-start hooks`);y(e,v(o,s));let l=t(r(s)).join(`
1
+ import{selectRegistryHooks as e}from"../../services/hookRegistry.mjs";import{additionalContextsFrom as t,decisionReason as n,decisionsFrom as r,failuresFrom as i,hookFailureMessage as a,isDenied as o,toolResultMessages as s}from"../../services/hookDecisions.mjs";import{sessionHookPayload as c,toolHookPayload as l}from"../../services/hookPayload.mjs";import{selectPluginHooks as u}from"../../services/pluginHooks.mjs";import{HOOK_EVENT as d}from"../../types/hooks.mjs";const f=`repository-hooks`,p={deliverAs:`steer`};async function m(t,n,r,i){let a=t.config().harness,o=a.root??n.cwd,[s,c]=await Promise.all([t.documents.registry(o),t.documents.plugins(a.pluginHooks)]),l=[...e(s.entries,{event:r,toolName:i,allowedGroups:a.hookGroups,inSubagent:!!process.env.PI_SUBAGENT_CHILD}),...u(c.documents,r,i)];return{repoRoot:o,sessionId:n.sessionManager.getSessionId(),hooks:l,failures:[...s.failure?[s.failure]:[],...c.failures]}}async function h(e,t,n){let r=e.config().harness,i=await e.documents.plugins(r.pluginHooks);return{repoRoot:r.root??t.cwd,sessionId:t.sessionManager.getSessionId(),hooks:u(i.documents,n),failures:i.failures}}async function g(e,t,n,r,i,a){let o=[];try{for(let[s,c]of n.hooks.entries())t.hasUI&&t.ui.setStatus(i,`${a} (${s+1}/${n.hooks.length})...`),o.push(await e.runner.run(c.hook,r,{repoRoot:n.repoRoot,pluginRoot:c.root}))}finally{t.hasUI&&t.ui.setStatus(i,void 0)}return o}function _(e,t){return[...e.failures,...i(t)]}function v(e,t){t.length!==0&&e.sendMessage({customType:`repository-hook-failure`,content:a(t),display:!0},p)}async function y(e,n,i,a,o=()=>!0){a?.throwIfAborted();let s=await m(n,i,d.sessionStart);a?.throwIfAborted();let l=await g(n,i,s,c(s.sessionId,s.repoRoot),`${f}:${s.sessionId}:start`,`Running session-start hooks`);if(a?.throwIfAborted(),!o())return;v(e,_(s,l));let u=t(r(l)).join(`
2
2
 
3
- `);l&&e.sendMessage({customType:`repository-hook-context`,content:l,display:!0},m)})}function x(e,i){e.on(`tool_call`,async(a,s)=>{let c=await h(i,s,d.preToolUse,a.toolName),u=await _(i,s,c,l(a,d.preToolUse,c.repoRoot,c.sessionId),`${p}:${a.toolCallId}:pre`,`Running pre-tool hooks for ${a.toolName}`),f=r(u);y(e,v(c,u));let m=f.find(o);if(m)return{block:!0,reason:n(m)??`Blocked by repository hook`};let g=t(f).join(`
3
+ `);u&&e.sendMessage({customType:`repository-hook-context`,content:u,display:!0},p)}function b(e,t){e.on(`session_start`,(n,r)=>{let i=t();if(!i?.isCurrent())return;let{readiness:a,session:o}=i;return a?a.start(r,(t,n)=>y(e,o,r,t,()=>i.isCurrent()&&n())):y(e,o,r)})}function x(e,t){e.on(`before_agent_start`,async(e,n)=>{let r=t();r?.isCurrent()&&await r.readiness?.wait(n)})}function S(e,i){e.on(`tool_call`,async(a,s)=>{let c=i();if(!c?.isCurrent()||(await c.readiness?.wait(s),!c.isCurrent()))return;let u=await m(c.session,s,d.preToolUse,a.toolName);if(!c.isCurrent())return;let p=l(a,d.preToolUse,u.repoRoot,u.sessionId),h=await g(c.session,s,u,p,`${f}:${a.toolCallId}:pre`,`Running pre-tool hooks for ${a.toolName}`);if(!c.isCurrent())return;let y=r(h);v(e,_(u,h));let b=y.find(o);if(b)return{block:!0,reason:n(b)??`Blocked by repository hook`};let x=t(y).join(`
4
4
 
5
- `);return g?{block:!0,reason:g}:void 0})}function S(e,t){e.on(`tool_result`,async(e,n)=>{let i=await h(t,n,d.postToolUse,e.toolName),c=await _(t,n,i,l(e,d.postToolUse,i.repoRoot,i.sessionId),`${p}:${e.toolCallId}:post`,`Running post-tool hooks for ${e.toolName}`),u=r(c),f=v(i,c),m=s(u);if(f.length>0&&m.push(a(f)),m.length!==0)return{content:[...e.content,{type:`text`,text:m.join(`
5
+ `);return x?{block:!0,reason:x}:void 0})}function C(e,t){e.on(`tool_result`,async(e,n)=>{let i=t();if(!i?.isCurrent()||(await i.readiness?.wait(n),!i.isCurrent()))return;let c=await m(i.session,n,d.postToolUse,e.toolName);if(!i.isCurrent())return;let u=l(e,d.postToolUse,c.repoRoot,c.sessionId),p=await g(i.session,n,c,u,`${f}:${e.toolCallId}:post`,`Running post-tool hooks for ${e.toolName}`);if(!i.isCurrent())return;let h=r(p),v=_(c,p),y=s(h);if(v.length>0&&y.push(a(v)),y.length!==0)return{content:[...e.content,{type:`text`,text:y.join(`
6
6
 
7
- `)}],isError:e.isError||u.some(o)}})}function C(e,t){e.on(`agent_settled`,async(e,n)=>{let r=await h(t,n,d.stop);await _(t,n,r,c(r.sessionId,r.repoRoot),`${p}:${r.sessionId}:stop`,`Running stop hooks`)})}function w(e,t){return[b,x,S,C].map(n=>(n(e,t),()=>void 0))}async function T(e,t){let n=await g(e,t,d.sessionEnd);await _(e,t,n,c(n.sessionId,n.repoRoot),`${p}:${n.sessionId}:end`,`Running session-end hooks`)}export{w as registerHookHandlers,T as runSessionEndHooks};
7
+ `)}],isError:e.isError||h.some(o)}})}function w(e,t){e.on(`agent_settled`,async(e,n)=>{let r=t();if(!r?.isCurrent()||(await r.readiness?.wait(n),!r.isCurrent()))return;let i=await m(r.session,n,d.stop);r.isCurrent()&&await g(r.session,n,i,c(i.sessionId,i.repoRoot),`${f}:${i.sessionId}:stop`,`Running stop hooks`)})}function T(e,t){return[b,x,S,C,w].map(n=>(n(e,t),()=>void 0))}async function E(e,t){let n=await h(e,t,d.sessionEnd);await g(e,t,n,c(n.sessionId,n.repoRoot),`${f}:${n.sessionId}:end`,`Running session-end hooks`)}export{T as registerHookHandlers,E as runSessionEndHooks};
8
8
  //# sourceMappingURL=hookHandlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"hookHandlers.mjs","names":[],"sources":["../../../src/adapters/pi/hookHandlers.ts"],"sourcesContent":["import { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n additionalContextsFrom,\n decisionReason,\n decisionsFrom,\n failuresFrom,\n hookFailureMessage,\n isDenied,\n toolResultMessages,\n} from '../../services/hookDecisions.ts';\nimport { sessionHookPayload, toolHookPayload } from '../../services/hookPayload.ts';\nimport { selectRegistryHooks } from '../../services/hookRegistry.ts';\nimport { selectPluginHooks } from '../../services/pluginHooks.ts';\nimport {\n HOOK_EVENT,\n type HookDocumentReader,\n type HookFailure,\n type HookOutcome,\n type HookPayload,\n type HookRunner,\n type HookToolEvent,\n type ResolvedHook,\n} from '../../types/hooks.ts';\n\nconst STATUS_PREFIX = 'repository-hooks';\nconst FAILURE_MESSAGE_TYPE = 'repository-hook-failure';\nconst CONTEXT_MESSAGE_TYPE = 'repository-hook-context';\nconst STEER = { deliverAs: 'steer' } as const;\nconst BLOCKED_BY_HOOK = 'Blocked by repository hook';\nconst SUBAGENT_ENVIRONMENT_FLAG = 'PI_SUBAGENT_CHILD';\nconst CONTEXT_SEPARATOR = '\\n\\n';\n\n/** The two collaborators a dispatch needs, both replaceable in tests. */\nexport interface HookSession {\n runner: HookRunner;\n documents: HookDocumentReader;\n}\n\n/** Everything one dispatch resolved before it ran anything. */\ninterface HookDispatch {\n repoRoot: string;\n sessionId: string;\n hooks: ResolvedHook[];\n failures: HookFailure[];\n}\n\n/**\n * Registry rows plus plugin hooks for one event, in that order.\n *\n * Both sources are read on every dispatch because either can change mid-session:\n * /mode switches the selected groups and a plugin can rewrite its own config.\n */\nasync function resolveHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n toolName?: string,\n): Promise<HookDispatch> {\n const harness = requireDoomConfigContext(ctx).harness;\n const repoRoot = harness.root ?? ctx.cwd;\n const [registry, plugins] = await Promise.all([\n session.documents.registry(repoRoot),\n session.documents.plugins(harness.pluginHooks),\n ]);\n const hooks = [\n ...selectRegistryHooks(registry.entries, {\n event: eventName,\n toolName,\n allowedGroups: harness.hookGroups,\n inSubagent: Boolean(process.env[SUBAGENT_ENVIRONMENT_FLAG]),\n }),\n ...selectPluginHooks(plugins.documents, eventName, toolName),\n ];\n return {\n repoRoot,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks,\n failures: [...(registry.failure ? [registry.failure] : []), ...plugins.failures],\n };\n}\n\n/** Session end is plugin-only: the registry has no binding for it. */\nasync function resolvePluginHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n): Promise<HookDispatch> {\n const harness = requireDoomConfigContext(ctx).harness;\n const plugins = await session.documents.plugins(harness.pluginHooks);\n return {\n repoRoot: harness.root ?? ctx.cwd,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks: selectPluginHooks(plugins.documents, eventName),\n failures: plugins.failures,\n };\n}\n\nasync function runHooks(\n session: HookSession,\n ctx: ExtensionContext,\n dispatch: HookDispatch,\n payload: HookPayload,\n statusKey: string,\n statusLabel: string,\n): Promise<HookOutcome[]> {\n const outcomes: HookOutcome[] = [];\n try {\n for (const [index, resolved] of dispatch.hooks.entries()) {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, `${statusLabel} (${index + 1}/${dispatch.hooks.length})...`);\n outcomes.push(\n await session.runner.run(resolved.hook, payload, { repoRoot: dispatch.repoRoot, pluginRoot: resolved.root }),\n );\n }\n } finally {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, undefined);\n }\n return outcomes;\n}\n\nfunction allFailures(dispatch: HookDispatch, outcomes: ReadonlyArray<HookOutcome>): HookFailure[] {\n return [...dispatch.failures, ...failuresFrom(outcomes)];\n}\n\nfunction steerHookFailures(pi: ExtensionAPI, failures: ReadonlyArray<HookFailure>): void {\n if (failures.length === 0) return;\n pi.sendMessage({ customType: FAILURE_MESSAGE_TYPE, content: hookFailureMessage(failures), display: true }, STEER);\n}\n\nfunction registerSessionStart(pi: ExtensionAPI, session: HookSession): void {\n pi.on('session_start', async (_event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.sessionStart);\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:start`,\n 'Running session-start hooks',\n );\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const context = additionalContextsFrom(decisionsFrom(outcomes)).join(CONTEXT_SEPARATOR);\n if (context) pi.sendMessage({ customType: CONTEXT_MESSAGE_TYPE, content: context, display: true }, STEER);\n });\n}\n\nfunction registerToolCall(pi: ExtensionAPI, session: HookSession): void {\n pi.on('tool_call', async (event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.preToolUse, event.toolName);\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.preToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:pre`,\n `Running pre-tool hooks for ${event.toolName}`,\n );\n const decisions = decisionsFrom(outcomes);\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const denied = decisions.find(isDenied);\n if (denied) return { block: true, reason: decisionReason(denied) ?? BLOCKED_BY_HOOK };\n const context = additionalContextsFrom(decisions).join(CONTEXT_SEPARATOR);\n return context ? { block: true, reason: context } : undefined;\n });\n}\n\nfunction registerToolResult(pi: ExtensionAPI, session: HookSession): void {\n pi.on('tool_result', async (event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.postToolUse, event.toolName);\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.postToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:post`,\n `Running post-tool hooks for ${event.toolName}`,\n );\n const decisions = decisionsFrom(outcomes);\n const failures = allFailures(dispatch, outcomes);\n const messages = toolResultMessages(decisions);\n if (failures.length > 0) messages.push(hookFailureMessage(failures));\n if (messages.length === 0) return undefined;\n // The result is appended to rather than replaced, so the tool's own output\n // still reaches the model alongside whatever the hook had to say.\n return {\n content: [...event.content, { type: 'text' as const, text: messages.join(CONTEXT_SEPARATOR) }],\n isError: event.isError || decisions.some(isDenied),\n };\n });\n}\n\nfunction registerAgentSettled(pi: ExtensionAPI, session: HookSession): void {\n pi.on('agent_settled', async (_event, ctx) => {\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.stop);\n // skipInSubagent on the workflow-stop binding keeps subagents from closing\n // the parent's workflow step.\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:stop`,\n 'Running stop hooks',\n );\n });\n}\n\n/**\n * Registers the four in-session lifecycle events and returns one disposer each.\n *\n * Pi owns handler deregistration across a reload, so each disposer only has to\n * drop this package's own reference to the session.\n */\nexport function registerHookHandlers(pi: ExtensionAPI, session: HookSession): Array<() => void> {\n return [registerSessionStart, registerToolCall, registerToolResult, registerAgentSettled].map((register) => {\n register(pi, session);\n return () => undefined;\n });\n}\n\n/** Runs the plugin SessionEnd hooks while the session is still usable. */\nexport async function runSessionEndHooks(session: HookSession, ctx: ExtensionContext): Promise<void> {\n const dispatch = await resolvePluginHooks(session, ctx, HOOK_EVENT.sessionEnd);\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:end`,\n 'Running session-end hooks',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"hookHandlers.mjs","names":[],"sources":["../../../src/adapters/pi/hookHandlers.ts"],"sourcesContent":["import type { DoomConfigContext } from '@agimon-ai/doompi-config/types';\nimport type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';\nimport {\n additionalContextsFrom,\n decisionReason,\n decisionsFrom,\n failuresFrom,\n hookFailureMessage,\n isDenied,\n toolResultMessages,\n} from '../../services/hookDecisions.ts';\nimport { sessionHookPayload, toolHookPayload } from '../../services/hookPayload.ts';\nimport { selectRegistryHooks } from '../../services/hookRegistry.ts';\nimport { selectPluginHooks } from '../../services/pluginHooks.ts';\nimport {\n HOOK_EVENT,\n type HookDocumentReader,\n type HookFailure,\n type HookOutcome,\n type HookPayload,\n type HookRunner,\n type HookToolEvent,\n type ResolvedHook,\n} from '../../types/hooks.ts';\n\nconst STATUS_PREFIX = 'repository-hooks';\nconst FAILURE_MESSAGE_TYPE = 'repository-hook-failure';\nconst CONTEXT_MESSAGE_TYPE = 'repository-hook-context';\nconst STEER = { deliverAs: 'steer' } as const;\nconst BLOCKED_BY_HOOK = 'Blocked by repository hook';\nconst SUBAGENT_ENVIRONMENT_FLAG = 'PI_SUBAGENT_CHILD';\nconst CONTEXT_SEPARATOR = '\\n\\n';\n\n/** The two collaborators a dispatch needs, both replaceable in tests. */\nexport interface HookSession {\n config(): DoomConfigContext;\n runner: HookRunner;\n documents: HookDocumentReader;\n}\n\n/** Package-scoped startup ordering supplied by the standard Pi adapter. */\nexport interface HookReadinessGate {\n start(\n context: ExtensionContext,\n operation: (signal: AbortSignal, isCurrent: () => boolean) => Promise<void>,\n ): void | Promise<void>;\n wait(context: ExtensionContext): Promise<void>;\n}\n\n/** One dependency-complete Hook runtime owned by a reactive Cordis fiber. */\nexport interface HookRuntime {\n readonly session: HookSession;\n readonly readiness?: HookReadinessGate;\n isCurrent(): boolean;\n}\n\nexport type HookRuntimeResolver = () => HookRuntime | undefined;\n\n/** Everything one dispatch resolved before it ran anything. */\ninterface HookDispatch {\n repoRoot: string;\n sessionId: string;\n hooks: ResolvedHook[];\n failures: HookFailure[];\n}\n\n/**\n * Registry rows plus plugin hooks for one event, in that order.\n *\n * Both sources are read on every dispatch because either can change mid-session:\n * /mode switches the selected groups and a plugin can rewrite its own config.\n */\nasync function resolveHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n toolName?: string,\n): Promise<HookDispatch> {\n const harness = session.config().harness;\n const repoRoot = harness.root ?? ctx.cwd;\n const [registry, plugins] = await Promise.all([\n session.documents.registry(repoRoot),\n session.documents.plugins(harness.pluginHooks),\n ]);\n const hooks = [\n ...selectRegistryHooks(registry.entries, {\n event: eventName,\n toolName,\n allowedGroups: harness.hookGroups,\n inSubagent: Boolean(process.env[SUBAGENT_ENVIRONMENT_FLAG]),\n }),\n ...selectPluginHooks(plugins.documents, eventName, toolName),\n ];\n return {\n repoRoot,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks,\n failures: [...(registry.failure ? [registry.failure] : []), ...plugins.failures],\n };\n}\n\n/** Session end is plugin-only: the registry has no binding for it. */\nasync function resolvePluginHooks(\n session: HookSession,\n ctx: ExtensionContext,\n eventName: string,\n): Promise<HookDispatch> {\n const harness = session.config().harness;\n const plugins = await session.documents.plugins(harness.pluginHooks);\n return {\n repoRoot: harness.root ?? ctx.cwd,\n sessionId: ctx.sessionManager.getSessionId(),\n hooks: selectPluginHooks(plugins.documents, eventName),\n failures: plugins.failures,\n };\n}\n\nasync function runHooks(\n session: HookSession,\n ctx: ExtensionContext,\n dispatch: HookDispatch,\n payload: HookPayload,\n statusKey: string,\n statusLabel: string,\n): Promise<HookOutcome[]> {\n const outcomes: HookOutcome[] = [];\n try {\n for (const [index, resolved] of dispatch.hooks.entries()) {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, `${statusLabel} (${index + 1}/${dispatch.hooks.length})...`);\n outcomes.push(\n await session.runner.run(resolved.hook, payload, { repoRoot: dispatch.repoRoot, pluginRoot: resolved.root }),\n );\n }\n } finally {\n if (ctx.hasUI) ctx.ui.setStatus(statusKey, undefined);\n }\n return outcomes;\n}\n\nfunction allFailures(dispatch: HookDispatch, outcomes: ReadonlyArray<HookOutcome>): HookFailure[] {\n return [...dispatch.failures, ...failuresFrom(outcomes)];\n}\n\nfunction steerHookFailures(pi: ExtensionAPI, failures: ReadonlyArray<HookFailure>): void {\n if (failures.length === 0) return;\n pi.sendMessage({ customType: FAILURE_MESSAGE_TYPE, content: hookFailureMessage(failures), display: true }, STEER);\n}\n\nasync function runSessionStartHooks(\n pi: ExtensionAPI,\n session: HookSession,\n ctx: ExtensionContext,\n signal?: AbortSignal,\n isCurrent: () => boolean = () => true,\n): Promise<void> {\n signal?.throwIfAborted();\n const dispatch = await resolveHooks(session, ctx, HOOK_EVENT.sessionStart);\n signal?.throwIfAborted();\n const outcomes = await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:start`,\n 'Running session-start hooks',\n );\n signal?.throwIfAborted();\n if (!isCurrent()) return;\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const context = additionalContextsFrom(decisionsFrom(outcomes)).join(CONTEXT_SEPARATOR);\n if (context) pi.sendMessage({ customType: CONTEXT_MESSAGE_TYPE, content: context, display: true }, STEER);\n}\n\nfunction registerSessionStart(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('session_start', (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n const { readiness, session } = runtime;\n if (readiness) {\n return readiness.start(ctx, (signal, isReady) =>\n runSessionStartHooks(pi, session, ctx, signal, () => runtime.isCurrent() && isReady()),\n );\n }\n return runSessionStartHooks(pi, session, ctx);\n });\n}\n\nfunction registerBeforeAgentStart(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('before_agent_start', async (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return;\n await runtime.readiness?.wait(ctx);\n });\n}\n\nfunction registerToolCall(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('tool_call', async (event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return undefined;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.preToolUse, event.toolName);\n if (!runtime.isCurrent()) return undefined;\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.preToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n runtime.session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:pre`,\n `Running pre-tool hooks for ${event.toolName}`,\n );\n if (!runtime.isCurrent()) return undefined;\n const decisions = decisionsFrom(outcomes);\n steerHookFailures(pi, allFailures(dispatch, outcomes));\n const denied = decisions.find(isDenied);\n if (denied) return { block: true, reason: decisionReason(denied) ?? BLOCKED_BY_HOOK };\n const context = additionalContextsFrom(decisions).join(CONTEXT_SEPARATOR);\n return context ? { block: true, reason: context } : undefined;\n });\n}\n\nfunction registerToolResult(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('tool_result', async (event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return undefined;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return undefined;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.postToolUse, event.toolName);\n if (!runtime.isCurrent()) return undefined;\n const payload = toolHookPayload(\n event as HookToolEvent,\n HOOK_EVENT.postToolUse,\n dispatch.repoRoot,\n dispatch.sessionId,\n );\n const outcomes = await runHooks(\n runtime.session,\n ctx,\n dispatch,\n payload,\n `${STATUS_PREFIX}:${event.toolCallId}:post`,\n `Running post-tool hooks for ${event.toolName}`,\n );\n if (!runtime.isCurrent()) return undefined;\n const decisions = decisionsFrom(outcomes);\n const failures = allFailures(dispatch, outcomes);\n const messages = toolResultMessages(decisions);\n if (failures.length > 0) messages.push(hookFailureMessage(failures));\n if (messages.length === 0) return undefined;\n // The result is appended to rather than replaced, so the tool's own output\n // still reaches the model alongside whatever the hook had to say.\n return {\n content: [...event.content, { type: 'text' as const, text: messages.join(CONTEXT_SEPARATOR) }],\n isError: event.isError || decisions.some(isDenied),\n };\n });\n}\n\nfunction registerAgentSettled(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): void {\n pi.on('agent_settled', async (_event, ctx) => {\n const runtime = resolveRuntime();\n if (!runtime?.isCurrent()) return;\n await runtime.readiness?.wait(ctx);\n if (!runtime.isCurrent()) return;\n const dispatch = await resolveHooks(runtime.session, ctx, HOOK_EVENT.stop);\n if (!runtime.isCurrent()) return;\n // skipInSubagent on the workflow-stop binding keeps subagents from closing\n // the parent's workflow step.\n await runHooks(\n runtime.session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:stop`,\n 'Running stop hooks',\n );\n });\n}\n\n/**\n * Registers the four in-session lifecycle events and returns one disposer each.\n *\n * Pi owns handler deregistration across a reload, so each disposer only has to\n * drop this package's own reference to the session.\n */\nexport function registerHookHandlers(pi: ExtensionAPI, resolveRuntime: HookRuntimeResolver): Array<() => void> {\n return [\n registerSessionStart,\n registerBeforeAgentStart,\n registerToolCall,\n registerToolResult,\n registerAgentSettled,\n ].map((register) => {\n register(pi, resolveRuntime);\n return () => undefined;\n });\n}\n\n/** Runs the plugin SessionEnd hooks while the session is still usable. */\nexport async function runSessionEndHooks(session: HookSession, ctx: ExtensionContext): Promise<void> {\n const dispatch = await resolvePluginHooks(session, ctx, HOOK_EVENT.sessionEnd);\n await runHooks(\n session,\n ctx,\n dispatch,\n sessionHookPayload(dispatch.sessionId, dispatch.repoRoot),\n `${STATUS_PREFIX}:${dispatch.sessionId}:end`,\n 'Running session-end hooks',\n );\n}\n"],"mappings":"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"}
@@ -8,11 +8,11 @@
8
8
  */
9
9
  /** Registry event names. Each is used twice per dispatch, so drift would be silent. */
10
10
  declare const HOOK_EVENT: {
11
- readonly sessionStart: "SessionStart";
12
- readonly preToolUse: "PreToolUse";
13
- readonly postToolUse: "PostToolUse";
14
- readonly stop: "Stop";
15
- readonly sessionEnd: "SessionEnd";
11
+ readonly sessionStart: 'SessionStart';
12
+ readonly preToolUse: 'PreToolUse';
13
+ readonly postToolUse: 'PostToolUse';
14
+ readonly stop: 'Stop';
15
+ readonly sessionEnd: 'SessionEnd';
16
16
  };
17
17
  type HookEventName = (typeof HOOK_EVENT)[keyof typeof HOOK_EVENT];
18
18
  interface HookCommand {
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.cts","names":[],"sources":["../../src/types/hooks.ts"],"mappings":";;;;;;;;;cASa;;;;;;;KAQD,wBAAwB,yBAAyB;UAE5C;EACf;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;;;UAIe,sBAAsB;EACrC;EACA;;EAEA;EACA;EACA;;EAEA;;UAGe;EACf;EACA,QAAQ;IAAQ;IAAe,KAAK;;;UAGrB;EACf,SAAS,eAAe;;;UAIT;EACf;EACA;;;UAIe;EACf;EACA,UAAU;;UAGK;EACf;EACA,QAAQ;;UAGO;EACf,QAAQ,eAAe;;;UAIR;EACf;EACA,QAAQ;;;UAIO;EACf,MAAM;EACN;;;UAIe;EACf;EACA;EACA;IACE;IACA;IACA;;;KAIQ;UAQK;EACf;EACA;EACA,QAAQ;;UAGO;EACf,WAAW;EACX,UAAU;;;;;;;;UASK;WACN;WACA;WACA;WACA;WACA;;;KAIC,cAAc;UAET;EACf;;EAEA;;;UAIe;EACf,IAAI,MAAM,aAAa,SAAS,aAAa,SAAS,iBAAiB,QAAQ;;;UAIhE;EACf,SAAS;EACT,UAAU;;;UAIK;EACf,WAAW;EACX,UAAU;;;UAIK;WACN;WACA;;;UAIM;EACf,SAAS,mBAAmB,QAAQ;EACpC,QAAQ,kBAAkB,wBAAwB,QAAQ"}
1
+ {"version":3,"file":"hooks.d.cts","names":[],"sources":["../../src/types/hooks.ts"],"mappings":";;;;;;;;;cASa;WACX;WACA;WACA;WACA;WACA;;KAGU,wBAAwB,yBAAyB;UAE5C;EACf;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;;;UAIe,sBAAsB;EACrC;EACA;;EAEA;EACA;EACA;;EAEA;;UAGe;EACf;EACA,QAAQ;IAAQ;IAAe,KAAK;;;UAGrB;EACf,SAAS,eAAe;;;UAIT;EACf;EACA;;;UAIe;EACf;EACA,UAAU;;UAGK;EACf;EACA,QAAQ;;UAGO;EACf,QAAQ,eAAe;;;UAIR;EACf;EACA,QAAQ;;;UAIO;EACf,MAAM;EACN;;;UAIe;EACf;EACA;EACA;IACE;IACA;IACA;;;KAIQ;UAQK;EACf;EACA;EACA,QAAQ;;UAGO;EACf,WAAW;EACX,UAAU;;;;;;;;UASK;WACN;WACA;WACA;WACA;WACA;;;KAIC,cAAc;UAET;EACf;;EAEA;;;UAIe;EACf,IAAI,MAAM,aAAa,SAAS,aAAa,SAAS,iBAAiB,QAAQ;;;UAIhE;EACf,SAAS;EACT,UAAU;;;UAIK;EACf,WAAW;EACX,UAAU;;;UAIK;WACN;WACA;;;UAIM;EACf,SAAS,mBAAmB,QAAQ;EACpC,QAAQ,kBAAkB,wBAAwB,QAAQ"}
@@ -8,11 +8,11 @@
8
8
  */
9
9
  /** Registry event names. Each is used twice per dispatch, so drift would be silent. */
10
10
  declare const HOOK_EVENT: {
11
- readonly sessionStart: "SessionStart";
12
- readonly preToolUse: "PreToolUse";
13
- readonly postToolUse: "PostToolUse";
14
- readonly stop: "Stop";
15
- readonly sessionEnd: "SessionEnd";
11
+ readonly sessionStart: 'SessionStart';
12
+ readonly preToolUse: 'PreToolUse';
13
+ readonly postToolUse: 'PostToolUse';
14
+ readonly stop: 'Stop';
15
+ readonly sessionEnd: 'SessionEnd';
16
16
  };
17
17
  type HookEventName = (typeof HOOK_EVENT)[keyof typeof HOOK_EVENT];
18
18
  interface HookCommand {
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.mts","names":[],"sources":["../../src/types/hooks.ts"],"mappings":";;;;;;;;;cASa;;;;;;;KAQD,wBAAwB,yBAAyB;UAE5C;EACf;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;;;UAIe,sBAAsB;EACrC;EACA;;EAEA;EACA;EACA;;EAEA;;UAGe;EACf;EACA,QAAQ;IAAQ;IAAe,KAAK;;;UAGrB;EACf,SAAS,eAAe;;;UAIT;EACf;EACA;;;UAIe;EACf;EACA,UAAU;;UAGK;EACf;EACA,QAAQ;;UAGO;EACf,QAAQ,eAAe;;;UAIR;EACf;EACA,QAAQ;;;UAIO;EACf,MAAM;EACN;;;UAIe;EACf;EACA;EACA;IACE;IACA;IACA;;;KAIQ;UAQK;EACf;EACA;EACA,QAAQ;;UAGO;EACf,WAAW;EACX,UAAU;;;;;;;;UASK;WACN;WACA;WACA;WACA;WACA;;;KAIC,cAAc;UAET;EACf;;EAEA;;;UAIe;EACf,IAAI,MAAM,aAAa,SAAS,aAAa,SAAS,iBAAiB,QAAQ;;;UAIhE;EACf,SAAS;EACT,UAAU;;;UAIK;EACf,WAAW;EACX,UAAU;;;UAIK;WACN;WACA;;;UAIM;EACf,SAAS,mBAAmB,QAAQ;EACpC,QAAQ,kBAAkB,wBAAwB,QAAQ"}
1
+ {"version":3,"file":"hooks.d.mts","names":[],"sources":["../../src/types/hooks.ts"],"mappings":";;;;;;;;;cASa;WACX;WACA;WACA;WACA;WACA;;KAGU,wBAAwB,yBAAyB;UAE5C;EACf;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;;;UAIe,sBAAsB;EACrC;EACA;;EAEA;EACA;EACA;;EAEA;;UAGe;EACf;EACA,QAAQ;IAAQ;IAAe,KAAK;;;UAGrB;EACf,SAAS,eAAe;;;UAIT;EACf;EACA;;;UAIe;EACf;EACA,UAAU;;UAGK;EACf;EACA,QAAQ;;UAGO;EACf,QAAQ,eAAe;;;UAIR;EACf;EACA,QAAQ;;;UAIO;EACf,MAAM;EACN;;;UAIe;EACf;EACA;EACA;IACE;IACA;IACA;;;KAIQ;UAQK;EACf;EACA;EACA,QAAQ;;UAGO;EACf,WAAW;EACX,UAAU;;;;;;;;UASK;WACN;WACA;WACA;WACA;WACA;;;KAIC,cAAc;UAET;EACf;;EAEA;;;UAIe;EACf,IAAI,MAAM,aAAa,SAAS,aAAa,SAAS,iBAAiB,QAAQ;;;UAIhE;EACf,SAAS;EACT,UAAU;;;UAIK;EACf,WAAW;EACX,UAAU;;;UAIK;WACN;WACA;;;UAIM;EACf,SAAS,mBAAmB,QAAQ;EACpC,QAAQ,kBAAkB,wBAAwB,QAAQ"}
@@ -1,8 +1,8 @@
1
1
  //#region src/types/telemetry.d.ts
2
2
  /** Telemetry events this package reports, and the port that records them. */
3
3
  declare const HOOK_TELEMETRY_EVENT: {
4
- readonly hookFailed: "doom_pi_hook.failed";
5
- readonly hookRegistryReadFailed: "doom_pi_hook.registry_read_failed";
4
+ readonly hookFailed: 'doom_pi_hook.failed';
5
+ readonly hookRegistryReadFailed: 'doom_pi_hook.registry_read_failed';
6
6
  };
7
7
  type HookTelemetryEventName = (typeof HOOK_TELEMETRY_EVENT)[keyof typeof HOOK_TELEMETRY_EVENT];
8
8
  type HookTelemetryAttributes = Record<string, string | number | boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.d.cts","names":[],"sources":["../../src/types/telemetry.ts"],"mappings":";;cACa;WAAA;WAAA;;KAKD,iCAAiC,mCAAmC;KACpE,0BAA0B;;;;;;UAOrB;EACf,YAAY,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B;EAClG,cAAc,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B"}
1
+ {"version":3,"file":"telemetry.d.cts","names":[],"sources":["../../src/types/telemetry.ts"],"mappings":";;cACa;WACX;WACA;;KAGU,iCAAiC,mCAAmC;KACpE,0BAA0B;;;;;;UAOrB;EACf,YAAY,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B;EAClG,cAAc,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B"}
@@ -1,8 +1,8 @@
1
1
  //#region src/types/telemetry.d.ts
2
2
  /** Telemetry events this package reports, and the port that records them. */
3
3
  declare const HOOK_TELEMETRY_EVENT: {
4
- readonly hookFailed: "doom_pi_hook.failed";
5
- readonly hookRegistryReadFailed: "doom_pi_hook.registry_read_failed";
4
+ readonly hookFailed: 'doom_pi_hook.failed';
5
+ readonly hookRegistryReadFailed: 'doom_pi_hook.registry_read_failed';
6
6
  };
7
7
  type HookTelemetryEventName = (typeof HOOK_TELEMETRY_EVENT)[keyof typeof HOOK_TELEMETRY_EVENT];
8
8
  type HookTelemetryAttributes = Record<string, string | number | boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.d.mts","names":[],"sources":["../../src/types/telemetry.ts"],"mappings":";;cACa;WAAA;WAAA;;KAKD,iCAAiC,mCAAmC;KACpE,0BAA0B;;;;;;UAOrB;EACf,YAAY,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B;EAClG,cAAc,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B"}
1
+ {"version":3,"file":"telemetry.d.mts","names":[],"sources":["../../src/types/telemetry.ts"],"mappings":";;cACa;WACX;WACA;;KAGU,iCAAiC,mCAAmC;KACpE,0BAA0B;;;;;;UAOrB;EACf,YAAY,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B;EAClG,cAAc,OAAO,wBAAwB,gBAAgB,aAAa,0BAA0B"}
package/llms.txt CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Claude-Code-compatible repository and plugin hooks for Doom Pi sessions.
4
4
 
5
- ## Resources
5
+ ## How to author
6
6
 
7
- - [Package README](./README.md): the `.doom/hooks.yaml` document, group selection, the event mapping, and what a hook decision can do.
7
+ - [Author DoomPi hooks](./src/prompts/doompi-author-hook/SKILL.md): Create `.doom/hooks.yaml` groups and Claude-compatible hook commands.
8
+
9
+ ## Package reference
10
+
11
+ - [Package README](./README.md): The hook registry, group selection, event mapping, decisions, and failure handling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-hook",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.20",
4
4
  "description": "Claude-Code-compatible repository and plugin hook runner for DoomPi sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -16,12 +16,21 @@
16
16
  "repository-hooks"
17
17
  ],
18
18
  "homepage": "https://agimon.ai",
19
+ "bugs": {
20
+ "url": "https://github.com/AgiFlow/doompi/issues"
21
+ },
19
22
  "license": "MIT",
20
23
  "author": "Vuong Ngo",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/AgiFlow/doompi.git",
27
+ "directory": "packages/default/doompi-hook"
28
+ },
21
29
  "files": [
22
30
  "dist",
23
31
  "llms.txt",
24
32
  "README.md",
33
+ "src/prompts",
25
34
  "LICENSE",
26
35
  "package.json"
27
36
  ],
@@ -48,21 +57,21 @@
48
57
  "dependencies": {
49
58
  "@deepseek-ai/cordis": "4.0.1",
50
59
  "yaml": "2.9.0",
51
- "@agimon-ai/doompi-config": "0.0.1-alpha.24",
52
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.24",
53
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.24"
60
+ "@agimon-ai/doompi-config": "0.0.1-alpha.42",
61
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.42",
62
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.42"
54
63
  },
55
64
  "devDependencies": {
56
- "@earendil-works/pi-coding-agent": "0.84.2",
57
- "@types/node": "26.1.1",
58
- "@vitest/coverage-v8": "4.1.10",
65
+ "@earendil-works/pi-coding-agent": "0.84.4",
66
+ "@types/node": "26.2.0",
67
+ "@vitest/coverage-v8": "4.1.11",
59
68
  "tsdown": "0.22.14",
60
- "typescript": "6.0.3",
61
- "vitest": "4.1.10",
62
- "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.24"
69
+ "typescript": "7.0.2",
70
+ "vitest": "4.1.11",
71
+ "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.40"
63
72
  },
64
73
  "peerDependencies": {
65
- "@earendil-works/pi-coding-agent": "0.84.2"
74
+ "@earendil-works/pi-coding-agent": "0.84.4"
66
75
  },
67
76
  "peerDependenciesMeta": {
68
77
  "@earendil-works/pi-coding-agent": {
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: doompi-author-hook
3
+ description: Author DoomPi repository or plugin hooks. Use when creating or changing .doom/hooks.yaml, selecting hook groups from modes.yaml, writing hook commands, or adapting Claude Code hook payloads and decisions to DoomPi.
4
+ ---
5
+
6
+ # Author DoomPi hooks
7
+
8
+ Write the smallest hook that enforces the requested policy, and treat every hook command as executable repository code.
9
+
10
+ ## Repository registry
11
+
12
+ Define repository and personal hooks in `.doom/hooks.yaml`:
13
+
14
+ ```yaml
15
+ groups:
16
+ safety:
17
+ core: true
18
+ hooks:
19
+ - event: PreToolUse
20
+ pi:
21
+ matcher: Bash
22
+ command: .doom/hooks/guard-destructive-commands.sh
23
+ timeout: 10
24
+ skipInSubagent: true
25
+ order: 0
26
+ ```
27
+
28
+ - Personal configuration comes from `~/.pi/.doom/hooks.yaml`; repository configuration comes from `<repo>/.doom/hooks.yaml`.
29
+ - A repository group replaces a personal group with the same ID as one complete value.
30
+ - `core: true` runs regardless of the selected groups. Otherwise, add the group ID to a layer's `hookGroups` in `modes.yaml`.
31
+ - `matcher` is a regular expression over Claude tool names such as `Bash` and `Write`, even though Pi uses lowercase tool names internally.
32
+ - Use `skipInSubagent: true` when running the hook in a child could duplicate or close parent-owned work. Lower `order` values run first, with declaration order as the tiebreaker.
33
+
34
+ Supported repository events are `SessionStart`, `PreToolUse`, `PostToolUse`, and `Stop`. `SessionEnd` is resolved only from plugin `hooks.json` documents.
35
+
36
+ ## Command contract
37
+
38
+ DoomPi runs each command through `/bin/bash -c` from the repository root and writes the Claude-compatible event payload to stdin. The environment includes `CLAUDE_PROJECT_DIR`, `CODEX_REPO_ROOT`, `ORIGINAL_REPO_PATH`, and `CLAUDE_PLUGIN_ROOT`. For `.doom/hooks.yaml`, `CLAUDE_PLUGIN_ROOT` is the root that declared the personal or repository document.
39
+
40
+ The final stdout line beginning with `{` is parsed as the hook decision. Use Claude Code decision fields only when the event supports them. `PreToolUse` may deny a tool or add context, `PostToolUse` may append context or mark a result as denied, and `SessionStart` may add context. `Stop` and `SessionEnd` run for side effects.
41
+
42
+ Hook failures are advisory: nonzero exits, invalid JSON, spawn failures, and timeouts are reported to the agent instead of crashing the turn. Set `timeout` in seconds only when the 10-second default is unsuitable. A timeout terminates the command's process group.
43
+
44
+ ## Verification
45
+
46
+ Before enabling a new hook broadly:
47
+
48
+ 1. Run the command directly with a representative JSON payload on stdin.
49
+ 2. Start DoomPi with the intended major mode and confirm the group appears in `doompi --explain` output.
50
+ 3. Exercise the exact lifecycle event in a disposable session, including a child session when `skipInSubagent` matters.
51
+ 4. Confirm failure output reaches the agent and that repeated shutdown does not repeat external side effects unexpectedly.