kodelyth-ecc 1.8.0 → 1.8.2

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 (91) hide show
  1. package/CLAUDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/bin/kodelyth-ecc.js +54 -5
  4. package/install.ps1 +30 -13
  5. package/install.sh +11 -4
  6. package/package.json +1 -1
  7. package/rules/common/agent-intent-routing.md +96 -195
  8. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
  9. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  10. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -41
  11. package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -36
  12. package/.github/ISSUE_TEMPLATE/new_agent.md +0 -52
  13. package/.github/PULL_REQUEST_TEMPLATE.md +0 -53
  14. package/.github/workflows/ci.yml +0 -45
  15. package/.github/workflows/publish.yml +0 -60
  16. package/brand/concepts.svg +0 -109
  17. package/brand/convert.js +0 -161
  18. package/brand/favicon.svg +0 -7
  19. package/brand/fb-cover.svg +0 -34
  20. package/brand/fb-profile.svg +0 -21
  21. package/brand/kodelyth-dark.svg +0 -24
  22. package/brand/kodelyth-light.svg +0 -24
  23. package/brand/kodelyth-mark.svg +0 -20
  24. package/brand/package.json +0 -12
  25. package/docs/dashboard.md +0 -211
  26. package/docs/evolve.md +0 -303
  27. package/docs/mcp-clients.md +0 -167
  28. package/docs/mcp.md +0 -178
  29. package/docs/replay.md +0 -244
  30. package/docs/supply-chain.md +0 -207
  31. package/docs/swarm.md +0 -243
  32. package/skills/skill-comply/tests/test_grader.py +0 -137
  33. package/skills/skill-comply/tests/test_parser.py +0 -90
  34. package/social/card-agents.svg +0 -104
  35. package/social/card-install.svg +0 -58
  36. package/social/card-main.svg +0 -54
  37. package/social/facebook-v150.svg +0 -110
  38. package/social/fb-ad-main.svg +0 -128
  39. package/social/fb-post-features.svg +0 -118
  40. package/social/fb-post-launch.svg +0 -144
  41. package/social/fb-post-platforms.svg +0 -135
  42. package/social/github-social-preview.svg +0 -151
  43. package/social/hype-compound-learning.svg +0 -129
  44. package/social/hype-devil-mode.svg +0 -240
  45. package/social/hype-mcp-server.svg +0 -148
  46. package/social/hype-parallel-agents.svg +0 -162
  47. package/social/hype-stats-hero.svg +0 -138
  48. package/social/og-image.svg +0 -151
  49. package/social/readme-agents.svg +0 -138
  50. package/social/readme-hero.svg +0 -102
  51. package/social/section-agents.svg +0 -69
  52. package/social/section-author.svg +0 -66
  53. package/social/section-dashboard.svg +0 -71
  54. package/social/section-devil.svg +0 -66
  55. package/social/section-hooks.svg +0 -63
  56. package/social/section-install.svg +0 -54
  57. package/social/section-learning.svg +0 -64
  58. package/social/section-mcp.svg +0 -58
  59. package/social/section-memory.svg +0 -69
  60. package/social/section-parallel.svg +0 -84
  61. package/social/section-routing.svg +0 -63
  62. package/social/twitter-threads.md +0 -621
  63. package/social/x-card-agents-grid.svg +0 -120
  64. package/social/x-card-free.svg +0 -71
  65. package/social/x-card-hook.svg +0 -94
  66. package/tests/dashboard/data.test.js +0 -391
  67. package/tests/dashboard/server.test.js +0 -257
  68. package/tests/evolve/analyze.test.js +0 -169
  69. package/tests/evolve/proposals.test.js +0 -173
  70. package/tests/evolve/stats.test.js +0 -167
  71. package/tests/hooks/branch-name-check.test.js +0 -184
  72. package/tests/hooks/smart-suggest.test.js +0 -149
  73. package/tests/hooks/test-reminder.test.js +0 -181
  74. package/tests/mcp/catalog.test.js +0 -98
  75. package/tests/mcp/client.test.js +0 -113
  76. package/tests/mcp/resources-prompts.test.js +0 -70
  77. package/tests/mcp/tools.test.js +0 -159
  78. package/tests/memory/auto-recall.test.js +0 -132
  79. package/tests/memory/instincts.test.js +0 -258
  80. package/tests/memory/store.test.js +0 -203
  81. package/tests/replay/bundle.test.js +0 -181
  82. package/tests/replay/replay.test.js +0 -131
  83. package/tests/router/classify.test.js +0 -164
  84. package/tests/run-all.js +0 -55
  85. package/tests/safety/patterns.test.js +0 -94
  86. package/tests/safety/prompt-injection-guard.test.js +0 -111
  87. package/tests/safety/token-budget.test.js +0 -119
  88. package/tests/supply-chain/manifest.test.js +0 -147
  89. package/tests/supply-chain/sbom.test.js +0 -170
  90. package/tests/supply-chain/verify.test.js +0 -146
  91. package/tests/swarm/build-plan.test.js +0 -188
