oh-my-githubcopilot 1.8.1 → 2.0.0-alpha.1cd01f4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +23 -3
  2. package/AGENTS.md +29 -25
  3. package/CHANGELOG.md +49 -336
  4. package/README.md +7 -2
  5. package/agents/code-reviewer.agent.md +5 -0
  6. package/agents/{simplifier.agent.md → code-simplifier.agent.md} +2 -2
  7. package/agents/debugger.agent.md +1 -1
  8. package/agents/document-specialist.agent.md +6 -1
  9. package/agents/{explorer.agent.md → explore.agent.md} +2 -2
  10. package/agents/security-reviewer.agent.md +1 -1
  11. package/agents/test-engineer.agent.md +3 -1
  12. package/bin/omp-statusline.mjs +12 -11
  13. package/bin/omp-statusline.mjs.map +2 -2
  14. package/bin/omp.mjs +885 -42
  15. package/bin/omp.mjs.map +4 -4
  16. package/dist/hooks/delegation-enforcer.mjs +65 -13
  17. package/dist/hooks/delegation-enforcer.mjs.map +4 -4
  18. package/dist/hooks/hud-emitter.mjs +80 -28
  19. package/dist/hooks/hud-emitter.mjs.map +4 -4
  20. package/dist/hooks/keyword-detector.mjs +117 -11
  21. package/dist/hooks/keyword-detector.mjs.map +4 -4
  22. package/dist/hooks/model-router.mjs +64 -12
  23. package/dist/hooks/model-router.mjs.map +4 -4
  24. package/dist/hooks/stop-continuation.mjs +66 -14
  25. package/dist/hooks/stop-continuation.mjs.map +4 -4
  26. package/dist/hooks/token-tracker.mjs +81 -19
  27. package/dist/hooks/token-tracker.mjs.map +4 -4
  28. package/dist/mcp/server.mjs +17 -16
  29. package/dist/mcp/server.mjs.map +2 -2
  30. package/extension/extension.mjs +659 -0
  31. package/hooks/hooks.json +6 -6
  32. package/package.json +3 -2
  33. package/plugin.json +23 -3
  34. package/skills/build-fix/SKILL.md +35 -0
  35. package/skills/cancel/SKILL.md +33 -0
  36. package/skills/ccg/SKILL.md +37 -0
  37. package/skills/code-review/SKILL.md +33 -0
  38. package/skills/deep-dive/SKILL.md +33 -0
  39. package/skills/deepinit/SKILL.md +33 -0
  40. package/skills/deepsearch/SKILL.md +33 -0
  41. package/skills/design/SKILL.md +37 -0
  42. package/skills/external-context/SKILL.md +33 -0
  43. package/skills/help/SKILL.md +33 -0
  44. package/skills/omp-doctor/SKILL.md +23 -1
  45. package/skills/omp-reference/SKILL.md +20 -24
  46. package/skills/remember/SKILL.md +39 -0
  47. package/skills/research/SKILL.md +1 -1
  48. package/skills/sciomc/SKILL.md +35 -0
  49. package/skills/security-review/SKILL.md +33 -0
  50. package/skills/self-improve/SKILL.md +35 -0
  51. package/skills/ultragoal/SKILL.md +33 -0
  52. package/skills/ultraqa/SKILL.md +33 -0
  53. package/skills/verify/SKILL.md +33 -0
  54. package/skills/visual-verdict/SKILL.md +35 -0
  55. package/skills/web-clone/SKILL.md +35 -0
  56. package/skills/writer-memory/SKILL.md +37 -0
  57. package/agents/orchestrator.agent.md +0 -26
  58. package/agents/researcher.agent.md +0 -18
  59. package/agents/reviewer.agent.md +0 -23
  60. package/agents/tester.agent.md +0 -20
@@ -1,5 +1,66 @@
1
1
  // src/hooks/model-router.mts
2
2
  import { fileURLToPath } from "url";
