mnemos-capture 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,13 +12,26 @@
12
12
 
13
13
  <p align="center"><strong>Capture anything. Your agents apply it.</strong></p>
14
14
 
15
- <p align="center">A knowledge pipeline that ingests the outside world and makes it directly actionable by any AI agent — over MCP.</p>
15
+ <p align="center">
16
+ Humans consume information. Agents execute information.<br/>
17
+ <em>Mnemos converts one into the other.</em>
18
+ </p>
19
+
20
+ <p align="center">A knowledge pipeline that turns what you learn into actionable context for AI agents.</p>
21
+
22
+ ---
23
+
24
+ Your agent only knows what it was trained on. It doesn't know the article you read this morning, the framework you found last week, or the decision you made last sprint.
25
+
26
+ **Mnemos bridges that gap.**
27
+
28
+ Capture an insight once. Mnemos extracts it, stores it as plain Markdown in your own GitHub repository, indexes it, and serves it to any MCP-compatible agent.
16
29
 
17
- Your agent only knows what it was trained on. It doesn't know the article you read this morning, the framework you found last week, or the decision you made last sprint. **Your AI agent finally knows what you've been reading.**
30
+ Then it goes further. At session start, Mnemos briefs your agent on what matters right now, surfaces relevant knowledge in context, synthesizes reusable rules, and turns insights into implementation plans.
18
31
 
19
- Mnemos is the pipeline between the two. Capture any insight once — Mnemos extracts it, stores it as plain Markdown in your own GitHub repo, and serves it to your agent over MCP. Then it goes further: at session start it briefs your agent on what matters right now, turns insights into implementation plans, and hands them to whatever assistant you run — Claude Code, Codex, Cursor, Gemini, anything MCP-compatible.
32
+ > **The result isn't another knowledge base.** It's a system that transforms what you learn into what your agents can execute.
20
33
 
21
- It's not a second brain, a vault, or a note-taking app — those store knowledge for *you* to read later. Mnemos makes knowledge your **agent acts on now**.
34
+ <p align="center"><sub>Works with <strong>Claude Code</strong> · <strong>Codex</strong> · <strong>Cursor</strong> · <strong>Gemini</strong> · and any MCP-compatible assistant.</sub></p>
22
35
 
23
36
 
24
37
  ## Get started
@@ -2,8 +2,7 @@
2
2
  // Used by Claude Code: claude mcp add mnemos -- npx mnemos-capture serve-mcp --key <api-key>
3
3
  const HOSTED_URL = "https://mnemos-capture.vercel.app/api/mcp";
4
4
  function sendMessage(msg) {
5
- const json = JSON.stringify(msg);
6
- process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
5
+ process.stdout.write(`${JSON.stringify(msg)}\n`);
7
6
  }
