smash-os-install 0.4.3 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/install.mjs +252 -131
  2. package/package.json +64 -58
  3. package/templates.mjs +3954 -1028
package/install.mjs CHANGED
@@ -10,13 +10,13 @@
10
10
  *
11
11
  * What it does:
12
12
  * 1. Prompts for project name and tech stack
13
- * 2. Writes CLAUDE.md + /ai/ skeleton + .smash-os-mode=local
13
+ * 2. Writes CLAUDE.md + /.smashOS/ skeleton + .smash-os-mode=local
14
14
  * 3. Installs SmashOS skills globally (~/.claude/skills/)
15
15
  * 4. No web app, no API keys, no cloud dependencies required
16
16
  */
17
17
 
18
18
  import { execSync } from 'child_process';
19
- import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
19
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'fs';
20
20
  import { join, dirname, basename } from 'path';
21
21
  import { fileURLToPath } from 'url';
22
22
  import { homedir } from 'os';
@@ -25,14 +25,14 @@ import chalk from 'chalk';
25
25
  import {
26
26
  getClaudeMd, getAiFiles, getFrontendFiles,
27
27
  agentFiles, settingsJson, bootHook, syncHook,
28
- localSkills, localSkillExtras,
28
+ localSkills, localSkillExtras, osDocsFiles,
29
29
  } from './templates.mjs';
30
30
 
31
31
  const cwd = process.cwd();
32
32
  const isUpgrade = process.argv.includes('--upgrade');
33
33
  const isVerify = process.argv.includes('--verify');
34
34
  const isUninstall = process.argv.includes('--uninstall');
35
- const SMASH_VERSION = '0.4.3';
35
+ const SMASH_VERSION = '0.4.6';
36
36
  const vaultConventions = join(process.env.USERPROFILE || process.env.HOME || homedir(), 'Desktop', 'SmashBurgerBar', 'SmashVault', 'Architecture', 'conventions.md');
37
37
  const globalConventions = join(homedir(), '.claude', 'conventions.md');
38
38
  const conventionsFile = existsSync(vaultConventions) ? vaultConventions : globalConventions;
