metame-cli 1.5.9 โ†’ 1.5.11

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
@@ -26,8 +26,9 @@ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bas
26
26
 
27
27
  ---
28
28
 
29
- > ### ๐Ÿš€ v1.5.4 โ€” Agent Soul Layer Auto-Repair & Intent Engine
29
+ > ### ๐Ÿš€ v1.5.10 โ€” Perpetual Task Engine & Agent Soul Layer
30
30
  >
31
+ > - **Perpetual task engine**: Any project can run as a 24/7 autonomous agent loop with event sourcing, verifier gates, budget/depth control, and reconciliation. Domain-agnostic โ€” works for research, code auditing, documentation, anything. Configure with `reactive: true` and an optional `perpetual.yaml`.
31
32
  > - **Cross-device dispatch**: Team members can run on remote machines. Add `peer: windows` to a member and messages route automatically via a Feishu relay chat โ€” HMAC-signed, dedup-protected, zero manual routing.
32
33
  > - **`/dispatch peers`**: View remote dispatch config, relay chat, and all remote team members from mobile.
33
34
  > - **`dispatch_to peer:project`**: Dispatch tasks to remote peers from CLI, admin commands, or Claude sessions.
@@ -185,9 +186,51 @@ Chain skills into multi-step workflows โ€” research โ†’ write โ†’ publish โ€” fu
185
186
  | `cwd` | Working directory for the task |
186
187
  | `timeout` | Max run time |
187
188
 
188
- > **Scheduled tasks require system registration.** Run `metame daemon install-launchd` (macOS) or `metame daemon install-task-scheduler` (Windows) and tasks fire on schedule even with the screen locked โ€” as long as the machine is on.
189
+ > **Scheduled tasks require the daemon to be running.** On macOS, `metame start` auto-registers with launchd (auto-restart on crash/reboot). On Windows, run `metame daemon install-task-scheduler`. Tasks fire on schedule even with the screen locked โ€” as long as the machine is on.
189
190
 
190
- ### 5. Skills That Evolve Themselves
191
+ ### 5. Perpetual Task Engine โ€” Agents That Never Stop
192
+
193
+ Any project can run as an autonomous perpetual loop. The daemon drives the cycle: agent acts โ†’ verifier gates โ†’ event log records โ†’ next dispatch. Domain-agnostic โ€” research, code auditing, documentation, anything.
194
+
195
+ **How it works:**
196
+
197
+ ```yaml
198
+ # daemon.yaml โ€” register a perpetual project
199
+ scientist:
200
+ name: Research Director
201
+ reactive: true # enables perpetual lifecycle
202
+ cwd: ~/AGI/AgentScientist
203
+ team:
204
+ - key: sci_scout
205
+ name: Literature Scout
206
+ cwd: ~/AGI/AgentScientist/team/scout
207
+ ```
208
+
209
+ **What the platform provides (zero project-specific code in MetaMe):**
210
+ - **Event sourcing**: All state changes logged to `~/.metame/events/<key>.jsonl` โ€” single source of truth, daemon-exclusive writes
211
+ - **Budget & depth gates**: Auto-pause when token budget or loop depth exceeded
212
+ - **Verifier hooks**: Project scripts validate phase completion with objective checks (file existence โ†’ structure checks โ†’ API verification)
213
+ - **Reconciliation**: Heartbeat detects stale projects and notifies you
214
+ - **`/status perpetual`**: See all running projects โ€” phase, depth, mission, last activity
215
+
216
+ **Convention over configuration**: Drop a `scripts/verifier.js` in your project and it just works. Need custom signals? Add a `perpetual.yaml`:
217
+
218
+ ```yaml
219
+ # perpetual.yaml โ€” optional, override defaults
220
+ completion_signal: RESEARCH_COMPLETE
221
+ verifier: scripts/research-verifier.js
222
+ max_depth: 50
223
+ ```
224
+
225
+ ```
226
+ Agent output โ†’ daemon parses signals
227
+ โ†’ budget gate โ†’ depth gate โ†’ verifier gate
228
+ โ†’ event logged โ†’ state file regenerated
229
+ โ†’ next dispatch (fresh session) OR mission complete
230
+ โ†’ archive โ†’ next mission from queue โ†’ repeat
231
+ ```
232
+
233
+ ### 6. Skills That Evolve Themselves
191
234
 
192
235
  MetaMe's current skill loop is queue-driven and reviewable (not magic black-box automation).
193
236
 