8
7
  async function proxyToHosted(apiKey, msg) {
9
8
  try {
@@ -62,31 +61,21 @@ export async function serveMcp() {
62
61
  process.stdin.setEncoding("utf-8");
63
62
  process.stdin.on("data", (chunk) => {
64
63
  buffer += chunk;
65
- while (true) {
66
- const headerEnd = buffer.indexOf("\r\n\r\n");
67
- if (headerEnd === -1)
68
- break;
69
- const header = buffer.slice(0, headerEnd);
70
- const match = header.match(/Content-Length:\s*(\d+)/i);
71
- if (!match) {
72
- buffer = buffer.slice(headerEnd + 4);
64
+ let newlineIdx;
65
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
66
+ const line = buffer.slice(0, newlineIdx).trim();
67
+ buffer = buffer.slice(newlineIdx + 1);
68
+ if (!line)
73
69
  continue;
74
- }
75
- const contentLength = parseInt(match[1], 10);
76
- const bodyStart = headerEnd + 4;
77
- if (buffer.length < bodyStart + contentLength)
78
- break;
79
- const body = buffer.slice(bodyStart, bodyStart + contentLength);
80
- buffer = buffer.slice(bodyStart + contentLength);
81
70
  try {
82
- const msg = JSON.parse(body);
71
+ const msg = JSON.parse(line);
83
72
  // Handle notifications/initialized locally (no response needed)
84
73
  if (msg.method === "notifications/initialized")
85
74
  continue;
86
75
  void proxyToHosted(apiKey, msg);
87
76
  }
88
77
  catch {
89
- process.stderr.write(`Failed to parse: ${body}\n`);
78
+ process.stderr.write(`Failed to parse: ${line}\n`);
90
79
  }
91
80
  }
92
81
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnemos-capture",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Capture anything. Your agents apply it. A knowledge pipeline that ingests the outside world and makes it directly actionable by any AI agent over MCP.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "build:cli": "tsc -p cli/tsconfig.json",
14
14
  "prepublishOnly": "npm run build:cli",
15
15
  "test": "vitest run",
16
- "test:watch": "vitest"
16
+ "test:watch": "vitest",
17
+ "test:e2e": "playwright test"
17
18
  },
18
19
  "keywords": [
19
20
  "ai",
@@ -47,6 +48,7 @@
47
48
  "react-dom": "^19.0.0"
48
49
  },
49
50
  "devDependencies": {
51
+ "@playwright/test": "^1.61.1",
50
52
  "@types/node": "^20",
51
53
  "@types/react": "^19",
52
54
  "@types/react-dom": "^19",
@@ -1,49 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, rmSync, readFileSync, existsSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- describe("config", () => {
6
- let tmpDir;
7
- let configPath;
8
- let origHome;
9
- beforeEach(() => {
10
- tmpDir = mkdtempSync(join(tmpdir(), "mnemos-config-test-"));
11
- configPath = join(tmpDir, ".mnemos", "config.json");
12
- origHome = process.env["HOME"];
13
- process.env["HOME"] = tmpDir;
14
- // Module-level CONFIG_DIR is computed from homedir() at import — reset cache
15
- vi.resetModules();
16
- });
17
- afterEach(() => {
18
- rmSync(tmpDir, { recursive: true, force: true });
19
- process.env["HOME"] = origHome;
20
- });
21
- it("returns an empty object when no config exists", async () => {
22
- const { readConfig } = await import("../config.js");
23
- expect(readConfig()).toEqual({});
24
- });
25
- it("writes and reads back the agent field", async () => {
26
- const { setConfigField, readConfig } = await import("../config.js");
27
- setConfigField("agent", "claude -p");
28
- expect(readConfig().agent).toBe("claude -p");
29
- expect(existsSync(configPath)).toBe(true);
30
- });
31
- it("persists the agent command verbatim, including flags", async () => {
32
- const { setConfigField } = await import("../config.js");
33
- setConfigField("agent", "aider --yes --message");
34
- const saved = JSON.parse(readFileSync(configPath, "utf-8"));
35
- expect(saved.agent).toBe("aider --yes --message");
36
- });
37
- it("preserves other fields when updating one", async () => {
38
- const { setConfigField, readConfig } = await import("../config.js");
39
- setConfigField("key", "mnemos_abc123");
40
- setConfigField("agent", "codex exec");
41
- const cfg = readConfig();
42
- expect(cfg.key).toBe("mnemos_abc123");
43
- expect(cfg.agent).toBe("codex exec");
44
- });
45
- it("throws on an unknown config field", async () => {
46
- const { setConfigField } = await import("../config.js");
47
- expect(() => setConfigField("provider", "openai")).toThrow(/Unknown config field/);
48
- });
49
- });
@@ -1,276 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, rmSync, readFileSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- // ── Helpers ──────────────────────────────────────────────────────────────────
6
- /** Capture stdout writes during a callback. */
7
- async function captureStdout(fn) {
8
- const chunks = [];
9
- const orig = process.stdout.write.bind(process.stdout);
10
- process.stdout.write = (chunk) => {
11
- chunks.push(String(chunk));
12
- return true;
13
- };
14
- try {
15
- await fn();
16
- }
17
- finally {
18
- process.stdout.write = orig;
19
- }
20
- return chunks.join("");
21
- }
22
- // ── setupHooks / setupPreToolHook ────────────────────────────────────────────
23
- describe("setupHooks", () => {
24
- let tmpDir;
25
- let settingsPath;
26
- let origHome;
27
- beforeEach(() => {
28
- tmpDir = mkdtempSync(join(tmpdir(), "mnemos-test-"));
29
- settingsPath = join(tmpDir, ".claude", "settings.json");
30
- origHome = process.env["HOME"];
31
- process.env["HOME"] = tmpDir;
32
- // Clear module cache so CLAUDE_SETTINGS_PATH is recomputed with the new HOME
33
- vi.resetModules();
34
- });
35
- afterEach(() => {
36
- rmSync(tmpDir, { recursive: true, force: true });
37
- process.env["HOME"] = origHome;
38
- });
39
- it("installs a SessionStart hook with the provided API key", async () => {
40
- const { setupHooks } = await import("../hooks.js");
41
- setupHooks("my-api-key", { silent: true });
42
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
43
- const cmd = settings.hooks.SessionStart[0]?.hooks[0]?.command ?? "";
44
- expect(cmd).toContain("inbox-check");
45
- expect(cmd).toContain("my-api-key");
46
- });
47
- it("includes --briefing flag when briefing option is true", async () => {
48
- const { setupHooks } = await import("../hooks.js");
49
- setupHooks("key123", { silent: true, briefing: true });
50
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
51
- const cmd = settings.hooks.SessionStart[0]?.hooks[0]?.command ?? "";
52
- expect(cmd).toContain("--briefing");
53
- });
54
- it("omits --briefing flag when briefing option is false", async () => {
55
- const { setupHooks } = await import("../hooks.js");
56
- setupHooks("key123", { silent: true, briefing: false });
57
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
58
- const cmd = settings.hooks.SessionStart[0]?.hooks[0]?.command ?? "";
59
- expect(cmd).not.toContain("--briefing");
60
- });
61
- it("also installs PreToolCall hook when vault option is true", async () => {
62
- const { setupHooks } = await import("../hooks.js");
63
- setupHooks("vault-key", { silent: true, vault: true });
64
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
65
- expect(settings.hooks.PreToolCall).toBeDefined();
66
- const cmd = settings.hooks.PreToolCall[0]?.hooks[0]?.command ?? "";
67
- expect(cmd).toContain("vault-check");
68
- expect(cmd).toContain("vault-key");
69
- });
70
- it("does NOT install PreToolCall hook when vault option is false", async () => {
71
- const { setupHooks } = await import("../hooks.js");
72
- setupHooks("no-vault-key", { silent: true, vault: false });
73
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
74
- expect(settings.hooks["PreToolCall"]).toBeUndefined();
75
- });
76
- it("replaces an existing mnemos hook rather than duplicating it", async () => {
77
- const { setupHooks } = await import("../hooks.js");
78
- setupHooks("old-key", { silent: true });
79
- setupHooks("new-key", { silent: true });
80
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
81
- const cmds = settings.hooks.SessionStart.flatMap((g) => g.hooks.map((h) => h.command));
82
- const mnemosHooks = cmds.filter((c) => c.includes("inbox-check"));
83
- expect(mnemosHooks).toHaveLength(1);
84
- expect(mnemosHooks[0]).toContain("new-key");
85
- });
86
- it("skipIfExists: keeps exactly one mnemos hook when called twice", async () => {
87
- const { setupHooks } = await import("../hooks.js");
88
- setupHooks("original-key", { silent: true });
89
- setupHooks("different-key", { silent: true, skipIfExists: true });
90
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
91
- const cmds = settings.hooks.SessionStart.flatMap((g) => g.hooks.map((h) => h.command));
92
- expect(cmds.filter((c) => c.includes("inbox-check"))).toHaveLength(1);
93
- });
94
- });
95
- // ── inboxCheck — briefing mode parsing ───────────────────────────────────────
96
- describe("inboxCheck — briefing mode", () => {
97
- let fetchSpy;
98
- beforeEach(() => {
99
- fetchSpy = vi.spyOn(globalThis, "fetch");
100
- });
101
- afterEach(() => {
102
- fetchSpy.mockRestore();
103
- });
104
- function makeMcpResponse(text) {
105
- return Promise.resolve(new Response(JSON.stringify({ result: { content: [{ text }] } }), { status: 200, headers: { "Content-Type": "application/json" } }));
106
- }
107
- it("strips the JSON block from display output", async () => {
108
- fetchSpy.mockReturnValue(makeMcpResponse(`\`\`\`json
109
- [{"filename":"inbox/a.md","why":"x","benefit":"y","where":"z.ts","applyNow":true}]
110
- \`\`\`
111
-
112
- ## Relevant Rules
113
- None applicable.`));
114
- const { inboxCheck } = await import("../hooks.js");
115
- const output = await captureStdout(() => inboxCheck("key", { briefing: true }));
116
- expect(output).not.toContain("```json");
117
- expect(output).toContain("## Relevant Rules");
118
- });
119
- it("appends generate_plan prompt when there are applyNow suggestions", async () => {
120
- fetchSpy.mockReturnValue(makeMcpResponse(`\`\`\`json
121
- [{"filename":"inbox/a.md","why":"x","benefit":"y","where":"z.ts","applyNow":true}]
122
- \`\`\`
123
-
124
- ## Apply Now
125
- One insight.`));
126
- const { inboxCheck } = await import("../hooks.js");
127
- const output = await captureStdout(() => inboxCheck("key", { briefing: true }));
128
- expect(output).toContain("generate_plan");
129
- expect(output).toContain("inbox/a.md");
130
- });
131
- it("does NOT append generate_plan prompt when all suggestions have applyNow=false", async () => {
132
- fetchSpy.mockReturnValue(makeMcpResponse(`\`\`\`json
133
- [{"filename":"inbox/b.md","why":"x","benefit":"y","where":"z.ts","applyNow":false}]
134
- \`\`\`
135
-
136
- ## Knowledge Vault
137
- One vault item.`));
138
- const { inboxCheck } = await import("../hooks.js");
139
- const output = await captureStdout(() => inboxCheck("key", { briefing: true }));
140
- expect(output).not.toContain("generate_plan");
141
- });
142
- it("does NOT append generate_plan prompt when JSON block is missing", async () => {
143
- fetchSpy.mockReturnValue(makeMcpResponse(`## Relevant Rules
144
- None applicable.`));
145
- const { inboxCheck } = await import("../hooks.js");
146
- const output = await captureStdout(() => inboxCheck("key", { briefing: true }));
147
- expect(output).not.toContain("generate_plan");
148
- });
149
- it("shows inbox count in non-briefing mode when count > 0", async () => {
150
- fetchSpy.mockReturnValue(makeMcpResponse("3 capture(s) in inbox:\n\n1. file.md"));
151
- const { inboxCheck } = await import("../hooks.js");
152
- const output = await captureStdout(() => inboxCheck("key"));
153
- expect(output).toContain("3");
154
- expect(output).toContain("inbox");
155
- });
156
- it("shows nothing in non-briefing mode when inbox is empty", async () => {
157
- fetchSpy.mockReturnValue(makeMcpResponse("Inbox is empty."));
158
- const { inboxCheck } = await import("../hooks.js");
159
- const output = await captureStdout(() => inboxCheck("key"));
160
- expect(output).toBe("");
161
- });
162
- it("silently fails when fetch throws", async () => {
163
- fetchSpy.mockRejectedValue(new Error("Network error"));
164
- const { inboxCheck } = await import("../hooks.js");
165
- // Should not throw
166
- await expect(inboxCheck("key", { briefing: true })).resolves.toBeUndefined();
167
- });
168
- });
169
- // ── vaultCheck — stdin parsing ────────────────────────────────────────────────
170
- describe("vaultCheck", () => {
171
- let fetchSpy;
172
- let origSessionId;
173
- beforeEach(() => {
174
- fetchSpy = vi.spyOn(globalThis, "fetch");
175
- origSessionId = process.env["CLAUDE_SESSION_ID"];
176
- // Use a unique session ID per test to avoid state pollution
177
- process.env["CLAUDE_SESSION_ID"] = `test-${Date.now()}-${Math.random()}`;
178
- });
179
- afterEach(() => {
180
- fetchSpy.mockRestore();
181
- process.env["CLAUDE_SESSION_ID"] = origSessionId;
182
- });
183
- function makeMcpResponse(text) {
184
- return Promise.resolve(new Response(JSON.stringify({ result: { content: [{ text }] } }), { status: 200, headers: { "Content-Type": "application/json" } }));
185
- }
186
- it("outputs vault match when fetch returns a high-score match", async () => {
187
- fetchSpy.mockReturnValue(makeMcpResponse("Mnemos vault match (score: 87%):\n\n**2026-05-01-error-ctx**\nCore idea here.\n- Key takeaway\n\nApply it? Run: `apply_capture` with filename: `inbox/2026-05-01-error-ctx.md`"));
188
- const { vaultCheck } = await import("../hooks.js");
189
- // Simulate piped stdin with an Edit tool call
190
- const hookData = JSON.stringify({
191
- tool_name: "Edit",
192
- tool_input: { file_path: "src/api/capture/route.ts" },
193
- });
194
- // Temporarily replace stdin
195
- const mockStdin = {
196
- isTTY: false,
197
- setEncoding: vi.fn(),
198
- on: vi.fn((event, cb) => {
199
- if (event === "data")
200
- cb(hookData);
201
- if (event === "end")
202
- cb();
203
- }),
204
- };
205
- const origStdin = process.stdin;
206
- Object.defineProperty(process, "stdin", { value: mockStdin, writable: true, configurable: true });
207
- const output = await captureStdout(() => vaultCheck("key"));
208
- Object.defineProperty(process, "stdin", { value: origStdin, writable: true, configurable: true });
209
- expect(output).toContain("vault match");
210
- });
211
- it("outputs nothing when vault_scan returns empty", async () => {
212
- fetchSpy.mockReturnValue(makeMcpResponse(""));
213
- const { vaultCheck } = await import("../hooks.js");
214
- const mockStdin = {
215
- isTTY: false,
216
- setEncoding: vi.fn(),
217
- on: vi.fn((event, cb) => {
218
- if (event === "data")
219
- cb(JSON.stringify({ tool_name: "Edit", tool_input: { file_path: "src/foo.ts" } }));
220
- if (event === "end")
221
- cb();
222
- }),
223
- };
224
- const origStdin = process.stdin;
225
- Object.defineProperty(process, "stdin", { value: mockStdin, writable: true, configurable: true });
226
- const output = await captureStdout(() => vaultCheck("key"));
227
- Object.defineProperty(process, "stdin", { value: origStdin, writable: true, configurable: true });
228
- expect(output).toBe("");
229
- });
230
- it("outputs nothing and does not call fetch when tool is not an editing tool", async () => {
231
- const { vaultCheck } = await import("../hooks.js");
232
- const mockStdin = {
233
- isTTY: false,
234
- setEncoding: vi.fn(),
235
- on: vi.fn((event, cb) => {
236
- if (event === "data")
237
- cb(JSON.stringify({ tool_name: "Bash", tool_input: { command: "ls" } }));
238
- if (event === "end")
239
- cb();
240
- }),
241
- };
242
- const origStdin = process.stdin;
243
- Object.defineProperty(process, "stdin", { value: mockStdin, writable: true, configurable: true });
244
- await vaultCheck("key");
245
- Object.defineProperty(process, "stdin", { value: origStdin, writable: true, configurable: true });
246
- expect(fetchSpy).not.toHaveBeenCalled();
247
- });
248
- it("outputs nothing when stdin is a TTY (not piped)", async () => {
249
- const { vaultCheck } = await import("../hooks.js");
250
- const mockStdin = { isTTY: true };
251
- const origStdin = process.stdin;
252
- Object.defineProperty(process, "stdin", { value: mockStdin, writable: true, configurable: true });
253
- const output = await captureStdout(() => vaultCheck("key"));
254
- Object.defineProperty(process, "stdin", { value: origStdin, writable: true, configurable: true });
255
- expect(output).toBe("");
256
- expect(fetchSpy).not.toHaveBeenCalled();
257
- });
258
- it("silently fails when fetch throws", async () => {
259
- fetchSpy.mockRejectedValue(new Error("Network error"));
260
- const { vaultCheck } = await import("../hooks.js");
261
- const mockStdin = {
262
- isTTY: false,
263
- setEncoding: vi.fn(),
264
- on: vi.fn((event, cb) => {
265
- if (event === "data")
266
- cb(JSON.stringify({ tool_name: "Edit", tool_input: { file_path: "src/foo.ts" } }));
267
- if (event === "end")
268
- cb();
269
- }),
270
- };
271
- const origStdin = process.stdin;
272
- Object.defineProperty(process, "stdin", { value: mockStdin, writable: true, configurable: true });
273
- await expect(vaultCheck("key")).resolves.toBeUndefined();
274
- Object.defineProperty(process, "stdin", { value: origStdin, writable: true, configurable: true });
275
- });
276
- });
@@ -1,65 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parsePlanFilenames, latestPlanFilename, parseChecklist, buildKosBranch, } from "../kos.js";
3
- const LIST_PLANS = `3 plan(s):
4
-
5
- - **2026-06-03-plan-error-handling.md**: Implementation Plan: Error Handling
6
- - **2026-06-05-plan-provider-abstraction.md**: Implementation Plan: Provider Abstraction
7
- - **2026-06-04-plan-vault.md**: Implementation Plan: Vault`;
8
- describe("parsePlanFilenames", () => {
9
- it("extracts all bolded plan filenames", () => {
10
- expect(parsePlanFilenames(LIST_PLANS)).toEqual([
11
- "2026-06-03-plan-error-handling.md",
12
- "2026-06-05-plan-provider-abstraction.md",
13
- "2026-06-04-plan-vault.md",
14
- ]);
15
- });
16
- it("returns empty array when there are no plans", () => {
17
- expect(parsePlanFilenames("No plans saved yet.")).toEqual([]);
18
- });
19
- });
20
- describe("latestPlanFilename", () => {
21
- it("returns the newest plan by date prefix", () => {
22
- expect(latestPlanFilename(LIST_PLANS)).toBe("2026-06-05-plan-provider-abstraction.md");
23
- });
24
- it("returns null when there are no plans", () => {
25
- expect(latestPlanFilename("No plans saved yet.")).toBeNull();
26
- });
27
- });
28
- describe("parseChecklist", () => {
29
- const PLAN = `# Implementation Plan: Foo
30
-
31
- ## Codebase Mapping
32
- | File | Capture | Change | Complexity | Effort |
33
- |------|---------|--------|------------|--------|
34
- | a.ts | x | edit | simple | low |
35
-
36
- ## Verification Checklist
37
- - [ ] Error messages include a handler prefix
38
- - [ ] No regressions in the happy path
39
- - [x] Already-done item still counts
40
-
41
- ## Notes
42
- Not part of the checklist.`;
43
- it("extracts checklist items without the checkbox prefix", () => {
44
- expect(parseChecklist(PLAN)).toEqual([
45
- "Error messages include a handler prefix",
46
- "No regressions in the happy path",
47
- "Already-done item still counts",
48
- ]);
49
- });
50
- it("stops at the next section heading", () => {
51
- expect(parseChecklist(PLAN)).not.toContain("Not part of the checklist.");
52
- });
53
- it("returns empty array when there is no checklist section", () => {
54
- expect(parseChecklist("# Plan\n\n## Context\nNothing here.")).toEqual([]);
55
- });
56
- });
57
- describe("buildKosBranch", () => {
58
- it("produces a deterministic, sortable branch name from a date", () => {
59
- const branch = buildKosBranch(new Date("2026-06-05T10:30:45.000Z"));
60
- expect(branch).toBe("mnemos/kos-2026-06-05-10-30-45");
61
- });
62
- it("always starts with the mnemos/kos- prefix", () => {
63
- expect(buildKosBranch()).toMatch(/^mnemos\/kos-/);
64
- });
65
- });