amalfa 0.0.0-reserved → 1.0.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 (139) hide show
  1. package/.biomeignore +19 -0
  2. package/:memory: +0 -0
  3. package/:memory:-shm +0 -0
  4. package/:memory:-wal +0 -0
  5. package/LICENSE +21 -0
  6. package/README.md +343 -13
  7. package/README.old.md +112 -0
  8. package/agents.config.json +11 -0
  9. package/amalfa.config.example.ts +100 -0
  10. package/biome.json +49 -0
  11. package/bun.lock +371 -0
  12. package/docs/AGENT_PROTOCOLS.md +28 -0
  13. package/docs/ARCHITECTURAL_OVERVIEW.md +123 -0
  14. package/docs/BENTO_BOXING_DEPRECATION.md +281 -0
  15. package/docs/Bun-SQLite.html +464 -0
  16. package/docs/COMMIT_GUIDELINES.md +367 -0
  17. package/docs/DEVELOPER_ONBOARDING.md +36 -0
  18. package/docs/Graph and Vector Database Best Practices.md +214 -0
  19. package/docs/PERFORMANCE_BASELINES.md +88 -0
  20. package/docs/REPOSITORY_CLEANUP_SUMMARY.md +261 -0
  21. package/docs/edge-generation-methods.md +57 -0
  22. package/docs/elevator-pitch.md +118 -0
  23. package/docs/graph-and-vector-database-playbook.html +480 -0
  24. package/docs/hardened-sqlite.md +85 -0
  25. package/docs/headless-knowledge-management.md +79 -0
  26. package/docs/john-kaye-flux-prompt.md +46 -0
  27. package/docs/keyboard-shortcuts.md +80 -0
  28. package/docs/opinion-proceed-pattern.md +29 -0
  29. package/docs/polyvis-nodes-edges-schema.md +77 -0
  30. package/docs/protocols/lab-protocol.md +30 -0
  31. package/docs/reaction-iquest-loop-coder.md +46 -0
  32. package/docs/services.md +60 -0
  33. package/docs/sqlite-wal-readonly-trap.md +228 -0
  34. package/docs/strategy/css-architecture.md +40 -0
  35. package/docs/test-document-cycle.md +83 -0
  36. package/docs/test_lifecycle_E2E.md +4 -0
  37. package/docs/the-bicameral-graph.md +83 -0
  38. package/docs/user-guide.md +70 -0
  39. package/docs/vision-helper.md +53 -0
  40. package/drizzle/0000_minor_iron_fist.sql +19 -0
  41. package/drizzle/meta/0000_snapshot.json +139 -0
  42. package/drizzle/meta/_journal.json +13 -0
  43. package/example_usage.ts +39 -0
  44. package/experiment.sh +35 -0
  45. package/hello +2 -0
  46. package/index.html +52 -0
  47. package/knowledge/excalibur.md +12 -0
  48. package/package.json +60 -15
  49. package/plans/experience-graph-integration.md +60 -0
  50. package/prompts/gemini-king-mode-prompt.md +46 -0
  51. package/public/docs/MCP_TOOLS.md +372 -0
  52. package/schemas/README.md +20 -0
  53. package/schemas/cda.schema.json +84 -0
  54. package/schemas/conceptual-lexicon.schema.json +75 -0
  55. package/scratchpads/dummy-debrief-boxed.md +39 -0
  56. package/scratchpads/dummy-debrief.md +27 -0
  57. package/scratchpads/scratchpad-design.md +50 -0
  58. package/scratchpads/scratchpad-scrolling.md +20 -0
  59. package/scratchpads/scratchpad-toc-disappearance.md +23 -0
  60. package/scratchpads/scratchpad-toc.md +28 -0
  61. package/scratchpads/test_gardener.md +7 -0
  62. package/src/EnlightenedTriad.ts +146 -0
  63. package/src/JIT_Triad.ts +137 -0
  64. package/src/cli.ts +318 -0
  65. package/src/config/constants.ts +7 -0
  66. package/src/config/defaults.ts +81 -0
  67. package/src/core/BentoNormalizer.ts +113 -0
  68. package/src/core/EdgeWeaver.ts +145 -0
  69. package/src/core/FractureLogic.ts +22 -0
  70. package/src/core/Harvester.ts +73 -0
  71. package/src/core/LLMClient.ts +93 -0
  72. package/src/core/LouvainGate.ts +67 -0
  73. package/src/core/MarkdownMasker.ts +49 -0
  74. package/src/core/README.md +11 -0
  75. package/src/core/SemanticMatcher.ts +89 -0
  76. package/src/core/SemanticWeaver.ts +96 -0
  77. package/src/core/TagEngine.ts +56 -0
  78. package/src/core/TimelineWeaver.ts +61 -0
  79. package/src/core/VectorEngine.ts +232 -0
  80. package/src/daemon/index.ts +221 -0
  81. package/src/data/experience/test_doc_1.md +2 -0
  82. package/src/data/experience/test_doc_2.md +2 -0
  83. package/src/db/schema.ts +46 -0
  84. package/src/demo-triad.ts +45 -0
  85. package/src/gardeners/AutoTagger.ts +116 -0
  86. package/src/gardeners/BaseGardener.ts +55 -0
  87. package/src/llm/EnlightenedProvider.ts +95 -0
  88. package/src/mcp/README.md +6 -0
  89. package/src/mcp/index.ts +341 -0
  90. package/src/pipeline/AmalfaIngestor.ts +262 -0
  91. package/src/pipeline/HarvesterPipeline.ts +101 -0
  92. package/src/pipeline/Ingestor.ts +555 -0
  93. package/src/pipeline/README.md +7 -0
  94. package/src/pipeline/SemanticHarvester.ts +222 -0
  95. package/src/resonance/DatabaseFactory.ts +100 -0
  96. package/src/resonance/README.md +148 -0
  97. package/src/resonance/cli/README.md +7 -0
  98. package/src/resonance/cli/ingest.ts +41 -0
  99. package/src/resonance/cli/migrate.ts +54 -0
  100. package/src/resonance/config.ts +40 -0
  101. package/src/resonance/daemon.ts +236 -0
  102. package/src/resonance/db.ts +422 -0
  103. package/src/resonance/pipeline/README.md +7 -0
  104. package/src/resonance/pipeline/extract.ts +89 -0
  105. package/src/resonance/pipeline/transform_docs.ts +60 -0
  106. package/src/resonance/schema.ts +138 -0
  107. package/src/resonance/services/embedder.ts +131 -0
  108. package/src/resonance/services/simpleTokenizer.ts +119 -0
  109. package/src/resonance/services/stats.ts +327 -0
  110. package/src/resonance/services/tokenizer.ts +159 -0
  111. package/src/resonance/transform/cda.ts +393 -0
  112. package/src/resonance/types/enriched-cda.ts +112 -0
  113. package/src/services/README.md +56 -0
  114. package/src/services/llama.ts +59 -0
  115. package/src/services/llamauv.ts +56 -0
  116. package/src/services/olmo3.ts +58 -0
  117. package/src/services/phi.ts +52 -0
  118. package/src/types/artifact.ts +12 -0
  119. package/src/utils/EnvironmentVerifier.ts +67 -0
  120. package/src/utils/Logger.ts +21 -0
  121. package/src/utils/ServiceLifecycle.ts +207 -0
  122. package/src/utils/ZombieDefense.ts +244 -0
  123. package/src/utils/validator.ts +264 -0
  124. package/substack/substack-playbook-1.md +95 -0
  125. package/substack/substack-playbook-2.md +78 -0
  126. package/tasks/ui-investigation.md +26 -0
  127. package/test-db +0 -0
  128. package/test-db-shm +0 -0
  129. package/test-db-wal +0 -0
  130. package/tests/canary/verify_pinch_check.ts +44 -0
  131. package/tests/fixtures/ingest_test.md +12 -0
  132. package/tests/fixtures/ingest_test_boxed.md +13 -0
  133. package/tests/fixtures/safety_test.md +45 -0
  134. package/tests/fixtures/safety_test_boxed.md +49 -0
  135. package/tests/fixtures/tagged_output.md +49 -0
  136. package/tests/fixtures/tagged_test.md +49 -0
  137. package/tests/mcp-server-settings.json +8 -0
  138. package/tsconfig.json +46 -0
  139. package/verify-embedder.ts +54 -0
