jev-agent-tools 0.1.4 → 0.3.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 (180) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/CONTRIBUTING.md +43 -0
  3. package/README.md +58 -17
  4. package/SECURITY.md +43 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +198 -0
  7. package/dist/adapters/ask-proof.js +200 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +234 -0
  11. package/dist/adapters/docs.js +192 -0
  12. package/dist/adapters/evidence-context.js +119 -0
  13. package/dist/adapters/exec.js +207 -0
  14. package/dist/adapters/files.js +418 -0
  15. package/dist/adapters/find.js +150 -0
  16. package/dist/adapters/git-base.js +32 -0
  17. package/dist/adapters/git-inventory.js +71 -0
  18. package/dist/adapters/git.js +483 -0
  19. package/dist/adapters/locate-file.js +197 -0
  20. package/dist/adapters/output-lines.js +46 -0
  21. package/dist/adapters/private-storage.js +106 -0
  22. package/dist/adapters/risk-callers.js +429 -0
  23. package/dist/adapters/runner-version.js +78 -0
  24. package/dist/adapters/shell.js +92 -0
  25. package/dist/adapters/syntax.js +187 -0
  26. package/dist/adapters/test-inventory.js +139 -0
  27. package/dist/adapters/usage.js +20 -0
  28. package/dist/adapters/utf8.js +47 -0
  29. package/dist/configuration.js +267 -0
  30. package/dist/constants.js +140 -0
  31. package/dist/core/ask-closure.js +282 -0
  32. package/dist/core/ask-proof.js +1 -0
  33. package/dist/core/ask-references.js +278 -0
  34. package/dist/core/asks.js +507 -0
  35. package/dist/core/batches.js +65 -0
  36. package/dist/core/command-output.js +224 -0
  37. package/dist/core/diff.js +178 -0
  38. package/dist/core/docs.js +302 -0
  39. package/dist/core/find.js +108 -0
  40. package/dist/core/git.js +1 -0
  41. package/dist/core/imports.js +550 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/lexical.js +132 -0
  44. package/dist/core/locate.js +169 -0
  45. package/dist/core/output.js +137 -0
  46. package/dist/core/pointer.js +29 -0
  47. package/dist/core/result-report.js +302 -0
  48. package/dist/core/risk-callers.js +851 -0
  49. package/dist/core/runner-version.js +45 -0
  50. package/dist/core/secret-path.js +34 -0
  51. package/dist/core/sections.js +230 -0
  52. package/dist/core/state.js +51 -0
  53. package/dist/core/syntax.js +1 -0
  54. package/dist/core/test-commands.js +334 -0
  55. package/dist/core/test-coverage.js +74 -0
  56. package/dist/core/test-discovery.js +1382 -0
  57. package/dist/core/test-evidence.js +527 -0
  58. package/dist/core/test-state.js +81 -0
  59. package/dist/core/truncate.js +12 -0
  60. package/dist/core/units.js +349 -0
  61. package/dist/describe.js +23 -0
  62. package/dist/guide.js +33 -0
  63. package/dist/host.js +24 -0
  64. package/dist/jev/client.js +456 -0
  65. package/dist/jev/pool.js +54 -0
  66. package/dist/jev/types.js +1 -0
  67. package/dist/mcp/main.js +124 -0
  68. package/dist/mcp/protocol.js +210 -0
  69. package/dist/mcp/tools.js +129 -0
  70. package/dist/presets/docs.js +62 -0
  71. package/dist/presets/risk.js +179 -0
  72. package/dist/presets/spec.js +81 -0
  73. package/dist/presets/witnesses.js +249 -0
  74. package/dist/render.js +114 -0
  75. package/dist/report-schema.js +1356 -0
  76. package/dist/result-types.js +1 -0
  77. package/dist/result.js +3 -0
  78. package/dist/runtime.js +1 -0
  79. package/dist/session.js +147 -0
  80. package/dist/texts/ask-files.js +3 -0
  81. package/dist/texts/ask.js +4 -0
  82. package/dist/texts/check-diff.js +20 -0
  83. package/dist/texts/configuration.js +1 -0
  84. package/dist/texts/find.js +19 -0
  85. package/dist/texts/guide.js +3 -0
  86. package/dist/texts/instructions.js +72 -0
  87. package/dist/texts/locate.js +15 -0
  88. package/dist/texts/select-tests.js +4 -0
  89. package/dist/tools/ask-files.js +450 -0
  90. package/dist/tools/ask-schema.js +70 -0
  91. package/dist/tools/ask.js +1147 -0
  92. package/dist/tools/check-diff.js +594 -0
  93. package/dist/tools/docs-check.js +408 -0
  94. package/dist/tools/find.js +682 -0
  95. package/dist/tools/locate.js +602 -0
  96. package/dist/tools/review-report.js +230 -0
  97. package/dist/tools/select-tests.js +821 -0
  98. package/dist/tools/spec-check.js +263 -0
  99. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  100. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  101. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  102. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  103. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  104. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  105. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  106. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  107. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  108. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  109. package/docs/agent-instructions.md +120 -0
  110. package/docs/design.md +16 -4
  111. package/docs/mcp.md +233 -0
  112. package/docs/tools/jev_ask.md +8 -5
  113. package/docs/tools/jev_ask_files.md +2 -1
  114. package/docs/tools/jev_check_diff.md +4 -1
  115. package/docs/tools/jev_find_files.md +2 -1
  116. package/docs/tools/jev_locate_in_file.md +5 -0
  117. package/docs/tools/jev_select_tests.md +4 -1
  118. package/package.json +19 -4
  119. package/rules/jev-ask.md +22 -1
  120. package/server.json +57 -0
  121. package/src/adapters/ask-files.ts +11 -3
  122. package/src/adapters/ask-proof.ts +69 -11
  123. package/src/adapters/canonical-path.ts +18 -0
  124. package/src/adapters/command.ts +102 -36
  125. package/src/adapters/docs.ts +33 -14
  126. package/src/adapters/evidence-context.ts +169 -0
  127. package/src/adapters/exec.ts +226 -0
  128. package/src/adapters/files.ts +146 -16
  129. package/src/adapters/find.ts +37 -7
  130. package/src/adapters/git-base.ts +7 -1
  131. package/src/adapters/git.ts +61 -8
  132. package/src/adapters/locate-file.ts +51 -9
  133. package/src/adapters/private-storage.ts +155 -0
  134. package/src/adapters/risk-callers.ts +7 -2
  135. package/src/adapters/shell.ts +113 -0
  136. package/src/adapters/test-inventory.ts +12 -4
  137. package/src/configuration.ts +55 -14
  138. package/src/constants.ts +37 -5
  139. package/src/core/ask-references.ts +262 -146
  140. package/src/core/asks.ts +79 -7
  141. package/src/core/command-output.ts +17 -1
  142. package/src/core/import-boundaries.ts +8 -3
  143. package/src/core/locate.ts +8 -5
  144. package/src/core/output.ts +34 -0
  145. package/src/core/result-report.ts +410 -0
  146. package/src/core/secret-path.ts +37 -0
  147. package/src/core/state.ts +8 -1
  148. package/src/core/units.ts +3 -2
  149. package/src/host.ts +11 -0
  150. package/src/index.ts +3 -0
  151. package/src/jev/client.ts +66 -16
  152. package/src/jev/types.ts +24 -3
  153. package/src/mcp/main.ts +135 -0
  154. package/src/mcp/protocol.ts +332 -0
  155. package/src/mcp/tools.ts +179 -0
  156. package/src/render.ts +109 -0
  157. package/src/report-schema.ts +1380 -0
  158. package/src/result-types.ts +234 -0
  159. package/src/result.ts +4 -1
  160. package/src/runtime.ts +6 -0
  161. package/src/session.ts +59 -0
  162. package/src/setup.ts +13 -5
  163. package/src/texts/ask-files.ts +4 -1
  164. package/src/texts/ask.ts +8 -1
  165. package/src/texts/check-diff.ts +7 -4
  166. package/src/texts/find.ts +8 -2
  167. package/src/texts/guide.ts +8 -16
  168. package/src/texts/instructions.ts +98 -0
  169. package/src/texts/locate.ts +8 -2
  170. package/src/texts/run-end.ts +2 -2
  171. package/src/texts/select-tests.ts +4 -1
  172. package/src/tools/ask-files.ts +311 -18
  173. package/src/tools/ask.ts +722 -95
  174. package/src/tools/check-diff.ts +337 -31
  175. package/src/tools/docs-check.ts +241 -38
  176. package/src/tools/find.ts +389 -29
  177. package/src/tools/locate.ts +387 -25
  178. package/src/tools/review-report.ts +308 -0
  179. package/src/tools/select-tests.ts +484 -23
  180. package/src/tools/spec-check.ts +194 -19
