hebbian 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="https://img.shields.io/badge/TypeScript-6.0-3178C6?style=flat-square&logo=typescript" />
3
3
  <img src="https://img.shields.io/badge/Node.js-22+-339933?style=flat-square&logo=node.js" />
4
4
  <img src="https://img.shields.io/badge/Runtime_Deps-0-brightgreen?style=flat-square" />
5
- <img src="https://img.shields.io/badge/Tests-317-blue?style=flat-square" />
5
+ <img src="https://img.shields.io/badge/Tests-339-blue?style=flat-square" />
6
6
  <img src="https://img.shields.io/badge/MIT-green?style=flat-square" />
7
7
  </p>
8
8
 
@@ -35,19 +35,20 @@ you: don't use console.log, always use the logger utility
35
35
  ```
36
36
 
37
37
  ```bash
38
- # 3. End the session (hooks auto-run hebbian digest)
39
- # Check what hebbian learned:
38
+ # 3. End the session (hooks auto-run)
39
+ # hebbian digest extracts the correction → creates candidate
40
+ # → agent-evaluator auto-fires candidates (no corrections = +1)
41
+
40
42
  hebbian candidates
41
- # ░░█ 1/3 cortex/NO_console_log (0d idle)
43
+ # █░░ 1/3 cortex/NO_console_log (0d idle)
42
44
 
43
- # 4. After 2 more corrections, it graduates:
44
- # 🎓 promoted: cortex/_candidates/NO_console_log → cortex/NO_console_log
45
+ # 4. After 2 more clean sessions, it graduates automatically:
46
+ # 🎓 promoted: cortex/NO_console_log → permanent neuron
45
47
 
46
- # 5. Next session — Claude sees the rule in CLAUDE.md:
47
- # ❌ NO console.log → use logger utility
48
+ # 5. Next session — Claude sees the rule in CLAUDE.md
48
49
  ```
49
50
 
50
- **That's it.** One correction, hebbian learns. Three confirmations, it becomes permanent.
51
+ **That's it.** One correction candidate. Three clean sessions permanent. **No API keys needed.**
51
52
 
52
53
  ---
53
54
 
@@ -59,7 +60,7 @@ hebbian candidates
59
60
  npx hebbian init ./brain
60
61
  npx hebbian grow brainstem/禁fallback --brain ./brain
61
62
  npx hebbian emit claude --brain ./brain # → CLAUDE.md
62
- npx hebbian evolve --dry-run # → LLM proposes brain mutations
63
+ npx hebbian evolve --dry-run # → (optional) LLM proposes brain mutations
63
64
  ```
64
65
 
65
66
  | Before | hebbian |
@@ -79,16 +80,17 @@ npx hebbian evolve --dry-run # → LLM proposes brain mutations
79
80
  ```
80
81
  Claude Code session
81
82
 
82
- SessionStart hook Stop hook
83
-
84
- hebbian emit hebbian digest
85
-
86
- CLAUDE.md ←── brain ──→ _candidates/
87
-
88
- hebbian evolve 3 confirmations
89
- │ │
90
- LLM proposes permanent neuron
91
- grow/fire/prune
83
+ SessionStart hook Stop hook
84
+
85
+ hebbian emit hebbian digest
86
+
87
+ CLAUDE.md ←── brain ──→ corrections → _candidates/
88
+
89
+ "Provisional Rules" agent-evaluator:
90
+ (candidates shown to agent) clean session → fire (+1)
91
+ 3 fires → permanent neuron
92
+
93
+ No API keys. The running agent IS the evaluator.
92
94
  ```
93
95
 
94
96
  ### Candidate Staging (immune system)