@@ -1,113 +0,0 @@
1
- // Tests for scripts/mcp/client.js — registry + connect helpers.
2
- // Uses the LOCAL Kodelyth MCP server itself as a real test target so we
3
- // exercise the full stdio handshake without needing external dependencies.
4
- 'use strict';
5
-
6
- const test = require('node:test');
7
- const assert = require('node:assert/strict');
8
- const fs = require('fs');
9
- const os = require('os');
10
- const path = require('path');
11
-
12
- // Isolate the registry to a tmp dir so we don't pollute ~/.kodelyth.
13
- const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-mcp-client-'));
14
- process.env.KODELYTH_MCP_CLIENT_DIR = TMP;
15
-
16
- const C = require('../../scripts/mcp/client.js');
17
-
18
- const ROOT = path.join(__dirname, '..', '..');
19
- const LOCAL_SERVER = path.join(ROOT, 'scripts', 'mcp', 'server.js');
20
-
21
- // ── Registry tests (no SDK required) ─────────────────────────────────────────
22
-
23
- test('registry: starts empty', () => {
24
- const reg = C.loadRegistry();
25
- assert.deepEqual(reg.servers, {});
26
- assert.deepEqual(C.listServers(), []);
27
- });
28
-
29
- test('registry: addServer persists with sane defaults', () => {
30
- const spec = C.addServer({
31
- name: 'self',
32
- command: process.execPath,
33
- args: [LOCAL_SERVER],
34
- env: { TEST_VAR: '1' },
35
- description: 'local self-test target',
36
- });
37
- assert.equal(spec.name, 'self');
38
- assert.equal(spec.command, process.execPath);
39
- assert.deepEqual(spec.args, [LOCAL_SERVER]);
40
- assert.equal(spec.env.TEST_VAR, '1');
41
- assert.ok(spec.added_at);
42
-
43
- // Persisted to disk:
44
- assert.ok(fs.existsSync(C.REGISTRY_FILE));
45
- const onDisk = JSON.parse(fs.readFileSync(C.REGISTRY_FILE, 'utf8'));
46
- assert.equal(onDisk.servers.self.name, 'self');
47
- });
48
-
49
- test('registry: rejects bad names', () => {
50
- assert.throws(() => C.addServer({ name: '', command: 'x' }));
51
- assert.throws(() => C.addServer({ name: 'has space',command: 'x' }));
52
- assert.throws(() => C.addServer({ name: 'self', command: '' }));
53
- });
54
-
55
- test('registry: getServer / listServers reflect adds + removes', () => {
56
- C.addServer({ name: 'second', command: '/usr/bin/true', args: [] });
57
- const list = C.listServers();
58
- assert.equal(list.length, 2, `expected 2, got ${list.length}`);
59
- const names = list.map(s => s.name).sort();
60
- assert.deepEqual(names, ['second', 'self']);
61
-
62
- assert.ok(C.getServer('self'));
63
- assert.equal(C.getServer('not-real'), null);
64
-
65
- assert.equal(C.removeServer('second'), true);
66
- assert.equal(C.removeServer('second'), false); // already gone
67
- assert.equal(C.listServers().length, 1);
68
- });
69
-
70
- // ── End-to-end: connect to local kodelyth MCP server and call tools ──────────
71
- // These tests require @modelcontextprotocol/sdk (optionalDependency).
72
- // Skip gracefully when it isn't installed (e.g. CI without optional deps).
73
- let hasSdk = false;
74
- try { require('@modelcontextprotocol/sdk/client/index.js'); hasSdk = true; } catch (_) {}
75
-
76
- test('client: connects to local kodelyth MCP server and lists tools', { skip: !hasSdk }, async () => {
77
- // self is already registered from the earlier test (running in same file).
78
- const out = await C.listTools('self');
79
- assert.ok(out.tools.length > 0, 'expected at least one tool from local kodelyth server');
80
- const names = new Set(out.tools.map(t => t.name));
81
- assert.ok(names.has('catalog_stats'), 'catalog_stats tool must be exposed');
82
- assert.ok(names.has('list_agents'), 'list_agents tool must be exposed');
83
- });
84
-
85
- test('client: callTool on local server returns a real result', { skip: !hasSdk }, async () => {
86
- const out = await C.callTool('self', 'catalog_stats', {});
87
- assert.ok(Array.isArray(out.content));
88
- assert.equal(out.content[0].type, 'text');
89
- const data = JSON.parse(out.content[0].text);
90
- assert.ok(data.agents > 0);
91
- assert.ok(data.skills > 0);
92
- assert.ok(data.commands > 0);
93
- });
94
-
95
- test('client: listResources surfaces kodelyth:// URIs', { skip: !hasSdk }, async () => {
96
- const out = await C.listResources('self');
97
- assert.ok(out.resources.length > 0);
98
- assert.ok(out.resources.every(r => r.uri.startsWith('kodelyth://')),
99
- 'every resource uri should start with kodelyth://');
100
- });
101
-
102
- test('client: listPrompts surfaces routing-rule', { skip: !hasSdk }, async () => {
103
- const out = await C.listPrompts('self');
104
- const names = new Set(out.prompts.map(p => p.name));
105
- assert.ok(names.has('routing-rule'));
106
- });
107
-
108
- test('client: connect fails clearly for unregistered server', async () => {
109
- await assert.rejects(
110
- () => C.connect('this-was-never-registered'),
111
- err => err.code === 'MCP_SERVER_NOT_REGISTERED'
112
- );
113
- });
@@ -1,70 +0,0 @@
1
- // Tests for scripts/mcp/resources.js and scripts/mcp/prompts.js
2
- 'use strict';
3
-
4
- const test = require('node:test');
5
- const assert = require('node:assert/strict');
6
-
7
- const RES = require('../../scripts/mcp/resources');
8
- const PROMPTS = require('../../scripts/mcp/prompts');
9
-
10
- test('resources.buildList returns agents/skills/commands/rules/bundles', () => {
11
- const list = RES.buildList();
12
- assert.ok(list.length > 200, `expected >200 resources, got ${list.length}`);
13
- const schemes = new Set(list.map(r => r.uri.split('://')[1].split('/')[0]));
14
- for (const expected of ['agents', 'skills', 'commands', 'rules', 'bundles']) {
15
- assert.ok(schemes.has(expected), `missing resource bucket: ${expected}`);
16
- }
17
- for (const r of list.slice(0, 5)) {
18
- assert.match(r.uri, /^kodelyth:\/\//);
19
- assert.equal(r.mimeType, 'text/markdown');
20
- }
21
- });
22
-
23
- test('resources.readResource returns body for a known agent', () => {
24
- const out = RES.readResource('kodelyth://agents/debug-detective');
25
- assert.ok(out.contents.length === 1);
26
- assert.equal(out.contents[0].mimeType, 'text/markdown');
27
- assert.match(out.contents[0].text, /debug-detective/);
28
- });
29
-
30
- test('resources.readResource rejects unknown URI scheme', () => {
31
- assert.throws(() => RES.readResource('weird://foo/bar'));
32
- });
33
-
34
- test('resources.readResource rejects unknown name', () => {
35
- assert.throws(() => RES.readResource('kodelyth://agents/no-such-agent'));
36
- });
37
-
38
- test('PROMPT_DEFINITIONS exposes routing-rule and overviews', () => {
39
- const names = PROMPTS.PROMPT_DEFINITIONS.map(p => p.name);
40
- for (const expected of [
41
- 'routing-rule',
42
- 'agents-overview',
43
- 'skills-overview',
44
- 'commands-overview',
45
- 'handoff-chains',
46
- 'devil-mode',
47
- ]) {
48
- assert.ok(names.includes(expected), `missing prompt: ${expected}`);
49
- }
50
- });
51
-
52
- test('routing-rule prompt returns the full intent routing rule body', () => {
53
- const out = PROMPTS.PROMPT_HANDLERS['routing-rule']();
54
- assert.ok(out.messages.length === 1);
55
- assert.equal(out.messages[0].role, 'user');
56
- assert.match(out.messages[0].content.text, /Agent Intent Routing/);
57
- });
58
-
59
- test('agents-overview returns a summary of all agents', () => {
60
- const out = PROMPTS.PROMPT_HANDLERS['agents-overview']();
61
- const text = out.messages[0].content.text;
62
- assert.match(text, /Kodelyth ECC — Agents/);
63
- assert.match(text, /debug-detective/);
64
- });
65
-
66
- test('devil-mode prompt returns the parallel command body', () => {
67
- const out = PROMPTS.PROMPT_HANDLERS['devil-mode']();
68
- const text = out.messages[0].content.text;
69
- assert.match(text, /devil-mode/i);
70
- });
@@ -1,159 +0,0 @@
1
- // Tests for scripts/mcp/tools.js — pure-function MCP tool handlers.
2
- 'use strict';
3
-
4
- const test = require('node:test');
5
- const assert = require('node:assert/strict');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const path = require('path');
9
-
10
- // Isolate the memory store to a tmp dir so capture/recall don't pollute
11
- // the user's real ~/.kodelyth/memory.
12
- const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-mcp-'));
13
- process.env.KODELYTH_MEMORY_DIR = TMP;
14
-
15
- // Load AFTER setting env so the memory store picks up the tmp dir.
16
- const T = require('../../scripts/mcp/tools');
17
-
18
- function parseJSON(toolResult) {
19
- const text = toolResult.content[0].text;
20
- return JSON.parse(text);
21
- }
22
-
23
- test('TOOL_DEFINITIONS exposes the expected tool surface', () => {
24
- const names = T.TOOL_DEFINITIONS.map(t => t.name);
25
- for (const expected of [
26
- 'route_intent',
27
- 'list_agents',
28
- 'list_skills',
29
- 'list_commands',
30
- 'list_rules',
31
- 'list_bundles',
32
- 'get_agent',
33
- 'get_skill',
34
- 'get_command',
35
- 'get_rule',
36
- 'get_bundle',
37
- 'recall_memory',
38
- 'capture_memory',
39
- 'memory_stats',
40
- 'catalog_stats',
41
- 'audit_skill_match',
42
- ]) {
43
- assert.ok(names.includes(expected), `missing tool: ${expected}`);
44
- }
45
- // Every tool has a handler entry.
46
- for (const t of T.TOOL_DEFINITIONS) {
47
- assert.equal(typeof T.TOOL_HANDLERS[t.name], 'function', `${t.name} handler missing`);
48
- assert.ok(t.description && t.description.length > 5, `${t.name} description too short`);
49
- assert.ok(t.inputSchema && t.inputSchema.type === 'object', `${t.name} inputSchema malformed`);
50
- }
51
- });
52
-
53
- test('route_intent rejects empty message', () => {
54
- const r = T.tool_route_intent({});
55
- assert.ok(r.isError, 'must error on missing message');
56
- });
57
-
58
- test('route_intent surfaces a relevant agent for a debug task', () => {
59
- const r = T.tool_route_intent({ message: 'i have a hard intermittent bug in production' });
60
- const data = parseJSON(r);
61
- assert.ok(Array.isArray(data.suggestions));
62
- assert.ok(data.suggestions.length > 0, 'should produce at least one suggestion');
63
- });
64
-
65
- test('route_intent honours top_k', () => {
66
- const r = T.tool_route_intent({ message: 'review this code for security and quality issues', top_k: 2 });
67
- const data = parseJSON(r);
68
- assert.ok(data.suggestions.length <= 2);
69
- });
70
-
71
- test('list_agents returns the full catalog', () => {
72
- const r = T.tool_list_agents();
73
- const data = parseJSON(r);
74
- assert.ok(data.count >= 60);
75
- assert.ok(Array.isArray(data.agents) && data.agents.length === data.count);
76
- });
77
-
78
- test('get_agent returns body for known agent', () => {
79
- const r = T.tool_get_agent({ name: 'debug-detective' });
80
- assert.ok(!r.isError);
81
- assert.match(r.content[0].text, /^# Agent: debug-detective/);
82
- });
83
-
84
- test('get_agent errors on unknown agent', () => {
85
- const r = T.tool_get_agent({ name: 'no-such-agent' });
86
- assert.ok(r.isError);
87
- });
88
-
89
- test('get_command tolerates leading slash', () => {
90
- const r = T.tool_get_command({ name: '/code-review' });
91
- assert.ok(!r.isError);
92
- assert.match(r.content[0].text, /^# Command: \/code-review/);
93
- });
94
-
95
- test('get_rule returns the intent routing rule', () => {
96
- const r = T.tool_get_rule({ name: 'agent-intent-routing' });
97
- assert.ok(!r.isError);
98
- assert.match(r.content[0].text, /Agent Intent Routing/);
99
- });
100
-
101
- test('get_bundle returns red-team cheat sheet', () => {
102
- const r = T.tool_get_bundle({ name: 'red-team' });
103
- assert.ok(!r.isError);
104
- assert.ok(r.content[0].text.length > 100);
105
- });
106
-
107
- test('catalog_stats returns counts', () => {
108
- const r = T.tool_catalog_stats();
109
- const s = parseJSON(r);
110
- assert.ok(s.agents > 0 && s.skills > 0 && s.commands > 0);
111
- });
112
-
113
- test('audit_skill_match suggests at least one skill for an observability task', () => {
114
- const r = T.tool_audit_skill_match({ task: 'add structured logging and tracing to a fastapi service' });
115
- const data = parseJSON(r);
116
- assert.ok(Array.isArray(data.suggestions));
117
- assert.ok(data.suggestions.length > 0, 'expected at least one skill suggestion');
118
- });
119
-
120
- test('capture_memory + recall_memory roundtrip in isolated tmp store', () => {
121
- // Pre-seed a few unrelated memories so BM25 IDF can rise above the store's
122
- // default minScore floor (with N=1 IDF stays ~0.29 which is filtered out).
123
- for (const seed of [
124
- { problem: 'fix a flaky CI build on github actions', approach: 'set timeout to 60s and retry once' },
125
- { problem: 'wire up redis caching for product list', approach: 'use 60s ttl plus stale-while-revalidate' },
126
- { problem: 'migrate postgres timestamps to utc', approach: 'add at::timestamptz coercion in alembic migration' },
127
- ]) {
128
- T.tool_capture_memory({ ...seed, tags: ['seed'], language: 'misc' });
129
- }
130
-
131
- const cap = T.tool_capture_memory({
132
- problem: 'mcp test problem keyword splatzilla',
133
- approach: 'documenting that capture_memory works end-to-end',
134
- tags: ['mcp', 'test'],
135
- language: 'javascript',
136
- });
137
- assert.ok(!cap.isError, 'capture must succeed');
138
- const capData = parseJSON(cap);
139
- assert.ok(capData.id && typeof capData.id === 'string');
140
- assert.ok(capData.captured_at);
141
-
142
- const rec = T.tool_recall_memory({ query: 'splatzilla', limit: 3 });
143
- assert.ok(!rec.isError);
144
- const recData = parseJSON(rec);
145
- assert.ok(recData.count >= 1, 'should recall the memory we just captured');
146
- assert.equal(recData.memories[0].problem, 'mcp test problem keyword splatzilla');
147
- });
148
-
149
- test('capture_memory rejects missing required fields', () => {
150
- const r = T.tool_capture_memory({ problem: 'only problem, no approach' });
151
- assert.ok(r.isError);
152
- });
153
-
154
- test('memory_stats reflects at least one stored memory', () => {
155
- const r = T.tool_memory_stats();
156
- assert.ok(!r.isError);
157
- const s = parseJSON(r);
158
- assert.ok(typeof s === 'object');
159
- });
@@ -1,132 +0,0 @@
1
- // Tests for hooks/memory/auto-recall.js — UserPromptSubmit auto chat detection
2
- 'use strict';
3
-
4
- const test = require('node:test');
5
- const assert = require('node:assert/strict');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const path = require('path');
9
- const { spawnSync } = require('child_process');
10
-
11
- const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-recall-'));
12
- process.env.KODELYTH_MEMORY_DIR = TMP;
13
-
14
- const HOOK = path.join(__dirname, '..', '..', 'hooks', 'memory', 'auto-recall.js');
15
- const store = require('../../scripts/memory/store');
16
-
17
- function runHook(payload) {
18
- const res = spawnSync(process.execPath, [HOOK], {
19
- input: JSON.stringify(payload),
20
- encoding: 'utf8',
21
- env: { ...process.env, KODELYTH_MEMORY_DIR: TMP },
22
- timeout: 5000,
23
- });
24
- return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status };
25
- }
26
-
27
- // Seed memory once
28
- store.capture({
29
- problem: 'Stripe webhook signature failed in production',
30
- approach: 'Switched body parser from json to raw, validated with constructEvent',
31
- tags: ['payments', 'stripe', 'webhooks'],
32
- project: '/test/payments',
33
- language: 'typescript',
34
- });
35
- store.capture({
36
- problem: 'Redis cluster split-brain after AZ failover',
37
- approach: 'Configured min-slaves-to-write to 2 to prevent stale primary writes',
38
- tags: ['redis', 'cluster', 'reliability'],
39
- project: '/test/payments',
40
- language: 'typescript',
41
- });
42
-
43
- test('auto-recall: returns nothing for empty prompt', () => {
44
- const r = runHook({ prompt: '', cwd: '/test/payments', session_id: 's1' });
45
- assert.equal(r.status, 0);
46
- assert.equal(r.stdout, '');
47
- });
48
-
49
- test('auto-recall: skips trivial prompts', () => {
50
- for (const trivial of ['ok', 'yes', 'thanks', 'cool']) {
51
- const r = runHook({ prompt: trivial, cwd: '/test/payments', session_id: 's-trivial' });
52
- assert.equal(r.stdout, '', `should skip "${trivial}"`);
53
- }
54
- });
55
-
56
- test('auto-recall: skips agent invocation prompts', () => {
57
- for (const cmd of ['use debug-detective', '@code-reviewer', '/help', 'invoke security-reviewer']) {
58
- const r = runHook({ prompt: cmd, cwd: '/test/payments', session_id: 's-cmd' });
59
- assert.equal(r.stdout, '', `should skip "${cmd}"`);
60
- }
61
- });
62
-
63
- test('auto-recall: skips short prompts', () => {
64
- const r = runHook({ prompt: 'fix bug', cwd: '/test/payments', session_id: 's-short' });
65
- assert.equal(r.stdout, '');
66
- });
67
-
68
- test('auto-recall: surfaces relevant memory for a real query', () => {
69
- const r = runHook({
70
- prompt: 'I need to add Stripe webhooks for subscription renewal events',
71
- cwd: '/test/payments',
72
- session_id: 's-fire-1',
73
- });
74
- assert.equal(r.status, 0);
75
- assert.ok(r.stdout.length > 0, 'should produce output');
76
- const out = JSON.parse(r.stdout);
77
- assert.ok(out.additionalContext.includes('Stripe webhook'));
78
- assert.equal(out.meta.source, 'kodelyth-memory:auto-recall');
79
- assert.equal(out.meta.recalledCount, 1);
80
- });
81
-
82
- test('auto-recall: suppresses repeat in same session', () => {
83
- // First call surfaces the memory
84
- runHook({
85
- prompt: 'help with stripe webhook integration',
86
- cwd: '/test/payments',
87
- session_id: 's-repeat',
88
- });
89
- // Second call same topic — should NOT surface again
90
- const r2 = runHook({
91
- prompt: 'about that stripe webhook problem, what about retries',
92
- cwd: '/test/payments',
93
- session_id: 's-repeat',
94
- });
95
- assert.equal(r2.stdout, '', 'repeat should be suppressed');
96
- });
97
-
98
- test('auto-recall: surfaces different memory for different topic in same session', () => {
99
- runHook({
100
- prompt: 'how do I handle stripe webhook errors',
101
- cwd: '/test/payments',
102
- session_id: 's-multi',
103
- });
104
- // Different topic — redis — should surface
105
- const r2 = runHook({
106
- prompt: 'redis cluster is acting weird after failover today',
107
- cwd: '/test/payments',
108
- session_id: 's-multi',
109
- });
110
- assert.ok(r2.stdout.length > 0, 'different topic should surface');
111
- const out = JSON.parse(r2.stdout);
112
- assert.ok(out.additionalContext.toLowerCase().includes('redis'));
113
- });
114
-
115
- test('auto-recall: returns nothing when no memory matches', () => {
116
- const r = runHook({
117
- prompt: 'what is the meaning of life and quantum entanglement',
118
- cwd: '/test/payments',
119
- session_id: 's-nomatch',
120
- });
121
- assert.equal(r.stdout, '');
122
- });
123
-
124
- test('auto-recall: never crashes on malformed payload', () => {
125
- const res = spawnSync(process.execPath, [HOOK], {
126
- input: 'not-json-at-all',
127
- encoding: 'utf8',
128
- env: { ...process.env, KODELYTH_MEMORY_DIR: TMP },
129
- timeout: 3000,
130
- });
131
- assert.equal(res.status, 0, 'must exit 0 even on garbage input');
132
- });