@@ -0,0 +1,169 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { parse, resolve, sep } from "node:path";
3
+ import { TIMEOUT_MS } from "../constants.ts";
4
+ import type { GitExec } from "../core/git.ts";
5
+ import type { State } from "../jev/types.ts";
6
+ import type { Cause } from "../result-types.ts";
7
+
8
+ export type EvidenceContext = {
9
+ authority: { path: string; origin: "host" | "server" };
10
+ authorityReason?: string;
11
+ requestedRoot?: string;
12
+ effectiveRoot?: {
13
+ path: string;
14
+ origin: "host" | "server" | "override";
15
+ commonDir?: string;
16
+ };
17
+ requestedBase?: string;
18
+ resolvedBase?: string;
19
+ };
20
+ export interface ContextDependencies {
21
+ exec: GitExec;
22
+ signal?: AbortSignal;
23
+ origin?: "host" | "server";
24
+ lstat?: typeof lstat;
25
+ realpath?: typeof realpath;
26
+ }
27
+ export type EvidenceContextResult =
28
+ | { ok: true; cwd: string; context: EvidenceContext }
29
+ | { ok: false; error: string; cause: Cause; context: EvidenceContext };
30
+
31
+ /** Bind each judgment (including controls) to its admitted per-call provenance. */
32
+ export function withEvidenceContext(
33
+ state: State,
34
+ context: EvidenceContext,
35
+ ): State {
36
+ const provenance = {
37
+ ...context,
38
+ authority: { ...context.authority },
39
+ ...(context.effectiveRoot
40
+ ? { effectiveRoot: { ...context.effectiveRoot } }
41
+ : {}),
42
+ };
43
+ const evidence = state.evidence;
44
+ // A producer's non-object evidence is content, not a metadata container.
45
+ if (
46
+ evidence !== undefined &&
47
+ (evidence === null ||
48
+ typeof evidence !== "object" ||
49
+ Array.isArray(evidence))
50
+ )
51
+ return { ...state, evidenceContext: provenance };
52
+ return { ...state, evidence: { ...evidence, context: provenance } };
53
+ }
54
+
55
+ /** Metadata-only admission. No evidence, commands, caches or judgments precede this. */
56
+ export async function resolveEvidenceContext(
57
+ initialCwd: string,
58
+ root: string | undefined,
59
+ deps: ContextDependencies,
60
+ ): Promise<EvidenceContextResult> {
61
+ const canonical = deps.realpath ?? realpath;
62
+ const inspect = deps.lstat ?? lstat;
63
+ let initial: string;
64
+ try {
65
+ initial = await canonical(initialCwd);
66
+ } catch {
67
+ return {
68
+ ok: false,
69
+ cause: "file_unavailable",
70
+ error: "Initial evidence authority cannot be canonicalized",
71
+ context: {
72
+ authority: { path: "", origin: deps.origin ?? "host" },
73
+ authorityReason: "Initial evidence authority cannot be canonicalized",
74
+ ...(root !== undefined ? { requestedRoot: root } : {}),
75
+ },
76
+ };
77
+ }
78
+ const origin = deps.origin ?? "host";
79
+ const context: EvidenceContext = {
80
+ authority: { path: initial, origin },
81
+ ...(root !== undefined ? { requestedRoot: root } : {}),
82
+ };
83
+ if (root === undefined) {
84
+ context.effectiveRoot = { path: initial, origin };
85
+ return { ok: true, cwd: initial, context };
86
+ }
87
+ const refuse = (
88
+ reason: string,
89
+ cause: Cause = "invalid_root",
90
+ ): EvidenceContextResult => ({
91
+ ok: false,
92
+ cause,
93
+ error: `Invalid root: ${reason}`,
94
+ context,
95
+ });
96
+ if (!root.trim()) return refuse("provide a non-empty directory");
97
+ if (root.split(/[\\/]/).includes(".."))
98
+ return refuse("parent traversal is forbidden");
99
+ const candidate = resolve(initial, root);
100
+ try {
101
+ let component = parse(candidate).root;
102
+ for (const part of candidate
103
+ .slice(component.length)
104
+ .split(sep)
105
+ .filter(Boolean)) {
106
+ component = resolve(component, part);
107
+ const metadata = await inspect(component);
108
+ if (metadata.isSymbolicLink())
109
+ return refuse("symbolic links are forbidden");
110
+ if (!metadata.isDirectory())
111
+ return refuse("root must be an existing directory");
112
+ }
113
+ const path = await canonical(candidate);
114
+ const git = async (cwd: string, args: string[]) => {
115
+ const result = await deps.exec("git", args, {
116
+ cwd,
117
+ timeout: TIMEOUT_MS,
118
+ signal: deps.signal,
119
+ });
120
+ if (result.code || result.killed)
121
+ throw new Error("Git metadata unavailable");
122
+ return args.includes("-z")
123
+ ? result.stdout
124
+ : result.stdout.replace(/\r?\n$/, "");
125
+ };
126
+ const initialTop = await canonical(
127
+ await git(initial, ["rev-parse", "--show-toplevel"]),
128
+ );
129
+ const candidateTop = await canonical(
130
+ await git(path, ["rev-parse", "--show-toplevel"]),
131
+ );
132
+ if (candidateTop !== path)
133
+ return refuse("root must equal a Git top-level directory");
134
+ const common = await canonical(
135
+ resolve(initial, await git(initial, ["rev-parse", "--git-common-dir"])),
136
+ );
137
+ const candidateCommon = await canonical(
138
+ resolve(path, await git(path, ["rev-parse", "--git-common-dir"])),
139
+ );
140
+ if (candidateCommon !== common)
141
+ return refuse("root belongs to a different repository");
142
+ const listed = await git(initial, [
143
+ "worktree",
144
+ "list",
145
+ "--porcelain",
146
+ "-z",
147
+ ]);
148
+ const registered = listed.split("\0\0").some((block) => {
149
+ const fields = block.split("\0");
150
+ const worktree = fields
151
+ .find((field) => field.startsWith("worktree "))
152
+ ?.slice(9);
153
+ return (
154
+ worktree !== undefined &&
155
+ resolve(worktree) === path &&
156
+ !fields.some((field) => /^prunable(?: |$)/.test(field))
157
+ );
158
+ });
159
+ if (path !== initialTop && !registered)
160
+ return refuse("root is not a registered live worktree");
161
+ context.effectiveRoot = { path, origin: "override", commonDir: common };
162
+ return { ok: true, cwd: path, context };
163
+ } catch (error) {
164
+ return refuse(
165
+ `cannot admit directory (${error instanceof Error ? error.message : String(error)})`,
166
+ deps.signal?.aborted ? "cancelled" : "invalid_root",
167
+ );
168
+ }
169
+ }
@@ -0,0 +1,226 @@
1
+ import { type ChildProcess, execFile, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import {
5
+ PROCESS_KILL_GRACE_MS,
6
+ PROCESS_PIPE_GRACE_MS,
7
+ PROCESS_TREE_TIMEOUT_MS,
8
+ } from "../constants.ts";
9
+ import type { GitExec } from "../core/git.ts";
10
+
11
+ /** Signal the managed process group, or await Windows process-tree termination. */
12
+ export async function killTree(
13
+ child: ChildProcess,
14
+ signal: NodeJS.Signals = "SIGTERM",
15
+ platform: NodeJS.Platform = process.platform,
16
+ ): Promise<void> {
17
+ const pid = child.pid;
18
+ if (pid === undefined) return;
19
+ if (platform === "win32") {
20
+ const taskkill = join(
21
+ process.env.SystemRoot ?? "C:\\Windows",
22
+ "System32",
23
+ "taskkill.exe",
24
+ );
25
+ const pids = [pid];
26
+ // MSYS fork/exec can leave Windows parent IDs pointing at vanished helper
27
+ // processes. Use the same Bash installation's POSIX table before killing
28
+ // the root; taskkill /T alone cannot find those ordinary descendants.
29
+ if (basename(child.spawnfile).toLowerCase() === "bash.exe") {
30
+ const directory = dirname(child.spawnfile);
31
+ const ps = [
32
+ join(directory, "ps.exe"),
33
+ join(directory, "..", "usr", "bin", "ps.exe"),
34
+ ].find(existsSync);
35
+ if (ps) {
36
+ const snapshot = Promise.withResolvers<string>();
37
+ execFile(
38
+ ps,
39
+ ["-e"],
40
+ { windowsHide: true, timeout: PROCESS_TREE_TIMEOUT_MS },
41
+ (error, stdout) => snapshot.resolve(error ? "" : stdout),
42
+ );
43
+ const rows = [];
44
+ for (const line of (await snapshot.promise).split("\n")) {
45
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s/.exec(line);
46
+ if (!match) continue;
47
+ rows.push({
48
+ pid: Number(match[1]),
49
+ parent: Number(match[2]),
50
+ windows: Number(match[4]),
51
+ });
52
+ }
53
+ const root = rows.find((row) => row.windows === pid);
54
+ if (root) {
55
+ const managed = new Set([root.pid]);
56
+ let added = true;
57
+ while (added) {
58
+ added = false;
59
+ for (const row of rows) {
60
+ if (!managed.has(row.pid) && managed.has(row.parent)) {
61
+ managed.add(row.pid);
62
+ if (row.windows > 0) pids.push(row.windows);
63
+ added = true;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ const { promise, resolve } = Promise.withResolvers<void>();
71
+ execFile(
72
+ taskkill,
73
+ [...new Set(pids)]
74
+ .flatMap((target) => ["/PID", String(target)])
75
+ .concat("/T", "/F"),
76
+ { windowsHide: true, timeout: PROCESS_TREE_TIMEOUT_MS },
77
+ (error) => {
78
+ if (error) child.kill();
79
+ resolve();
80
+ },
81
+ );
82
+ await promise;
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(-pid, signal);
87
+ } catch {
88
+ child.kill(signal);
89
+ }
90
+ return;
91
+ }
92
+
93
+ function groupExists(child: ChildProcess): boolean {
94
+ if (child.pid === undefined) return false;
95
+ try {
96
+ process.kill(-child.pid, 0);
97
+ return true;
98
+ } catch (error) {
99
+ return !(
100
+ error instanceof Error &&
101
+ "code" in error &&
102
+ error.code === "ESRCH"
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Capture a process without a shell. Timeout and abort finish tree termination
109
+ * before resolving, even when the direct child exits before its descendants.
110
+ * Deliberately detached descendants are not sandboxed or tracked.
111
+ */
112
+ export const spawnExec: GitExec = (command, args, options) => {
113
+ const { promise, resolve, reject } = Promise.withResolvers<{
114
+ stdout: string;
115
+ stderr: string;
116
+ code: number;
117
+ killed: boolean;
118
+ }>();
119
+ const posix = process.platform !== "win32";
120
+ // Git's bin/bash.exe forwards to usr/bin/bash.exe. Spawn the MSYS process
121
+ // directly so its Windows PID can be matched in the POSIX process table.
122
+ if (!posix && basename(command).toLowerCase() === "bash.exe") {
123
+ const direct = join(dirname(command), "..", "usr", "bin", "bash.exe");
124
+ if (existsSync(direct)) command = direct;
125
+ }
126
+ const child = spawn(command, args, {
127
+ cwd: options.cwd,
128
+ shell: false,
129
+ stdio: ["ignore", "pipe", "pipe"],
130
+ windowsHide: true,
131
+ detached: posix,
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ let killed = false;
136
+ let spawned = false;
137
+ let settled = false;
138
+ let finishing = false;
139
+ let force: NodeJS.Timeout | undefined;
140
+ let pipes: NodeJS.Timeout | undefined;
141
+ let termination: Promise<void> | undefined;
142
+ let terminated: (() => void) | undefined;
143
+ child.stdout.setEncoding("utf8");
144
+ child.stderr.setEncoding("utf8");
145
+ child.stdout.on("data", (chunk: string) => {
146
+ stdout += chunk;
147
+ });
148
+ child.stderr.on("data", (chunk: string) => {
149
+ stderr += chunk;
150
+ });
151
+ const kill = () => {
152
+ if (killed || settled) return;
153
+ killed = true;
154
+ if (posix) {
155
+ void killTree(child, "SIGTERM");
156
+ const completion = Promise.withResolvers<void>();
157
+ termination = completion.promise;
158
+ terminated = completion.resolve;
159
+ // Keep this timer referenced: a closing transport must not exit
160
+ // before a surviving member of the managed group receives SIGKILL.
161
+ force = setTimeout(() => {
162
+ void killTree(child, "SIGKILL");
163
+ completion.resolve();
164
+ }, PROCESS_KILL_GRACE_MS);
165
+ } else termination = killTree(child);
166
+ };
167
+ const timer =
168
+ options.timeout > 0 ? setTimeout(kill, options.timeout) : undefined;
169
+ if (options.signal?.aborted) kill();
170
+ else options.signal?.addEventListener("abort", kill, { once: true });
171
+ const cleanup = () => {
172
+ clearTimeout(timer);
173
+ clearTimeout(force);
174
+ clearTimeout(pipes);
175
+ options.signal?.removeEventListener("abort", kill);
176
+ };
177
+ const finish = async (code: number | null, signal: NodeJS.Signals | null) => {
178
+ if (settled || finishing) return;
179
+ finishing = true;
180
+ clearTimeout(pipes);
181
+ if (posix && terminated && !groupExists(child)) {
182
+ clearTimeout(force);
183
+ terminated();
184
+ }
185
+ if (termination) await termination;
186
+ settled = true;
187
+ cleanup();
188
+ resolve({
189
+ stdout,
190
+ stderr,
191
+ code: code ?? (signal ? 128 + signalNumber(signal) : 1),
192
+ killed,
193
+ });
194
+ };
195
+ child.on("spawn", () => {
196
+ spawned = true;
197
+ });
198
+ child.on("error", (error) => {
199
+ if (spawned) return;
200
+ settled = true;
201
+ cleanup();
202
+ reject(error);
203
+ });
204
+ child.on("exit", (code, signal) => {
205
+ // An escaped descendant may hold inherited pipes open indefinitely.
206
+ pipes = setTimeout(() => {
207
+ child.stdout.destroy();
208
+ child.stderr.destroy();
209
+ void finish(code, signal);
210
+ }, PROCESS_PIPE_GRACE_MS);
211
+ });
212
+ child.on("close", (code, signal) => {
213
+ void finish(code, signal);
214
+ });
215
+ return promise;
216
+ };
217
+
218
+ function signalNumber(signal: NodeJS.Signals): number {
219
+ const numbers: Partial<Record<NodeJS.Signals, number>> = {
220
+ SIGHUP: 1,
221
+ SIGINT: 2,
222
+ SIGKILL: 9,
223
+ SIGTERM: 15,
224
+ };
225
+ return numbers[signal] ?? 0;
226
+ }