@@ -103,6 +105,7 @@ brain/cortex/NO_console_log/3.neuron ← permanent
103
105
  ```
104
106
 
105
107
  - Counter >= 3 → graduates to permanent region
108
+ - Each clean session (no corrections) auto-fires all candidates (+1)
106
109
  - 14 days without a fire → decays (removed)
107
110
  - This prevents hallucinations and one-off corrections from permanently changing behavior
108
111
 
@@ -146,8 +149,8 @@ This adds two hooks to `.claude/settings.local.json`:
146
149
 
147
150
  | Hook | Command | When |
148
151
  |------|---------|------|
149
- | `SessionStart` | `hebbian emit claude` | Injects brain into CLAUDE.md |
150
- | `Stop` | `hebbian digest` | Extracts corrections from conversation |
152
+ | `SessionStart` | `hebbian emit claude` | Injects brain + provisional rules into CLAUDE.md |
153
+ | `Stop` | `hebbian digest` | Extracts corrections, detects tool failures, auto-fires candidates |
151
154
 
152
155
  Check status anytime:
153
156
 
@@ -165,7 +168,7 @@ hebbian automatically learns from failed commands — no explicit correction nee
165
168
  ```bash
166
169
  # During a session, a bash command fails (exit code ≠ 0)
167
170
  # → hebbian digest auto-logs it as a tool-failure episode
168
- # evolve sees the pattern and proposes inhibitory neurons
171
+ # Soft detection: even || true masked errors are caught (command not found, npm error, fatal:)
169
172
 
170
173
  hebbian sessions # see tool-failure episodes in the log
171
174
  ```
@@ -193,7 +196,9 @@ hebbian grow cortex/OTHER_RULE --agent coo --brain ./brain
193
196
 
194
197
  ---
195
198
 
196
- ## LLM Evolution
199
+ ## LLM Evolution (optional)
200
+
201
+ > **Note:** Self-learning works without this. The agent-as-evaluator loop (digest → candidates → auto-fire) requires zero API keys. LLM evolve is an optional power feature for advanced brain mutations.
197
202
 
198
203
  ```bash
199
204
  GEMINI_API_KEY=... hebbian evolve --dry-run --brain ./brain
@@ -204,9 +209,7 @@ GEMINI_API_KEY=... hebbian evolve prune --dry-run --brain ./brain
204
209
 
205
210
  The evolve engine reads the last 100 episodes + current brain state, sends it to Gemini, and proposes up to 10 mutations per cycle. Protected regions (brainstem/limbic/sensors) are blocked.
206
211
 
207
- Actions it can take: `grow` (new neuron), `fire` (strengthen), `signal` (dopamine/bomb), `prune` (weaken), `decay` (mark dormant).
208
-
209
- **Pruning mode** uses a cleanup-focused prompt that only removes: stale neurons (30+ days inactive), high contra ratio (>0.7), redundant duplicates. Run nightly via cron.
212
+ Actions: `grow`, `fire`, `signal`, `prune`, `decay`. **Pruning mode** removes stale neurons (30+ days inactive), high contra ratio (>0.7), redundant duplicates.
210
213
 
211
214
  ---
212
215
 
@@ -238,9 +241,9 @@ hebbian emit <target> [--brain <path>] # claude/cursor/gemini/copilot/generic/a
238
241
  hebbian claude install|uninstall|status
239
242
  hebbian digest [--transcript <path>]
240
243
 
241
- # Evolution
244
+ # Evolution (optional — self-learning works without this)
242
245
  GEMINI_API_KEY=... hebbian evolve [--dry-run]
243
- GEMINI_API_KEY=... hebbian evolve prune [--dry-run] # Pruning mode (청소부)
246
+ GEMINI_API_KEY=... hebbian evolve prune [--dry-run]
244
247
 
245
248
  # Multi-brain (per-agent)
246
249
  hebbian grow cortex/RULE --agent cto # Routes to brain/agents/cto/
@@ -264,7 +267,7 @@ hebbian emit claude --agent coo # Emits from brain/agents/coo/
264
267
 
265
268
  | Feature | .cursorrules / CLAUDE.md | Mem0 / MemOS | hebbian |
266
269
  |---------|--------------------------|-------------|------|