@@ -226,7 +226,10 @@ function findClaude() {
226
226
 
227
227
  function loadRegistry() {
228
228
  if (!existsSync(REGISTRY_FILE)) return [];
229
- try { return JSON.parse(readFileSync(REGISTRY_FILE, 'utf8')); } catch { return []; }
229
+ try {
230
+ const raw = JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
231
+ return raw.filter(p => p && p.path && !p.path.includes('..'));
232
+ } catch { return []; }
230
233
  }
231
234
 
232
235
  function saveRegistry(projects) {
@@ -235,7 +238,7 @@ function saveRegistry(projects) {
235
238
  }
236
239
 
237
240
  function schtask(name, batPath, schedule) {
238
- const user = process.env.USERNAME || process.env.USER || 'Administrator';
241
+ const user = (process.env.USERNAME || process.env.USER || 'Administrator').replace(/[^a-zA-Z0-9._\-]/g, '_');
239
242
  try {
240
243
  autoRun(`schtasks /create /tn "SmashOS\\${name}" /tr "${batPath}" ${schedule} /f /ru "${user}"`);
241
244
  console.log(' ' + chalk.green('✓') + ' SmashOS\\' + name);
@@ -246,7 +249,7 @@ function schtask(name, batPath, schedule) {
246
249
 
247
250
  function buildBats(projects, claudeExe) {
248
251
  const base = `"${claudeExe}" --dangerously-skip-permissions --print`;
249
- const home = process.env.USERPROFILE || process.env.HOME || 'C:\\Users\\Administrator';
252
+ const home = process.env.USERPROFILE || process.env.HOME || homedir();
250
253
 
251
254
  // Scoped tool allowlists per task type — never grant unrestricted access
252
255
  const SCOPE = {
@@ -257,25 +260,29 @@ function buildBats(projects, claudeExe) {
257
260
  const C_MED = `${base} --max-turns 50 ${SCOPE.medium}`;
258
261
  const hdr = `@echo off\nset CLAUDE=${base} --max-turns 20 ${SCOPE.light}\n`;
259
262
 
263
+ const safeN = n => n.replace(/[^a-zA-Z0-9._-]/g, '_');
264
+ const safePath = p => p.replace(/"/g, '');
260
265
  function block(p, prompt, slug, cmd = C) {
261
266
  const log = join(SMASH_BASE, p.name, 'logs', `${slug}.log`);
262
- return `\n:: === ${p.name} ===\ncd /d "${p.path}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${cmd} "${prompt}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
267
+ return `\n:: === ${safeN(p.name)} ===\ncd /d "${safePath(p.path)}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${cmd} "${prompt}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
263
268
  }
264
269
  function blockCmd(p, cmd, slug, exe = C) {
265
270
  const log = join(SMASH_BASE, p.name, 'logs', `${slug}.log`);
266
- return `\n:: === ${p.name} ===\ncd /d "${p.path}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${exe} "${cmd}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
271
+ return `\n:: === ${safeN(p.name)} ===\ncd /d "${safePath(p.path)}"\necho [%date% %time%] Running ${slug}... >> "${log}"\n${exe} "${cmd}" >> "${log}" 2>&1\necho [%date% %time%] Done. >> "${log}"`;
267
272
  }
268
273
  function bat(slug, getBlock) { return hdr + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
269
- function batMed(slug, getBlock) { return `@echo off\nset CLAUDE=${base} ${SCOPE.medium}\n` + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
274
+ function batMed(slug, getBlock) { return `@echo off\nset CLAUDE=${base} --max-turns 50 ${SCOPE.medium}\n` + '\n' + projects.map(p => getBlock(p)).join('\n') + '\n'; }
270
275
 
271
276
  return [
272
277
  { taskName: 'Projects\\LockCleanup', batFile: join(SCRIPTS_DIR, 'lock-cleanup.bat'), schedule: '/sc hourly /st 00:00', content: bat('lock-cleanup', p => block(p, 'Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', 'lock-cleanup')) },
273
- { taskName: 'Projects\\MemoryConsolidation', batFile: join(SCRIPTS_DIR, 'memory-consolidation.bat'), schedule: '/sc daily /st 01:00', content: bat('memory-consolidation', p => block(p, 'Read all files in ai/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', 'memory-consolidation')) },
278
+ { taskName: 'Projects\\MemoryConsolidation', batFile: join(SCRIPTS_DIR, 'memory-consolidation.bat'), schedule: '/sc daily /st 01:00', content: bat('memory-consolidation', p => block(p, 'Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', 'memory-consolidation')) },
274
279
  { taskName: 'Projects\\NightlyAudit', batFile: join(SCRIPTS_DIR, 'nightly-audit.bat'), schedule: '/sc daily /st 02:00', content: batMed('nightly-audit', p => blockCmd(p, '/smash-os:audit', 'nightly-audit', C_MED)) },
275
280
  { taskName: '_skills\\DreamMemory', batFile: join(SKILL_SCRIPTS, 'dream-memory.bat'), schedule: '/sc daily /st 04:00', content: `@echo off\ncd /d "${home}"\necho [%date% %time%] Running dream-memory... >> "${join(SKILL_LOGS, 'dream-memory.log')}"\n"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools Read,Write,Glob,Grep "/dream-memory" >> "${join(SKILL_LOGS, 'dream-memory.log')}" 2>&1\necho [%date% %time%] Done. >> "${join(SKILL_LOGS, 'dream-memory.log')}"\n` },
276
281
  { taskName: 'Projects\\WeeklyImprovement', batFile: join(SCRIPTS_DIR, 'weekly-improvement.bat'), schedule: '/sc weekly /d MON /st 05:00', content: batMed('weekly-improvement', p => blockCmd(p, '/smash-os:run weekly-improvement', 'weekly-improvement', C_MED)) },
277
282
  { taskName: '_skills\\SkillEvolution', batFile: join(SKILL_SCRIPTS, 'skill-evolution.bat'), schedule: '/sc weekly /d MON /st 06:00', content: `@echo off\ncd /d "${home}"\necho [%date% %time%] Running skill evolution... >> "${join(SKILL_LOGS, 'skill-evolution.log')}"\n"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools Read,Write,Glob,Grep "/skill-evolution" >> "${join(SKILL_LOGS, 'skill-evolution.log')}" 2>&1\necho [%date% %time%] Done. >> "${join(SKILL_LOGS, 'skill-evolution.log')}"\n` },
278
283
  { taskName: 'Projects\\RoleEvolution', batFile: join(SCRIPTS_DIR, 'role-evolution.bat'), schedule: '/sc weekly /d MON /st 06:30', content: batMed('role-evolution', p => blockCmd(p, '/smash-os:role-improve', 'role-evolution', C_MED)) },
284
+ { taskName: 'Projects\\HarnessEvolution', batFile: join(SCRIPTS_DIR, 'harness-evolution.bat'), schedule: '/sc weekly /d MON /st 07:00', content: batMed('harness-evolution', p => blockCmd(p, '/smash-os:evolve', 'harness-evolution', C_MED)) },
285
+ { taskName: 'Projects\\DailyLogReport', batFile: join(SCRIPTS_DIR, 'daily-log-report.bat'), schedule: '/sc daily /st 03:00', content: bat('daily-log-report', p => block(p, `Scan all .log files in ${join(SMASH_BASE, p.name, 'logs')}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.`, 'daily-log-report')) },
279
286
  ];
280
287
  }
281
288
 
@@ -299,7 +306,7 @@ function setupWindowsTaskScheduler(projects, claudeExe, projectName) {
299
306
  for (const b of bats) schtask(b.taskName, b.batFile, b.schedule);
300
307
 
301
308
  console.log('');
302
- console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution'));
309
+ console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution'));
303
310
  console.log(chalk.dim(` Logs: ${join(SMASH_BASE, projectName, 'logs')}`));
304
311
  }
305
312
 
@@ -319,8 +326,9 @@ function setupMacOSLaunchAgents(projects, claudeExe, projectName) {
319
326
  medium: 'Read,Write,Edit,Bash,Glob,Grep',
320
327
  };
321
328
 
329
+ const xmlEsc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
322
330
  function plist(label, args, workDir, logFile, calendarInterval) {
323
- const argXml = args.map(a => `\t\t<string>${a}</string>`).join('\n');
331
+ const argXml = args.map(a => `\t\t<string>${xmlEsc(a)}</string>`).join('\n');
324
332
  const intervalXml = Object.entries(calendarInterval)
325
333
  .map(([k, v]) => `\t\t<key>${k}</key>\n\t\t<integer>${v}</integer>`)
326
334
  .join('\n');
@@ -335,92 +343,77 @@ function setupMacOSLaunchAgents(projects, claudeExe, projectName) {
335
343
  ${argXml}
336
344
  \t</array>
337
345
  \t<key>WorkingDirectory</key>
338
- \t<string>${workDir}</string>
346
+ \t<string>${xmlEsc(workDir)}</string>
339
347
  \t<key>StartCalendarInterval</key>
340
348
  \t<dict>
341
349
  ${intervalXml}
342
350
  \t</dict>
343
351
  \t<key>StandardOutPath</key>
344
- \t<string>${logFile}</string>
352
+ \t<string>${xmlEsc(logFile)}</string>
345
353
  \t<key>StandardErrorPath</key>
346
- \t<string>${logFile}</string>
354
+ \t<string>${xmlEsc(logFile)}</string>
347
355
  \t<key>RunAtLoad</key>
348
356
  \t<false/>
349
357
  </dict>
350
358
  </plist>`;
351
359
  }
352
360
 
353
- function projectArgs(prompt, tools, turns = 20) {
361
+ function paArgs(prompt, tools, turns = 20) {
354
362
  return [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', String(turns), '--allowedTools', tools, prompt];
355
363
  }
364
+ const safePName = n => n.replace(/[^a-zA-Z0-9._-]/g, '_');
356
365
 
357
- const tasks = [
358
- {
359
- label: 'com.smash-os.lock-cleanup',
360
- args: projectArgs('Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
361
- workDir: projects[0]?.path || homedir(),
362
- logFile: join(SMASH_BASE, projectName, 'logs', 'lock-cleanup.log'),
363
- interval: { Hour: 0, Minute: 0 },
364
- },
365
- {
366
- label: 'com.smash-os.memory-consolidation',
367
- args: projectArgs('Read all files in ai/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
368
- workDir: projects[0]?.path || homedir(),
369
- logFile: join(SMASH_BASE, projectName, 'logs', 'memory-consolidation.log'),
370
- interval: { Hour: 1, Minute: 0 },
371
- },
372
- {
373
- label: 'com.smash-os.nightly-audit',
374
- args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:audit'],
375
- workDir: projects[0]?.path || homedir(),
376
- logFile: join(SMASH_BASE, projectName, 'logs', 'nightly-audit.log'),
377
- interval: { Hour: 2, Minute: 0 },
378
- },
379
- {
380
- label: 'com.smash-os.dream-memory',
381
- args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/dream-memory'],
382
- workDir: homedir(),
383
- logFile: join(SKILL_LOGS, 'dream-memory.log'),
384
- interval: { Hour: 4, Minute: 0 },
385
- },
386
- {
387
- label: 'com.smash-os.weekly-improvement',
388
- args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:run weekly-improvement'],
389
- workDir: projects[0]?.path || homedir(),
390
- logFile: join(SMASH_BASE, projectName, 'logs', 'weekly-improvement.log'),
391
- interval: { Weekday: 1, Hour: 5, Minute: 0 },
392
- },
393
- {
394
- label: 'com.smash-os.skill-evolution',
395
- args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/skill-evolution'],
396
- workDir: homedir(),
397
- logFile: join(SKILL_LOGS, 'skill-evolution.log'),
398
- interval: { Weekday: 1, Hour: 6, Minute: 0 },
399
- },
400
- {
401
- label: 'com.smash-os.role-evolution',
402
- args: [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:role-improve'],
403
- workDir: projects[0]?.path || homedir(),
404
- logFile: join(SMASH_BASE, projectName, 'logs', 'role-evolution.log'),
405
- interval: { Weekday: 1, Hour: 6, Minute: 30 },
406
- },
407
- ];
408
-
409
- let written = 0;
410
- for (const t of tasks) {
411
- const plistPath = join(launchAgentsDir, `${t.label}.plist`);
412
- writeFileSync(plistPath, plist(t.label, t.args, t.workDir, t.logFile, t.interval), 'utf8');
366
+ function writePlist(label, args, workDir, logFile, interval) {
367
+ const plistPath = join(launchAgentsDir, `${label}.plist`);
368
+ writeFileSync(plistPath, plist(label, args, workDir, logFile, interval), 'utf8');
413
369
  try {
414
370
  autoRun(`launchctl load "${plistPath}"`);
415
- console.log(' ' + chalk.green('✓') + ' ' + t.label);
371
+ console.log(' ' + chalk.green('✓') + ' ' + label);
416
372
  } catch {
417
- console.log(' ' + chalk.yellow('⚠') + ' ' + t.label + chalk.dim(' — written but not loaded (run launchctl load manually)'));
373
+ console.log(' ' + chalk.yellow('⚠') + ' ' + label + chalk.dim(' — written but not loaded (run launchctl load manually)'));
418
374
  }
419
- written++;
375
+ }
376
+
377
+ // Skill tasks — not project-specific, always run from homedir (one plist each)
378
+ writePlist('com.smash-os.dream-memory',
379
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/dream-memory'],
380
+ homedir(), join(SKILL_LOGS, 'dream-memory.log'), { Hour: 4, Minute: 0 });
381
+ writePlist('com.smash-os.skill-evolution',
382
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '20', '--allowedTools', scope.light, '/skill-evolution'],
383
+ homedir(), join(SKILL_LOGS, 'skill-evolution.log'), { Weekday: 1, Hour: 6, Minute: 0 });
384
+
385
+ // Project tasks — one plist per (task, project) so all registered projects get automation
386
+ let written = 2; // skill tasks already written above
387
+ for (const p of projects) {
388
+ const pn = safePName(p.name);
389
+ const pLogDir = join(SMASH_BASE, p.name, 'logs');
390
+ mkdirSync(pLogDir, { recursive: true });
391
+ writePlist(`com.smash-os.lock-cleanup.${pn}`,
392
+ paArgs('Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours and report what was cleaned. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
393
+ p.path, join(pLogDir, 'lock-cleanup.log'), { Hour: 0, Minute: 0 });
394
+ writePlist(`com.smash-os.memory-consolidation.${pn}`,
395
+ paArgs('Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days into a single summary entry. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.', scope.light),
396
+ p.path, join(pLogDir, 'memory-consolidation.log'), { Hour: 1, Minute: 0 });
397
+ writePlist(`com.smash-os.nightly-audit.${pn}`,
398
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:audit'],
399
+ p.path, join(pLogDir, 'nightly-audit.log'), { Hour: 2, Minute: 0 });
400
+ writePlist(`com.smash-os.daily-log-report.${pn}`,
401
+ paArgs(`Scan all .log files in ${pLogDir}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning.`, scope.light),
402
+ p.path, join(pLogDir, 'daily-log-report.log'), { Hour: 3, Minute: 0 });
403
+ writePlist(`com.smash-os.weekly-improvement.${pn}`,
404
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:run weekly-improvement'],
405
+ p.path, join(pLogDir, 'weekly-improvement.log'), { Weekday: 1, Hour: 5, Minute: 0 });
406
+ writePlist(`com.smash-os.role-evolution.${pn}`,
407
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:role-improve'],
408
+ p.path, join(pLogDir, 'role-evolution.log'), { Weekday: 1, Hour: 6, Minute: 30 });
409
+ writePlist(`com.smash-os.harness-evolution.${pn}`,
410
+ [claudeExe, '--dangerously-skip-permissions', '--print', '--max-turns', '50', '--allowedTools', scope.medium, '/smash-os:evolve'],
411
+ p.path, join(pLogDir, 'harness-evolution.log'), { Weekday: 1, Hour: 7, Minute: 0 });
412
+ written += 7;
420
413
  }
421
414
 
422
415
  console.log('');
423
- console.log(chalk.dim(` ${written} LaunchAgents installed (hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution)`));
416
+ console.log(chalk.dim(` ${written} LaunchAgents installed (hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution)`));
424
417
  console.log(chalk.dim(` Logs: ${join(SMASH_BASE, projectName, 'logs')}`));
425
418
  }
426
419
 
@@ -438,31 +431,52 @@ function setupLinuxCron(projects, claudeExe, projectName) {
438
431
  medium: 'Read,Write,Edit,Bash,Glob,Grep',
439
432
  };
440
433
 
441
- const projectDir = projects[0]?.path || homedir();
442
- const logDir = join(SMASH_BASE, projectName, 'logs');
443
- const C = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools ${scope.light}`;
444
- const C_MED = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 50 --allowedTools ${scope.medium}`;
445
-
446
- const entries = [
447
- `0 * * * * cd "${projectDir}" && ${C} "Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(logDir, 'lock-cleanup.log')}" 2>&1`,
448
- `0 1 * * * cd "${projectDir}" && ${C} "Read all files in ai/memory/. Consolidate duplicates and summarise entries older than 30 days. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(logDir, 'memory-consolidation.log')}" 2>&1`,
449
- `0 2 * * * cd "${projectDir}" && ${C_MED} "/smash-os:audit" >> "${join(logDir, 'nightly-audit.log')}" 2>&1`,
434
+ const primaryLogDir = join(SMASH_BASE, projectName, 'logs');
435
+ const C = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 20 --allowedTools ${scope.light}`;
436
+ const C_MED = `"${claudeExe}" --dangerously-skip-permissions --print --max-turns 50 --allowedTools ${scope.medium}`;
437
+
438
+ // Build per-project entries (one set per registered project)
439
+ const entries = [];
440
+ for (const p of projects) {
441
+ const pDir = p.path;
442
+ const pLogDir = join(SMASH_BASE, p.name, 'logs');
443
+ mkdirSync(pLogDir, { recursive: true });
444
+ entries.push(
445
+ `0 * * * * cd "${pDir}" && ${C} "Review .claude/scheduled_tasks.lock and any stale pipeline lock files. Delete any locks older than 2 hours. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'lock-cleanup.log')}" 2>&1`,
446
+ `0 1 * * * cd "${pDir}" && ${C} "Read all files in .smashOS/memory/. Consolidate duplicates and summarise entries older than 30 days. Save the updated files. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'memory-consolidation.log')}" 2>&1`,
447
+ `0 2 * * * cd "${pDir}" && ${C_MED} "/smash-os:audit" >> "${join(pLogDir, 'nightly-audit.log')}" 2>&1`,
448
+ `0 5 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:run weekly-improvement" >> "${join(pLogDir, 'weekly-improvement.log')}" 2>&1`,
449
+ `30 6 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:role-improve" >> "${join(pLogDir, 'role-evolution.log')}" 2>&1`,
450
+ `0 7 * * 1 cd "${pDir}" && ${C_MED} "/smash-os:evolve" >> "${join(pLogDir, 'harness-evolution.log')}" 2>&1`,
451
+ `0 3 * * * cd "${pDir}" && ${C} "Scan all .log files in ${pLogDir}. For each log file identify errors, warnings, task failures, and notable events. Append a new dated section to .smashOS/todo/todo.md with: (1) status row per scheduled task showing ran/failed/skipped, (2) errors or issues requiring attention with context, (3) concrete next-step suggestions. Create .smashOS/todo/todo.md if it does not exist. Act autonomously. Do not ask for clarification. If uncertain, use best judgment and log your reasoning." >> "${join(pLogDir, 'daily-log-report.log')}" 2>&1`,
452
+ );
453
+ }
454
+ // Skill tasks (not project-specific — always run from homedir)
455
+ entries.push(
450
456
  `0 4 * * * cd "${homedir()}" && ${C} "/dream-memory" >> "${join(SKILL_LOGS, 'dream-memory.log')}" 2>&1`,
451
- `0 5 * * 1 cd "${projectDir}" && ${C_MED} "/smash-os:run weekly-improvement" >> "${join(logDir, 'weekly-improvement.log')}" 2>&1`,
452
457
  `0 6 * * 1 cd "${homedir()}" && ${C} "/skill-evolution" >> "${join(SKILL_LOGS, 'skill-evolution.log')}" 2>&1`,
453
- `30 6 * * 1 cd "${projectDir}" && ${C_MED} "/smash-os:role-improve" >> "${join(logDir, 'role-evolution.log')}" 2>&1`,
454
- ];
458
+ );
455
459
 
456
460
  const smashMarker = '# SmashOS — managed by smash-os-install';
457
461
  let currentCrontab = '';
458
462
  try { currentCrontab = autoRun('crontab -l'); } catch { /* no existing crontab */ }
459
463
 
460
- // Strip existing SmashOS block
464
+ // Strip existing SmashOS block (find end dynamically to handle variable entry counts)
461
465
  const lines = currentCrontab.split('\n');
462
466
  const startIdx = lines.findIndex(l => l === smashMarker);
463
- const filteredLines = startIdx >= 0
464
- ? lines.filter((_, i) => i < startIdx || i > startIdx + entries.length)
465
- : lines;
467
+ let filteredLines;
468
+ if (startIdx >= 0) {
469
+ let endIdx = lines.length;
470
+ for (let i = startIdx + 1; i < lines.length; i++) {
471
+ if (lines[i].trim() === '' || (!lines[i].startsWith('#') && !lines[i].includes('smash-os') && lines[i].trim() !== '')) {
472
+ endIdx = i - 1;
473
+ break;
474
+ }
475
+ }
476
+ filteredLines = lines.filter((_, i) => i < startIdx || i > endIdx);
477
+ } else {
478
+ filteredLines = lines;
479
+ }
466
480
 
467
481
  const newCrontab = [...filteredLines.filter(l => l !== ''), '', smashMarker, ...entries, ''].join('\n');
468
482
 
@@ -476,8 +490,8 @@ function setupLinuxCron(projects, claudeExe, projectName) {
476
490
  }
477
491
 
478
492
  console.log('');
479
- console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution'));
480
- console.log(chalk.dim(` Logs: ${logDir}`));
493
+ console.log(chalk.dim(' Schedule: hourly lock-cleanup · 1am memory · 2am audit · 3am log-report · 4am dream-memory · Mon 5am improvement · Mon 6am skill-evolution · Mon 6:30am role-evolution · Mon 7am harness-evolution'));
494
+ console.log(chalk.dim(` Logs: ${primaryLogDir}`));
481
495
  }
482
496
 
483
497
  // ─── Platform-agnostic automation dispatcher ──────────────────────────────────
@@ -542,12 +556,15 @@ async function runUpgrade() {
542
556
  const { toUpdate } = await prompts({
543
557
  type: 'multiselect',
544
558
  name: 'toUpdate',
545
- message: 'What to update? (ai/memory/ and ai/context/ are never touched)',
559
+ message: 'What to update? (.smashOS/memory/ and .smashOS/context/ are never touched)',
546
560
  choices: [
547
561
  { title: 'Agents (.claude/agents/) — sub-agent definitions', value: 'agents', selected: true },
548
562
  { title: 'Hooks (.claude/hooks/) — boot + sync hooks', value: 'hooks', selected: true },
549
563
  { title: 'Skills (~/.claude/skills/) — slash commands', value: 'skills', selected: true },
550
- { title: 'Roles (ai/roles/) role definitions', value: 'roles', selected: false },
564
+ { title: 'Docs (.smashOS/docs/) SmashOS documentation', value: 'docs', selected: true },
565
+ { title: 'Workflows (.smashOS/workflows/) — pipeline definitions', value: 'workflows', selected: true },
566
+ { title: 'Roles (.smashOS/roles/) — role definitions', value: 'roles', selected: false },
567
+ { title: 'CLAUDE.md — patch missing sections (non-destructive)', value: 'claudemd', selected: true },
551
568
  ],
552
569
  hint: '← Space to toggle, Enter to confirm',
553
570
  }, { onCancel: () => { console.log(chalk.yellow('\n Upgrade cancelled.')); process.exit(0); } });
@@ -592,17 +609,73 @@ async function runUpgrade() {
592
609
  }
593
610
  }
594
611
 
612
+ if (toUpdate.includes('docs')) {
613
+ console.log(chalk.dim(' Updating docs...'));
614
+ for (const [relPath, content] of Object.entries(osDocsFiles)) {
615
+ writeFile(relPath, content);
616
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
617
+ }
618
+ }
619
+
620
+ // Detect frontend project from installed files (avoids hardcoding isFrontend=false)
621
+ const isFrontend = existsSync(join(cwd, '.smashOS', 'roles', 'frontend-developer.md'));
622
+
623
+ if (toUpdate.includes('workflows')) {
624
+ console.log(chalk.dim(' Updating workflows...'));
625
+ const workflowFiles = getAiFiles(basename(cwd), 'TypeScript', isFrontend, '', '', '');
626
+ for (const [relPath, content] of Object.entries(workflowFiles)) {
627
+ if (relPath.startsWith('.smashOS/workflows/')) {
628
+ writeFile(relPath, content);
629
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
630
+ }
631
+ }
632
+ }
633
+
595
634
  if (toUpdate.includes('roles')) {
596
635
  console.log(chalk.dim(' Updating AI roles...'));
597
- const roleFiles = getAiFiles(basename(cwd), 'TypeScript', false, '', '', '');
636
+ const roleFiles = getAiFiles(basename(cwd), 'TypeScript', isFrontend, '', '', '');
598
637
  for (const [relPath, content] of Object.entries(roleFiles)) {
599
- if (relPath.startsWith('ai/roles/')) {
638
+ if (relPath.startsWith('.smashOS/roles/')) {
600
639
  writeFile(relPath, content);
601
640
  console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
602
641
  }
603
642
  }
604
643
  }
605
644
 
645
+ if (toUpdate.includes('claudemd')) {
646
+ const claudeMdPath = join(cwd, 'CLAUDE.md');
647
+ if (existsSync(claudeMdPath)) {
648
+ const existing = readFileSync(claudeMdPath, 'utf8');
649
+ if (!existing.includes('## Agent Dispatch Rules')) {
650
+ const agentDispatchSection = `
651
+ ## Agent Dispatch Rules
652
+
653
+ **Fresh agents** (use \`subagent_type\`): any agent that writes files, produces a verdict, or gates the pipeline.
654
+ **Forks** (no \`subagent_type\`): noisy intermediate work — only the structured summary returns to main context.
655
+
656
+ **Parallel dispatch:**
657
+ - Independent read-only agents: fire in a single message (same Agent tool call batch)
658
+ - Write agents on non-overlapping files: dispatch with \`isolation: "worktree"\`
659
+ - Maximum 3 parallel agents at once; merge before proceeding
660
+
661
+ **Background agents:**
662
+ - Long-running read-only work (test suite, large scans): use \`run_in_background: true\`
663
+ - Collect background results before the synthesis/reporter step
664
+
665
+ **Brief discipline:** Write a 3-5 sentence brief per agent — what to do, exact file paths, output schema, pipeline context. Do NOT pass a context dump.
666
+
667
+ `;
668
+ const patched = existing.replace(/(\n## Tech Stack\b)/, `${agentDispatchSection}$1`);
669
+ writeFileSync(claudeMdPath, patched, 'utf8');
670
+ console.log(' ' + chalk.green('✓') + ' CLAUDE.md' + chalk.dim(' (Agent Dispatch Rules patched in)'));
671
+ } else {
672
+ console.log(' ' + chalk.dim('·') + ' CLAUDE.md already has Agent Dispatch Rules — skipped');
673
+ }
674
+ } else {
675
+ console.log(' ' + chalk.yellow('⚠') + ' CLAUDE.md not found — skipped');
676
+ }
677
+ }
678
+
606
679
  writeFile('.smash-os-version', SMASH_VERSION + '\n');
607
680
  console.log('');
608
681
  console.log(chalk.bold.green(` ✓ SmashOS upgraded to v${SMASH_VERSION}`));
@@ -616,14 +689,37 @@ async function runVerify() {
616
689
  const checks = [
617
690
  ['.smash-os-mode', 'mode flag'],
618
691
  ['CLAUDE.md', 'CLAUDE.md'],
619
- ['ai/orchestrator.md', 'orchestrator'],
620
- ['ai/context/product.md', 'product context'],
621
- ['ai/context/architecture.md', 'architecture context'],
622
- ['ai/roles/staff-engineer.md', 'Staff Engineer role'],
623
- ['ai/roles/senior-developer.md', 'Senior Developer role'],
624
- ['ai/roles/qa-engineer.md', 'QA Engineer role'],
692
+ ['.smashOS/orchestrator.md', 'orchestrator'],
693
+ ['.smashOS/context/product.md', 'product context'],
694
+ ['.smashOS/context/architecture.md', 'architecture context'],
695
+ ['.smashOS/context/coding-standards.md', 'coding standards'],
696
+ ['.smashOS/context/evaluation-rubrics.md', 'evaluation rubrics'],
697
+ ['.smashOS/roles/staff-engineer.md', 'Staff Engineer role'],
698
+ ['.smashOS/roles/senior-developer.md', 'Senior Developer role'],
699
+ ['.smashOS/roles/qa-engineer.md', 'QA Engineer role'],
700
+ ['.smashOS/roles/security-engineer.md', 'Security Engineer role'],
701
+ ['.smashOS/roles/product-manager.md', 'Product Manager role'],
702
+ ['.smashOS/roles/devops-engineer.md', 'DevOps Engineer role'],
703
+ ['.smashOS/workflows/feature.md', 'feature workflow'],
704
+ ['.smashOS/workflows/harness-evolution.md', 'harness-evolution workflow'],
705
+ ['.smashOS/docs/01-quickstart.md', 'docs'],
706
+ ['.claude/agents/senior-dev-implementation-planner.md', 'senior-dev-implementation-planner agent'],
707
+ ['.claude/agents/qa-test-runner.md', 'qa-test-runner agent'],
708
+ ['.claude/agents/senior-dev-code-writer.md', 'senior-dev-code-writer agent'],
709
+ ['.claude/agents/senior-dev-test-writer.md', 'senior-dev-test-writer agent'],
710
+ ['.claude/agents/devops-deploy-runner.md', 'devops-deploy-runner agent'],
711
+ ['.claude/agents/devops-smoke-tester.md', 'devops-smoke-tester agent'],
712
+ ['.smashOS/context/hard-rules.md', 'hard rules'],
713
+ ['.smashOS/context/memory-schema.md', 'memory schema'],
714
+ ['.smashOS/context/pipeline-continuation.md', 'pipeline continuation'],
715
+ ['.smashOS/context/role-protocols.md', 'role protocols'],
716
+ ['.smashOS/workflows/bug-fix.md', 'bug-fix workflow'],
717
+ ['.smashOS/workflows/weekly-improvement.md', 'weekly-improvement workflow'],
718
+ ['.smashOS/roles/ui-ux-designer.md', 'UI/UX Designer role'],
719
+ ['.smashOS/roles/frontend-developer.md', 'Frontend Developer role'],
625
720
  ['.claude/settings.json', 'settings.json'],
626
721
  ['.claude/hooks/smash-os-boot.mjs', 'boot hook'],
722
+ ['.claude/hooks/smash-os-sync.mjs', 'sync hook'],
627
723
  ];
628
724
  let passed = 0;
629
725
  let failed = 0;
@@ -661,7 +757,7 @@ async function runUninstall() {
661
757
  console.log(chalk.bold(' SmashOS Uninstall'));
662
758
  console.log('');
663
759
  console.log(chalk.dim(' This will remove all SmashOS harness files from this project.'));
664
- console.log(chalk.yellow(' ⚠ ai/memory/ and ai/context/ will be preserved by default.'));
760
+ console.log(chalk.yellow(' ⚠ .smashOS/memory/ and .smashOS/context/ will be preserved by default.'));
665
761
  console.log('');
666
762
 
667
763
  const { confirm } = await prompts({
@@ -679,7 +775,7 @@ async function runUninstall() {
679
775
  const { removeUserData } = await prompts({
680
776
  type: 'confirm',
681
777
  name: 'removeUserData',
682
- message: 'Also delete ai/memory/ and ai/context/? (your project notes — NOT recommended)',
778
+ message: 'Also delete .smashOS/memory/ and .smashOS/context/? (your project notes — NOT recommended)',
683
779
  initial: false,
684
780
  }, { onCancel: () => ({ removeUserData: false }) });
685
781
 
@@ -699,18 +795,29 @@ async function runUninstall() {
699
795
  remove('CLAUDE.md', 'CLAUDE.md');
700
796
 
701
797
  // Harness directories (never touch user content by default)
702
- remove('.claude-state', '.claude-state/');
703
798
  remove('.planning', '.planning/');
704
- remove('ai/orchestrator.md', 'ai/orchestrator.md');
705
- remove('ai/roles', 'ai/roles/');
706
- remove('ai/workflows', 'ai/workflows/');
799
+ remove('.smashOS/orchestrator.md', '.smashOS/orchestrator.md');
800
+ remove('.smashOS/roles', '.smashOS/roles/');
801
+ remove('.smashOS/workflows', '.smashOS/workflows/');
802
+ remove('.smashOS/docs', '.smashOS/docs/');
707
803
  remove('.claude/agents', '.claude/agents/');
708
804
  remove('.claude/hooks/smash-os-boot.mjs', '.claude/hooks/smash-os-boot.mjs');
709
805
  remove('.claude/hooks/smash-os-sync.mjs', '.claude/hooks/smash-os-sync.mjs');
710
806
 
711
807
  if (removeUserData) {
712
- remove('ai/memory', 'ai/memory/');
713
- remove('ai/context', 'ai/context/');
808
+ remove('.claude-state', '.claude-state/');
809
+ remove('.smashOS/memory', '.smashOS/memory/');
810
+ remove('.smashOS/context', '.smashOS/context/');
811
+ remove('.smashOS/todo', '.smashOS/todo/');
812
+ }
813
+
814
+ // Remove globally-installed skills
815
+ for (const skillName of Object.keys(localSkills)) {
816
+ const skillDir = join(homedir(), '.claude', 'skills', skillName);
817
+ if (existsSync(skillDir)) {
818
+ rmSync(skillDir, { recursive: true, force: true });
819
+ console.log(' ' + chalk.red('–') + ' ' + chalk.white(`~/.claude/skills/${skillName}/`));
820
+ }
714
821
  }
715
822
 
716
823
  // Scrub smash-os hooks from .claude/settings.json
@@ -721,8 +828,10 @@ async function runUninstall() {
721
828
  if (s.hooks) {
722
829
  for (const event of Object.keys(s.hooks)) {
723
830
  s.hooks[event] = (s.hooks[event] || []).filter(h => {
724
- const cmd = typeof h === 'string' ? h : (h.command || '');
725
- return !cmd.includes('smash-os');
831
+ if (typeof h === 'string') return !h.includes('smash-os');
832
+ if (h.command) return !h.command.includes('smash-os');
833
+ if (h.hooks) return !h.hooks.some(inner => inner.command?.includes('smash-os'));
834
+ return true;
726
835
  });
727
836
  if (s.hooks[event].length === 0) delete s.hooks[event];
728
837
  }
@@ -748,15 +857,13 @@ async function runUninstall() {
748
857
  // macOS: unload and remove LaunchAgents
749
858
  if (process.platform === 'darwin') {
750
859
  const laDir = join(homedir(), 'Library', 'LaunchAgents');
751
- const smashPlists = ['lock-cleanup', 'memory-consolidation', 'nightly-audit', 'docs-regeneration',
752
- 'weekly-improvement', 'dream-memory', 'skill-evolution', 'role-evolution'];
753
- for (const name of smashPlists) {
754
- const p = join(laDir, `com.smash-os.${name}.plist`);
755
- if (existsSync(p)) {
756
- try { execSync(`launchctl unload "${p}"`, { stdio: 'pipe' }); } catch { /* not loaded */ }
757
- rmSync(p, { force: true });
758
- console.log(' ' + chalk.red('–') + ` com.smash-os.${name}.plist`);
759
- }
860
+ let smashPlists = [];
861
+ try { smashPlists = readdirSync(laDir).filter(f => f.startsWith('com.smash-os.') && f.endsWith('.plist')); } catch { /* dir not found */ }
862
+ for (const plistFile of smashPlists) {
863
+ const p = join(laDir, plistFile);
864
+ try { execSync(`launchctl unload "${p}"`, { stdio: 'pipe' }); } catch { /* not loaded */ }
865
+ rmSync(p, { force: true });
866
+ console.log(' ' + chalk.red('–') + ` ${plistFile}`);
760
867
  }
761
868
  }
762
869
 
@@ -768,7 +875,15 @@ async function runUninstall() {
768
875
  if (current.includes(marker)) {
769
876
  const lines = current.split('\n');
770
877
  const start = lines.findIndex(l => l === marker);
771
- const filtered = lines.filter((_, i) => i < start || i > start + 10).filter(l => l !== '');
878
+ // Find block end dynamically: next blank line or end of file after the marker
879
+ let end = lines.length;
880
+ for (let i = start + 1; i < lines.length; i++) {
881
+ if (lines[i].trim() === '' || (!lines[i].startsWith('#') && !lines[i].includes('smash-os') && lines[i].trim() !== '')) {
882
+ end = i - 1;
883
+ break;
884
+ }
885
+ }
886
+ const filtered = lines.filter((_, i) => i < start || i > end).filter(l => l !== '');
772
887
  execSync('crontab -', { input: filtered.join('\n') + '\n', encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
773
888
  console.log(' ' + chalk.red('–') + ' Linux crontab: SmashOS entries removed');
774
889
  }
@@ -1009,6 +1124,14 @@ if (isFrontend) {
1009
1124
  }
1010
1125
  }
1011
1126
 
1127
+ console.log('');
1128
+ console.log(chalk.dim(' Docs (.smashOS/docs/):'));
1129
+ for (const [relPath, content] of Object.entries(osDocsFiles)) {
1130
+ writeFile(relPath, content);
1131
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(relPath));
1132
+ written++;
1133
+ }
1134
+
1012
1135
  writeFile('.claude/hooks/smash-os-boot.mjs', bootHook);
1013
1136
  console.log(' ' + chalk.green('✓') + ' ' + chalk.white('.claude/hooks/smash-os-boot.mjs'));
1014
1137
  written++;
@@ -1049,7 +1172,7 @@ console.log(' ' + chalk.white('/smash-os:status') + chalk.dim('
1049
1172
  console.log(' ' + chalk.white('/smash-os:doctor') + chalk.dim(' — diagnose any missing files'));
1050
1173
  console.log(' ' + chalk.white('/smash-os:run feature "Add X"') + chalk.dim(' — run your first pipeline'));
1051
1174
  console.log('');
1052
- console.log(chalk.dim(' Next: fill in ai/context/ with your project details.'));
1175
+ console.log(chalk.dim(' Next: fill in .smashOS/context/ with your project details.'));
1053
1176
  console.log('');
1054
1177
 
1055
1178
  // ── Guided tour opt-in ────────────────────────────────────────────────────────
@@ -1072,9 +1195,9 @@ if (wantsTour) {
1072
1195
  console.log(chalk.dim(' Shows your harness state, recent decisions, and pipeline health.'));
1073
1196
  console.log('');
1074
1197
  console.log(' 3. Fill in your project context:');
1075
- console.log(' ' + chalk.white('ai/context/product.md') + chalk.dim(' ← what your product does'));
1076
- console.log(' ' + chalk.white('ai/context/architecture.md') + chalk.dim(' ← your tech stack and key decisions'));
1077
- console.log(' ' + chalk.white('ai/context/coding-standards.md') + chalk.dim(' ← your style rules'));
1198
+ console.log(' ' + chalk.white('.smashOS/context/product.md') + chalk.dim(' ← what your product does'));
1199
+ console.log(' ' + chalk.white('.smashOS/context/architecture.md') + chalk.dim(' ← your tech stack and key decisions'));
1200
+ console.log(' ' + chalk.white('.smashOS/context/coding-standards.md') + chalk.dim(' ← your style rules'));
1078
1201
  console.log('');
1079
1202
  console.log(' 4. Run your first pipeline:');
1080
1203
  console.log(' ' + chalk.white('/smash-os:run feature "Add a health check endpoint"'));
@@ -1089,5 +1212,3 @@ if (wantsTour) {
1089
1212
 
1090
1213
  if (setupAutomation) runAutomation(cwd);
1091
1214
 
1092
- console.log(chalk.dim(' Optional: run /smash-os:setup-mcps to add MCP integrations (context7, playwright, chrome-devtools, etc.)'));
1093
- console.log('');