3
+
4
+ // src/hooks/runner.mts
5
+ import { appendFileSync, mkdirSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ async function readStdin() {
9
+ const readStdinActual = async () => {
10
+ const chunks = [];
11
+ for await (const chunk of process.stdin) {
12
+ chunks.push(String(chunk));
13
+ }
14
+ return chunks.join("");
15
+ };
16
+ const stdinTimeout = new Promise(
17
+ (resolve) => setTimeout(
18
+ () => resolve(""),
19
+ parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
20
+ )
21
+ );
22
+ return Promise.race([readStdinActual(), stdinTimeout]);
23
+ }
24
+ function logHookFailure(hook, reason) {
25
+ try {
26
+ process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
27
+ `);
28
+ } catch {
29
+ }
30
+ try {
31
+ const logsDir = join(homedir(), ".omp", "logs");
32
+ mkdirSync(logsDir, { recursive: true });
33
+ const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
34
+ appendFileSync(join(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
35
+ } catch {
36
+ }
37
+ }
38
+ async function runHookMain(processHook2, options = {}) {
39
+ let outputJson;
40
+ try {
41
+ const input = JSON.parse(await readStdin());
42
+ const serialized = JSON.stringify(processHook2(input));
43
+ if (typeof serialized !== "string") {
44
+ throw new Error("hook produced no serializable output");
45
+ }
46
+ outputJson = serialized;
47
+ } catch (err) {
48
+ const reason = err instanceof Error ? err.message : String(err);
49
+ logHookFailure(options.hookName ?? "unknown", reason);
50
+ const failOpen = {
51
+ ...options.failOpenDecision ? { decision: "allow" } : {},
52
+ status: "error",
53
+ latencyMs: 0,
54
+ mutations: [],
55
+ log: [`fail-open: ${reason}`]
56
+ };
57
+ outputJson = JSON.stringify(failOpen);
58
+ }
59
+ console.log(outputJson);
60
+ process.exitCode = 0;
61
+ }
62
+
63
+ // src/hooks/model-router.mts
3
64
  var TIER_RECOMMENDATIONS = {
4
65
  high: "model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)",
5
66
  standard: "model: claude-sonnet-4.6 recommended for this task (standard implementation and review)",
@@ -48,10 +109,10 @@ function processHook(input) {
48
109
  }
49
110
  }
50
111
  function getAgentTier(agentId) {
51
- if (["orchestrator", "architect", "planner", "reviewer-security", "critic"].includes(agentId)) {
112
+ if (["orchestrator", "architect", "planner", "security-reviewer", "critic", "debugger", "code-reviewer", "analyst", "designer", "code-simplifier"].includes(agentId)) {
52
113
  return "high";
53
114
  }
54
- if (["explorer", "writer"].includes(agentId)) {
115
+ if (["explore"].includes(agentId)) {
55
116
  return "fast";
56
117
  }
57
118
  return "standard";
@@ -62,16 +123,7 @@ function agentTierToModel(tier) {
62
123
  return "sonnet";
63
124
  }
64
125
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
65
- const input = JSON.parse(await readStdin());
66
- const output = processHook(input);
67
- console.log(JSON.stringify(output));
68
- }
69
- async function readStdin() {
70
- const chunks = [];
71
- for await (const chunk of process.stdin) {
72
- chunks.push(chunk);
73
- }
74
- return chunks.join("");
126
+ await runHookMain(processHook, { failOpenDecision: true, hookName: "model-router" });
75
127
  }
76
128
  export {
77
129
  processHook
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/model-router.mts"],
4
- "sourcesContent": ["/**\n * model-router hook\n * Trigger: pre-cycle (PreToolUse equivalent)\n * Priority: 80\n *\n * Reads agent frontmatter model_tier and adds advisory additionalContext.\n */\n\nexport interface HookInput {\n hook_type: \"PreToolUse\";\n tool_name?: string;\n agent_id?: string;\n session_id?: string;\n}\n\nexport interface HookOutput {\n decision?: \"allow\";\n additionalContext?: string;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_model\"; model: \"opus\" | \"sonnet\" | \"haiku\" }>;\n log: string[];\n}\n\n// Model tier recommendations \u2014 advisory only\nconst TIER_RECOMMENDATIONS: Record<string, string> = {\n high: \"model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)\",\n standard: \"model: claude-sonnet-4.6 recommended for this task (standard implementation and review)\",\n fast: \"model: gpt-5.4-mini or haiku recommended for quick lookups and formatting\",\n};\n\n// Default if agent tier unknown\nconst DEFAULT_TIER = \"standard\";\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n\n try {\n if (input.hook_type !== \"PreToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const agentId = input.agent_id;\n if (!agentId) {\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Agent tier is determined by agent frontmatter in the agent definition files.\n // This hook reads agent metadata from the session state or agent registry.\n // For now, we use a simple mapping based on known agent tiers.\n const agentTier = getAgentTier(agentId);\n const recommendation = TIER_RECOMMENDATIONS[agentTier] || TIER_RECOMMENDATIONS[DEFAULT_TIER];\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_model\", model: agentTierToModel(agentTier) },\n ];\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n additionalContext: recommendation,\n mutations,\n log: [`${agentId} \u2192 tier: ${agentTier} \u2192 ${agentTierToModel(agentTier)}`],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\nfunction getAgentTier(agentId: string): string {\n // Tier 1 \u2014 High\n if ([\"orchestrator\", \"architect\", \"planner\", \"reviewer-security\", \"critic\"].includes(agentId)) {\n return \"high\";\n }\n // Tier 3 \u2014 Fast\n if ([\"explorer\", \"writer\"].includes(agentId)) {\n return \"fast\";\n }\n // Tier 2 \u2014 Standard (default)\n return \"standard\";\n}\n\nfunction agentTierToModel(tier: string): \"opus\" | \"sonnet\" | \"haiku\" {\n if (tier === \"high\") return \"opus\";\n if (tier === \"fast\") return \"haiku\";\n return \"sonnet\";\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported)\nimport { fileURLToPath } from \"url\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n const input: HookInput = JSON.parse(await readStdin());\n const output = processHook(input);\n console.log(JSON.stringify(output));\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n return chunks.join(\"\");\n}\n"],
5
- "mappings": ";AAwGA,SAAS,qBAAqB;AA/E9B,IAAM,uBAA+C;AAAA,EACnD,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AACR;AAGA,IAAM,eAAe;AAEd,SAAS,YAAY,OAA8B;AACxD,QAAM,QAAQ,KAAK,IAAI;AAEvB,MAAI;AACF,QAAI,MAAM,cAAc,cAAc;AACpC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,WAAW,CAAC;AAAA,QACZ,KAAK,CAAC;AAAA,MACR;AAAA,IACF;AAEA,UAAM,UAAU,MAAM;AACtB,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,WAAW,CAAC;AAAA,QACZ,KAAK,CAAC;AAAA,MACR;AAAA,IACF;AAKA,UAAM,YAAY,aAAa,OAAO;AACtC,UAAM,iBAAiB,qBAAqB,SAAS,KAAK,qBAAqB,YAAY;AAE3F,UAAM,YAAqC;AAAA,MACzC,EAAE,MAAM,aAAa,OAAO,iBAAiB,SAAS,EAAE;AAAA,IAC1D;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,mBAAmB;AAAA,MACnB;AAAA,MACA,KAAK,CAAC,GAAG,OAAO,iBAAY,SAAS,WAAM,iBAAiB,SAAS,CAAC,EAAE;AAAA,IAC1E;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,UAAU,GAAG,EAAE;AAAA,IACvB;AAAA,EACF;AACF;AAEA,SAAS,aAAa,SAAyB;AAE7C,MAAI,CAAC,gBAAgB,aAAa,WAAW,qBAAqB,QAAQ,EAAE,SAAS,OAAO,GAAG;AAC7F,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,YAAY,QAAQ,EAAE,SAAS,OAAO,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA2C;AACnE,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,SAAO;AACT;AAKA,IAAI,QAAQ,KAAK,CAAC,MAAM,cAAc,YAAY,GAAG,GAAG;AACtD,QAAM,QAAmB,KAAK,MAAM,MAAM,UAAU,CAAC;AACrD,QAAM,SAAS,YAAY,KAAK;AAChC,UAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;AACpC;AAEA,eAAe,YAA6B;AAC1C,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,QAAQ,OAAO;AACvC,WAAO,KAAK,KAAK;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,EAAE;AACvB;",
6
- "names": []
3
+ "sources": ["../../src/hooks/model-router.mts", "../../src/hooks/runner.mts"],
4
+ "sourcesContent": ["/**\n * model-router hook\n * Trigger: pre-cycle (PreToolUse equivalent)\n * Priority: 80\n *\n * Reads agent frontmatter model_tier and adds advisory additionalContext.\n */\n\nexport interface HookInput {\n hook_type: \"PreToolUse\";\n tool_name?: string;\n agent_id?: string;\n session_id?: string;\n}\n\nexport interface HookOutput {\n decision?: \"allow\";\n additionalContext?: string;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_model\"; model: \"opus\" | \"sonnet\" | \"haiku\" }>;\n log: string[];\n}\n\n// Model tier recommendations \u2014 advisory only\nconst TIER_RECOMMENDATIONS: Record<string, string> = {\n high: \"model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)\",\n standard: \"model: claude-sonnet-4.6 recommended for this task (standard implementation and review)\",\n fast: \"model: gpt-5.4-mini or haiku recommended for quick lookups and formatting\",\n};\n\n// Default if agent tier unknown\nconst DEFAULT_TIER = \"standard\";\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n\n try {\n if (input.hook_type !== \"PreToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const agentId = input.agent_id;\n if (!agentId) {\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Agent tier is determined by agent frontmatter in the agent definition files.\n // This hook reads agent metadata from the session state or agent registry.\n // For now, we use a simple mapping based on known agent tiers.\n const agentTier = getAgentTier(agentId);\n const recommendation = TIER_RECOMMENDATIONS[agentTier] || TIER_RECOMMENDATIONS[DEFAULT_TIER];\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_model\", model: agentTierToModel(agentTier) },\n ];\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n additionalContext: recommendation,\n mutations,\n log: [`${agentId} \u2192 tier: ${agentTier} \u2192 ${agentTierToModel(agentTier)}`],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\nfunction getAgentTier(agentId: string): string {\n // Tier 1 \u2014 High (\"orchestrator\" is the top-level coordinator role, not a delegatable agent)\n if ([\"orchestrator\", \"architect\", \"planner\", \"security-reviewer\", \"critic\", \"debugger\", \"code-reviewer\", \"analyst\", \"designer\", \"code-simplifier\"].includes(agentId)) {\n return \"high\";\n }\n // Tier 3 \u2014 Fast\n if ([\"explore\"].includes(agentId)) {\n return \"fast\";\n }\n // Tier 2 \u2014 Standard (default)\n return \"standard\";\n}\n\nfunction agentTierToModel(tier: string): \"opus\" | \"sonnet\" | \"haiku\" {\n if (tier === \"high\") return \"opus\";\n if (tier === \"fast\") return \"haiku\";\n return \"sonnet\";\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { failOpenDecision: true, hookName: \"model-router\" });\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["processHook"]
7
7
  }
@@ -1,14 +1,75 @@
1
1
  // src/hooks/stop-continuation.mts
2
2
  import { readFileSync } from "fs";
3
+ import { homedir as homedir2 } from "os";
4
+ import { join as join2 } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // src/hooks/runner.mts
8
+ import { appendFileSync, mkdirSync } from "fs";
3
9
  import { homedir } from "os";
4
10
  import { join } from "path";
5
- import { fileURLToPath } from "url";
11
+ async function readStdin() {
12
+ const readStdinActual = async () => {
13
+ const chunks = [];
14
+ for await (const chunk of process.stdin) {
15
+ chunks.push(String(chunk));
16
+ }
17
+ return chunks.join("");
18
+ };
19
+ const stdinTimeout = new Promise(
20
+ (resolve) => setTimeout(
21
+ () => resolve(""),
22
+ parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
23
+ )
24
+ );
25
+ return Promise.race([readStdinActual(), stdinTimeout]);
26
+ }
27
+ function logHookFailure(hook, reason) {
28
+ try {
29
+ process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
30
+ `);
31
+ } catch {
32
+ }
33
+ try {
34
+ const logsDir = join(homedir(), ".omp", "logs");
35
+ mkdirSync(logsDir, { recursive: true });
36
+ const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
37
+ appendFileSync(join(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
38
+ } catch {
39
+ }
40
+ }
41
+ async function runHookMain(processHook2, options = {}) {
42
+ let outputJson;
43
+ try {
44
+ const input = JSON.parse(await readStdin());
45
+ const serialized = JSON.stringify(processHook2(input));
46
+ if (typeof serialized !== "string") {
47
+ throw new Error("hook produced no serializable output");
48
+ }
49
+ outputJson = serialized;
50
+ } catch (err) {
51
+ const reason = err instanceof Error ? err.message : String(err);
52
+ logHookFailure(options.hookName ?? "unknown", reason);
53
+ const failOpen = {
54
+ ...options.failOpenDecision ? { decision: "allow" } : {},
55
+ status: "error",
56
+ latencyMs: 0,
57
+ mutations: [],
58
+ log: [`fail-open: ${reason}`]
59
+ };
60
+ outputJson = JSON.stringify(failOpen);
61
+ }
62
+ console.log(outputJson);
63
+ process.exitCode = 0;
64
+ }
65
+
66
+ // src/hooks/stop-continuation.mts
6
67
  function getModeStatePath(mode, sessionId) {
7
- const base = join(homedir(), ".omp", "state");
68
+ const base = join2(homedir2(), ".omp", "state");
8
69
  if (sessionId) {
9
- return join(base, "sessions", sessionId, `${mode}-state.json`);
70
+ return join2(base, "sessions", sessionId, `${mode}-state.json`);
10
71
  }
11
- return join(base, `${mode}-state.json`);
72
+ return join2(base, `${mode}-state.json`);
12
73
  }
13
74
  function readModeState(mode, sessionId) {
14
75
  try {
@@ -66,16 +127,7 @@ function processHook(input) {
66
127
  }
67
128
  }
68
129
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
69
- const input = JSON.parse(await readStdin());
70
- const output = processHook(input);
71
- console.log(JSON.stringify(output));
72
- }
73
- async function readStdin() {
74
- const chunks = [];
75
- for await (const chunk of process.stdin) {
76
- chunks.push(chunk);
77
- }
78
- return chunks.join("");
130
+ await runHookMain(processHook, { hookName: "stop-continuation" });
79
131
  }
80
132
  export {
81
133
  processHook
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/stop-continuation.mts"],
4
- "sourcesContent": ["/**\n * stop-continuation hook\n * Trigger: post-message (SessionEnd equivalent)\n * Priority: 50\n *\n * Detects active persistent modes (ralph, ultrawork, team) and\n * returns continue instructions so the user can decide whether\n * to keep going.\n */\n\nimport { readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface HookInput {\n hook_type: \"SessionEnd\";\n session_id?: string;\n message?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"stop\"; reason: string } | { type: \"log\"; level: \"info\"; message: string }>;\n log: string[];\n}\n\ninterface ModeState {\n active?: boolean;\n mode?: string;\n linked_ultrawork?: boolean;\n linked_team?: boolean;\n}\n\nfunction getModeStatePath(mode: string, sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, `${mode}-state.json`);\n }\n return join(base, `${mode}-state.json`);\n}\n\nfunction readModeState(mode: string, sessionId?: string): ModeState | null {\n try {\n const path = getModeStatePath(mode, sessionId);\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n// Priority order for checking: team > ralph > ultrawork\nconst PERSISTENT_MODES = [\"team\", \"ralph\", \"ultrawork\"];\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"SessionEnd\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Check for active persistent modes\n for (const mode of PERSISTENT_MODES) {\n const state = readModeState(mode, input.session_id);\n if (state?.active) {\n const reason = `${mode} mode is still active.`;\n log.push(`Stop continuation: ${reason}`);\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [\n {\n type: \"stop\",\n reason: `${reason} Use /cancel to end it, or continue the session to keep going.`,\n },\n { type: \"log\", level: \"info\", message: reason },\n ],\n log,\n };\n }\n }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [\"No persistent modes active\"],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported)\nimport { fileURLToPath } from \"url\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n const input: HookInput = JSON.parse(await readStdin());\n const output = processHook(input);\n console.log(JSON.stringify(output));\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n return chunks.join(\"\");\n}\n"],
5
- "mappings": ";AAUA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,YAAY;AAgGrB,SAAS,qBAAqB;AAzE9B,SAAS,iBAAiB,MAAc,WAA4B;AAClE,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ,OAAO;AAC5C,MAAI,WAAW;AACb,WAAO,KAAK,MAAM,YAAY,WAAW,GAAG,IAAI,aAAa;AAAA,EAC/D;AACA,SAAO,KAAK,MAAM,GAAG,IAAI,aAAa;AACxC;AAEA,SAAS,cAAc,MAAc,WAAsC;AACzE,MAAI;AACF,UAAM,OAAO,iBAAiB,MAAM,SAAS;AAC7C,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,mBAAmB,CAAC,QAAQ,SAAS,WAAW;AAE/C,SAAS,YAAY,OAA8B;AACxD,QAAM,QAAQ,KAAK,IAAI;AACvB,QAAM,MAAgB,CAAC;AAEvB,MAAI;AACF,QAAI,MAAM,cAAc,cAAc;AACpC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,WAAW,CAAC;AAAA,QACZ,KAAK,CAAC;AAAA,MACR;AAAA,IACF;AAGA,eAAW,QAAQ,kBAAkB;AACnC,YAAM,QAAQ,cAAc,MAAM,MAAM,UAAU;AAClD,UAAI,OAAO,QAAQ;AACjB,cAAM,SAAS,GAAG,IAAI;AACtB,YAAI,KAAK,sBAAsB,MAAM,EAAE;AAEvC,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,WAAW,KAAK,IAAI,IAAI;AAAA,UACxB,WAAW;AAAA,YACT;AAAA,cACE,MAAM;AAAA,cACN,QAAQ,GAAG,MAAM;AAAA,YACnB;AAAA,YACA,EAAE,MAAM,OAAO,OAAO,QAAQ,SAAS,OAAO;AAAA,UAChD;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,4BAA4B;AAAA,IACpC;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,UAAU,GAAG,EAAE;AAAA,IACvB;AAAA,EACF;AACF;AAKA,IAAI,QAAQ,KAAK,CAAC,MAAM,cAAc,YAAY,GAAG,GAAG;AACtD,QAAM,QAAmB,KAAK,MAAM,MAAM,UAAU,CAAC;AACrD,QAAM,SAAS,YAAY,KAAK;AAChC,UAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;AACpC;AAEA,eAAe,YAA6B;AAC1C,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,QAAQ,OAAO;AACvC,WAAO,KAAK,KAAK;AAAA,EACnB;AACA,SAAO,OAAO,KAAK,EAAE;AACvB;",
6
- "names": []
3
+ "sources": ["../../src/hooks/stop-continuation.mts", "../../src/hooks/runner.mts"],
4
+ "sourcesContent": ["/**\n * stop-continuation hook\n * Trigger: post-message (SessionEnd equivalent)\n * Priority: 50\n *\n * Detects active persistent modes (ralph, ultrawork, team) and\n * returns continue instructions so the user can decide whether\n * to keep going.\n */\n\nimport { readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface HookInput {\n hook_type: \"SessionEnd\";\n session_id?: string;\n message?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"stop\"; reason: string } | { type: \"log\"; level: \"info\"; message: string }>;\n log: string[];\n}\n\ninterface ModeState {\n active?: boolean;\n mode?: string;\n linked_ultrawork?: boolean;\n linked_team?: boolean;\n}\n\nfunction getModeStatePath(mode: string, sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, `${mode}-state.json`);\n }\n return join(base, `${mode}-state.json`);\n}\n\nfunction readModeState(mode: string, sessionId?: string): ModeState | null {\n try {\n const path = getModeStatePath(mode, sessionId);\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n// Priority order for checking: team > ralph > ultrawork\nconst PERSISTENT_MODES = [\"team\", \"ralph\", \"ultrawork\"];\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"SessionEnd\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Check for active persistent modes\n for (const mode of PERSISTENT_MODES) {\n const state = readModeState(mode, input.session_id);\n if (state?.active) {\n const reason = `${mode} mode is still active.`;\n log.push(`Stop continuation: ${reason}`);\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [\n {\n type: \"stop\",\n reason: `${reason} Use /cancel to end it, or continue the session to keep going.`,\n },\n { type: \"log\", level: \"info\", message: reason },\n ],\n log,\n };\n }\n }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [\"No persistent modes active\"],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { hookName: \"stop-continuation\" });\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
5
+ "mappings": ";AAUA,SAAS,oBAAoB;AAC7B,SAAS,WAAAA,gBAAe;AACxB,SAAS,QAAAC,aAAY;AAiGrB,SAAS,qBAAqB;;;AC/F9B,SAAS,gBAAgB,iBAAiB;AAC1C,SAAS,eAAe;AACxB,SAAS,YAAY;AAqBrB,eAAsB,YAA6B;AACjD,QAAM,kBAAkB,YAA6B;AACnD,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,KAAK,CAAC;AAAA,IAC3B;AACA,WAAO,OAAO,KAAK,EAAE;AAAA,EACvB;AAEA,QAAM,eAAe,IAAI;AAAA,IAAgB,CAAC,YACxC;AAAA,MACE,MAAM,QAAQ,EAAE;AAAA,MACf,SAAS,QAAQ,IAAI,6BAA6B,KAAK,KAAK;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO,QAAQ,KAAK,CAAC,gBAAgB,GAAG,YAAY,CAAC;AACvD;AAOA,SAAS,eAAe,MAAc,QAAsB;AAC1D,MAAI;AACF,YAAQ,OAAO,MAAM,wBAAwB,IAAI,KAAK,MAAM;AAAA,CAAI;AAAA,EAClE,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,UAAU,KAAK,QAAQ,GAAG,QAAQ,MAAM;AAC9C,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACtC,UAAM,SAAS,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,MAAM,OAAO,CAAC;AAC5E,mBAAe,KAAK,SAAS,qBAAqB,GAAG,SAAS,MAAM,OAAO;AAAA,EAC7E,QAAQ;AAAA,EAER;AACF;AAOA,eAAsB,YACpBC,cACA,UAA0B,CAAC,GACZ;AACf,MAAI;AACJ,MAAI;AACF,UAAM,QAAQ,KAAK,MAAM,MAAM,UAAU,CAAC;AAC1C,UAAM,aAAa,KAAK,UAAUA,aAAY,KAAK,CAAC;AACpD,QAAI,OAAO,eAAe,UAAU;AAClC,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AACA,iBAAa;AAAA,EACf,SAAS,KAAK;AACZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,mBAAe,QAAQ,YAAY,WAAW,MAAM;AACpD,UAAM,WAA2B;AAAA,MAC/B,GAAI,QAAQ,mBAAmB,EAAE,UAAU,QAAiB,IAAI,CAAC;AAAA,MACjE,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,cAAc,MAAM,EAAE;AAAA,IAC9B;AACA,iBAAa,KAAK,UAAU,QAAQ;AAAA,EACtC;AACA,UAAQ,IAAI,UAAU;AACtB,UAAQ,WAAW;AACrB;;;ADzEA,SAAS,iBAAiB,MAAc,WAA4B;AAClE,QAAM,OAAOC,MAAKC,SAAQ,GAAG,QAAQ,OAAO;AAC5C,MAAI,WAAW;AACb,WAAOD,MAAK,MAAM,YAAY,WAAW,GAAG,IAAI,aAAa;AAAA,EAC/D;AACA,SAAOA,MAAK,MAAM,GAAG,IAAI,aAAa;AACxC;AAEA,SAAS,cAAc,MAAc,WAAsC;AACzE,MAAI;AACF,UAAM,OAAO,iBAAiB,MAAM,SAAS;AAC7C,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,mBAAmB,CAAC,QAAQ,SAAS,WAAW;AAE/C,SAAS,YAAY,OAA8B;AACxD,QAAM,QAAQ,KAAK,IAAI;AACvB,QAAM,MAAgB,CAAC;AAEvB,MAAI;AACF,QAAI,MAAM,cAAc,cAAc;AACpC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,WAAW,CAAC;AAAA,QACZ,KAAK,CAAC;AAAA,MACR;AAAA,IACF;AAGA,eAAW,QAAQ,kBAAkB;AACnC,YAAM,QAAQ,cAAc,MAAM,MAAM,UAAU;AAClD,UAAI,OAAO,QAAQ;AACjB,cAAM,SAAS,GAAG,IAAI;AACtB,YAAI,KAAK,sBAAsB,MAAM,EAAE;AAEvC,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,WAAW,KAAK,IAAI,IAAI;AAAA,UACxB,WAAW;AAAA,YACT;AAAA,cACE,MAAM;AAAA,cACN,QAAQ,GAAG,MAAM;AAAA,YACnB;AAAA,YACA,EAAE,MAAM,OAAO,OAAO,QAAQ,SAAS,OAAO;AAAA,UAChD;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,4BAA4B;AAAA,IACpC;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,UAAU,GAAG,EAAE;AAAA,IACvB;AAAA,EACF;AACF;AAOA,IAAI,QAAQ,KAAK,CAAC,MAAM,cAAc,YAAY,GAAG,GAAG;AACtD,QAAM,YAAY,aAAa,EAAE,UAAU,oBAAoB,CAAC;AAClE;",
6
+ "names": ["homedir", "join", "processHook", "join", "homedir"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  // src/hooks/token-tracker.mts
2
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
3
- import { homedir as homedir2 } from "os";
4
- import { join as join2 } from "path";
2
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
3
+ import { homedir as homedir3 } from "os";
4
+ import { join as join3 } from "path";
5
5
 
6
6
  // src/spending/tracker.mts
7
7
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
@@ -64,6 +64,67 @@ function incrementSpending(sessionId) {
64
64
 
65
65
  // src/hooks/token-tracker.mts
66
66
  import { fileURLToPath } from "url";
67
+
68
+ // src/hooks/runner.mts
69
+ import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
70
+ import { homedir as homedir2 } from "os";
71
+ import { join as join2 } from "path";
72
+ async function readStdin() {
73
+ const readStdinActual = async () => {
74
+ const chunks = [];
75
+ for await (const chunk of process.stdin) {
76
+ chunks.push(String(chunk));
77
+ }
78
+ return chunks.join("");
79
+ };
80
+ const stdinTimeout = new Promise(
81
+ (resolve) => setTimeout(
82
+ () => resolve(""),
83
+ parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
84
+ )
85
+ );
86
+ return Promise.race([readStdinActual(), stdinTimeout]);
87
+ }
88
+ function logHookFailure(hook, reason) {
89
+ try {
90
+ process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
91
+ `);
92
+ } catch {
93
+ }
94
+ try {
95
+ const logsDir = join2(homedir2(), ".omp", "logs");
96
+ mkdirSync2(logsDir, { recursive: true });
97
+ const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
98
+ appendFileSync(join2(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
99
+ } catch {
100
+ }
101
+ }
102
+ async function runHookMain(processHook2, options = {}) {
103
+ let outputJson;
104
+ try {
105
+ const input = JSON.parse(await readStdin());
106
+ const serialized = JSON.stringify(processHook2(input));
107
+ if (typeof serialized !== "string") {
108
+ throw new Error("hook produced no serializable output");
109
+ }
110
+ outputJson = serialized;
111
+ } catch (err) {
112
+ const reason = err instanceof Error ? err.message : String(err);
113
+ logHookFailure(options.hookName ?? "unknown", reason);
114
+ const failOpen = {
115
+ ...options.failOpenDecision ? { decision: "allow" } : {},
116
+ status: "error",
117
+ latencyMs: 0,
118
+ mutations: [],
119
+ log: [`fail-open: ${reason}`]
120
+ };
121
+ outputJson = JSON.stringify(failOpen);
122
+ }
123
+ console.log(outputJson);
124
+ process.exitCode = 0;
125
+ }
126
+
127
+ // src/hooks/token-tracker.mts
67
128
  var MODEL_CONTEXTS = {
68
129
  "claude-sonnet-4.5": 2e5,
69
130
  "claude-sonnet-4": 2e5,
@@ -85,14 +146,14 @@ function estimateTokens(input) {
85
146
  }
86
147
  }
87
148
  function getStatePath(sessionId) {
88
- const base = join2(homedir2(), ".omp", "state");
149
+ const base = join3(homedir3(), ".omp", "state");
89
150
  if (sessionId) {
90
- return join2(base, "sessions", sessionId, "session.json");
151
+ return join3(base, "sessions", sessionId, "session.json");
91
152
  }
92
- return join2(base, "session.json");
153
+ return join3(base, "session.json");
93
154
  }
94
155
  function ensureDir(path) {
95
- mkdirSync2(path.substring(0, path.lastIndexOf("/")), { recursive: true });
156
+ mkdirSync3(path.substring(0, path.lastIndexOf("/")), { recursive: true });
96
157
  }
97
158
  function processHook(input) {
98
159
  const start = Date.now();
@@ -109,7 +170,13 @@ function processHook(input) {
109
170
  const statePath = getStatePath(input.session_id);
110
171
  let state;
111
172
  try {
112
- state = JSON.parse(readFileSync2(statePath, "utf-8"));
173
+ const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
174
+ state = {
175
+ ...raw,
176
+ warnings_issued: new Set(
177
+ Array.isArray(raw.warnings_issued) ? raw.warnings_issued : []
178
+ )
179
+ };
113
180
  } catch {
114
181
  const fallbackModel = input.model ?? "default";
115
182
  state = {
@@ -138,7 +205,11 @@ function processHook(input) {
138
205
  }
139
206
  try {
140
207
  ensureDir(statePath);
141
- writeFileSync2(statePath, JSON.stringify(state), "utf-8");
208
+ writeFileSync2(
209
+ statePath,
210
+ JSON.stringify({ ...state, warnings_issued: Array.from(state.warnings_issued) }),
211
+ "utf-8"
212
+ );
142
213
  } catch (e) {
143
214
  log.push(`Failed to write state: ${e}`);
144
215
  }
@@ -163,16 +234,7 @@ function processHook(input) {
163
234
  }
164
235
  }
165
236
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
166
- const input = JSON.parse(await readStdin());
167
- const output = processHook(input);
168
- console.log(JSON.stringify(output));
169
- }
170
- async function readStdin() {
171
- const chunks = [];
172
- for await (const chunk of process.stdin) {
173
- chunks.push(chunk);
174
- }
175
- return chunks.join("");
237
+ await runHookMain(processHook, { hookName: "token-tracker" });
176
238
  }
177
239
  export {
178
240
  MODEL_CONTEXTS,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/token-tracker.mts", "../../src/spending/tracker.mts"],
4
- "sourcesContent": ["/**\n * token-tracker hook\n * Trigger: post-message (PostToolUse equivalent)\n * Priority: 70\n *\n * Estimates token usage from character counts (1 token \u2248 4 chars).\n * Accumulates in session state. Warns at 60%, 80%, 90% thresholds.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { incrementSpending } from \"../spending/tracker.mjs\";\n\nexport interface HookInput {\n hook_type: \"PostToolUse\";\n tool_name?: string;\n tool_input?: unknown;\n tool_output?: unknown;\n session_id?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_token_budget\"; budget: number } | { type: \"emit_hud\"; hudEmit: unknown } | { type: \"log\"; level: \"info\" | \"warn\" | \"error\"; message: string }>;\n log: string[];\n}\n\ninterface SessionState {\n tokens_estimated: number;\n token_budget: number;\n context_pct: number;\n warnings_issued: Set<string>;\n}\n\n// Model context windows in tokens (for future model-specific budget lookup)\n// Exported for potential external use\nexport const MODEL_CONTEXTS = {\n \"claude-sonnet-4.5\": 200_000,\n \"claude-sonnet-4\": 200_000,\n \"claude-sonnet-4.6\": 200_000,\n \"claude-opus-4.6\": 200_000,\n \"gpt-5\": 128_000,\n \"gpt-5.4-mini\": 128_000,\n \"gemini-3-pro\": 128_000,\n default: 200_000,\n};\n\nconst WARNING_THRESHOLDS = [60, 80, 90];\n\nexport function estimateTokens(input: unknown): number {\n if (!input) return 0;\n try {\n const str = typeof input === \"string\" ? input : JSON.stringify(input);\n return Math.ceil(str.length / 4);\n } catch {\n return 0;\n }\n}\n\nfunction getStatePath(sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, \"session.json\");\n }\n return join(base, \"session.json\");\n}\n\nfunction ensureDir(path: string): void {\n mkdirSync(path.substring(0, path.lastIndexOf(\"/\")), { recursive: true });\n}\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"PostToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const statePath = getStatePath(input.session_id);\n let state: SessionState;\n\n try {\n state = JSON.parse(readFileSync(statePath, \"utf-8\"));\n } catch {\n // Initialize state if not found \u2014 budget derived from model when available\n const fallbackModel = (input as { model?: string }).model ?? \"default\";\n state = {\n tokens_estimated: 0,\n token_budget: (MODEL_CONTEXTS as Record<string, number>)[fallbackModel] ?? MODEL_CONTEXTS[\"default\"] ?? 200_000,\n context_pct: 0,\n warnings_issued: new Set(),\n };\n }\n\n const inputTokens = estimateTokens(input.tool_input);\n const outputTokens = estimateTokens(input.tool_output);\n const delta = inputTokens + outputTokens;\n\n state.tokens_estimated += delta;\n state.context_pct = Math.min(100, Math.round((state.tokens_estimated / state.token_budget) * 100));\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_token_budget\", budget: state.token_budget },\n ];\n\n // Check warning thresholds\n for (const threshold of WARNING_THRESHOLDS) {\n const key = `warn_${threshold}`;\n if (state.context_pct >= threshold && !state.warnings_issued.has(key)) {\n state.warnings_issued.add(key);\n const message =\n threshold >= 90\n ? `CRITICAL: Context at ${state.context_pct}%. Tokens near budget limit.`\n : threshold >= 80\n ? `WARNING: Context at ${state.context_pct}%. Consider enabling ecomode.`\n : `INFO: Context at ${state.context_pct}%.`;\n mutations.push({ type: \"log\", level: threshold >= 80 ? \"warn\" : \"info\", message });\n log.push(message);\n }\n }\n\n // Write state back\n try {\n ensureDir(statePath);\n writeFileSync(statePath, JSON.stringify(state), \"utf-8\");\n } catch (e) {\n log.push(`Failed to write state: ${e}`);\n }\n\n // Track premium request spending\n const sessionId = input.session_id ?? `omp-${Date.now()}`;\n try { incrementSpending(sessionId); } catch { /* non-blocking */ }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations,\n log,\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported)\nimport { fileURLToPath } from \"url\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n const input: HookInput = JSON.parse(await readStdin());\n const output = processHook(input);\n console.log(JSON.stringify(output));\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n return chunks.join(\"\");\n}\n", "/**\n * Spending tracker for OMP.\n * Tracks premium API requests per session and per calendar month.\n * Persists to ~/.omp/state/spending-monthly.json\n *\n * // v1.1 known limitation: no /omp:spending reset command. To reset monthly counter manually: rm ~/.omp/state/spending-monthly.json\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join, dirname } from \"path\";\nimport type { SpendingState } from \"./types.mjs\";\n\nconst SPENDING_PATH = join(homedir(), \".omp\", \"state\", \"spending-monthly.json\");\n\nfunction currentMonth(): string {\n const now = new Date();\n return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n}\n\nexport function loadSpending(sessionId: string): SpendingState {\n let raw: SpendingState;\n try {\n raw = JSON.parse(readFileSync(SPENDING_PATH, \"utf-8\")) as SpendingState;\n } catch {\n // Missing or malformed file \u2014 start fresh\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month: currentMonth(),\n monthlyPremiumRequests: 0,\n };\n }\n\n const month = currentMonth();\n\n // Reset monthly counter when month rolls over\n if (raw.month !== month) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: 0,\n };\n }\n\n // Reset session counter when session changes\n if (raw.sessionId !== sessionId) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: raw.monthlyPremiumRequests,\n };\n }\n\n return { ...raw, version: 1 };\n}\n\nexport function saveSpending(state: SpendingState): void {\n try {\n mkdirSync(dirname(SPENDING_PATH), { recursive: true });\n writeFileSync(SPENDING_PATH, JSON.stringify(state, null, 2), \"utf-8\");\n } catch (e) {\n console.warn(`[OMP] spending: failed to save state: ${e}`);\n }\n}\n\nexport function incrementSpending(sessionId: string): SpendingState {\n const state = loadSpending(sessionId);\n state.sessionPremiumRequests += 1;\n state.monthlyPremiumRequests += 1;\n saveSpending(state);\n return state;\n}\n"],
5
- "mappings": "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",
6
- "names": ["readFileSync", "writeFileSync", "mkdirSync", "homedir", "join", "join", "homedir", "mkdirSync", "readFileSync", "writeFileSync"]
3
+ "sources": ["../../src/hooks/token-tracker.mts", "../../src/spending/tracker.mts", "../../src/hooks/runner.mts"],
4
+ "sourcesContent": ["/**\n * token-tracker hook\n * Trigger: post-message (PostToolUse equivalent)\n * Priority: 70\n *\n * Estimates token usage from character counts (1 token \u2248 4 chars).\n * Accumulates in session state. Warns at 60%, 80%, 90% thresholds.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { incrementSpending } from \"../spending/tracker.mjs\";\n\nexport interface HookInput {\n hook_type: \"PostToolUse\";\n tool_name?: string;\n tool_input?: unknown;\n tool_output?: unknown;\n session_id?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_token_budget\"; budget: number } | { type: \"emit_hud\"; hudEmit: unknown } | { type: \"log\"; level: \"info\" | \"warn\" | \"error\"; message: string }>;\n log: string[];\n}\n\ninterface SessionState {\n tokens_estimated: number;\n token_budget: number;\n context_pct: number;\n warnings_issued: Set<string>;\n}\n\n// Model context windows in tokens (for future model-specific budget lookup)\n// Exported for potential external use\nexport const MODEL_CONTEXTS = {\n \"claude-sonnet-4.5\": 200_000,\n \"claude-sonnet-4\": 200_000,\n \"claude-sonnet-4.6\": 200_000,\n \"claude-opus-4.6\": 200_000,\n \"gpt-5\": 128_000,\n \"gpt-5.4-mini\": 128_000,\n \"gemini-3-pro\": 128_000,\n default: 200_000,\n};\n\nconst WARNING_THRESHOLDS = [60, 80, 90];\n\nexport function estimateTokens(input: unknown): number {\n if (!input) return 0;\n try {\n const str = typeof input === \"string\" ? input : JSON.stringify(input);\n return Math.ceil(str.length / 4);\n } catch {\n return 0;\n }\n}\n\nfunction getStatePath(sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, \"session.json\");\n }\n return join(base, \"session.json\");\n}\n\nfunction ensureDir(path: string): void {\n mkdirSync(path.substring(0, path.lastIndexOf(\"/\")), { recursive: true });\n}\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"PostToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const statePath = getStatePath(input.session_id);\n let state: SessionState;\n\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Record<string, unknown>;\n // warnings_issued is persisted as an array (a Set JSON-serializes to {}).\n // Rehydrate to a Set; tolerate legacy state files where it is {} or missing.\n state = {\n ...raw,\n warnings_issued: new Set(\n Array.isArray(raw.warnings_issued) ? (raw.warnings_issued as string[]) : []\n ),\n } as unknown as SessionState;\n } catch {\n // Initialize state if not found \u2014 budget derived from model when available\n const fallbackModel = (input as { model?: string }).model ?? \"default\";\n state = {\n tokens_estimated: 0,\n token_budget: (MODEL_CONTEXTS as Record<string, number>)[fallbackModel] ?? MODEL_CONTEXTS[\"default\"] ?? 200_000,\n context_pct: 0,\n warnings_issued: new Set(),\n };\n }\n\n const inputTokens = estimateTokens(input.tool_input);\n const outputTokens = estimateTokens(input.tool_output);\n const delta = inputTokens + outputTokens;\n\n state.tokens_estimated += delta;\n state.context_pct = Math.min(100, Math.round((state.tokens_estimated / state.token_budget) * 100));\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_token_budget\", budget: state.token_budget },\n ];\n\n // Check warning thresholds\n for (const threshold of WARNING_THRESHOLDS) {\n const key = `warn_${threshold}`;\n if (state.context_pct >= threshold && !state.warnings_issued.has(key)) {\n state.warnings_issued.add(key);\n const message =\n threshold >= 90\n ? `CRITICAL: Context at ${state.context_pct}%. Tokens near budget limit.`\n : threshold >= 80\n ? `WARNING: Context at ${state.context_pct}%. Consider enabling ecomode.`\n : `INFO: Context at ${state.context_pct}%.`;\n mutations.push({ type: \"log\", level: threshold >= 80 ? \"warn\" : \"info\", message });\n log.push(message);\n }\n }\n\n // Write state back \u2014 persist warnings_issued as an array since a Set\n // JSON-serializes to {} and would break .has() on the next invocation.\n try {\n ensureDir(statePath);\n writeFileSync(\n statePath,\n JSON.stringify({ ...state, warnings_issued: Array.from(state.warnings_issued) }),\n \"utf-8\"\n );\n } catch (e) {\n log.push(`Failed to write state: ${e}`);\n }\n\n // Track premium request spending\n const sessionId = input.session_id ?? `omp-${Date.now()}`;\n try { incrementSpending(sessionId); } catch { /* non-blocking */ }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations,\n log,\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { hookName: \"token-tracker\" });\n}\n", "/**\n * Spending tracker for OMP.\n * Tracks premium API requests per session and per calendar month.\n * Persists to ~/.omp/state/spending-monthly.json\n *\n * // v1.1 known limitation: no /omp:spending reset command. To reset monthly counter manually: rm ~/.omp/state/spending-monthly.json\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join, dirname } from \"path\";\nimport type { SpendingState } from \"./types.mjs\";\n\nconst SPENDING_PATH = join(homedir(), \".omp\", \"state\", \"spending-monthly.json\");\n\nfunction currentMonth(): string {\n const now = new Date();\n return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n}\n\nexport function loadSpending(sessionId: string): SpendingState {\n let raw: SpendingState;\n try {\n raw = JSON.parse(readFileSync(SPENDING_PATH, \"utf-8\")) as SpendingState;\n } catch {\n // Missing or malformed file \u2014 start fresh\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month: currentMonth(),\n monthlyPremiumRequests: 0,\n };\n }\n\n const month = currentMonth();\n\n // Reset monthly counter when month rolls over\n if (raw.month !== month) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: 0,\n };\n }\n\n // Reset session counter when session changes\n if (raw.sessionId !== sessionId) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: raw.monthlyPremiumRequests,\n };\n }\n\n return { ...raw, version: 1 };\n}\n\nexport function saveSpending(state: SpendingState): void {\n try {\n mkdirSync(dirname(SPENDING_PATH), { recursive: true });\n writeFileSync(SPENDING_PATH, JSON.stringify(state, null, 2), \"utf-8\");\n } catch (e) {\n console.warn(`[OMP] spending: failed to save state: ${e}`);\n }\n}\n\nexport function incrementSpending(sessionId: string): SpendingState {\n const state = loadSpending(sessionId);\n state.sessionPremiumRequests += 1;\n state.monthlyPremiumRequests += 1;\n saveSpending(state);\n return state;\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["readFileSync", "writeFileSync", "mkdirSync", "homedir", "join", "mkdirSync", "homedir", "join", "processHook", "join", "homedir", "mkdirSync", "readFileSync", "writeFileSync"]
7
7
  }
@@ -28324,7 +28324,7 @@ var TOOLS = [
28324
28324
  },
28325
28325
  {
28326
28326
  name: "omp_get_agents",
28327
- description: "List all 18 OMP agents with their IDs, tiers, tools, and roles",
28327
+ description: "List all 19 OMP agents with their IDs, tiers, tools, and roles",
28328
28328
  inputSchema: { type: "object", properties: {} }
28329
28329
  },
28330
28330
  {
@@ -28336,24 +28336,25 @@ var TOOLS = [
28336
28336
  agentId: {
28337
28337
  type: "string",
28338
28338
  enum: [
28339
- "orchestrator",
28340
- "explorer",
28341
- "planner",
28342
- "executor",
28343
- "verifier",
28344
- "writer",
28345
- "reviewer",
28346
- "designer",
28347
- "researcher",
28348
- "tester",
28349
- "debugger",
28339
+ "analyst",
28350
28340
  "architect",
28341
+ "code-reviewer",
28342
+ "code-simplifier",
28343
+ "critic",
28344
+ "debugger",
28345
+ "designer",
28346
+ "document-specialist",
28347
+ "executor",
28348
+ "explore",
28351
28349
  "git-master",
28350
+ "planner",
28351
+ "qa-tester",
28352
+ "scientist",
28352
28353
  "security-reviewer",
28353
- "data",
28354
- "mobile",
28355
- "performance",
28356
- "integration"
28354
+ "test-engineer",
28355
+ "tracer",
28356
+ "verifier",
28357
+ "writer"
28357
28358
  ]
28358
28359
  },
28359
28360
  task: { type: "string", description: "Task description" },