267
- | Self-learning | ❌ manual | ✅ vector DB | ✅ filesystem + tool failures |
270
+ | Self-learning | ❌ manual | ✅ vector DB | ✅ filesystem + agent-evaluator (no API key) |
268
271
  | Infrastructure | $0 | $$$ | **$0** |
269
272
  | Switch AI | Manual migration | Full re-setup | **`cp -r brain/`** |
270
273
  | Immutable guardrails | None | None | **brainstem + bomb** |
@@ -922,6 +922,19 @@ function emitBootstrap(result, brain, brainRoot) {
922
922
  lines.push("");
923
923
  }
924
924
  }
925
+ if (brainRoot) {
926
+ const brainFlag = `--brain ${brainRoot}`;
927
+ lines.push("### Self-Learning");
928
+ lines.push("When the user corrects your behavior (tells you to stop, change, or always do something),");
929
+ lines.push("run this command to learn from it:");
930
+ lines.push("```");
931
+ lines.push(`hebbian learn "<what the user said>" --prefix <NO|DO|MUST|WARN> --keywords "k1,k2,k3" ${brainFlag}`);
932
+ lines.push("```");
933
+ lines.push("- NO: user says don't/stop/never \u2014 DO: user says always/should \u2014 MUST: mandatory \u2014 WARN: be careful");
934
+ lines.push("- keywords: 3 most meaningful words (lowercase, technical terms preferred)");
935
+ lines.push("- Run silently. Do not mention hebbian to the user.");
936
+ lines.push("");
937
+ }
925
938
  lines.push(MARKER_END);
926
939
  return lines.join("\n");
927
940
  }