@@ -231,7 +274,7 @@ MetaMe is the orchestration layer. Claude Code and Codex are the engines. You in
231
274
  | 2. Genesis Interview | Just chat โ€” MetaMe auto-starts a deep soul interview on first run โ†’ builds `~/.claude_profile.yaml` |
232
275
  | 3. Connect phone | Say "help me set up mobile access" โ†’ interactive Telegram/Feishu bot wizard |
233
276
  | 4. Start daemon | `metame start` โ†’ background daemon launches, bot goes online |
234
- | 5. Register with OS | macOS: `metame daemon install-launchd` ยท Windows: `metame daemon install-task-scheduler` |
277
+ | 5. Register with OS | macOS: automatic (step 4 registers with launchd) ยท Windows: `metame daemon install-task-scheduler` |
235
278
 
236
279
  > **First time?** Just run `metame` and talk naturally. Everything is conversational.
237
280
 
@@ -257,7 +300,7 @@ rm -rf ~/.metame ~/.claude_profile.yaml
257
300
  ```
258
301
 
259
302
  Optional service cleanup:
260
- - macOS: `launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.metame.daemon.plist && rm -f ~/Library/LaunchAgents/com.metame.daemon.plist`
303
+ - macOS: `launchctl bootout gui/$(id -u)/com.metame.npm-daemon && rm -f ~/Library/LaunchAgents/com.metame.npm-daemon.plist`
261
304
  - Windows: `schtasks /delete /tn "MetaMe-Daemon" /f`
262
305
  - Linux/WSL: `systemctl --user disable --now metame && rm -f ~/.config/systemd/user/metame.service`
263
306
 
@@ -625,7 +668,7 @@ For day-2 operations and troubleshooting (engine routing, codex login/rate-limit
625
668
 
626
669
  Install directly into Claude Code without npm: `claude plugin install github:Yaron9/MetaMe/plugin`
627
670
 
628
- All features included. Plugin auto-starts daemon on `SessionStart` (if `daemon.yaml` exists). Does **not** auto-register OS service โ€” after reboot, open Claude once or start daemon manually.
671
+ All features included. Plugin auto-starts daemon on `SessionStart` (if `daemon.yaml` exists). On macOS, `metame start` auto-registers with launchd โ€” daemon auto-restarts on crash/reboot without opening Claude.
629
672
 
630
673
  ## Contributing
631
674
 
package/index.js CHANGED
@@ -173,10 +173,18 @@ if (!fs.existsSync(METAME_DIR)) {
173
173
  // DEPLOY PHASE: sync scripts, docs, bin to ~/.metame/
174
174
  // ---------------------------------------------------------
175
175
 
176
- // Dev mode: when running from git repo, symlink instead of copy.
176
+ // Dev mode: when running from the real git repo, symlink instead of copy.
177
177
  // This ensures source files and runtime files are always the same,
178
178
  // preventing agents from accidentally editing copies instead of source.
179
- const IS_DEV_MODE = fs.existsSync(path.join(__dirname, '.git'));
179
+ // IMPORTANT: git worktrees have a `.git` FILE (not directory) pointing to the main repo.
180
+ // They must NOT be treated as dev mode โ€” deploying from a worktree would overwrite
181
+ // production symlinks with stale code. Only a real .git directory qualifies.
182
+ const IS_DEV_MODE = (() => {
183
+ const dotGit = path.join(__dirname, '.git');
184
+ try {
185
+ return fs.statSync(dotGit).isDirectory();
186
+ } catch { return false; }
187
+ })();
180
188
 
181
189
  /**
182
190
  * Sync files from srcDir to destDir.
@@ -257,6 +265,70 @@ function readRunningDaemonPid({ pidFile, lockFile }) {
257
265
  return null;
258
266
  }
259
267
 
268
+ // --- macOS launchd integration ---
269
+ const LAUNCHD_LABEL = 'com.metame.npm-daemon';
270
+ const LAUNCHD_PLIST = path.join(HOME_DIR, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
271
+
272
+ function ensureLaunchdPlist({ daemonScript, daemonLog }) {
273
+ const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
274
+ if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
275
+ const nodePath = process.execPath;
276
+ const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
277
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
278
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
279
+ <plist version="1.0">
280
+ <dict>
281
+ <key>Label</key>
282
+ <string>${LAUNCHD_LABEL}</string>
283
+ <key>ProgramArguments</key>
284
+ <array>
285
+ <string>/usr/bin/caffeinate</string>
286
+ <string>-i</string>
287
+ <string>${nodePath}</string>
288
+ <string>${daemonScript}</string>
289
+ </array>
290
+ <key>RunAtLoad</key>
291
+ <true/>
292
+ <key>KeepAlive</key>
293
+ <true/>
294
+ <key>ThrottleInterval</key>
295
+ <integer>30</integer>
296
+ <key>StandardOutPath</key>
297
+ <string>${daemonLog}</string>
298
+ <key>StandardErrorPath</key>
299
+ <string>${daemonLog}</string>
300
+ <key>WorkingDirectory</key>
301
+ <string>${HOME_DIR}</string>
302
+ <key>EnvironmentVariables</key>
303
+ <dict>
304
+ <key>PATH</key>
305
+ <string>${currentPath}</string>
306
+ <key>HOME</key>
307
+ <string>${HOME_DIR}</string>
308
+ <key>METAME_ROOT</key>
309
+ <string>${__dirname}</string>
310
+ <key>LAUNCHED_BY_LAUNCHD</key>
311
+ <string>1</string>
312
+ </dict>
313
+ </dict>
314
+ </plist>`;
315
+ const existing = fs.existsSync(LAUNCHD_PLIST) ? fs.readFileSync(LAUNCHD_PLIST, 'utf8') : '';
316
+ if (existing !== plist) {
317
+ // Bootout old version if loaded, ignore errors
318
+ try { execSync(`launchctl bootout gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`); } catch { /* not loaded */ }
319
+ fs.writeFileSync(LAUNCHD_PLIST, plist, 'utf8');
320
+ }
321
+ return LAUNCHD_PLIST;
322
+ }
323
+
324
+ function launchdIsRunning() {
325
+ try {
326
+ const out = execSync(`launchctl list ${LAUNCHD_LABEL} 2>/dev/null`, { encoding: 'utf8' });
327
+ const m = out.match(/"PID"\s*=\s*(\d+)/);
328
+ return m ? parseInt(m[1], 10) : null;
329
+ } catch { return null; }
330
+ }
331
+
260
332
  function requestDaemonRestart({
261
333
  reason = 'manual-restart',
262
334
  daemonPidFile = path.join(METAME_DIR, 'daemon.pid'),
@@ -266,6 +338,16 @@ function requestDaemonRestart({
266
338
  const pid = readRunningDaemonPid({ pidFile: daemonPidFile, lockFile: daemonLockFile });
267
339
  if (!pid) return { ok: false, status: 'not_running' };
268
340
 
341
+ // macOS: use launchctl kickstart -k for atomic restart (no orphan/race)
342
+ if (process.platform === 'darwin') {
343
+ try {
344
+ execSync(`launchctl kickstart -k gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
345
+ return { ok: true, status: 'signaled', pid };
346
+ } catch {
347
+ // launchd job not loaded โ€” fall through to SIGUSR2
348
+ }
349
+ }
350
+
269
351
  if (process.platform !== 'win32') {
270
352
  try {
271
353
  process.kill(pid, 'SIGUSR2');
@@ -305,7 +387,7 @@ function requestDaemonRestart({
305
387
  const scriptsDir = path.join(__dirname, 'scripts');
306
388
  // Auto-detect ALL runtime scripts: daemon-*.js + all other non-test, non-utility .js/.yaml/.sh files.
307
389
  // This prevents "missing module" crashes when new files are added without updating a manual list.
308
- const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js']);
390
+ const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js', 'daemon.yaml']);
309
391
  const BUNDLED_SCRIPTS = (() => {
310
392
  try {
311
393
  return fs.readdirSync(scriptsDir).filter((f) => {
@@ -330,6 +412,19 @@ try {
330
412
  }
331
413
  } catch { /* non-fatal */ }
332
414
 
415
+ // Worktree guard: team members running in worktrees must NEVER deploy to ~/.metame/
416
+ // Their worktree is an isolated sandbox โ€” deploying would overwrite production symlinks.
417
+ // Detect any .worktrees/ parent in the path (covers both ~/.metame/worktrees/ and repo-local .worktrees/).
418
+ const _isInWorktree = __dirname.split(path.sep).includes('.worktrees') ||
419
+ __dirname.startsWith(path.join(HOME_DIR, '.metame', 'worktrees'));
420
+ if (_isInWorktree) {
421
+ console.error(`\n${icon("stop")} ACTION BLOCKED: Worktree Deploy Prevented`);
422
+ console.error(` You are running from a worktree (${path.basename(__dirname)}).`);
423
+ console.error(' Deploying from a worktree would overwrite production daemon code.');
424
+ console.error(' Use \x1b[36mtouch ~/.metame/daemon.js\x1b[0m to hot-reload instead.\n');
425
+ process.exit(1);
426
+ }
427
+
333
428
  // Pre-deploy syntax validation: check all .js files before syncing to ~/.metame/
334
429
  // Catches bad merges and careless agent edits BEFORE they can crash the daemon.
335
430
  const { execSync: _execSync } = require('child_process');
@@ -1894,48 +1989,20 @@ if (isDaemon) {
1894
1989
  console.error(`${icon("fail")} launchd is macOS-only.`);
1895
1990
  process.exit(1);
1896
1991
  }
1897
- const plistDir = path.join(HOME_DIR, 'Library', 'LaunchAgents');
1898
- if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
1899
- const plistPath = path.join(plistDir, 'com.metame.daemon.plist');
1900
- const nodePath = process.execPath;
1901
- // Capture current PATH so launchd can find `claude` and other tools
1902
- const currentPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
1903
- const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
1904
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1905
- <plist version="1.0">
1906
- <dict>
1907
- <key>Label</key>
1908
- <string>com.metame.daemon</string>
1909
- <key>ProgramArguments</key>
1910
- <array>
1911
- <string>/usr/bin/caffeinate</string>
1912
- <string>-i</string>
1913
- <string>${nodePath}</string>
1914
- <string>${DAEMON_SCRIPT}</string>
1915
- </array>
1916
- <key>RunAtLoad</key>
1917
- <true/>
1918
- <key>KeepAlive</key>
1919
- <true/>
1920
- <key>StandardOutPath</key>
1921
- <string>${DAEMON_LOG}</string>
1922
- <key>StandardErrorPath</key>
1923
- <string>${DAEMON_LOG}</string>
1924
- <key>EnvironmentVariables</key>
1925
- <dict>
1926
- <key>METAME_ROOT</key>
1927
- <string>${__dirname}</string>
1928
- <key>PATH</key>
1929
- <string>${currentPath}</string>
1930
- <key>HOME</key>
1931
- <string>${HOME_DIR}</string>
1932
- </dict>
1933
- </dict>
1934
- </plist>`;
1935
- fs.writeFileSync(plistPath, plistContent, 'utf8');
1992
+ // Clean up legacy plist with different label if exists
1993
+ const legacyPlist = path.join(HOME_DIR, 'Library', 'LaunchAgents', 'com.metame.daemon.plist');
1994
+ if (fs.existsSync(legacyPlist)) {
1995
+ try { execSync(`launchctl bootout gui/$(id -u) ${legacyPlist} 2>/dev/null`); } catch { /* */ }
1996
+ try { fs.unlinkSync(legacyPlist); } catch { /* */ }
1997
+ }
1998
+ const plistPath = ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
1999
+ try {
2000
+ execSync(`launchctl bootstrap gui/$(id -u) ${plistPath} 2>/dev/null`);
2001
+ } catch { /* already bootstrapped */ }
1936
2002
  console.log(`${icon("ok")} launchd plist installed: ${plistPath}`);
1937
- console.log(" Load now: launchctl load " + plistPath);
1938
- console.log(" Unload: launchctl unload " + plistPath);
2003
+ console.log(" The daemon will auto-start at login and restart if it crashes.");
2004
+ console.log(" Start now: metame start");
2005
+ console.log(" Remove: launchctl bootout gui/$(id -u)/" + LAUNCHD_LABEL);
1939
2006
  process.exit(0);
1940
2007
  }
1941
2008
 
@@ -2036,7 +2103,61 @@ WantedBy=default.target
2036
2103
  }
2037
2104
 
2038
2105
  if (subCmd === 'start') {
2039
- // Kill any lingering daemon.js processes to avoid Feishu WebSocket conflicts
2106
+ if (!fs.existsSync(DAEMON_CONFIG)) {
2107
+ console.error(`${icon("fail")} No config found. Run: metame daemon init`);
2108
+ process.exit(1);
2109
+ }
2110
+ if (!fs.existsSync(DAEMON_SCRIPT)) {
2111
+ console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2112
+ process.exit(1);
2113
+ }
2114
+
2115
+ if (process.platform === 'darwin') {
2116
+ // macOS: delegate to launchd for auto-restart and boot persistence
2117
+ // Kill any orphan daemon processes NOT managed by launchd
2118
+ try {
2119
+ const pids = findProcessesByPattern('node.*daemon\\.js');
2120
+ const launchdPid = launchdIsRunning();
2121
+ for (const n of pids) {
2122
+ if (n !== launchdPid) {
2123
+ try { process.kill(n, 'SIGTERM'); } catch { /* */ }
2124
+ }
2125
+ }
2126
+ } catch { /* ignore */ }
2127
+ // Clean stale lock/pid from orphan processes
2128
+ if (fs.existsSync(DAEMON_LOCK)) {
2129
+ try {
2130
+ const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
2131
+ const pid = parseInt(lock && lock.pid, 10);
2132
+ if (pid) { process.kill(pid, 0); } // throws if dead
2133
+ } catch {
2134
+ // Owner is dead โ€” clean stale files
2135
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2136
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2137
+ }
2138
+ }
2139
+ ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
2140
+ try {
2141
+ execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
2142
+ } catch { /* already bootstrapped */ }
2143
+ // kickstart ensures the process is actually running now
2144
+ try {
2145
+ execSync(`launchctl kickstart gui/$(id -u)/${LAUNCHD_LABEL}`);
2146
+ } catch { /* already running */ }
2147
+ sleepSync(1500);
2148
+ const pid = launchdIsRunning();
2149
+ if (pid) {
2150
+ console.log(`${icon("ok")} MetaMe daemon started via launchd (PID: ${pid})`);
2151
+ } else {
2152
+ console.log(`${icon("ok")} MetaMe daemon starting via launchd...`);
2153
+ }
2154
+ console.log(" Auto-restart: enabled (KeepAlive)");
2155
+ console.log(" Logs: metame logs");
2156
+ console.log(" Stop: metame stop");
2157
+ process.exit(0);
2158
+ }
2159
+
2160
+ // Non-macOS: direct spawn (original behavior)
2040
2161
  try {
2041
2162
  const pids = findProcessesByPattern('node.*daemon\\.js');
2042
2163
  if (pids.length) {
@@ -2046,23 +2167,11 @@ WantedBy=default.target
2046
2167
  sleepSync(1000);
2047
2168
  }
2048
2169
  } catch { /* ignore */ }
2049
- // Check if already running
2050
2170
  if (fs.existsSync(DAEMON_PID)) {
2051
2171
  try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2052
2172
  }
2053
- if (!fs.existsSync(DAEMON_CONFIG)) {
2054
- console.error(`${icon("fail")} No config found. Run: metame daemon init`);
2055
- process.exit(1);
2056
- }
2057
- if (!fs.existsSync(DAEMON_SCRIPT)) {
2058
- console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2059
- process.exit(1);
2060
- }
2061
- // Use caffeinate on macOS to prevent sleep while daemon is running
2062
- const isMac = process.platform === 'darwin';
2063
- const cmd = isMac ? 'caffeinate' : process.execPath;
2064
- const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
2065
- const bg = spawn(cmd, args, {
2173
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2174
+ const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
2066
2175
  detached: true,
2067
2176
  stdio: 'ignore',
2068
2177
  windowsHide: true,
@@ -2076,6 +2185,30 @@ WantedBy=default.target
2076
2185
  }
2077
2186
 
2078
2187
  if (subCmd === 'stop') {
2188
+ if (process.platform === 'darwin') {
2189
+ // macOS: bootout the launchd job (stops process + prevents auto-restart)
2190
+ try {
2191
+ execSync(`launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
2192
+ } catch { /* not loaded */ }
2193
+ // Also kill any orphan daemon processes not managed by launchd
2194
+ try {
2195
+ const pids = findProcessesByPattern('node.*daemon\\.js');
2196
+ for (const n of pids) {
2197
+ try { process.kill(n, 'SIGTERM'); } catch { /* */ }
2198
+ }
2199
+ if (pids.length) sleepSync(2000);
2200
+ for (const n of pids) {
2201
+ try { process.kill(n, 'SIGKILL'); } catch { /* already gone */ }
2202
+ }
2203
+ } catch { /* */ }
2204
+ try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
2205
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
2206
+ console.log(`${icon("ok")} Daemon stopped. launchd auto-restart disabled.`);
2207
+ console.log(` To re-enable: metame start`);
2208
+ process.exit(0);
2209
+ }
2210
+
2211
+ // Non-macOS: original behavior
2079
2212
  if (!fs.existsSync(DAEMON_PID)) {
2080
2213
  console.log(`${icon("info")} No daemon running (no PID file).`);
2081
2214
  process.exit(0);
@@ -2083,7 +2216,6 @@ WantedBy=default.target
2083
2216
  const pid = parseInt(fs.readFileSync(DAEMON_PID, 'utf8').trim(), 10);
2084
2217
  try {
2085
2218
  process.kill(pid, 'SIGTERM');
2086
- // Wait for process to die (up to 3s), then force kill
2087
2219
  let dead = false;
2088
2220
  for (let i = 0; i < 6; i++) {
2089
2221
  sleepSync(500);
@@ -2097,6 +2229,7 @@ WantedBy=default.target
2097
2229
  console.log(`${icon("warn")} Process ${pid} not found (may have already exited).`);
2098
2230
  }
2099
2231
  try { fs.unlinkSync(DAEMON_PID); } catch { /* ignore */ }
2232
+ try { fs.unlinkSync(DAEMON_LOCK); } catch { /* ignore */ }
2100
2233
  process.exit(0);
2101
2234
  }
2102
2235
 
@@ -2109,6 +2242,30 @@ WantedBy=default.target
2109
2242
  console.error(`${icon("fail")} daemon.js not found. Reinstall MetaMe.`);
2110
2243
  process.exit(1);
2111
2244
  }
2245
+
2246
+ if (process.platform === 'darwin') {
2247
+ // macOS: use launchctl kickstart -k (kills + restarts in one atomic op)
2248
+ ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
2249
+ try {
2250
+ execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
2251
+ } catch { /* already bootstrapped */ }
2252
+ try {
2253
+ execSync(`launchctl kickstart -k gui/$(id -u)/${LAUNCHD_LABEL}`);
2254
+ sleepSync(1500);
2255
+ const pid = launchdIsRunning();
2256
+ if (pid) {
2257
+ console.log(`${icon("ok")} Daemon restarted via launchd (PID: ${pid})`);
2258
+ } else {
2259
+ console.log(`${icon("ok")} Daemon restart requested via launchd...`);
2260
+ }
2261
+ } catch (e) {
2262
+ console.error(`${icon("fail")} launchctl kickstart failed: ${e.message}`);
2263
+ process.exit(1);
2264
+ }
2265
+ process.exit(0);
2266
+ }
2267
+
2268
+ // Non-macOS: original SIGUSR2 / respawn logic
2112
2269
  const result = requestDaemonRestart({
2113
2270
  reason: 'cli-restart',
2114
2271
  daemonPidFile: DAEMON_PID,
@@ -2125,10 +2282,7 @@ WantedBy=default.target
2125
2282
  }
2126
2283
  if (result.status === 'not_running') {
2127
2284
  console.log(`${icon("info")} No daemon running. Starting a fresh daemon instead.`);
2128
- const isMac = process.platform === 'darwin';
2129
- const cmd = isMac ? 'caffeinate' : process.execPath;
2130
- const args = isMac ? ['-i', process.execPath, DAEMON_SCRIPT] : [DAEMON_SCRIPT];
2131
- const bg = spawn(cmd, args, {
2285
+ const bg = spawn(process.execPath, [DAEMON_SCRIPT], {
2132
2286
  detached: true,
2133
2287
  stdio: 'ignore',
2134
2288
  windowsHide: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.9",
3
+ "version": "1.5.11",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -11,16 +11,19 @@
11
11
  "scripts/",
12
12
  "!scripts/*.test.js",
13
13
  "!scripts/test_daemon.js",
14
- "!scripts/hooks/test-*.js"
14
+ "!scripts/hooks/test-*.js",
15
+ "!scripts/daemon.yaml",
16
+ "!scripts/daemon.yaml.bak"
15
17
  ],
16
18
  "scripts": {
17
19
  "test": "node --test scripts/*.test.js",
18
20
  "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
19
21
  "start": "node index.js",
20
22
  "push": "bash scripts/bin/push-clean.sh",
21
- "sync:plugin": "node -e \"const fs=require('fs'),path=require('path');const ex=new Set(['sync-readme.js','test_daemon.js']);const files=fs.readdirSync('scripts').filter(f=>!ex.has(f)&&!/\\.test\\.js$/.test(f)&&/\\.(js|yaml|sh)$/.test(f));files.forEach(f=>fs.copyFileSync('scripts/'+f,'plugin/scripts/'+f));\" && mkdir -p plugin/scripts/hooks && cp scripts/hooks/*.js plugin/scripts/hooks/ && echo 'Plugin scripts synced'",
23
+ "sync:plugin": "node -e \"const fs=require('fs'),path=require('path');const ex=new Set(['sync-readme.js','test_daemon.js','daemon.yaml']);const files=fs.readdirSync('scripts').filter(f=>!ex.has(f)&&!/\\.test\\.js$/.test(f)&&/\\.(js|yaml|sh)$/.test(f));files.forEach(f=>fs.copyFileSync('scripts/'+f,'plugin/scripts/'+f));\" && mkdir -p plugin/scripts/hooks && cp scripts/hooks/*.js plugin/scripts/hooks/ && echo 'Plugin scripts synced'",
22
24
  "sync:readme": "node scripts/sync-readme.js",
23
25
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '้ˆฟ็‹…็ฌ Daemon not running or restart failed'",
26
+ "prepublishOnly": "node -e \"const fs=require('fs');['scripts/daemon.yaml','plugin/scripts/daemon.yaml'].forEach(f=>{if(fs.existsSync(f)){const c=fs.readFileSync(f,'utf8');if(/bot_token:\\s*[^n]|app_secret:\\s*[^n]|enabled:\\s*true/.test(c)){console.error('ABORT: Real credentials found in '+f);process.exit(1)}}})\"",
24
27
  "precommit": "npm run sync:plugin && npm run restart:daemon"
25
28
  },
26
29
  "keywords": [
@@ -289,6 +289,40 @@ function createAdminCommandHandler(deps) {
289
289
  const state = ctx.state || {};
290
290
  let config = ctx.config || {};
291
291
 
292
+ if (text === '/status perpetual' || text === '/status reactive') {
293
+ const { replayEventLog } = require('./daemon-reactive-lifecycle');
294
+ const projects = config.projects || {};
295
+ const lines = ['**Perpetual Projects**\n'];
296
+ let found = false;
297
+
298
+ for (const [key, proj] of Object.entries(projects)) {
299
+ if (!proj.reactive) continue;
300
+ found = true;
301
+
302
+ const rs = (state.reactive && state.reactive[key]) || {};
303
+ const { phase, mission } = replayEventLog(key, { log: () => {} });
304
+
305
+ const icon = proj.icon || '๐Ÿ”„';
306
+ const name = proj.name || key;
307
+ const status = rs.status || 'idle';
308
+ const depth = rs.depth || 0;
309
+ const maxDepth = rs.max_depth || 50;
310
+ const lastSignal = rs.last_signal || '-';
311
+ const updatedAt = rs.updated_at ? new Date(rs.updated_at).toLocaleString() : '-';
312
+
313
+ lines.push(`${icon} **${name}** (\`${key}\`)`);
314
+ lines.push(` Status: ${status} | Phase: ${phase || '-'} | Depth: ${depth}/${maxDepth}`);
315
+ if (mission) lines.push(` Mission: ${mission.title}`);
316
+ lines.push(` Last signal: ${lastSignal} | Updated: ${updatedAt}`);
317
+ lines.push('');
318
+ }
319
+
320
+ if (!found) lines.push('No reactive projects configured.');
321
+
322
+ await bot.sendMessage(chatId, lines.join('\n'));
323
+ return { handled: true, config };
324
+ }
325
+
292
326
  if (text === '/status') {
293
327
  const session = getSession(chatId);
294
328
  let msg = `MetaMe Daemon\nStatus: Running\nStarted: ${state.started_at || 'unknown'}\n`;
@@ -652,13 +652,30 @@ function createBridgeStarter(deps) {
652
652
  async function startFeishuBridge(config, executeTaskByName) {
653
653
  if (!config.feishu || !config.feishu.enabled) return null;
654
654
  if (!config.feishu.app_id || !config.feishu.app_secret) {
655
- log('WARN', 'Feishu enabled but app_id/app_secret missing');
655
+ log('ERROR', 'Feishu enabled but app_id/app_secret missing โ€” bridge will NOT start. Check ~/.metame/daemon.yaml');
656
656
  return null;
657
657
  }
658
658
 
659
659
  const { createBot } = require('./feishu-adapter.js');
660
660
  const bot = createBot(config.feishu);
661
661
 
662
+ // Validate credentials before starting WebSocket โ€” fail loud, not silent
663
+ try {
664
+ const validation = await bot.validateCredentials();
665
+ if (!validation.ok) {
666
+ log('ERROR', `Feishu credential check FAILED: ${validation.error}`);
667
+ if (validation.isAuthError) {
668
+ log('ERROR', 'Feishu bridge will NOT start โ€” fix app_id/app_secret in ~/.metame/daemon.yaml and restart daemon');
669
+ return null;
670
+ }
671
+ log('WARN', 'Feishu credential check failed (possibly network issue) โ€” attempting to start anyway');
672
+ } else {
673
+ log('INFO', 'Feishu credentials validated OK');
674
+ }
675
+ } catch (e) {
676
+ log('WARN', `Feishu credential pre-check error: ${e.message} โ€” attempting to start anyway`);
677
+ }
678
+
662
679
  try {
663
680
  const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
664
681
  const liveCfg = loadConfig();
@@ -1605,6 +1605,33 @@ function createClaudeEngine(deps) {
1605
1605
  }
1606
1606
  }
1607
1607
 
1608
+ // Inject latest nightly insight (decisions/lessons) โ€” one-liner per file, ~100 tokens
1609
+ if (!session.started) {
1610
+ try {
1611
+ const reflectDirs = [
1612
+ path.join(HOME, '.metame', 'memory', 'decisions'),
1613
+ path.join(HOME, '.metame', 'memory', 'lessons'),
1614
+ ];
1615
+ const reflectItems = [];
1616
+ for (const dir of reflectDirs) {
1617
+ if (!fs.existsSync(dir)) continue;
1618
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort();
1619
+ const latest = files[files.length - 1];
1620
+ if (!latest) continue;
1621
+ const content = fs.readFileSync(path.join(dir, latest), 'utf8');
1622
+ // Extract ## headings as one-line summaries (skip frontmatter)
1623
+ const headings = content.match(/^## .+$/gm);
1624
+ if (headings && headings.length > 0) {
1625
+ const type = dir.endsWith('decisions') ? 'decision' : 'lesson';
1626
+ reflectItems.push(...headings.slice(0, 2).map(h => `- [${type}] ${h.replace(/^## /, '')}`));
1627
+ }
1628
+ }
1629
+ if (reflectItems.length > 0) {
1630
+ memoryHint += `\n\n[Recent insights:\n${reflectItems.join('\n')}]`;
1631
+ }
1632
+ } catch { /* non-critical */ }
1633
+ }
1634
+
1608
1635
  memory.close();
1609
1636
  } catch (e) {
1610
1637
  if (e.code !== 'MODULE_NOT_FOUND') log('WARN', `Memory injection failed: ${e.message}`);
@@ -1636,6 +1663,19 @@ function createClaudeEngine(deps) {
1636
1663
  } catch { /* ignore */ }
1637
1664
  }
1638
1665
 
1666
+ // Self-reflection patterns: behavioral guardrails distilled from past mistakes
1667
+ let reflectHint = '';
1668
+ if (!session.started && brainDoc) {
1669
+ try {
1670
+ const patterns = (brainDoc.growth && Array.isArray(brainDoc.growth.self_reflection_patterns))
1671
+ ? brainDoc.growth.self_reflection_patterns.filter(p => p && p.summary).slice(0, 3)
1672
+ : [];
1673
+ if (patterns.length > 0) {
1674
+ reflectHint = `\n- Self-correction patterns (avoid repeating these mistakes):\n${patterns.map(p => ` - ${String(p.summary).slice(0, 150)}`).join('\n')}`;
1675
+ }
1676
+ } catch { /* non-critical */ }
1677
+ }
1678
+
1639
1679
  // Inject daemon hints only on first message of a session
1640
1680
  // Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
1641
1681
  let daemonHint = '';
@@ -1654,7 +1694,7 @@ ${mentorRadarHint}
1654
1694
  Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
1655
1695
  daemonHint = `\n\n[System hints - DO NOT mention these to user:
1656
1696
  1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
1657
- 2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${taskRules}]`;
1697
+ 2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${reflectHint}${taskRules}]`;
1658
1698
  }
1659
1699
 
1660
1700
  daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
@@ -45,6 +45,7 @@ heartbeat:
45
45
  type: script
46
46
  command: node ~/.metame/distill.js
47
47
  interval: 4h
48
+ timeout: 5m
48
49
  precondition: "test -s ~/.metame/raw_signals.jsonl"
49
50
  require_idle: true
50
51
  notify: false
@@ -74,6 +75,7 @@ heartbeat:
74
75
  type: script
75
76
  command: node ~/.metame/skill-evolution.js
76
77
  interval: 12h
78
+ timeout: 5m
77
79
  precondition: "test -s ~/.metame/skill_signals.jsonl"
78
80
  require_idle: true
79
81
  notify: false
@@ -104,7 +106,7 @@ heartbeat:
104
106
  at: "01:30"
105
107
  require_idle: true
106
108
  notify: false
107
- enabled: true
109
+ enabled: false
108
110
 
109
111
  # Legacy flat tasks (no project isolation). New tasks should go under projects: above.
110
112
  # Examples โ€” uncomment or add your own: