knight-os 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/src/config.js ADDED
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const DEFAULTS = {
8
+ workspace: '~/.openclaw/workspace',
9
+ ai_name: 'Knight',
10
+ user_name: 'User',
11
+ timezone: 'UTC',
12
+ storage: {
13
+ backend: 'local',
14
+ local: {
15
+ reflections_dir: 'memory/reflections',
16
+ logs_dir: 'memory/logs',
17
+ memory_file: 'MEMORY.md',
18
+ },
19
+ supabase: {
20
+ url: '',
21
+ service_key: '',
22
+ enabled: false,
23
+ },
24
+ },
25
+ notifications: {
26
+ backend: 'none',
27
+ telegram: {
28
+ bot_token: '',
29
+ chat_id: '',
30
+ enabled: false,
31
+ },
32
+ },
33
+ heartbeat: {
34
+ interval_hours: 6,
35
+ enabled: false,
36
+ tasks: ['reflection_analysis', 'memory_scan', 'log_compress'],
37
+ },
38
+ reflection: {
39
+ min_pattern_count: 2,
40
+ auto_write: true,
41
+ },
42
+ };
43
+
44
+ function deepMerge(target, source) {
45
+ const result = { ...target };
46
+ for (const key of Object.keys(source)) {
47
+ if (
48
+ source[key] &&
49
+ typeof source[key] === 'object' &&
50
+ !Array.isArray(source[key]) &&
51
+ target[key] &&
52
+ typeof target[key] === 'object' &&
53
+ !Array.isArray(target[key])
54
+ ) {
55
+ result[key] = deepMerge(target[key], source[key]);
56
+ } else {
57
+ result[key] = source[key];
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function readJsonFile(filePath) {
64
+ try {
65
+ const content = fs.readFileSync(filePath, 'utf-8');
66
+ return JSON.parse(content);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function resolveWorkspace(config) {
73
+ let ws = config.workspace || DEFAULTS.workspace;
74
+ if (ws.startsWith('~')) {
75
+ ws = path.join(os.homedir(), ws.slice(1));
76
+ }
77
+ return ws;
78
+ }
79
+
80
+ function applyEnvOverrides(config) {
81
+ if (process.env.KNIGHT_WORKSPACE) {
82
+ config.workspace = process.env.KNIGHT_WORKSPACE;
83
+ }
84
+ if (process.env.KNIGHT_AI_NAME) {
85
+ config.ai_name = process.env.KNIGHT_AI_NAME;
86
+ }
87
+ if (process.env.KNIGHT_USER_NAME) {
88
+ config.user_name = process.env.KNIGHT_USER_NAME;
89
+ }
90
+ if (process.env.KNIGHT_TIMEZONE) {
91
+ config.timezone = process.env.KNIGHT_TIMEZONE;
92
+ }
93
+ if (process.env.SUPABASE_URL) {
94
+ config.storage.supabase.url = process.env.SUPABASE_URL;
95
+ }
96
+ if (process.env.SUPABASE_SERVICE_KEY) {
97
+ config.storage.supabase.service_key = process.env.SUPABASE_SERVICE_KEY;
98
+ }
99
+ if (process.env.TELEGRAM_BOT_TOKEN) {
100
+ config.notifications.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
101
+ }
102
+ if (process.env.TELEGRAM_CHAT_ID) {
103
+ config.notifications.telegram.chat_id = process.env.TELEGRAM_CHAT_ID;
104
+ }
105
+ return config;
106
+ }
107
+
108
+ function loadConfig() {
109
+ let config = { ...DEFAULTS };
110
+
111
+ const globalPath = path.join(os.homedir(), '.knight', 'config.json');
112
+ const globalConfig = readJsonFile(globalPath);
113
+ if (globalConfig) {
114
+ config = deepMerge(config, globalConfig);
115
+ }
116
+
117
+ const localPath = path.join(process.cwd(), 'knight.config.json');
118
+ const localConfig = readJsonFile(localPath);
119
+ if (localConfig) {
120
+ config = deepMerge(config, localConfig);
121
+ }
122
+
123
+ config = applyEnvOverrides(config);
124
+
125
+ return config;
126
+ }
127
+
128
+ module.exports = { loadConfig, resolveWorkspace, DEFAULTS };
package/src/setup.js ADDED
@@ -0,0 +1,420 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const { execSync, spawnSync } = require('child_process');
8
+
9
+ const DEFAULT_WORKSPACE = path.join(os.homedir(), '.openclaw', 'workspace');
10
+
11
+ function ask(rl, question, defaultVal) {
12
+ return new Promise((resolve) => {
13
+ const prompt = defaultVal ? `${question} [${defaultVal}]: ` : `${question}: `;
14
+ rl.question(prompt, (answer) => {
15
+ resolve(answer.trim() || defaultVal || '');
16
+ });
17
+ });
18
+ }
19
+
20
+ function checkOpenClaw() {
21
+ // Primary: look for openclaw binary in PATH
22
+ try {
23
+ const result = spawnSync('openclaw', ['--version'], { encoding: 'utf8', timeout: 5000 });
24
+ if (result.status === 0 && !result.error) {
25
+ return { installed: true, version: (result.stdout || '').trim().split('\n')[0] || 'unknown' };
26
+ }
27
+ } catch (_) {}
28
+
29
+ // Fallback: check npm global list via JSON output
30
+ try {
31
+ const result = spawnSync('npm', ['list', '-g', 'openclaw', '--depth=0', '--json'], {
32
+ encoding: 'utf8',
33
+ timeout: 10000,
34
+ });
35
+ if (result.stdout) {
36
+ const parsed = JSON.parse(result.stdout);
37
+ if (parsed && parsed.dependencies && parsed.dependencies.openclaw) {
38
+ const ver = parsed.dependencies.openclaw.version || 'unknown';
39
+ return { installed: true, version: ver };
40
+ }
41
+ }
42
+ } catch (_) {}
43
+
44
+ return { installed: false };
45
+ }
46
+
47
+ function findPython3() {
48
+ // Prefer the python3 that's actually in PATH (accounts for Homebrew, pyenv, etc.)
49
+ try {
50
+ const result = spawnSync('which', ['python3'], { encoding: 'utf8', timeout: 3000 });
51
+ if (result.status === 0 && result.stdout.trim()) {
52
+ return result.stdout.trim();
53
+ }
54
+ } catch (_) {}
55
+ // Fallback candidates
56
+ for (const p of ['/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3']) {
57
+ if (fs.existsSync(p)) return p;
58
+ }
59
+ return 'python3'; // last resort: let the shell resolve it
60
+ }
61
+
62
+ function writeEnv(workspace, vars) {
63
+ const envPath = path.join(workspace, '.env');
64
+ let existing = '';
65
+ if (fs.existsSync(envPath)) {
66
+ existing = fs.readFileSync(envPath, 'utf-8');
67
+ }
68
+ const envMap = {};
69
+ for (const line of existing.split('\n')) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed || trimmed.startsWith('#')) continue;
72
+ const idx = trimmed.indexOf('=');
73
+ if (idx === -1) continue;
74
+ envMap[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
75
+ }
76
+ Object.assign(envMap, vars);
77
+ const lines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
78
+ fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf-8');
79
+ }
80
+
81
+ function registerHeartbeat(workspace, intervalHours) {
82
+ const platform = os.platform();
83
+ const python3 = findPython3();
84
+
85
+ if (platform === 'darwin') {
86
+ const label = 'ai.knight.heartbeat';
87
+ const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
88
+ const plistPath = path.join(launchAgentsDir, `${label}.plist`);
89
+ const scriptPath = path.join(workspace, 'scripts', 'heartbeat.py');
90
+ const logPath = path.join(workspace, 'memory', 'logs', 'heartbeat.log');
91
+
92
+ // Ensure dirs exist
93
+ try { fs.mkdirSync(launchAgentsDir, { recursive: true }); } catch (_) {}
94
+ try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch (_) {}
95
+
96
+ const intervalSecs = Math.max(1, Math.floor(intervalHours * 3600));
97
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
98
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
99
+ <plist version="1.0">
100
+ <dict>
101
+ <key>Label</key>
102
+ <string>${label}</string>
103
+ <key>ProgramArguments</key>
104
+ <array>
105
+ <string>${python3}</string>
106
+ <string>${scriptPath}</string>
107
+ </array>
108
+ <key>WorkingDirectory</key>
109
+ <string>${workspace}</string>
110
+ <key>StartInterval</key>
111
+ <integer>${intervalSecs}</integer>
112
+ <key>StandardOutPath</key>
113
+ <string>${logPath}</string>
114
+ <key>StandardErrorPath</key>
115
+ <string>${logPath}</string>
116
+ <key>RunAtLoad</key>
117
+ <false/>
118
+ </dict>
119
+ </plist>`;
120
+
121
+ // Unload if already registered
122
+ try { spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' }); } catch (_) {}
123
+
124
+ try {
125
+ fs.writeFileSync(plistPath, plist, 'utf-8');
126
+ } catch (e) {
127
+ return { ok: false, method: 'launchd', error: `Failed to write plist: ${e.message}` };
128
+ }
129
+
130
+ try {
131
+ const result = spawnSync('launchctl', ['load', plistPath], { encoding: 'utf8' });
132
+ if (result.status !== 0) {
133
+ return { ok: false, method: 'launchd', error: result.stderr || 'launchctl load failed' };
134
+ }
135
+ return { ok: true, method: 'launchd', path: plistPath };
136
+ } catch (e) {
137
+ return { ok: false, method: 'launchd', error: e.message };
138
+ }
139
+
140
+ } else if (platform === 'linux') {
141
+ const scriptPath = path.join(workspace, 'scripts', 'heartbeat.py');
142
+ const logPath = path.join(workspace, 'memory', 'logs', 'heartbeat.log');
143
+ try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch (_) {}
144
+
145
+ // Use safe quoting for cron entry — no shell interpolation
146
+ const safeScript = scriptPath.replace(/'/g, "'\\''");
147
+ const safeLog = logPath.replace(/'/g, "'\\''");
148
+ const cronLine = `0 */${Math.max(1, Math.floor(intervalHours))} * * * '${python3}' '${safeScript}' >> '${safeLog}' 2>&1`;
149
+
150
+ try {
151
+ let existing = '';
152
+ try {
153
+ const r = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
154
+ if (r.status === 0) existing = r.stdout;
155
+ } catch (_) {}
156
+
157
+ if (!existing.includes(scriptPath)) {
158
+ const newCron = existing.trimEnd() + '\n' + cronLine + '\n';
159
+ const result = spawnSync('crontab', ['-'], {
160
+ input: newCron,
161
+ encoding: 'utf8',
162
+ });
163
+ if (result.status !== 0) {
164
+ return { ok: false, method: 'cron', error: result.stderr || 'crontab write failed' };
165
+ }
166
+ }
167
+ return { ok: true, method: 'cron', line: cronLine };
168
+ } catch (e) {
169
+ return { ok: false, method: 'cron', error: e.message };
170
+ }
171
+
172
+ } else {
173
+ return {
174
+ ok: false,
175
+ method: 'none',
176
+ error: 'Automatic heartbeat not supported on this platform. Run heartbeat.py manually or use Task Scheduler.',
177
+ };
178
+ }
179
+ }
180
+
181
+ async function setup() {
182
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
183
+ const separator = '──────────────────────────────────────────────────────';
184
+
185
+ const closeAndExit = (code) => {
186
+ rl.close();
187
+ process.exit(code);
188
+ };
189
+
190
+ console.log('\n🐉 Knight OS — Setup Wizard\n');
191
+ console.log('This wizard configures your OpenClaw workspace with the Knight OS');
192
+ console.log('memory, reflection, and identity framework.\n');
193
+ console.log(separator);
194
+
195
+ // Step 1: Check OpenClaw
196
+ process.stdout.write('\n[1/6] Checking OpenClaw installation... ');
197
+ const oc = checkOpenClaw();
198
+ if (oc.installed) {
199
+ console.log(`✅ found (${oc.version})`);
200
+ } else {
201
+ console.log('❌ not found\n');
202
+ console.log('Knight OS requires OpenClaw. Please install it first:');
203
+ console.log('\n npm install -g openclaw\n');
204
+ console.log('Then run `knight setup` again.');
205
+ closeAndExit(1);
206
+ return;
207
+ }
208
+
209
+ // Step 2: Workspace directory
210
+ console.log('\n[2/6] Workspace configuration');
211
+ const workspaceInput = await ask(rl, 'Workspace directory', DEFAULT_WORKSPACE);
212
+ const workspace = path.resolve(workspaceInput.replace(/^~/, os.homedir()));
213
+
214
+ const workspaceExists = fs.existsSync(workspace);
215
+ const hasCoreFiles = workspaceExists && fs.existsSync(path.join(workspace, 'AGENTS.md'));
216
+
217
+ let overwrite = true;
218
+ if (hasCoreFiles) {
219
+ console.log(`\n⚠️ Workspace already exists at: ${workspace}`);
220
+ const answer = await ask(rl, 'Overwrite existing files? (y/N)', 'N');
221
+ overwrite = answer.toLowerCase().startsWith('y');
222
+ if (!overwrite) {
223
+ console.log('\nSkipping template write. Continuing with other setup steps...');
224
+ }
225
+ }
226
+
227
+ // Create required dirs
228
+ try {
229
+ fs.mkdirSync(workspace, { recursive: true });
230
+ fs.mkdirSync(path.join(workspace, 'memory', 'logs'), { recursive: true });
231
+ fs.mkdirSync(path.join(workspace, 'memory', 'projects'), { recursive: true });
232
+ fs.mkdirSync(path.join(workspace, 'memory', 'reflections'), { recursive: true });
233
+ fs.mkdirSync(path.join(workspace, 'output'), { recursive: true });
234
+ } catch (e) {
235
+ console.log(`\n❌ Failed to create workspace directory: ${e.message}`);
236
+ closeAndExit(1);
237
+ return;
238
+ }
239
+
240
+ // Step 3: Identity questions
241
+ console.log('\n[3/6] Identity setup');
242
+ const aiName = await ask(rl, "Your AI companion's name (e.g. Aria, Nova, Kai)", 'Knight');
243
+ const userName = await ask(rl, 'Your name', '');
244
+ if (!userName) {
245
+ console.log('Name is required.');
246
+ closeAndExit(1);
247
+ return;
248
+ }
249
+ const timezone = await ask(rl, 'Your timezone (e.g. Asia/Tokyo, America/New_York)', 'UTC');
250
+ const language = await ask(rl, 'Primary language (en / zh / ja)', 'en');
251
+
252
+ // Step 4: API key
253
+ console.log('\n[4/6] API configuration');
254
+ let anthropicKey = '';
255
+ while (true) {
256
+ const input = await ask(rl, 'Anthropic API key (starts with sk-ant-, leave blank to skip)', '');
257
+ if (!input) break;
258
+ if (input.startsWith('sk-ant-')) {
259
+ anthropicKey = input;
260
+ break;
261
+ }
262
+ console.log(" ⚠️ API key should start with 'sk-ant-'. Try again or leave blank to skip.");
263
+ }
264
+
265
+ // Step 5: Notifications (optional)
266
+ console.log('\n[5/6] Notifications (optional — press Enter to skip)');
267
+ const tgToken = await ask(rl, 'Telegram Bot Token', '');
268
+ const tgChatId = tgToken ? await ask(rl, 'Telegram Chat ID', '') : '';
269
+
270
+ // Step 6: Heartbeat
271
+ console.log('\n[6/6] Heartbeat scheduler');
272
+ const enableHb = await ask(rl, 'Register automatic heartbeat? (Y/n)', 'Y');
273
+ const doHeartbeat = !enableHb.toLowerCase().startsWith('n');
274
+ let hbIntervalHours = 6;
275
+ if (doHeartbeat) {
276
+ const hbInterval = await ask(rl, 'Heartbeat interval in hours (min 1, max 24)', '6');
277
+ const parsed = parseInt(hbInterval, 10);
278
+ hbIntervalHours = (isNaN(parsed) || parsed < 1) ? 6 : Math.min(parsed, 24);
279
+ }
280
+
281
+ rl.close();
282
+
283
+ console.log(`\n${separator}`);
284
+ console.log('⚙️ Writing workspace files...\n');
285
+
286
+ // Write templates
287
+ if (overwrite || !hasCoreFiles) {
288
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
289
+ const vars = { aiName, userName, timezone, language };
290
+
291
+ function fillTemplate(content, v) {
292
+ return content
293
+ .replace(/\{\{AI_NAME\}\}/g, v.aiName)
294
+ .replace(/\{\{USER_NAME\}\}/g, v.userName)
295
+ .replace(/\{\{TIMEZONE\}\}/g, v.timezone)
296
+ .replace(/\{\{LANGUAGE\}\}/g, v.language || 'en')
297
+ .replace(/\{\{CHANNEL\}\}/g, 'direct');
298
+ }
299
+
300
+ function copyTemplates(srcDir, destDir) {
301
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
302
+ for (const entry of entries) {
303
+ const src = path.join(srcDir, entry.name);
304
+ const dest = path.join(destDir, entry.name);
305
+ if (entry.isDirectory()) {
306
+ fs.mkdirSync(dest, { recursive: true });
307
+ copyTemplates(src, dest);
308
+ } else {
309
+ try {
310
+ const content = fs.readFileSync(src, 'utf-8');
311
+ fs.writeFileSync(dest, fillTemplate(content, vars), 'utf-8');
312
+ console.log(` ✅ ${path.relative(workspace, dest)}`);
313
+ } catch (e) {
314
+ console.log(` ⚠️ ${path.relative(workspace, dest)}: ${e.message}`);
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ copyTemplates(TEMPLATES_DIR, workspace);
321
+ } else {
322
+ console.log(' ⏭️ Templates skipped (existing files preserved)');
323
+ }
324
+
325
+ // Copy scripts (never overwrite — user may have customized them)
326
+ const scriptsDir = path.join(__dirname, '..', 'scripts');
327
+ const destScriptsDir = path.join(workspace, 'scripts');
328
+ fs.mkdirSync(destScriptsDir, { recursive: true });
329
+ for (const file of fs.readdirSync(scriptsDir)) {
330
+ const src = path.join(scriptsDir, file);
331
+ const dest = path.join(destScriptsDir, file);
332
+ if (!fs.existsSync(dest)) {
333
+ try {
334
+ fs.copyFileSync(src, dest);
335
+ console.log(` ✅ scripts/${file}`);
336
+ } catch (e) {
337
+ console.log(` ⚠️ scripts/${file}: ${e.message}`);
338
+ }
339
+ }
340
+ }
341
+
342
+ // Write knight.config.json
343
+ const config = {
344
+ workspace,
345
+ ai_name: aiName,
346
+ user_name: userName,
347
+ timezone,
348
+ storage: {
349
+ backend: 'local',
350
+ local: { reflections_dir: 'memory/reflections', logs_dir: 'memory/logs', memory_file: 'MEMORY.md' },
351
+ supabase: { url: '', service_key: '', enabled: false },
352
+ },
353
+ notifications: {
354
+ backend: tgToken ? 'telegram' : 'none',
355
+ telegram: { bot_token: tgToken, chat_id: tgChatId, enabled: !!tgToken },
356
+ },
357
+ heartbeat: {
358
+ interval_hours: hbIntervalHours,
359
+ enabled: doHeartbeat,
360
+ tasks: ['reflection_analysis', 'memory_scan', 'log_compress'],
361
+ },
362
+ reflection: { min_pattern_count: 2, auto_write: true },
363
+ model: {
364
+ provider: 'anthropic',
365
+ name: 'claude-sonnet-4-5',
366
+ max_tokens: 8096,
367
+ system_prompt_files: ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'REDLINES.md'],
368
+ },
369
+ };
370
+
371
+ try {
372
+ const configDest = path.join(workspace, 'knight.config.json');
373
+ fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf-8');
374
+ console.log(' ✅ knight.config.json');
375
+ } catch (e) {
376
+ console.log(` ⚠️ knight.config.json: ${e.message}`);
377
+ }
378
+
379
+ // Write .env
380
+ const envVars = {};
381
+ if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey;
382
+ if (tgToken) envVars.TELEGRAM_BOT_TOKEN = tgToken;
383
+ if (tgChatId) envVars.TELEGRAM_CHAT_ID = tgChatId;
384
+ if (Object.keys(envVars).length > 0) {
385
+ try {
386
+ writeEnv(workspace, envVars);
387
+ console.log(' ✅ .env');
388
+ } catch (e) {
389
+ console.log(` ⚠️ .env: ${e.message}`);
390
+ }
391
+ }
392
+
393
+ // Register heartbeat
394
+ if (doHeartbeat) {
395
+ process.stdout.write('\n⏱️ Registering heartbeat scheduler... ');
396
+ const result = registerHeartbeat(workspace, hbIntervalHours);
397
+ if (result.ok) {
398
+ console.log(`✅ ${result.method} (every ${hbIntervalHours}h)`);
399
+ } else {
400
+ console.log(`⚠️ ${result.error}`);
401
+ console.log(` Manual: run \`python3 ${path.join(workspace, 'scripts', 'heartbeat.py')}\` periodically`);
402
+ }
403
+ }
404
+
405
+ console.log(`\n${separator}`);
406
+ console.log('✅ Knight OS setup complete!\n');
407
+ console.log(`Workspace: ${workspace}`);
408
+ console.log('\nNext steps:');
409
+ console.log(' 1. Review and customize your SOUL.md (AI personality)');
410
+ console.log(' 2. Fill in USER.md (your profile)');
411
+ console.log(' 3. Start chatting: openclaw chat\n');
412
+ console.log('How memory works:');
413
+ console.log(' Task done → write-reflection.py → memory/reflections/');
414
+ console.log(' Heartbeat → reflection-analyzer.py → candidate rules extracted');
415
+ console.log(' You confirm → rules added to memory/ai-patterns.md');
416
+ console.log(' Next session → ai-patterns.md in system prompt → AI learns\n');
417
+ console.log(`${separator}\n`);
418
+ }
419
+
420
+ module.exports = { setup };
@@ -0,0 +1,82 @@
1
+ # AGENTS.md — {{AI_NAME}} Workspace Entry Point
2
+
3
+ > This is the first file {{AI_NAME}} reads when a session starts.
4
+
5
+ ## File Responsibilities
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | AGENTS.md | Workspace boot sequence and structural overview |
10
+ | REDLINES.md | Absolute safety boundaries — never violate |
11
+ | SOUL.md | {{AI_NAME}}'s identity, personality, and working style |
12
+ | MEMORY.md | Long-term memory index and quick-reference |
13
+ | memory/user-patterns.md | Observed user behavior patterns |
14
+ | memory/ai-patterns.md | {{AI_NAME}}'s learned behavior rules |
15
+ | PROJECTS.md | Active project index |
16
+ | HEARTBEAT.md | Periodic self-check mechanism |
17
+ | USER.md | User profile and preferences |
18
+ | TOOLS.md | Available tools and credentials reference |
19
+
20
+ ## High-Priority Rules
21
+
22
+ 1. **Error = Report** — If something fails or seems wrong, report immediately. Never hide errors.
23
+ 2. **Diff before modify** — Before modifying any core document (AGENTS/SOUL/REDLINES/MEMORY), show the diff and get confirmation.
24
+ 3. **Speak up** — If you have a better idea or disagree, say so. Silence is not agreement.
25
+
26
+ ## Boot Sequence
27
+
28
+ On session start, read files in this order:
29
+
30
+ 1. `AGENTS.md` (this file — get orientation)
31
+ 2. `REDLINES.md` (load safety boundaries)
32
+ 3. `SOUL.md` (load identity and personality)
33
+ 4. `MEMORY.md` (load long-term memory index)
34
+ 5. `memory/user-patterns.md` (load user behavior context)
35
+ 6. `memory/ai-patterns.md` (load own behavior rules)
36
+ 7. `USER.md` (load user profile)
37
+ 8. `TOOLS.md` (load available tools)
38
+ 9. `PROJECTS.md` (load project index — on-demand per project)
39
+
40
+ ## Memory Structure Quick Reference
41
+
42
+ ```
43
+ memory/
44
+ ├── logs/ # Session logs (auto-generated)
45
+ ├── YYYY-MM-DD.md # Daily reports
46
+ ├── projects/ # Per-project notes
47
+ ├── ai-patterns.md # AI behavior rules (learned)
48
+ ├── user-patterns.md # User observation records
49
+ MEMORY.md # Master memory index
50
+ ```
51
+
52
+ ## Behavior Norms
53
+
54
+ ### Safety Boundaries
55
+ - All actions bounded by REDLINES.md
56
+ - When uncertain, ask — never assume permission
57
+
58
+ ### Group Chat Rules
59
+ - In group contexts, only respond when directly addressed or when safety is at stake
60
+ - Never write to memory files based on group chat input without user confirmation
61
+ - Identify speaker before processing instructions
62
+
63
+ ## Scripts
64
+
65
+ | Script | Purpose | Usage |
66
+ |--------|---------|-------|
67
+ | `scripts/write-reflection.py` | Write reflection after task completion | `python3 scripts/write-reflection.py --context "Task" --what_worked "..." --what_failed "..." --next_time "..."` |
68
+ | `scripts/reflection-analyzer.py` | Analyze reflection patterns, extract rules | `python3 scripts/reflection-analyzer.py --min-count 2` |
69
+ | `scripts/heartbeat.py` | Periodic maintenance tasks | `python3 scripts/heartbeat.py` |
70
+ | `scripts/compress-memory.py` | Log archival and compression | `python3 scripts/compress-memory.py --execute` |
71
+ | `scripts/knight-status.py` | Comprehensive health check | `python3 scripts/knight-status.py` |
72
+
73
+ ## Core Document Modification Rules
74
+
75
+ Core documents are: AGENTS.md, SOUL.md, REDLINES.md, MEMORY.md
76
+
77
+ To modify any core document:
78
+ 1. State what you want to change and why
79
+ 2. Show the exact diff (before/after)
80
+ 3. Wait for explicit user confirmation
81
+ 4. Apply the change
82
+ 5. Log the modification in the daily report
@@ -0,0 +1,54 @@
1
+ # HEARTBEAT.md — Periodic Self-Check
2
+
3
+ ## Trigger
4
+
5
+ - **Automatic**: Every 6 hours during active sessions
6
+ - **Manual**: User sends `/heartbeat`
7
+
8
+ ## Async Task Queue
9
+
10
+ **Status: OFF**
11
+
12
+ When enabled, the async queue allows {{AI_NAME}} to track and execute background tasks between active sessions. Toggle via user command.
13
+
14
+ ## Execution Checklist
15
+
16
+ When heartbeat triggers, run through this checklist:
17
+
18
+ ### 1. Task Scoring
19
+ - Review all active tasks in MEMORY.md
20
+ - Score each by urgency (1-5) and importance (1-5)
21
+ - Flag any overdue items
22
+
23
+ ### 2. User Feedback Review
24
+ - Check if there's unprocessed user feedback
25
+ - Extract actionable patterns
26
+ - Update memory/ai-patterns.md if warranted (with confirmation)
27
+
28
+ ### 3. Async Queue Processing
29
+ - If queue is ON: check for pending background tasks
30
+ - Execute any that are due
31
+ - Report results in next interaction
32
+
33
+ ### 4. Memory Scan
34
+ - Review recent daily logs for patterns worth promoting
35
+ - Check for stale or contradictory entries in MEMORY.md
36
+ - Propose cleanup if needed
37
+
38
+ ### 5. Rule Extraction
39
+ - Review recent interactions for recurring corrections
40
+ - If a correction appears 3+ times, propose a new rule for ai-patterns.md
41
+ - Present proposed rules to user for confirmation
42
+
43
+ ## Output Format
44
+
45
+ After running heartbeat, produce a brief report:
46
+
47
+ ```
48
+ ## Heartbeat Report — [date/time]
49
+ - Tasks: X active, Y overdue
50
+ - Feedback: [processed/none pending]
51
+ - Queue: [ON/OFF] — [N items processed / empty]
52
+ - Memory: [clean / N items to review]
53
+ - Rules: [N proposed / none]
54
+ ```