hebbian 0.1.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/LICENSE +21 -0
- package/README.md +201 -0
- package/bin/hebbian.js +199 -0
- package/lib/constants.js +83 -0
- package/lib/decay.js +100 -0
- package/lib/dedup.js +66 -0
- package/lib/emit.js +376 -0
- package/lib/fire.js +59 -0
- package/lib/grow.js +98 -0
- package/lib/init.js +89 -0
- package/lib/rollback.js +34 -0
- package/lib/scanner.js +227 -0
- package/lib/signal.js +68 -0
- package/lib/similarity.js +62 -0
- package/lib/snapshot.js +46 -0
- package/lib/subsumption.js +77 -0
- package/lib/watch.js +79 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IISweetHeartII
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/Node.js-22+-339933?style=flat-square&logo=node.js" />
|
|
3
|
+
<img src="https://img.shields.io/badge/Dependencies-0-brightgreen?style=flat-square" />
|
|
4
|
+
<img src="https://img.shields.io/badge/Tests-120-blue?style=flat-square" />
|
|
5
|
+
<img src="https://img.shields.io/badge/MIT-green?style=flat-square" />
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center"><a href="README.ko.md">한국어</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
|
9
|
+
|
|
10
|
+
# hebbian
|
|
11
|
+
|
|
12
|
+
### *Folder-as-neuron brain for any AI agent.*
|
|
13
|
+
|
|
14
|
+
> "Neurons that fire together, wire together." — Donald Hebb (1949)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## TL;DR
|
|
19
|
+
|
|
20
|
+
**`mkdir` replaces system prompts.** Folders are neurons. Paths are sentences. Counter files are synaptic weights.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx hebbian init ./brain
|
|
24
|
+
npx hebbian grow brainstem/禁fallback --brain ./brain
|
|
25
|
+
npx hebbian fire brainstem/禁fallback --brain ./brain
|
|
26
|
+
npx hebbian emit claude --brain ./brain # → CLAUDE.md
|
|
27
|
+
npx hebbian emit all --brain ./brain # → All AI formats at once
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Before | hebbian |
|
|
31
|
+
|--------|------|
|
|
32
|
+
| 1000-line prompts, manually edited | `mkdir` one folder |
|
|
33
|
+
| Vector DB $70/mo | **$0** (folders = DB) |
|
|
34
|
+
| Switch AI → full migration | `cp -r brain/` — 1 second |
|
|
35
|
+
| Rule violation → wishful thinking | `bomb.neuron` → **cascade halt** |
|
|
36
|
+
| Rules managed by humans | Correction → auto neuron growth |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Why "hebbian"?
|
|
41
|
+
|
|
42
|
+
Donald Hebb's 1949 principle: **neurons that fire together, wire together.** Repeated corrections strengthen synaptic pathways. That's exactly what this tool does — every `hebbian fire` increments a counter, and only frequently-fired neurons survive. Natural selection on your filesystem.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How It Works
|
|
47
|
+
|
|
48
|
+
### Folder = Neuron. Path = Sentence.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
brain/cortex/frontend/禁console_log/40.neuron
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Read it: "Cortex > Frontend > Never use console.log. Reinforced 40 times."
|
|
55
|
+
|
|
56
|
+
### 7-Region Subsumption Architecture
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
P0: brainstem → Absolute laws (immutable, read-only)
|
|
60
|
+
P1: limbic → Emotion filters
|
|
61
|
+
P2: hippocampus → Memory, session restore
|
|
62
|
+
P3: sensors → Environment constraints
|
|
63
|
+
P4: cortex → Knowledge, skills (largest region)
|
|
64
|
+
P5: ego → Tone, personality
|
|
65
|
+
P6: prefrontal → Goals, projects
|
|
66
|
+
|
|
67
|
+
Rule: Lower P always suppresses higher P.
|
|
68
|
+
bomb.neuron in any region → cascade halt.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### File Types
|
|
72
|
+
|
|
73
|
+
| File | Meaning | Biology |
|
|
74
|
+
|------|---------|---------|
|
|
75
|
+
| `N.neuron` | Excitatory counter (strength) | Synaptic weight |
|
|
76
|
+
| `N.contra` | Inhibitory counter | Inhibitory synapse |
|
|
77
|
+
| `dopamineN.neuron` | Reward signal | Dopamine |
|
|
78
|
+
| `bomb.neuron` | Circuit breaker | Pain response |
|
|
79
|
+
| `memoryN.neuron` | Episode recording | Long-term memory |
|
|
80
|
+
| `*.dormant` | Inactive marker | Sleep pruning |
|
|
81
|
+
| `.axon` | Cross-region link | Axon connection |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## CLI Reference
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hebbian init <path> # Create brain with 7 regions
|
|
89
|
+
hebbian emit <target> [--brain <path>] # Compile rules
|
|
90
|
+
hebbian fire <neuron-path> # Increment counter (+1)
|
|
91
|
+
hebbian grow <neuron-path> # Create neuron (with merge detection)
|
|
92
|
+
hebbian rollback <neuron-path> # Decrement counter (min=1)
|
|
93
|
+
hebbian signal <type> <neuron-path> # Add signal (dopamine/bomb/memory)
|
|
94
|
+
hebbian decay [--days N] # Mark inactive neurons dormant
|
|
95
|
+
hebbian watch # Auto-recompile on changes
|
|
96
|
+
hebbian diag # Brain diagnostics
|
|
97
|
+
hebbian stats # Brain statistics
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Emit Targets
|
|
101
|
+
|
|
102
|
+
| Target | Output File |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `claude` | `CLAUDE.md` |
|
|
105
|
+
| `cursor` | `.cursorrules` |
|
|
106
|
+
| `gemini` | `.gemini/GEMINI.md` |
|
|
107
|
+
| `copilot` | `.github/copilot-instructions.md` |
|
|
108
|
+
| `generic` | `.neuronrc` |
|
|
109
|
+
| `all` | All of the above |
|
|
110
|
+
|
|
111
|
+
### Environment Variables
|
|
112
|
+
|
|
113
|
+
| Variable | Purpose | Default |
|
|
114
|
+
|----------|---------|---------|
|
|
115
|
+
| `HEBBIAN_BRAIN` | Brain directory path | `./brain` |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 3-Tier Emission
|
|
120
|
+
|
|
121
|
+
hebbian compiles your brain into 3 tiers:
|
|
122
|
+
|
|
123
|
+
| Tier | File | Tokens | When |
|
|
124
|
+
|------|------|--------|------|
|
|
125
|
+
| 1 | Target file (CLAUDE.md, etc.) | ~500 | Auto-loaded by AI |
|
|
126
|
+
| 2 | `brain/_index.md` | ~1000 | AI reads at session start |
|
|
127
|
+
| 3 | `brain/{region}/_rules.md` | Variable | AI reads on demand |
|
|
128
|
+
|
|
129
|
+
This keeps the token budget lean. 293 neurons → ~500 tokens at startup.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Synaptic Consolidation
|
|
134
|
+
|
|
135
|
+
When you `hebbian grow`, the system checks for similar existing neurons using Jaccard similarity:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
hebbian grow cortex/frontend/禁console_logging --brain ./brain
|
|
139
|
+
# → "禁console_logging" ≈ "禁console_log" (Jaccard ≥ 0.6)
|
|
140
|
+
# → Fires existing neuron instead of creating duplicate
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Consolidation over duplication.** Hebbian principle in action.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Compared to
|
|
148
|
+
|
|
149
|
+
| Feature | .cursorrules | Mem0/Letta | hebbian |
|
|
150
|
+
|---------|-------------|------------|------|
|
|
151
|
+
| 1000+ rules | Token overflow | Vector DB | Folder tree |
|
|
152
|
+
| Infrastructure | $0 | $$$$ | **$0** |
|
|
153
|
+
| Switch AI | Manual migration | Full re-setup | **`cp -r brain/`** |
|
|
154
|
+
| Self-growth | Manual | Bot-based | **Counter-based** |
|
|
155
|
+
| Immutable guardrails | None | None | **brainstem + bomb** |
|
|
156
|
+
| Audit trail | Hidden | DB logs | **`ls -R` = full history** |
|
|
157
|
+
| Runtime deps | N/A | Many | **0** |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Zero Dependencies
|
|
162
|
+
|
|
163
|
+
hebbian uses only Node.js built-in modules:
|
|
164
|
+
|
|
165
|
+
- `node:fs` — filesystem operations
|
|
166
|
+
- `node:path` — path handling
|
|
167
|
+
- `node:util` — CLI argument parsing
|
|
168
|
+
- `node:http` — REST API (planned)
|
|
169
|
+
|
|
170
|
+
**Runtime dependencies: 0.** Matches the Go version's zero-dep philosophy, but with `npx` instant execution.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Governance
|
|
175
|
+
|
|
176
|
+
120 tests pass in ~850ms:
|
|
177
|
+
|
|
178
|
+
- **SCC** (Subsumption Cascade Correctness): 17/17 = **100%**
|
|
179
|
+
- **MLA** (Memory Lifecycle Accuracy): 15/15 = **100%**
|
|
180
|
+
- Scanner: 15 tests
|
|
181
|
+
- Lifecycle: 18 tests
|
|
182
|
+
- Emit: 16 tests
|
|
183
|
+
- Similarity: 12 tests
|
|
184
|
+
- CLI E2E: 9 tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
node --test # Run all tests
|
|
188
|
+
node --test test/governance.test.js # Governance only
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Inspired By
|
|
194
|
+
|
|
195
|
+
[NeuronFS](https://github.com/rhino-acoustic/NeuronFS) — the original Go implementation that proved folders can be neurons. hebbian is a JavaScript reimagination, designed for the npm ecosystem and zero-dependency operation.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
package/bin/hebbian.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// hebbian — Folder-as-Neuron Brain for Any AI Agent
|
|
4
|
+
//
|
|
5
|
+
// "Neurons that fire together, wire together." — Donald Hebb (1949)
|
|
6
|
+
//
|
|
7
|
+
// USAGE:
|
|
8
|
+
// hebbian init <path> Create brain with 7 regions
|
|
9
|
+
// hebbian emit <target> [--brain <path>] Compile rules (claude/cursor/gemini/copilot/generic/all)
|
|
10
|
+
// hebbian fire <neuron-path> Increment neuron counter
|
|
11
|
+
// hebbian grow <neuron-path> Create neuron (with merge detection)
|
|
12
|
+
// hebbian rollback <neuron-path> Decrement counter (min=1)
|
|
13
|
+
// hebbian signal <type> <neuron-path> Add dopamine/bomb/memory signal
|
|
14
|
+
// hebbian decay [--days N] Mark inactive neurons dormant (default 30 days)
|
|
15
|
+
// hebbian watch [--brain <path>] Watch for changes + auto-recompile
|
|
16
|
+
// hebbian diag Print brain diagnostics
|
|
17
|
+
// hebbian stats Print brain statistics
|
|
18
|
+
|
|
19
|
+
import { parseArgs } from 'node:util';
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
const VERSION = '0.1.0';
|
|
24
|
+
|
|
25
|
+
const HELP = `
|
|
26
|
+
hebbian v${VERSION} — Folder-as-neuron brain for any AI agent.
|
|
27
|
+
|
|
28
|
+
"Neurons that fire together, wire together." — Donald Hebb (1949)
|
|
29
|
+
|
|
30
|
+
USAGE:
|
|
31
|
+
hebbian <command> [options]
|
|
32
|
+
|
|
33
|
+
COMMANDS:
|
|
34
|
+
init <path> Create brain with 7 regions
|
|
35
|
+
emit <target> [--brain <path>] Compile rules (claude/cursor/gemini/copilot/generic/all)
|
|
36
|
+
fire <neuron-path> Increment neuron counter (+1)
|
|
37
|
+
grow <neuron-path> Create neuron (with merge detection)
|
|
38
|
+
rollback <neuron-path> Decrement neuron counter (min=1)
|
|
39
|
+
signal <type> <neuron-path> Add signal (dopamine/bomb/memory)
|
|
40
|
+
decay [--days N] Mark inactive neurons dormant (default 30)
|
|
41
|
+
dedup Batch merge similar neurons (Jaccard >= 0.6)
|
|
42
|
+
snapshot Git commit current brain state
|
|
43
|
+
watch Watch for changes + auto-recompile
|
|
44
|
+
diag Print brain diagnostics
|
|
45
|
+
stats Print brain statistics
|
|
46
|
+
|
|
47
|
+
OPTIONS:
|
|
48
|
+
--brain <path> Brain directory (default: $HEBBIAN_BRAIN or ./brain)
|
|
49
|
+
--help, -h Show this help
|
|
50
|
+
--version, -v Show version
|
|
51
|
+
|
|
52
|
+
EXAMPLES:
|
|
53
|
+
hebbian init ./my-brain
|
|
54
|
+
hebbian grow cortex/frontend/禁console_log --brain ./my-brain
|
|
55
|
+
hebbian fire cortex/frontend/禁console_log --brain ./my-brain
|
|
56
|
+
hebbian emit claude --brain ./my-brain
|
|
57
|
+
hebbian emit all
|
|
58
|
+
`.trim();
|
|
59
|
+
|
|
60
|
+
/** Resolve brain root path from --brain flag, env var, or defaults */
|
|
61
|
+
function resolveBrainRoot(brainFlag) {
|
|
62
|
+
if (brainFlag) return resolve(brainFlag);
|
|
63
|
+
if (process.env.HEBBIAN_BRAIN) return resolve(process.env.HEBBIAN_BRAIN);
|
|
64
|
+
if (existsSync(resolve('./brain'))) return resolve('./brain');
|
|
65
|
+
return resolve(process.env.HOME || '~', 'hebbian', 'brain');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main(argv) {
|
|
69
|
+
const { values, positionals } = parseArgs({
|
|
70
|
+
args: argv,
|
|
71
|
+
options: {
|
|
72
|
+
brain: { type: 'string', short: 'b' },
|
|
73
|
+
days: { type: 'string', short: 'd' },
|
|
74
|
+
port: { type: 'string', short: 'p' },
|
|
75
|
+
help: { type: 'boolean', short: 'h' },
|
|
76
|
+
version: { type: 'boolean', short: 'v' },
|
|
77
|
+
},
|
|
78
|
+
allowPositionals: true,
|
|
79
|
+
strict: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (values.version) {
|
|
83
|
+
console.log(`hebbian v${VERSION}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const command = positionals[0];
|
|
88
|
+
|
|
89
|
+
if (values.help || !command) {
|
|
90
|
+
console.log(HELP);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const brainRoot = resolveBrainRoot(values.brain);
|
|
95
|
+
|
|
96
|
+
switch (command) {
|
|
97
|
+
case 'init': {
|
|
98
|
+
const target = positionals[1];
|
|
99
|
+
if (!target) {
|
|
100
|
+
console.error('Usage: hebbian init <path>');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const { initBrain } = await import('../lib/init.js');
|
|
104
|
+
await initBrain(resolve(target));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'emit': {
|
|
108
|
+
const target = positionals[1];
|
|
109
|
+
if (!target) {
|
|
110
|
+
console.error('Usage: hebbian emit <target> (claude/cursor/gemini/copilot/generic/all)');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const { emitToTarget } = await import('../lib/emit.js');
|
|
114
|
+
await emitToTarget(brainRoot, target);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'fire': {
|
|
118
|
+
const neuronPath = positionals[1];
|
|
119
|
+
if (!neuronPath) {
|
|
120
|
+
console.error('Usage: hebbian fire <neuron-path>');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const { fireNeuron } = await import('../lib/fire.js');
|
|
124
|
+
await fireNeuron(brainRoot, neuronPath);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'grow': {
|
|
128
|
+
const neuronPath = positionals[1];
|
|
129
|
+
if (!neuronPath) {
|
|
130
|
+
console.error('Usage: hebbian grow <neuron-path>');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const { growNeuron } = await import('../lib/grow.js');
|
|
134
|
+
await growNeuron(brainRoot, neuronPath);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'rollback': {
|
|
138
|
+
const neuronPath = positionals[1];
|
|
139
|
+
if (!neuronPath) {
|
|
140
|
+
console.error('Usage: hebbian rollback <neuron-path>');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const { rollbackNeuron } = await import('../lib/rollback.js');
|
|
144
|
+
await rollbackNeuron(brainRoot, neuronPath);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'signal': {
|
|
148
|
+
const signalType = positionals[1];
|
|
149
|
+
const neuronPath = positionals[2];
|
|
150
|
+
if (!signalType || !neuronPath) {
|
|
151
|
+
console.error('Usage: hebbian signal <type> <neuron-path> (type: dopamine/bomb/memory)');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const { signalNeuron } = await import('../lib/signal.js');
|
|
155
|
+
await signalNeuron(brainRoot, neuronPath, signalType);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'decay': {
|
|
159
|
+
const days = values.days ? parseInt(values.days, 10) : 30;
|
|
160
|
+
const { runDecay } = await import('../lib/decay.js');
|
|
161
|
+
await runDecay(brainRoot, days);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'dedup': {
|
|
165
|
+
const { runDedup } = await import('../lib/dedup.js');
|
|
166
|
+
runDedup(brainRoot);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'snapshot': {
|
|
170
|
+
const { gitSnapshot } = await import('../lib/snapshot.js');
|
|
171
|
+
gitSnapshot(brainRoot);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'watch': {
|
|
175
|
+
const { startWatch } = await import('../lib/watch.js');
|
|
176
|
+
await startWatch(brainRoot);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'diag':
|
|
180
|
+
case 'stats': {
|
|
181
|
+
const { scanBrain } = await import('../lib/scanner.js');
|
|
182
|
+
const { runSubsumption } = await import('../lib/subsumption.js');
|
|
183
|
+
const brain = scanBrain(brainRoot);
|
|
184
|
+
const result = runSubsumption(brain);
|
|
185
|
+
const { printDiag } = await import('../lib/emit.js');
|
|
186
|
+
printDiag(brain, result);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
default:
|
|
190
|
+
console.error(`Unknown command: ${command}`);
|
|
191
|
+
console.log(HELP);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
197
|
+
console.error(err.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// hebbian — Constants & Configuration
|
|
2
|
+
//
|
|
3
|
+
// AXIOMS:
|
|
4
|
+
// 1. Folder = Neuron (name is meaning, depth is specificity)
|
|
5
|
+
// 2. File = Firing Trace (N.neuron = counter, dopamineN = reward, bomb = pain)
|
|
6
|
+
// 3. Path = Sentence (brain/cortex/quality/no_hardcoded → "cortex > quality > no_hardcoded")
|
|
7
|
+
// 4. Counter = Activation (higher = stronger/myelinated path)
|
|
8
|
+
// 5. AI writes back (counter increment = experience growth)
|
|
9
|
+
|
|
10
|
+
/** Ordered list of brain regions by subsumption priority (P0 → P6) */
|
|
11
|
+
export const REGIONS = [
|
|
12
|
+
'brainstem',
|
|
13
|
+
'limbic',
|
|
14
|
+
'hippocampus',
|
|
15
|
+
'sensors',
|
|
16
|
+
'cortex',
|
|
17
|
+
'ego',
|
|
18
|
+
'prefrontal',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Region → priority index (lower = higher priority) */
|
|
22
|
+
export const REGION_PRIORITY = /** @type {Record<string, number>} */ ({
|
|
23
|
+
brainstem: 0,
|
|
24
|
+
limbic: 1,
|
|
25
|
+
hippocampus: 2,
|
|
26
|
+
sensors: 3,
|
|
27
|
+
cortex: 4,
|
|
28
|
+
ego: 5,
|
|
29
|
+
prefrontal: 6,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Region display icons */
|
|
33
|
+
export const REGION_ICONS = /** @type {Record<string, string>} */ ({
|
|
34
|
+
brainstem: '\u{1F6E1}\uFE0F', // 🛡️
|
|
35
|
+
limbic: '\u{1F493}', // 💓
|
|
36
|
+
hippocampus: '\u{1F4DD}', // 📝
|
|
37
|
+
sensors: '\u{1F441}\uFE0F', // 👁️
|
|
38
|
+
cortex: '\u{1F9E0}', // 🧠
|
|
39
|
+
ego: '\u{1F3AD}', // 🎭
|
|
40
|
+
prefrontal: '\u{1F3AF}', // 🎯
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Region Korean descriptions */
|
|
44
|
+
export const REGION_KO = /** @type {Record<string, string>} */ ({
|
|
45
|
+
brainstem: '양심/본능',
|
|
46
|
+
limbic: '감정 필터',
|
|
47
|
+
hippocampus: '기록/기억',
|
|
48
|
+
sensors: '환경 제약',
|
|
49
|
+
cortex: '지식/기술',
|
|
50
|
+
ego: '성향/톤',
|
|
51
|
+
prefrontal: '목표/계획',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Minimum counter value for a neuron to appear in emitted output */
|
|
55
|
+
export const EMIT_THRESHOLD = 5;
|
|
56
|
+
|
|
57
|
+
/** Days a new neuron gets spotlight coverage regardless of counter */
|
|
58
|
+
export const SPOTLIGHT_DAYS = 7;
|
|
59
|
+
|
|
60
|
+
/** Jaccard similarity threshold for neuron merge detection */
|
|
61
|
+
export const JACCARD_THRESHOLD = 0.6;
|
|
62
|
+
|
|
63
|
+
/** Default number of days before marking a neuron dormant */
|
|
64
|
+
export const DECAY_DAYS = 30;
|
|
65
|
+
|
|
66
|
+
/** Maximum recursion depth when walking brain directories */
|
|
67
|
+
export const MAX_DEPTH = 6;
|
|
68
|
+
|
|
69
|
+
/** Emit target → file path mapping */
|
|
70
|
+
export const EMIT_TARGETS = /** @type {Record<string, string>} */ ({
|
|
71
|
+
gemini: '.gemini/GEMINI.md',
|
|
72
|
+
cursor: '.cursorrules',
|
|
73
|
+
claude: 'CLAUDE.md',
|
|
74
|
+
copilot: '.github/copilot-instructions.md',
|
|
75
|
+
generic: '.neuronrc',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Valid signal types */
|
|
79
|
+
export const SIGNAL_TYPES = ['dopamine', 'bomb', 'memory'];
|
|
80
|
+
|
|
81
|
+
/** Emit markers for injection into existing files */
|
|
82
|
+
export const MARKER_START = '<!-- HEBBIAN:START -->';
|
|
83
|
+
export const MARKER_END = '<!-- HEBBIAN:END -->';
|
package/lib/decay.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// hebbian — Decay (dormancy sweep)
|
|
2
|
+
//
|
|
3
|
+
// Neurons that haven't been touched in N days are marked dormant.
|
|
4
|
+
// Dormancy = *.dormant file in the neuron directory.
|
|
5
|
+
// Dormant neurons are excluded from emission and activation counts.
|
|
6
|
+
//
|
|
7
|
+
// "Most neurons die. Only the repeatedly fired ones survive." — Natural selection on OS.
|
|
8
|
+
|
|
9
|
+
import { readdirSync, statSync, writeFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { REGIONS, MAX_DEPTH } from './constants.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sweep the brain and mark inactive neurons as dormant.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} brainRoot - Absolute path to brain root
|
|
17
|
+
* @param {number} days - Number of days of inactivity before dormancy
|
|
18
|
+
* @returns {{ scanned: number, decayed: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function runDecay(brainRoot, days) {
|
|
21
|
+
const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
22
|
+
let scanned = 0;
|
|
23
|
+
let decayed = 0;
|
|
24
|
+
|
|
25
|
+
for (const regionName of REGIONS) {
|
|
26
|
+
const regionPath = join(brainRoot, regionName);
|
|
27
|
+
if (!existsSync(regionPath)) continue;
|
|
28
|
+
const result = decayWalk(regionPath, threshold, 0);
|
|
29
|
+
scanned += result.scanned;
|
|
30
|
+
decayed += result.decayed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`\u{1F4A4} decay: scanned ${scanned} neurons, decayed ${decayed} (>${days} days inactive)`);
|
|
34
|
+
return { scanned, decayed };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} dir
|
|
39
|
+
* @param {number} threshold - Timestamp threshold
|
|
40
|
+
* @param {number} depth
|
|
41
|
+
* @returns {{ scanned: number, decayed: number }}
|
|
42
|
+
*/
|
|
43
|
+
function decayWalk(dir, threshold, depth) {
|
|
44
|
+
if (depth > MAX_DEPTH) return { scanned: 0, decayed: 0 };
|
|
45
|
+
|
|
46
|
+
let scanned = 0;
|
|
47
|
+
let decayed = 0;
|
|
48
|
+
let entries;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
return { scanned: 0, decayed: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if this directory is a neuron (has .neuron files)
|
|
57
|
+
let hasNeuronFile = false;
|
|
58
|
+
let isDormant = false;
|
|
59
|
+
let latestMod = 0;
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.isFile()) {
|
|
63
|
+
if (entry.name.endsWith('.neuron')) {
|
|
64
|
+
hasNeuronFile = true;
|
|
65
|
+
try {
|
|
66
|
+
const st = statSync(join(dir, entry.name));
|
|
67
|
+
if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
if (entry.name.endsWith('.dormant')) {
|
|
71
|
+
isDormant = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (hasNeuronFile) {
|
|
77
|
+
scanned++;
|
|
78
|
+
if (!isDormant && latestMod < threshold) {
|
|
79
|
+
const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1000));
|
|
80
|
+
writeFileSync(
|
|
81
|
+
join(dir, 'decay.dormant'),
|
|
82
|
+
`Dormant since ${new Date().toISOString()} (${age} days inactive)`,
|
|
83
|
+
'utf8',
|
|
84
|
+
);
|
|
85
|
+
decayed++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Recurse
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
const sub = decayWalk(join(dir, entry.name), threshold, depth + 1);
|
|
94
|
+
scanned += sub.scanned;
|
|
95
|
+
decayed += sub.decayed;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { scanned, decayed };
|
|
100
|
+
}
|
package/lib/dedup.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// hebbian — Batch Deduplication
|
|
2
|
+
//
|
|
3
|
+
// Scans all neurons in a region and merges duplicates via Jaccard similarity.
|
|
4
|
+
// When a match is found (>= threshold), fires the higher-counter neuron
|
|
5
|
+
// and marks the lower-counter one dormant.
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { JACCARD_THRESHOLD } from './constants.js';
|
|
10
|
+
import { tokenize, jaccardSimilarity } from './similarity.js';
|
|
11
|
+
import { fireNeuron } from './fire.js';
|
|
12
|
+
import { scanBrain } from './scanner.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run batch deduplication across all regions.
|
|
16
|
+
* @param {string} brainRoot
|
|
17
|
+
* @returns {{ scanned: number, merged: number }}
|
|
18
|
+
*/
|
|
19
|
+
export function runDedup(brainRoot) {
|
|
20
|
+
const brain = scanBrain(brainRoot);
|
|
21
|
+
let scanned = 0;
|
|
22
|
+
let merged = 0;
|
|
23
|
+
|
|
24
|
+
for (const region of brain.regions) {
|
|
25
|
+
const neurons = region.neurons.filter((n) => !n.isDormant);
|
|
26
|
+
scanned += neurons.length;
|
|
27
|
+
|
|
28
|
+
// O(n^2) pairwise comparison within each region
|
|
29
|
+
const consumed = new Set();
|
|
30
|
+
for (let i = 0; i < neurons.length; i++) {
|
|
31
|
+
if (consumed.has(i)) continue;
|
|
32
|
+
const tokensI = tokenize(neurons[i].name);
|
|
33
|
+
|
|
34
|
+
for (let j = i + 1; j < neurons.length; j++) {
|
|
35
|
+
if (consumed.has(j)) continue;
|
|
36
|
+
const tokensJ = tokenize(neurons[j].name);
|
|
37
|
+
const sim = jaccardSimilarity(tokensI, tokensJ);
|
|
38
|
+
|
|
39
|
+
if (sim >= JACCARD_THRESHOLD) {
|
|
40
|
+
// Keep the one with higher counter, mark other dormant
|
|
41
|
+
const [keep, drop] = neurons[i].counter >= neurons[j].counter
|
|
42
|
+
? [neurons[i], neurons[j]]
|
|
43
|
+
: [neurons[j], neurons[i]];
|
|
44
|
+
|
|
45
|
+
// Fire the keeper to absorb the dropped counter
|
|
46
|
+
const relKeep = `${region.name}/${keep.path}`;
|
|
47
|
+
fireNeuron(brainRoot, relKeep);
|
|
48
|
+
|
|
49
|
+
// Mark the dropped neuron dormant
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(drop.fullPath, 'dedup.dormant'),
|
|
52
|
+
`Merged into ${keep.path} on ${new Date().toISOString()}`,
|
|
53
|
+
'utf8',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
consumed.add(neurons[i] === drop ? i : j);
|
|
57
|
+
merged++;
|
|
58
|
+
console.log(`\u{1F500} merged: "${drop.path}" → "${keep.path}" (sim=${sim.toFixed(2)})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`\u{1F9F9} dedup: scanned ${scanned} neurons, merged ${merged}`);
|
|
65
|
+
return { scanned, merged };
|
|
66
|
+
}
|