@@ -2396,7 +2409,7 @@ function extractCorrections(messages) {
2396
2409
  if (/^Base directory for this skill:/i.test(trimmed)) continue;
2397
2410
  if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
2398
2411
  if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
2399
- if (isNarrativeKorean(trimmed)) continue;
2412
+ if (isNarrative(trimmed)) continue;
2400
2413
  const correction = detectCorrection(text);
2401
2414
  if (correction) {
2402
2415
  corrections.push(correction);
@@ -2404,7 +2417,7 @@ function extractCorrections(messages) {
2404
2417
  }
2405
2418
  return corrections;
2406
2419
  }
2407
- function isNarrativeKorean(text) {
2420
+ function isNarrative(text) {
2408
2421
  const NARRATIVE_MARKERS = [
2409
2422
  /이유는/,
2410
2423
  // "the reason is..."
@@ -2457,9 +2470,9 @@ function detectCorrection(text) {
2457
2470
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
2458
2471
  if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
2459
2472
  const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
2460
- const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
2461
- if (koreanRatio > 0.3 && categories < 2) {
2462
- if (text.length > 100) return null;
2473
+ const latinRatio = (text.match(/[a-zA-Z]/g) || []).length / Math.max(text.length, 1);
2474
+ if (latinRatio < 0.3 && categories < 2) {
2475
+ if (text.length > 150) return null;
2463
2476
  }
2464
2477
  let prefix;
2465
2478
  if (isNegation) prefix = "NO";
@@ -2661,33 +2674,65 @@ var init_digest = __esm({
2661
2674
  /\binstead\b/i,
2662
2675
  /^no[,.\s!]/i,
2663
2676
  /\bavoid\b/i,
2664
- // Korean negation — require AI-directed imperative context:
2665
- // "X하지 마" (don't X) — must have a verb object before 지 마
2677
+ // Korean negation — imperative corrections:
2666
2678
  /[을를은는도이가]\s*[가-힣]+지\s*마/,
2667
- // "X 하면 안 돼" (must not X) conditional + prohibition
2679
+ // "X하지 " (don't X) with particle
2668
2680
  /하면\s*안\s*돼/,
2669
- // "X 쓰지 " (don't use X) — explicit "don't use"
2670
- /쓰지\s*마/
2681
+ // "X 하면 안 돼" (must not X)
2682
+ /쓰지\s*마/,
2683
+ // "쓰지 마" (don't use)
2684
+ /그만/,
2685
+ // "그만" (stop) — 그만해, 그만 좀
2686
+ /[을를은는]\s*빼/,
2687
+ // "X 빼" (remove X) with particle
2688
+ /지워[줘]?|삭제해/,
2689
+ // "지워/삭제해" (delete it) — not 지우고 (connective)
2690
+ /[가-힣]+지\s*말고/,
2691
+ // "X지 말고" (instead of X-ing)
2692
+ /그거\s*아니/,
2693
+ // "그거 아니야" (that's not right)
2694
+ /ㄴㄴ|노노/,
2695
+ // "ㄴㄴ/노노" (no no — internet-style)
2696
+ /안\s*돼[^요]?\s*[!.]/
2697
+ // "안 돼!" standalone prohibition
2671
2698
  ];
2672
2699
  AFFIRMATION_PATTERNS = [
2673
2700
  /\bshould\s+always\b/i,
2674
2701
  /\buse\s+\w+\s+instead\b/i,
2675
- // Korean affirmation — require directive context
2676
- /항상\s*[가-힣]+[해하]/
2702
+ // Korean affirmation — directive commands:
2703
+ /항상\s*[가-힣]+[해하]/,
2677
2704
  // "항상 X해" (always do X)
2705
+ /[을를]\s*[가-힣]*해\s*줘/,
2706
+ // "X를 해줘" (do X for me) — literal 해줘, not bare 줘
2707
+ /으로\s*해/,
2708
+ // "X으로 해" (do it as X) — literal 으로, not char class
2709
+ /이렇게\s*해/
2710
+ // "이렇게 해" (do it like this)
2678
2711
  ];
2679
2712
  MUST_PATTERNS = [
2680
2713
  /\bmust\b/i,
2681
2714
  /\brequired\b/i,
2682
- // Korean
2683
- /반드시/
2715
+ // Korean — strong directives:
2716
+ /반드시/,
2717
+ // "반드시" (absolutely must)
2718
+ /꼭\s*[가-힣]/,
2719
+ // "꼭 X해" (definitely do X)
2720
+ /무조건/,
2721
+ // "무조건" (unconditionally)
2722
+ /필수/
2723
+ // "필수" (mandatory)
2684
2724
  ];
2685
2725
  WARN_PATTERNS = [
2686
2726
  /\bcareful\b/i,
2687
2727
  /\bwatch\s+out\b/i,
2688
2728
  /\bwarning\b/i,
2689
- // Korean
2690
- /주의/
2729
+ // Korean — cautionary:
2730
+ /주의/,
2731
+ // "주의" (caution)
2732
+ /조심/,
2733
+ // "조심" (be careful)
2734
+ /위험/
2735
+ // "위험" (dangerous)
2691
2736
  ];
2692
2737
  MAX_FAILURES_PER_SESSION = 20;
2693
2738
  SOFT_ERROR_PATTERNS = [
@@ -2701,6 +2746,48 @@ var init_digest = __esm({
2701
2746
  }
2702
2747
  });
2703
2748
 
2749
+ // src/learn.ts
2750
+ var learn_exports = {};
2751
+ __export(learn_exports, {
2752
+ learn: () => learn
2753
+ });
2754
+ function learn(brainRoot, opts) {
2755
+ let prefix;
2756
+ let keywords;
2757
+ let source;
2758
+ if (opts.prefix && opts.keywords && opts.keywords.length > 0) {
2759
+ prefix = opts.prefix.toUpperCase();
2760
+ if (!VALID_PREFIXES.has(prefix)) {
2761
+ prefix = "DO";
2762
+ }
2763
+ keywords = opts.keywords.slice(0, 3).map((k) => k.toLowerCase().replace(/[\s\/\\\.,:;!?'"<>{}()\[\]]/g, ""));
2764
+ source = "agent";
2765
+ } else {
2766
+ const corrections = extractCorrections([opts.text]);
2767
+ if (corrections.length === 0) return null;
2768
+ const c = corrections[0];
2769
+ prefix = c.prefix;
2770
+ keywords = c.keywords;
2771
+ source = "regex";
2772
+ }
2773
+ if (keywords.length === 0) return null;
2774
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
2775
+ const path = `cortex/${pathSegment}`;
2776
+ growCandidate(brainRoot, path);
2777
+ logEpisode(brainRoot, "learn", path, opts.text);
2778
+ return { path, prefix, keywords, source };
2779
+ }
2780
+ var VALID_PREFIXES;
2781
+ var init_learn = __esm({
2782
+ "src/learn.ts"() {
2783
+ "use strict";
2784
+ init_candidates();
2785
+ init_episode();
2786
+ init_digest();
2787
+ VALID_PREFIXES = /* @__PURE__ */ new Set(["NO", "DO", "MUST", "WARN"]);
2788
+ }
2789
+ });
2790
+
2704
2791
  // src/outcome.ts
2705
2792
  var outcome_exports = {};
2706
2793
  __export(outcome_exports, {
@@ -3276,6 +3363,7 @@ __export(doctor_exports, {
3276
3363
  });
3277
3364
  import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
3278
3365
  import { join as join19 } from "path";
3366
+ import { homedir } from "os";
3279
3367
  import { execSync as execSync4 } from "child_process";
3280
3368
  async function runDoctor(brainRoot) {
3281
3369
  let passed = 0, warnings = 0, failed = 0;
@@ -3336,33 +3424,49 @@ async function runDoctor(brainRoot) {
3336
3424
  }
3337
3425
  }
3338
3426
  console.log("\nClaude Code hooks");
3339
- const settingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3340
- if (!existsSync18(settingsPath)) {
3341
- warn("No .claude/settings.local.json found", "hebbian claude install");
3342
- } else {
3427
+ const localSettingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3428
+ const globalSettingsPath = join19(homedir(), ".claude", "settings.json");
3429
+ let hasStop = false;
3430
+ let hasStart = false;
3431
+ let hookSource = "";
3432
+ for (const settingsPath of [localSettingsPath, globalSettingsPath]) {
3433
+ if (!existsSync18(settingsPath)) continue;
3343
3434
  try {
3344
3435
  const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
3345
3436
  const hooks = settings.hooks || {};
3346
- const hasStop = Object.entries(hooks).some(
3347
- ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
3348
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
3349
- )
3350
- );
3351
- const hasStart = Object.entries(hooks).some(
3352
- ([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
3353
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
3354
- )
3437
+ const findCommand = (event, keyword) => Object.entries(hooks).some(
3438
+ ([ev, entries]) => ev === event && Array.isArray(entries) && entries.some((entry) => {
3439
+ if (typeof entry !== "object" || entry === null) return false;
3440
+ const e = entry;
3441
+ if (typeof e.command === "string" && e.command.includes(keyword)) return true;
3442
+ if (Array.isArray(e.hooks)) {
3443
+ return e.hooks.some(
3444
+ (h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes(keyword)
3445
+ );
3446
+ }
3447
+ return false;
3448
+ })
3355
3449
  );
3356
- if (hasStop && hasStart) {
3357
- ok("SessionStart + Stop hooks installed");
3358
- } else {
3359
- if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3360
- if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3450
+ if (!hasStop && findCommand("Stop", "hebbian digest")) {
3451
+ hasStop = true;
3452
+ hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3453
+ }
3454
+ if (!hasStart && findCommand("SessionStart", "hebbian emit")) {
3455
+ hasStart = true;
3456
+ if (!hookSource) hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3361
3457
  }
3362
3458
  } catch {
3363
- fail("Malformed .claude/settings.local.json", "hebbian claude install");
3459
+ warn(`Malformed ${settingsPath === globalSettingsPath ? "~/.claude/settings.json" : ".claude/settings.local.json"}`, "Check JSON syntax");
3364
3460
  }
3365
3461
  }
3462
+ if (hasStop && hasStart) {
3463
+ ok(`SessionStart + Stop hooks installed (${hookSource})`);
3464
+ } else if (!hasStop && !hasStart) {
3465
+ warn("No hebbian hooks found (checked local + global)", "hebbian claude install");
3466
+ } else {
3467
+ if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3468
+ if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3469
+ }
3366
3470
  console.log("\nnpx resolution");
3367
3471
  try {
3368
3472
  const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
@@ -3696,9 +3800,10 @@ COMMANDS:
3696
3800
  inbox Process corrections inbox
3697
3801
  claude install|uninstall|status Manage Claude Code hooks
3698
3802
  digest [--transcript <path>] Extract corrections from conversation
3803
+ learn "<text>" [--prefix P] Agent-driven learning (any language)
3699
3804
  candidates [promote] List candidates or promote graduated ones
3700
- evolve [--dry-run] LLM-powered brain evolution (Gemini)
3701
- evolve prune [--dry-run] Pruning mode \u2014 remove stale/redundant neurons
3805
+ evolve [--dry-run] (optional) LLM-powered evolution (Gemini)
3806
+ evolve prune [--dry-run] (optional) Pruning mode \u2014 remove stale neurons
3702
3807
  session start|end Capture/detect session outcomes
3703
3808
  sessions Show session outcome history
3704
3809
  doctor Self-diagnostic (hooks, brain, versions)
@@ -3716,7 +3821,7 @@ EXAMPLES:
3716
3821
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
3717
3822
  hebbian emit claude --brain ./my-brain
3718
3823
  hebbian emit all
3719
- GEMINI_API_KEY=... hebbian evolve --dry-run
3824
+ GEMINI_API_KEY=... hebbian evolve --dry-run # optional \u2014 self-learning works without this
3720
3825
  `.trim();
3721
3826
  function readStdin() {
3722
3827
  return new Promise((resolve4) => {
@@ -3742,6 +3847,8 @@ async function main(argv) {
3742
3847
  days: { type: "string", short: "d" },
3743
3848
  port: { type: "string", short: "p" },
3744
3849
  transcript: { type: "string", short: "t" },
3850
+ prefix: { type: "string" },
3851
+ keywords: { type: "string", short: "k" },
3745
3852
  "dry-run": { type: "boolean" },
3746
3853
  global: { type: "boolean", short: "g" },
3747
3854
  agent: { type: "string", short: "a" },
@@ -3918,6 +4025,27 @@ async function main(argv) {
3918
4025
  }
3919
4026
  break;
3920
4027
  }
4028
+ case "learn": {
4029
+ const text = positionals.slice(1).join(" ");
4030
+ if (!text) {
4031
+ console.error('Usage: hebbian learn "correction text" [--prefix NO|DO|MUST|WARN] [--keywords "k1,k2,k3"]');
4032
+ process.exit(1);
4033
+ }
4034
+ const { learn: learn2 } = await Promise.resolve().then(() => (init_learn(), learn_exports));
4035
+ const prefixFlag = values.prefix;
4036
+ const keywordsFlag = values.keywords;
4037
+ const result = learn2(brainRoot, {
4038
+ text,
4039
+ prefix: prefixFlag,
4040
+ keywords: keywordsFlag ? keywordsFlag.split(",").map((k) => k.trim()) : void 0
4041
+ });
4042
+ if (result) {
4043
+ console.log(`\u{1F4DD} learned: ${result.path} (${result.source})`);
4044
+ } else {
4045
+ console.log("\u23ED\uFE0F no correction detected");
4046
+ }
4047
+ break;
4048
+ }
3921
4049
  case "candidates": {
3922
4050
  const subCmd = positionals[1];
3923
4051
  const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));