@cruxy/cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agent/loop.d.ts +14 -0
  2. package/dist/agent/loop.js +47 -1
  3. package/dist/agent/session.d.ts +11 -1
  4. package/dist/agent/session.js +14 -1
  5. package/dist/brand/index.d.ts +1 -0
  6. package/dist/brand/index.js +1 -0
  7. package/dist/brand/voice.d.ts +74 -0
  8. package/dist/brand/voice.js +73 -0
  9. package/dist/cli/commands/checkpoint.js +1 -1
  10. package/dist/cli/commands/hooks.d.ts +8 -0
  11. package/dist/cli/commands/hooks.js +83 -0
  12. package/dist/cli/commands/init.js +1 -1
  13. package/dist/cli/commands/pr.js +1 -1
  14. package/dist/cli/commands/rollback.js +1 -1
  15. package/dist/cli/commands/run.js +13 -3
  16. package/dist/cli/commands/skills.js +2 -2
  17. package/dist/cli/program.js +5 -2
  18. package/dist/cli/repl.d.ts +2 -1
  19. package/dist/cli/repl.js +54 -3
  20. package/dist/cli/session-factory.d.ts +2 -2
  21. package/dist/cli/session-factory.js +4 -2
  22. package/dist/config/schema.d.ts +81 -30
  23. package/dist/config/schema.js +22 -0
  24. package/dist/constants.d.ts +9 -0
  25. package/dist/constants.js +9 -0
  26. package/dist/errors/constructors.d.ts +16 -0
  27. package/dist/errors/constructors.js +57 -0
  28. package/dist/errors/types.d.ts +11 -0
  29. package/dist/errors/types.js +19 -0
  30. package/dist/hooks/config.d.ts +21 -0
  31. package/dist/hooks/config.js +253 -0
  32. package/dist/hooks/index.d.ts +6 -0
  33. package/dist/hooks/index.js +6 -0
  34. package/dist/hooks/runner.d.ts +76 -0
  35. package/dist/hooks/runner.js +114 -0
  36. package/dist/hooks/service.d.ts +38 -0
  37. package/dist/hooks/service.js +49 -0
  38. package/dist/hooks/slash.d.ts +48 -0
  39. package/dist/hooks/slash.js +58 -0
  40. package/dist/hooks/trust.d.ts +46 -0
  41. package/dist/hooks/trust.js +106 -0
  42. package/dist/hooks/types.d.ts +147 -0
  43. package/dist/hooks/types.js +61 -0
  44. package/dist/onboarding/steps.js +1 -1
  45. package/dist/tools/shell/exec.d.ts +53 -0
  46. package/dist/tools/shell/exec.js +128 -0
  47. package/dist/tools/shell/run-command.d.ts +4 -0
  48. package/dist/tools/shell/run-command.js +26 -116
  49. package/package.json +1 -1
@@ -4,7 +4,7 @@ import type { CheckpointService } from "../checkpoint/index.js";
4
4
  import type { SandboxService } from "../sandbox/index.js";
5
5
  import type { StreamRenderer } from "../render/index.js";
6
6
  import { type ApproveAction } from "../tools/index.js";
7
- import { Session } from "../agent/index.js";
7
+ import { Session, type LifecycleHookRunner } from "../agent/index.js";
8
8
  /**
9
9
  * Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
10
10
  * whole point: a tool mutates only *after* `requestApproval` resolves, so
@@ -24,4 +24,4 @@ export declare function withCheckpointGate(requestApproval: (action: ApproveActi
24
24
  * over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
25
25
  * approves → executes. Plan mode is fully opt-in; the default path is unchanged.
26
26
  */
27
- export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService): Session;
27
+ export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService, hooks?: LifecycleHookRunner): Session;
@@ -5,7 +5,7 @@ import { getGitInfo } from "../utils/git.js";
5
5
  import { ApprovalService, InteractivePolicy, SessionAllowlist, classify, defaultPromptIO, } from "../approval/index.js";
6
6
  import { shouldUseColor } from "../errors/index.js";
7
7
  import { buildDefaultRegistry } from "../tools/index.js";
8
- import { Session } from "../agent/index.js";
8
+ import { Session, } from "../agent/index.js";
9
9
  import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
10
10
  import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