@@ -0,0 +1,58 @@
1
+ import { join } from "node:path";
2
+ import { ServiceLifecycle } from "../utils/ServiceLifecycle";
3
+
4
+ // --- Configuration ---
5
+ const BASE_DIR = join(import.meta.dir, "../../experiments/enlightenment");
6
+ const BIN_PATH = join(BASE_DIR, "llama.cpp/build/bin/llama-server");
7
+ const MODEL_PATH = join(BASE_DIR, "vectors/Olmo-3-7B-Think-Q4_K_M.gguf");
8
+
9
+ const PORT = 8084;
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args[0] || "serve";
13
+
14
+ // --- Service Lifecycle ---
15
+
16
+ const lifecycle = new ServiceLifecycle({
17
+ name: "Olmo-3",
18
+ pidFile: ".olmo3.pid",
19
+ logFile: ".olmo3.log",
20
+ entryPoint: "src/services/olmo3.ts",
21
+ });
22
+
23
+ // --- Server Logic ---
24
+
25
+ async function runServer() {
26
+ console.log(`šŸš€ Starting Olmo-3 Server on port ${PORT}...`);
27
+
28
+ const cmd = [
29
+ BIN_PATH,
30
+ "-m",
31
+ MODEL_PATH,
32
+ "--port",
33
+ PORT.toString(),
34
+ "--ctx-size",
35
+ "8192", // Reduced context for stability on Metal
36
+ "--n-gpu-layers",
37
+ "99", // Offload to GPU/Metal
38
+ "--jinja", // Jinja2 template support
39
+ "--reasoning-format",
40
+ "deepseek", // Separate partial thinking
41
+ "-fa",
42
+ "on", // Flash Attention
43
+ "--temp",
44
+ "0.6",
45
+ ];
46
+
47
+ const serverProcess = Bun.spawn(cmd, {
48
+ stdout: "inherit",
49
+ stderr: "inherit",
50
+ });
51
+
52
+ // Wait for process to exit
53
+ await serverProcess.exited;
54
+ }
55
+
56
+ // --- Dispatch ---
57
+
58
+ await lifecycle.run(command, runServer);
@@ -0,0 +1,52 @@
1
+ import { join } from "node:path";
2
+ import { ServiceLifecycle } from "../utils/ServiceLifecycle";
3
+
4
+ // --- Configuration ---
5
+ const BASE_DIR = join(import.meta.dir, "../../experiments/enlightenment");
6
+ const BIN_PATH = join(BASE_DIR, "llama.cpp/build/bin/llama-server");
7
+ const MODEL_PATH = join(BASE_DIR, "vectors/Phi-3.5-mini-instruct-Q4_K_M.gguf");
8
+
9
+ const PORT = 8082;
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args[0] || "serve";
13
+
14
+ // --- Service Lifecycle ---
15
+
16
+ const lifecycle = new ServiceLifecycle({
17
+ name: "Phi-3.5",
18
+ pidFile: ".phi.pid",
19
+ logFile: ".phi.log",
20
+ entryPoint: "src/services/phi.ts",
21
+ });
22
+
23
+ // --- Server Logic ---
24
+
25
+ async function runServer() {
26
+ console.log(`šŸš€ Starting Phi-3.5 Server on port ${PORT}...`);
27
+
28
+ const cmd = [
29
+ BIN_PATH,
30
+ "-m",
31
+ MODEL_PATH,
32
+ "--port",
33
+ PORT.toString(),
34
+ "--ctx-size",
35
+ "4096",
36
+ "--n-gpu-layers",
37
+ "99", // Offload to GPU/Metal
38
+ "--log-disable", // Keep clean for Phi
39
+ ];
40
+
41
+ const serverProcess = Bun.spawn(cmd, {
42
+ stdout: "inherit",
43
+ stderr: "inherit",
44
+ });
45
+
46
+ // Wait for process to exit
47
+ await serverProcess.exited;
48
+ }
49
+
50
+ // --- Dispatch ---
51
+
52
+ await lifecycle.run(command, runServer);
@@ -0,0 +1,12 @@
1
+ export interface IngestionArtifact {
2
+ id: string;
3
+ type: "playbook" | "debrief";
4
+ order_index: number;
5
+ payload: {
6
+ title: string;
7
+ content: string;
8
+ domain: string;
9
+ layer: string;
10
+ metadata?: Record<string, unknown>;
11
+ };
12
+ }
@@ -0,0 +1,67 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import settings from "../../polyvis.settings.json";
4
+
5
+ /**
6
+ * EnvironmentVerifier
7
+ *
8
+ * Responsible for verifying that the runtime environment matches the
9
+ * expectations set in polyvis.settings.json.
10
+ *
11
+ * Usage:
12
+ * await EnvironmentVerifier.verifyOrExit();
13
+ */
14
+ export const EnvironmentVerifier = {
15
+ async verifyOrExit(): Promise<void> {
16
+ const errors: string[] = [];
17
+ const cwd = process.cwd();
18
+
19
+ console.error(`šŸ›”ļø [Env] Verifying filesystem context in: ${cwd}`);
20
+
21
+ // 1. Verify Database Directory
22
+ const dbPath = settings.paths.database.resonance;
23
+ const dbDir = resolve(cwd, dbPath, "..");
24
+ if (!existsSync(dbDir)) {
25
+ errors.push(`Database directory missing: ${dbDir}`);
26
+ }
27
+
28
+ // 2. Verify Source Directories
29
+ // We iterate over the 'sources' config to ensure target folders exist
30
+ const experienceSources = settings.paths.sources.experience;
31
+ for (const source of experienceSources) {
32
+ const absPath = resolve(cwd, source.path);
33
+ if (!existsSync(absPath)) {
34
+ console.warn(
35
+ ` āš ļø Optional source directory missing: ${source.path} (created automatically by some tools, but worth noting)`,
36
+ );
37
+ // We don't hard fail on standard folders, as they might be empty/missing in a fresh repo.
38
+ // But specifically for 'docs' or critical ones we might want to be stricter.
39
+ // For now, we WARN.
40
+ }
41
+ }
42
+
43
+ // 3. Verify Static Assets (Lexicon/CDA)
44
+ const lexiconPath = resolve(cwd, settings.paths.sources.persona.lexicon);
45
+ if (!existsSync(lexiconPath)) {
46
+ errors.push(`Critical Artifact missing: Lexicon (${lexiconPath})`);
47
+ }
48
+
49
+ const cdaPath = resolve(cwd, settings.paths.sources.persona.cda);
50
+ if (!existsSync(cdaPath)) {
51
+ errors.push(`Critical Artifact missing: CDA (${cdaPath})`);
52
+ }
53
+
54
+ if (errors.length > 0) {
55
+ console.error("\nāŒ Environment Verification Failed:");
56
+ errors.forEach((e) => {
57
+ console.error(` - ${e}`);
58
+ });
59
+ console.error(
60
+ "\nPlease ensure you are running from the project root and all assets are present.",
61
+ );
62
+ process.exit(1);
63
+ }
64
+
65
+ console.error(" āœ… Environment Verified.");
66
+ },
67
+ };
@@ -0,0 +1,21 @@
1
+ import pino, { type Logger } from "pino";
2
+ export type { Logger };
3
+
4
+ // Configure the base logger
5
+ const loggerConfig: pino.LoggerOptions = {
6
+ level: process.env.LOG_LEVEL || "info",
7
+ base: undefined, // Remove pid/hostname from logs for cleanliness
8
+ timestamp: pino.stdTimeFunctions.isoTime,
9
+ };
10
+
11
+ // Default to stderr (fd 2) to prevent polluting stdout (critical for MCP/CLI piping)
12
+ export const rootLogger = pino(loggerConfig, pino.destination(2));
13
+
14
+ /**
15
+ * Creates a child logger with a bound component name.
16
+ * @param component The name of the component (e.g., 'Ingestor', 'MCP')
17
+ * @returns A pino logger instance suitable for that component
18
+ */
19
+ export const getLogger = (component: string) => {
20
+ return rootLogger.child({ component });
21
+ };
@@ -0,0 +1,207 @@
1
+ import { existsSync } from "node:fs";
2
+ import { unlink } from "node:fs/promises";
3
+ import { ZombieDefense } from "./ZombieDefense";
4
+
5
+ export interface ServiceConfig {
6
+ name: string; // e.g. "Daemon"
7
+ pidFile: string; // e.g. ".daemon.pid"
8
+ logFile: string; // e.g. ".daemon.log"
9
+ entryPoint: string; // e.g. "src/resonance/daemon.ts"
10
+ }
11
+
12
+ export class ServiceLifecycle {
13
+ constructor(private config: ServiceConfig) {}
14
+
15
+ private async isRunning(pid: number): Promise<boolean> {
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ } catch (_e) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Start the service in the background (detached).
26
+ */
27
+ async start() {
28
+ // Enforce clean state first (kill duplicates)
29
+ await ZombieDefense.assertClean(this.config.name, true);
30
+
31
+ // Check if already running based on PID file
32
+ if (await Bun.file(this.config.pidFile).exists()) {
33
+ const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
34
+ if (await this.isRunning(pid)) {
35
+ console.log(`āš ļø ${this.config.name} is already running (PID: ${pid})`);
36
+ return;
37
+ }
38
+ console.log(
39
+ `āš ļø Found stale PID file for ${this.config.name}. Clearing...`,
40
+ );
41
+ await unlink(this.config.pidFile);
42
+ }
43
+
44
+ const logFile = Bun.file(this.config.logFile);
45
+ await Bun.write(logFile, ""); // Truncate logs
46
+
47
+ // Spawn subprocess
48
+ const subprocess = Bun.spawn(
49
+ ["bun", "run", this.config.entryPoint, "serve"],
50
+ {
51
+ cwd: process.cwd(),
52
+ detached: true,
53
+ stdout: logFile,
54
+ stderr: logFile,
55
+ },
56
+ );
57
+
58
+ await Bun.write(this.config.pidFile, subprocess.pid.toString());
59
+ subprocess.unref();
60
+
61
+ console.log(
62
+ `āœ… ${this.config.name} started in background (PID: ${subprocess.pid})`,
63
+ );
64
+ console.log(`šŸ“ Logs: ${this.config.logFile}`);
65
+ }
66
+
67
+ /**
68
+ * Stop the service using the PID file.
69
+ */
70
+ async stop() {
71
+ if (!(await Bun.file(this.config.pidFile).exists())) {
72
+ console.log(`ā„¹ļø ${this.config.name} is not running.`);
73
+ return;
74
+ }
75
+
76
+ const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
77
+
78
+ if (await this.isRunning(pid)) {
79
+ console.log(`šŸ›‘ Stopping ${this.config.name} (PID: ${pid})...`);
80
+ process.kill(pid, "SIGTERM");
81
+
82
+ let attempts = 0;
83
+ // Wait up to 1 second
84
+ while ((await this.isRunning(pid)) && attempts < 10) {
85
+ await new Promise((r) => setTimeout(r, 100));
86
+ attempts++;
87
+ }
88
+
89
+ if (await this.isRunning(pid)) {
90
+ console.log("āš ļø Process did not exit gracefully. Force killing...");
91
+ process.kill(pid, "SIGKILL");
92
+ }
93
+ console.log(`āœ… ${this.config.name} stopped.`);
94
+ } else {
95
+ console.log("āš ļø Stale PID file found. Cleaning up.");
96
+ }
97
+
98
+ try {
99
+ await unlink(this.config.pidFile);
100
+ } catch (e: unknown) {
101
+ const err = e as { code?: string; message: string };
102
+ if (err.code !== "ENOENT") {
103
+ console.warn(`āš ļø Failed to remove PID file: ${err.message}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check status of the service.
110
+ */
111
+ async status() {
112
+ if (await Bun.file(this.config.pidFile).exists()) {
113
+ const pid = parseInt(await Bun.file(this.config.pidFile).text(), 10);
114
+ if (await this.isRunning(pid)) {
115
+ console.log(`🟢 ${this.config.name} is RUNNING (PID: ${pid})`);
116
+ return;
117
+ }
118
+ console.log(`šŸ”“ ${this.config.name} is NOT RUNNING (Stale PID: ${pid})`);
119
+ } else {
120
+ console.log(`āšŖļø ${this.config.name} is STOPPED`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Wrapper for the foreground 'serve' command logic.
126
+ * Use this to wrap your actual server startup code.
127
+ */
128
+ async serve(serverLogic: () => Promise<void>, checkZombies = true) {
129
+ // Enforce clean state (ensure we aren't running as a zombie of ourselves)
130
+ if (checkZombies) {
131
+ await ZombieDefense.assertClean(`${this.config.name} (Serve)`);
132
+ }
133
+
134
+ // Register cleanup handlers to remove PID file on exit/crash/kill
135
+ let cleanupCalled = false;
136
+ const cleanup = async (signal?: string) => {
137
+ if (cleanupCalled) return; // Prevent double cleanup
138
+ cleanupCalled = true;
139
+
140
+ try {
141
+ if (await Bun.file(this.config.pidFile).exists()) {
142
+ await unlink(this.config.pidFile);
143
+ if (signal) {
144
+ console.error(
145
+ `\n🧹 ${this.config.name}: PID file cleaned up on ${signal}`,
146
+ );
147
+ }
148
+ }
149
+ } catch (_e) {
150
+ // Ignore cleanup errors (file might already be deleted)
151
+ }
152
+ };
153
+
154
+ // Register signal handlers
155
+ process.on("SIGINT", () => cleanup("SIGINT").then(() => process.exit(0)));
156
+ process.on("SIGTERM", () => cleanup("SIGTERM").then(() => process.exit(0)));
157
+ process.on("exit", () => {
158
+ // Note: exit event is synchronous, so we do sync cleanup
159
+ if (!cleanupCalled && existsSync(this.config.pidFile)) {
160
+ cleanupCalled = true;
161
+ try {
162
+ Bun.write(this.config.pidFile, ""); // Truncate to mark as stale
163
+ } catch {}
164
+ }
165
+ });
166
+
167
+ await serverLogic();
168
+ }
169
+
170
+ /**
171
+ * Main CLI dispatch logic.
172
+ */
173
+ async run(
174
+ command: string,
175
+ serverLogic: () => Promise<void>,
176
+ checkZombies = true,
177
+ ) {
178
+ switch (command) {
179
+ case "start":
180
+ await this.start();
181
+ process.exit(0);
182
+ break;
183
+ case "stop":
184
+ await this.stop();
185
+ process.exit(0);
186
+ break;
187
+ case "status":
188
+ await this.status();
189
+ process.exit(0);
190
+ break;
191
+ case "restart":
192
+ await this.stop();
193
+ await new Promise((r) => setTimeout(r, 500));
194
+ await this.start();
195
+ process.exit(0);
196
+ break;
197
+ case "serve":
198
+ await this.serve(serverLogic, checkZombies);
199
+ break;
200
+ default:
201
+ console.log(
202
+ `Unknown command '${command}'. Use: start, stop, status, restart, or serve`,
203
+ );
204
+ process.exit(1);
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,244 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ export interface ZombieReport {
7
+ ghosts: string[];
8
+ duplicates: string[];
9
+ unknowns: string[];
10
+ clean: boolean;
11
+ }
12
+
13
+ // Services intended to run as singletons
14
+ const WHITELIST = [
15
+ "src/resonance/daemon.ts",
16
+ "src/mcp/index.ts",
17
+ "scripts/cli/dev.ts",
18
+ "src/resonance/cli/ingest.ts",
19
+ "bun run build:data",
20
+ "bun run mcp",
21
+ "bun run daemon",
22
+ "bun run dev",
23
+ "bun run watch:css",
24
+ "bun run watch:js",
25
+ "scripts/verify/test_mcp_query.ts",
26
+ "src/services/olmo3.ts",
27
+ "src/services/phi.ts",
28
+ "src/services/llama.ts",
29
+ "src/services/llamauv.ts",
30
+ "scripts/cli/servers.ts",
31
+ "bun run olmo3",
32
+ "bun run phi",
33
+ "bun run llama",
34
+ "bun run llamauv",
35
+ "bun run servers",
36
+ ];
37
+
38
+ export const ZombieDefense = {
39
+ /**
40
+ * Scan the environment for unauthorized or stale processes.
41
+ */
42
+ async scan(excludePids: string[] = []): Promise<ZombieReport> {
43
+ const report: ZombieReport = {
44
+ ghosts: [],
45
+ duplicates: [],
46
+ unknowns: [],
47
+ clean: true,
48
+ };
49
+
50
+ const protectedPids = new Set([
51
+ process.pid.toString(),
52
+ process.ppid.toString(),
53
+ ...excludePids,
54
+ ]);
55
+
56
+ // 1. Ghost Check (Deleted File Handles)
57
+ try {
58
+ const { stdout } = await execAsync("lsof +L1");
59
+ const lines = stdout.split("\n");
60
+ // Strict Filter: Only worry about resonance.db or files in our project scope
61
+ report.ghosts = lines.filter((l) => {
62
+ if (!l.includes("bun")) return false;
63
+ // Must be relevant file
64
+ return l.includes("resonance.db") || l.includes(process.cwd());
65
+ });
66
+ } catch (_e) {
67
+ // lsof exits 1 if nothing found
68
+ }
69
+
70
+ // 2. Process Table Check
71
+ try {
72
+ const { stdout } = await execAsync("ps aux | grep bun | grep -v grep");
73
+ const processes = stdout.split("\n").filter((l) => l.trim().length > 0);
74
+ const activeMap = new Map<string, number>();
75
+
76
+ processes.forEach((p) => {
77
+ // Ignore self and parent immediately
78
+ const match = p.match(/\s+(\d+)\s+/);
79
+ if (match?.[1] && protectedPids.has(match[1])) return;
80
+
81
+ // Strict Filter: Must be in our CWD or explicit bun run
82
+ if (!p.includes(process.cwd()) && !p.includes("bun run")) return;
83
+
84
+ const isWhitelisted = WHITELIST.some((w) => p.includes(w));
85
+
86
+ if (isWhitelisted) {
87
+ WHITELIST.forEach((w) => {
88
+ if (p.includes(w)) {
89
+ // HEURISTIC: Don't count "bun run scripts/foo.ts" and "bun scripts/foo.ts" as duplicates of each other if they are the same PID (obviously),
90
+ // but here we already filtered by PID.
91
+ // We need to be careful about the wrapper vs the actual process.
92
+
93
+ const count = (activeMap.get(w) || 0) + 1;
94
+ activeMap.set(w, count);
95
+ if (count > 1) {
96
+ report.duplicates.push(p);
97
+ }
98
+ }
99
+ });
100
+ } else {
101
+ // Ignore self (if running from a script calling this)
102
+ if (
103
+ !p.includes("detect_zombies.ts") &&
104
+ !p.includes("ZombieDefense")
105
+ ) {
106
+ report.unknowns.push(p);
107
+ }
108
+ }
109
+ });
110
+ } catch (_e) {
111
+ // No bun processes
112
+ }
113
+
114
+ if (
115
+ report.ghosts.length > 0 ||
116
+ report.duplicates.length > 0 ||
117
+ report.unknowns.length > 0
118
+ ) {
119
+ report.clean = false;
120
+ }
121
+
122
+ return report;
123
+ },
124
+
125
+ /**
126
+ * Helper: Extract PIDs from report lines
127
+ */
128
+ extractPids(lines: string[]): string[] {
129
+ const pids = new Set<string>();
130
+ lines.forEach((line) => {
131
+ const match = line.match(/\s+(\d+)\s+/);
132
+ if (match?.[1]) {
133
+ pids.add(match[1]);
134
+ }
135
+ });
136
+ return Array.from(pids);
137
+ },
138
+
139
+ /**
140
+ * Terminate identified zombie PIDs
141
+ */
142
+ async killZombies(report: ZombieReport) {
143
+ let targets = [
144
+ ...new Set([
145
+ ...this.extractPids(report.ghosts),
146
+ ...this.extractPids(report.duplicates),
147
+ ]),
148
+ ];
149
+
150
+ // SAFETY: Exclude self
151
+ const selfPid = process.pid.toString();
152
+ targets = targets.filter((pid) => pid !== selfPid);
153
+
154
+ if (targets.length === 0) return;
155
+
156
+ console.error(
157
+ `šŸ”Ŗ Killing ${targets.length} zombie processes: ${targets.join(", ")}`,
158
+ );
159
+ try {
160
+ await execAsync(`kill -9 ${targets.join(" ")}`);
161
+ console.error(" āœ… Zombies terminated.");
162
+ } catch (err) {
163
+ const e = err as Error;
164
+ console.error(` āŒ Failed to kill zombies: ${e.message}`);
165
+ }
166
+ },
167
+
168
+ /**
169
+ * Enforce a clean state. Exits process if zombies found.
170
+ * @param serviceName Name of the service calling this guard
171
+ * @param interactive If true, prompts user to kill zombies.
172
+ */
173
+ async assertClean(serviceName: string, interactive = false) {
174
+ if (process.env.SKIP_ZOMBIE_CHECK === "true") {
175
+ return;
176
+ }
177
+ console.error(`šŸ›”ļø [${serviceName}] Running Zombie Defense Protocol...`);
178
+ let report = await this.scan();
179
+
180
+ if (report.clean) {
181
+ console.error(" āœ… Environment Clean.");
182
+ return;
183
+ }
184
+
185
+ console.error("\nšŸ›‘ STARTUP ABORTED: ZOMBIE PROCESSES DETECTED");
186
+
187
+ if (report.ghosts.length > 0) {
188
+ console.error("\nšŸ‘» GHOSTS (Holding deleted files):");
189
+ report.ghosts.forEach((g) => {
190
+ console.error(` ${g}`);
191
+ });
192
+ }
193
+
194
+ if (report.duplicates.length > 0) {
195
+ console.error("\nšŸ‘Æ DUPLICATES (Service already running?):");
196
+ report.duplicates.forEach((d) => {
197
+ console.error(` ${d}`);
198
+ });
199
+ }
200
+
201
+ if (report.unknowns.length > 0) {
202
+ console.error(
203
+ "\nšŸ‘½ UNKNOWNS (Rogue processes - Manual Check Recommended):",
204
+ );
205
+ report.unknowns.forEach((u) => {
206
+ console.error(` ${u}`);
207
+ });
208
+ }
209
+
210
+ if (interactive) {
211
+ const targets = this.extractPids([
212
+ ...report.ghosts,
213
+ ...report.duplicates,
214
+ ]);
215
+ if (targets.length > 0) {
216
+ process.stderr.write(
217
+ `\nšŸ‘‡ Found ${targets.length} confirmable zombies. Kill and Proceed? [y/N] `,
218
+ );
219
+ const answer = await new Promise<string>((resolve) => {
220
+ // Simple one-off prompt since Bun used here might not have 'prompt' in all envs
221
+ // Using Bun.stdin reader
222
+ process.stdin.once("data", (d) => resolve(d.toString().trim()));
223
+ });
224
+
225
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
226
+ await this.killZombies(report);
227
+ // Re-scan to verify
228
+ report = await this.scan();
229
+ if (report.clean) {
230
+ console.error(" āœ… Environment Cleared. Proceeding...");
231
+ return;
232
+ }
233
+ console.error(" āŒ Environment still dirty after kill attempt.");
234
+ }
235
+ }
236
+ } else {
237
+ console.error(
238
+ "\nšŸ‘‰ ACTION REQUIRED: Run 'pkill -f bun' to clear the environment.",
239
+ );
240
+ }
241
+
242
+ process.exit(1);
243
+ },
244
+ };