11
11
  /**
@@ -87,7 +87,7 @@ export function withCheckpointGate(requestApproval, checkpoints, cwd) {
87
87
  * over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
88
88
  * approves → executes. Plan mode is fully opt-in; the default path is unchanged.
89
89
  */
90
- export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox) {
90
+ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox, hooks) {
91
91
  const provider = createProvider({
92
92
  provider: config.model.provider,
93
93
  apiKey,
@@ -166,6 +166,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
166
166
  projectInstructions,
167
167
  planMode: true,
168
168
  planRunner,
169
+ hooks,
169
170
  });
170
171
  }
171
172
  const approval = new ApprovalService({
@@ -181,5 +182,6 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
181
182
  ctx,
182
183
  git,
183
184
  projectInstructions,
185
+ hooks,
184
186
  });
185
187
  }
@@ -339,6 +339,32 @@ export declare const SandboxConfigSchema: z.ZodObject<{
339
339
  mounts?: string[] | undefined;
340
340
  }>;
341
341
  export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
342
+ /**
343
+ * Hooks + custom slash commands (C.19). Hooks run user-authored shell commands
344
+ * on lifecycle events — a real execution surface — so they are OFF by default
345
+ * and, when on, still funnel through the SAME U.3 gate + C.16 sandbox as any
346
+ * agent command (never a bypass). The hook *definitions* live in layered
347
+ * `hooks.json` files (project > user, source-tracked for the trust model); this
348
+ * schema holds only the global toggles.
349
+ */
350
+ export declare const HooksConfigSchema: z.ZodObject<{
351
+ /** Master switch. When false, NO hook ever fires (config with hooks stays
352
+ * inert until explicitly enabled). */
353
+ enabled: z.ZodDefault<z.ZodBoolean>;
354
+ /**
355
+ * Whether to interactively prompt to trust a project's hooks the first time
356
+ * they would run. When false, untrusted project hooks fail loud
357
+ * (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
358
+ */
359
+ trustPrompt: z.ZodDefault<z.ZodBoolean>;
360
+ }, "strict", z.ZodTypeAny, {
361
+ enabled: boolean;
362
+ trustPrompt: boolean;
363
+ }, {
364
+ enabled?: boolean | undefined;
365
+ trustPrompt?: boolean | undefined;
366
+ }>;
367
+ export type HooksConfig = z.infer<typeof HooksConfigSchema>;
342
368
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
343
369
  export declare const McpServerSchema: z.ZodObject<{
344
370
  command: z.ZodOptional<z.ZodString>;
@@ -654,6 +680,23 @@ export declare const CruxyConfigSchema: z.ZodObject<{
654
680
  timeout?: number | undefined;
655
681
  mounts?: string[] | undefined;
656
682
  }>>;
683
+ hooks: z.ZodDefault<z.ZodObject<{
684
+ /** Master switch. When false, NO hook ever fires (config with hooks stays
685
+ * inert until explicitly enabled). */
686
+ enabled: z.ZodDefault<z.ZodBoolean>;
687
+ /**
688
+ * Whether to interactively prompt to trust a project's hooks the first time
689
+ * they would run. When false, untrusted project hooks fail loud
690
+ * (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
691
+ */
692
+ trustPrompt: z.ZodDefault<z.ZodBoolean>;
693
+ }, "strict", z.ZodTypeAny, {
694
+ enabled: boolean;
695
+ trustPrompt: boolean;
696
+ }, {
697
+ enabled?: boolean | undefined;
698
+ trustPrompt?: boolean | undefined;
699
+ }>>;
657
700
  mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
658
701
  command: z.ZodOptional<z.ZodString>;
659
702
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -672,6 +715,23 @@ export declare const CruxyConfigSchema: z.ZodObject<{
672
715
  cruxy: {
673
716
  gatewayUrl: string;
674
717
  };
718
+ checkpoint: {
719
+ enabled: boolean;
720
+ retention: number;
721
+ };
722
+ sandbox: {
723
+ image: string;
724
+ enabled: boolean;
725
+ memory: string;
726
+ network: "none" | "host-loopback" | "full";
727
+ pids: number;
728
+ cpus: number;
729
+ mounts: string[];
730
+ timeout?: number | undefined;
731
+ };
732
+ approval: {
733
+ mode: "prompt";
734
+ };
675
735
  model: {
676
736
  provider: "cruxy" | "anthropic" | "openai" | "custom";
677
737
  model: string;
@@ -702,9 +762,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
702
762
  compactThreshold: number;
703
763
  keepRecentMessages: number;
704
764
  };
705
- approval: {
706
- mode: "prompt";
707
- };
708
765
  index: {
709
766
  search: {
710
767
  defaultK: number;
@@ -720,10 +777,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
720
777
  overlapLines: number;
721
778
  };
722
779
  };
723
- checkpoint: {
724
- enabled: boolean;
725
- retention: number;
726
- };
727
780
  subagent: {
728
781
  maxDepth: number;
729
782
  defaultBudget: {
@@ -737,15 +790,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
737
790
  captureBytes: number;
738
791
  command?: string | undefined;
739
792
  };
740
- sandbox: {
741
- image: string;
793
+ hooks: {
742
794
  enabled: boolean;
743
- memory: string;
744
- network: "none" | "host-loopback" | "full";
745
- pids: number;
746
- cpus: number;
747
- mounts: string[];
748
- timeout?: number | undefined;
795
+ trustPrompt: boolean;
749
796
  };
750
797
  mcpServers: Record<string, {
751
798
  command?: string | undefined;
@@ -757,6 +804,23 @@ export declare const CruxyConfigSchema: z.ZodObject<{
757
804
  cruxy?: {
758
805
  gatewayUrl?: string | undefined;
759
806
  } | undefined;
807
+ checkpoint?: {
808
+ enabled?: boolean | undefined;
809
+ retention?: number | undefined;
810
+ } | undefined;
811
+ sandbox?: {
812
+ image?: string | undefined;
813
+ enabled?: boolean | undefined;
814
+ memory?: string | undefined;
815
+ network?: "none" | "host-loopback" | "full" | undefined;
816
+ pids?: number | undefined;
817
+ cpus?: number | undefined;
818
+ timeout?: number | undefined;
819
+ mounts?: string[] | undefined;
820
+ } | undefined;
821
+ approval?: {
822
+ mode?: "prompt" | undefined;
823
+ } | undefined;
760
824
  model?: {
761
825
  provider?: "cruxy" | "anthropic" | "openai" | "custom" | undefined;
762
826
  model?: string | undefined;
@@ -787,9 +851,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
787
851
  compactThreshold?: number | undefined;
788
852
  keepRecentMessages?: number | undefined;
789
853
  } | undefined;
790
- approval?: {
791
- mode?: "prompt" | undefined;
792
- } | undefined;
793
854
  index?: {
794
855
  search?: {
795
856
  defaultK?: number | undefined;
@@ -805,10 +866,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
805
866
  overlapLines?: number | undefined;
806
867
  } | undefined;
807
868
  } | undefined;
808
- checkpoint?: {
809
- enabled?: boolean | undefined;
810
- retention?: number | undefined;
811
- } | undefined;
812
869
  subagent?: {
813
870
  maxDepth?: number | undefined;
814
871
  defaultBudget?: {
@@ -822,15 +879,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
822
879
  command?: string | undefined;
823
880
  captureBytes?: number | undefined;
824
881
  } | undefined;
825
- sandbox?: {
826
- image?: string | undefined;
882
+ hooks?: {
827
883
  enabled?: boolean | undefined;
828
- memory?: string | undefined;
829
- network?: "none" | "host-loopback" | "full" | undefined;
830
- pids?: number | undefined;
831
- cpus?: number | undefined;
832
- timeout?: number | undefined;
833
- mounts?: string[] | undefined;
884
+ trustPrompt?: boolean | undefined;
834
885
  } | undefined;
835
886
  mcpServers?: Record<string, {
836
887
  command?: string | undefined;
@@ -233,6 +233,27 @@ export const SandboxConfigSchema = z
233
233
  mounts: z.array(z.string().min(1)).default([]),
234
234
  })
235
235
  .strict();
236
+ /**
237
+ * Hooks + custom slash commands (C.19). Hooks run user-authored shell commands
238
+ * on lifecycle events — a real execution surface — so they are OFF by default
239
+ * and, when on, still funnel through the SAME U.3 gate + C.16 sandbox as any
240
+ * agent command (never a bypass). The hook *definitions* live in layered
241
+ * `hooks.json` files (project > user, source-tracked for the trust model); this
242
+ * schema holds only the global toggles.
243
+ */
244
+ export const HooksConfigSchema = z
245
+ .object({
246
+ /** Master switch. When false, NO hook ever fires (config with hooks stays
247
+ * inert until explicitly enabled). */
248
+ enabled: z.boolean().default(false),
249
+ /**
250
+ * Whether to interactively prompt to trust a project's hooks the first time
251
+ * they would run. When false, untrusted project hooks fail loud
252
+ * (CRUXY_E_HOOK_UNTRUSTED) instead of prompting — never auto-trust.
253
+ */
254
+ trustPrompt: z.boolean().default(true),
255
+ })
256
+ .strict();
236
257
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
237
258
  export const McpServerSchema = z
238
259
  .object({
@@ -256,6 +277,7 @@ export const CruxyConfigSchema = z
256
277
  subagent: SubagentConfigSchema.default({}),
257
278
  test: TestConfigSchema.default({}),
258
279
  sandbox: SandboxConfigSchema.default({}),
280
+ hooks: HooksConfigSchema.default({}),
259
281
  mcpServers: z.record(z.string(), McpServerSchema).default({}),
260
282
  logLevel: z.enum(LOG_LEVELS).default("info"),
261
283
  })
@@ -21,6 +21,15 @@ export declare const PROJECT_INSTRUCTION_FILENAMES: string[];
21
21
  * root for shipped builtins.
22
22
  */
23
23
  export declare const SKILLS_DIR_NAME = "skills";
24
+ /** Hook definitions file, under the project dir (`<cwd>/.cruxy/hooks.json`) and
25
+ * the global dir (`~/.cruxy/hooks.json`). Pure data, validated, never eval'd (C.19). */
26
+ export declare const HOOKS_FILE_NAME = "hooks.json";
27
+ /** Custom slash-command directory (`<cwd>/.cruxy/commands`, `~/.cruxy/commands`);
28
+ * one `<name>.md` per command (frontmatter + template body), mirroring skills. */
29
+ export declare const COMMANDS_DIR_NAME = "commands";
30
+ /** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
31
+ * never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
32
+ export declare const TRUST_FILE_NAME = "trust.json";
24
33
  /**
25
34
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
26
35
  * Anchored the same way as the package.json lookup above: both `dist/` and
package/dist/constants.js CHANGED
@@ -41,6 +41,15 @@ export const PROJECT_INSTRUCTION_FILENAMES = ["CRUXY.md", "AGENTS.md"];
41
41
  * root for shipped builtins.
42
42
  */
43
43
  export const SKILLS_DIR_NAME = "skills";
44
+ /** Hook definitions file, under the project dir (`<cwd>/.cruxy/hooks.json`) and
45
+ * the global dir (`~/.cruxy/hooks.json`). Pure data, validated, never eval'd (C.19). */
46
+ export const HOOKS_FILE_NAME = "hooks.json";
47
+ /** Custom slash-command directory (`<cwd>/.cruxy/commands`, `~/.cruxy/commands`);
48
+ * one `<name>.md` per command (frontmatter + template body), mirroring skills. */
49
+ export const COMMANDS_DIR_NAME = "commands";
50
+ /** Per-repo hook-trust record, in the GLOBAL dir only (`~/.cruxy/trust.json`) —
51
+ * never in a repo, so cloning carries no trust (C.19 supply-chain safety). */
52
+ export const TRUST_FILE_NAME = "trust.json";
44
53
  /**
45
54
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
46
55
  * Anchored the same way as the package.json lookup above: both `dist/` and
@@ -46,6 +46,22 @@ export declare function sandboxImage(image: string, underlying?: unknown): Cruxy
46
46
  * can't launch is never papered over with a host run.
47
47
  */
48
48
  export declare function sandboxExec(underlying?: unknown): CruxyError;
49
+ /**
50
+ * A blocking hook failed (non-zero exit, gate-declined, or errored). The action
51
+ * that fired it is aborted fail-closed — a failing pre-check must stop, never
52
+ * proceed. Advisory hooks report but never raise this.
53
+ */
54
+ export declare function hookFailed(name: string, reason: string, underlying?: unknown): CruxyError;
55
+ /**
56
+ * A project (a cloned/opened repo) defines hooks that have not been trusted for
57
+ * this machine. Cruxy NEVER runs another author's hooks silently — trust is an
58
+ * explicit, per-repo decision recorded in your home, not in the repo.
59
+ */
60
+ export declare function hookUntrusted(root: string, count: number): CruxyError;
61
+ /** A malformed hook definition — excluded from the catalog, never eval'd. */
62
+ export declare function hookInvalid(reason: string, source?: string): CruxyError;
63
+ /** A malformed custom slash-command definition — excluded and surfaced. */
64
+ export declare function slashInvalid(reason: string, source?: string): CruxyError;
49
65
  /**
50
66
  * A side-effecting action needs approval but cruxy can't ask (non-interactive,
51
67
  * no policy). Default-deny — never auto-approve. A distinct exit code (10) so CI
@@ -282,6 +282,63 @@ export function sandboxExec(underlying) {
282
282
  underlying,
283
283
  });
284
284
  }
285
+ // ── hooks + custom slash commands (exit 13) — C.19 ────────────────────────────
286
+ /**
287
+ * A blocking hook failed (non-zero exit, gate-declined, or errored). The action
288
+ * that fired it is aborted fail-closed — a failing pre-check must stop, never
289
+ * proceed. Advisory hooks report but never raise this.
290
+ */
291
+ export function hookFailed(name, reason, underlying) {
292
+ return new CruxyError({
293
+ code: ErrorCode.HookFailed,
294
+ title: `blocking hook "${name}" failed — action aborted`,
295
+ cause: reason,
296
+ nextSteps: [
297
+ `fix the hook command, or mark it advisory (blocking: false) if it should not block`,
298
+ "run `cruxy hooks list` to inspect the configured hooks",
299
+ ],
300
+ meta: { name },
301
+ underlying,
302
+ });
303
+ }
304
+ /**
305
+ * A project (a cloned/opened repo) defines hooks that have not been trusted for
306
+ * this machine. Cruxy NEVER runs another author's hooks silently — trust is an
307
+ * explicit, per-repo decision recorded in your home, not in the repo.
308
+ */
309
+ export function hookUntrusted(root, count) {
310
+ return new CruxyError({
311
+ code: ErrorCode.HookUntrusted,
312
+ title: `this project defines ${count} hook${count === 1 ? "" : "s"} that are not trusted`,
313
+ cause: "project hooks are authored by whoever wrote the repo; cruxy will not run them until you review and trust them",
314
+ nextSteps: [
315
+ "review them with `cruxy hooks list`",
316
+ `trust them with \`cruxy hooks trust ${root}\` (after reviewing)`,
317
+ "or set hooks.enabled=false to disable hooks entirely",
318
+ ],
319
+ meta: { root, count },
320
+ });
321
+ }
322
+ /** A malformed hook definition — excluded from the catalog, never eval'd. */
323
+ export function hookInvalid(reason, source) {
324
+ return new CruxyError({
325
+ code: ErrorCode.HookInvalid,
326
+ title: "a hook definition is malformed",
327
+ cause: reason,
328
+ nextSteps: ["fix the hook definition; see `cruxy hooks list` for details"],
329
+ meta: { source },
330
+ });
331
+ }
332
+ /** A malformed custom slash-command definition — excluded and surfaced. */
333
+ export function slashInvalid(reason, source) {
334
+ return new CruxyError({
335
+ code: ErrorCode.SlashInvalid,
336
+ title: "a custom slash-command definition is malformed",
337
+ cause: reason,
338
+ nextSteps: ["fix the command definition (frontmatter + body)"],
339
+ meta: { source },
340
+ });
341
+ }
285
342
  // ── approval (exit 10) ────────────────────────────────────────────────────────
286
343
  /**
287
344
  * A side-effecting action needs approval but cruxy can't ask (non-interactive,
@@ -57,6 +57,17 @@ export declare const ErrorCode: {
57
57
  readonly SandboxUnavailable: "CRUXY_E_SANDBOX_UNAVAILABLE";
58
58
  readonly SandboxImage: "CRUXY_E_SANDBOX_IMAGE";
59
59
  readonly SandboxExec: "CRUXY_E_SANDBOX_EXEC";
60
+ /** A blocking hook failed (non-zero exit / declined / errored) → the action
61
+ * is aborted fail-closed. Advisory hooks never raise this. */
62
+ readonly HookFailed: "CRUXY_E_HOOK_FAILED";
63
+ /** A project defines hooks that have not been trusted for this repo — they are
64
+ * never run silently (supply-chain safety). */
65
+ readonly HookUntrusted: "CRUXY_E_HOOK_UNTRUSTED";
66
+ /** A malformed hook definition — excluded from the catalog and surfaced,
67
+ * never eval'd. */
68
+ readonly HookInvalid: "CRUXY_E_HOOK_INVALID";
69
+ /** A malformed custom slash-command definition — excluded and surfaced. */
70
+ readonly SlashInvalid: "CRUXY_E_SLASH_INVALID";
60
71
  };
61
72
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
62
73
  /** The process exit code for an error code (defaults to 1 for safety). */
@@ -70,6 +70,18 @@ export const ErrorCode = {
70
70
  SandboxUnavailable: "CRUXY_E_SANDBOX_UNAVAILABLE",
71
71
  SandboxImage: "CRUXY_E_SANDBOX_IMAGE",
72
72
  SandboxExec: "CRUXY_E_SANDBOX_EXEC",
73
+ // hooks + custom slash commands (exit 13) — C.19
74
+ /** A blocking hook failed (non-zero exit / declined / errored) → the action
75
+ * is aborted fail-closed. Advisory hooks never raise this. */
76
+ HookFailed: "CRUXY_E_HOOK_FAILED",
77
+ /** A project defines hooks that have not been trusted for this repo — they are
78
+ * never run silently (supply-chain safety). */
79
+ HookUntrusted: "CRUXY_E_HOOK_UNTRUSTED",
80
+ /** A malformed hook definition — excluded from the catalog and surfaced,
81
+ * never eval'd. */
82
+ HookInvalid: "CRUXY_E_HOOK_INVALID",
83
+ /** A malformed custom slash-command definition — excluded and surfaced. */
84
+ SlashInvalid: "CRUXY_E_SLASH_INVALID",
73
85
  };
74
86
  /**
75
87
  * Category exit codes. Distinct per category so a caller (CI, a script) can
@@ -123,6 +135,13 @@ const EXIT_CODES = {
123
135
  [ErrorCode.SandboxUnavailable]: 12,
124
136
  [ErrorCode.SandboxImage]: 12,
125
137
  [ErrorCode.SandboxExec]: 12,
138
+ // Hooks + custom slash commands (C.19). A blocking-hook failure and an
139
+ // untrusted project are execution-safety stops; malformed definitions are
140
+ // usage/config problems but share the category for a greppable exit code.
141
+ [ErrorCode.HookFailed]: 13,
142
+ [ErrorCode.HookUntrusted]: 13,
143
+ [ErrorCode.HookInvalid]: 13,
144
+ [ErrorCode.SlashInvalid]: 13,
126
145
  };
127
146
  /** The process exit code for an error code (defaults to 1 for safety). */
128
147
  export function exitCodeFor(code) {
@@ -0,0 +1,21 @@
1
+ import { type HookCatalog } from "./types.js";
2
+ /**
3
+ * Discovery for hooks + custom slash commands (C.19), mirroring the C.18 skills
4
+ * loader: scan layered sources (project > user), validate every definition
5
+ * fail-loud, exclude the malformed (collected as {@link HookConfigError}, never
6
+ * thrown past the loader, never eval'd), and resolve precedence. Hook
7
+ * definitions carry their {@link HookSource} — the trust model depends on
8
+ * telling a repo's hooks (project) apart from your own (user).
9
+ */
10
+ /** The two source directories (each a `.cruxy` dir holding `hooks.json` and
11
+ * `commands/`). */
12
+ export interface HookSources {
13
+ /** `<cwd>/.cruxy` */
14
+ project: string;
15
+ /** `~/.cruxy` */
16
+ user: string;
17
+ }
18
+ /** The real sources for a project root. */
19
+ export declare function defaultHookSources(cwd: string): HookSources;
20
+ /** Load and resolve the full hook + command catalog. */
21
+ export declare function loadHookCatalog(sources: HookSources): Promise<HookCatalog>;