moveros 4.1.0 → 4.1.2

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/install.js CHANGED
@@ -327,10 +327,11 @@ function validateKey(key) {
327
327
  // ─── CLI ─────────────────────────────────────────────────────────────────────
328
328
  function parseArgs() {
329
329
  const args = process.argv.slice(2);
330
- const opts = { vault: "", key: "" };
330
+ const opts = { vault: "", key: "", update: false };
331
331
  for (let i = 0; i < args.length; i++) {
332
332
  if (args[i] === "--vault" && args[i + 1]) opts.vault = args[++i];
333
333
  else if (args[i] === "--key" && args[i + 1]) opts.key = args[++i];
334
+ else if (args[i] === "--update" || args[i] === "-u") opts.update = true;
334
335
  else if (args[i] === "--help" || args[i] === "-h") {
335
336
  ln();
336
337
  ln(` ${bold("mover os")} ${dim("installer")}`);
@@ -340,6 +341,7 @@ function parseArgs() {
340
341
  ln(` ${dim("Options")}`);
341
342
  ln(` --key KEY License key (skip interactive prompt)`);
342
343
  ln(` --vault PATH Obsidian vault path (skip detection)`);
344
+ ln(` --update, -u Quick update (auto-detect vault + agents, no prompts)`);
343
345
  ln();
344
346
  process.exit(0);
345
347
  }
@@ -347,6 +349,181 @@ function parseArgs() {
347
349
  return opts;
348
350
  }
349
351
 
352
+ // ─── Obsidian vault detection ───────────────────────────────────────────────
353
+ function detectObsidianVaults() {
354
+ const configPaths = {
355
+ win32: path.join(os.homedir(), "AppData", "Roaming", "Obsidian", "obsidian.json"),
356
+ darwin: path.join(os.homedir(), "Library", "Application Support", "obsidian", "obsidian.json"),
357
+ linux: path.join(os.homedir(), ".config", "obsidian", "obsidian.json"),
358
+ };
359
+ const configPath = configPaths[process.platform];
360
+ if (!configPath || !fs.existsSync(configPath)) return [];
361
+ try {
362
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
363
+ if (!config.vaults) return [];
364
+ return Object.values(config.vaults)
365
+ .map((v) => v.path)
366
+ .filter((p) => p && fs.existsSync(p))
367
+ .sort();
368
+ } catch {
369
+ return [];
370
+ }
371
+ }
372
+
373
+ // ─── Change detection (update mode) ─────────────────────────────────────────
374
+ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
375
+ const home = os.homedir();
376
+ const result = { workflows: [], hooks: [], rules: null, templates: [] };
377
+
378
+ // --- Workflows: compare source vs first installed destination ---
379
+ const wfSrc = path.join(bundleDir, "src", "workflows");
380
+ const wfDests = [
381
+ selectedAgentIds.includes("claude-code") && path.join(home, ".claude", "commands"),
382
+ selectedAgentIds.includes("cursor") && path.join(home, ".cursor", "commands"),
383
+ selectedAgentIds.includes("antigravity") && path.join(home, ".gemini", "antigravity", "global_workflows"),
384
+ ].filter(Boolean);
385
+ const wfDest = wfDests.find((d) => fs.existsSync(d));
386
+
387
+ if (fs.existsSync(wfSrc)) {
388
+ for (const file of fs.readdirSync(wfSrc).filter((f) => f.endsWith(".md"))) {
389
+ const srcContent = fs.readFileSync(path.join(wfSrc, file), "utf8");
390
+ const destFile = wfDest && path.join(wfDest, file);
391
+ if (!destFile || !fs.existsSync(destFile)) {
392
+ result.workflows.push({ file, status: "new" });
393
+ } else {
394
+ const destContent = fs.readFileSync(destFile, "utf8");
395
+ result.workflows.push({
396
+ file,
397
+ status: srcContent === destContent ? "unchanged" : "changed",
398
+ });
399
+ }
400
+ }
401
+ }
402
+
403
+ // --- Hooks: compare with CRLF normalization ---
404
+ const hooksSrc = path.join(bundleDir, "src", "hooks");
405
+ const hooksDest = path.join(home, ".claude", "hooks");
406
+ if (fs.existsSync(hooksSrc) && selectedAgentIds.includes("claude-code")) {
407
+ for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh") || f.endsWith(".md"))) {
408
+ const srcContent = fs.readFileSync(path.join(hooksSrc, file), "utf8")
409
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
410
+ const destFile = path.join(hooksDest, file);
411
+ if (!fs.existsSync(destFile)) {
412
+ result.hooks.push({ file, status: "new" });
413
+ } else {
414
+ const destContent = fs.readFileSync(destFile, "utf8")
415
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
416
+ result.hooks.push({
417
+ file,
418
+ status: srcContent === destContent ? "unchanged" : "changed",
419
+ });
420
+ }
421
+ }
422
+ }
423
+
424
+ // --- Rules: compare source vs first installed destination ---
425
+ const rulesSrc = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
426
+ const rulesDests = [
427
+ selectedAgentIds.includes("claude-code") && path.join(home, ".claude", "CLAUDE.md"),
428
+ selectedAgentIds.includes("cursor") && path.join(home, ".cursor", "rules", "mover-os.mdc"),
429
+ selectedAgentIds.includes("gemini-cli") && path.join(home, ".gemini", "GEMINI.md"),
430
+ ].filter(Boolean);
431
+ const rulesDest = rulesDests.find((d) => fs.existsSync(d));
432
+ if (fs.existsSync(rulesSrc) && rulesDest) {
433
+ const srcContent = fs.readFileSync(rulesSrc, "utf8");
434
+ const destContent = fs.readFileSync(rulesDest, "utf8");
435
+ result.rules = srcContent === destContent ? "unchanged" : "changed";
436
+ } else {
437
+ result.rules = "unchanged";
438
+ }
439
+
440
+ // --- Templates: non-Engine vault files ---
441
+ const structDir = path.join(bundleDir, "src", "structure");
442
+ if (fs.existsSync(structDir)) {
443
+ const walkTemplates = (dir, rel) => {
444
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
445
+ const entryRel = path.join(rel, entry.name);
446
+ if (entry.isDirectory()) {
447
+ walkTemplates(path.join(dir, entry.name), entryRel);
448
+ } else {
449
+ const relNorm = entryRel.replace(/\\/g, "/");
450
+ if (relNorm.includes("02_Areas") && relNorm.includes("Engine")) continue;
451
+ const srcContent = fs.readFileSync(path.join(dir, entry.name), "utf8");
452
+ const destFile = path.join(vaultPath, entryRel);
453
+ if (!fs.existsSync(destFile)) {
454
+ result.templates.push({ file: entryRel, status: "new" });
455
+ } else {
456
+ const destContent = fs.readFileSync(destFile, "utf8");
457
+ result.templates.push({
458
+ file: entryRel,
459
+ status: srcContent === destContent ? "unchanged" : "changed",
460
+ });
461
+ }
462
+ }
463
+ }
464
+ };
465
+ walkTemplates(structDir, "");
466
+ }
467
+
468
+ return result;
469
+ }
470
+
471
+ function countChanges(changes) {
472
+ let n = 0;
473
+ n += changes.workflows.filter((f) => f.status !== "unchanged").length;
474
+ n += changes.hooks.filter((f) => f.status !== "unchanged").length;
475
+ if (changes.rules === "changed") n++;
476
+ n += changes.templates.filter((f) => f.status !== "unchanged").length;
477
+ return n;
478
+ }
479
+
480
+ function displayChangeSummary(changes, installedVersion, newVersion) {
481
+ // Version line
482
+ if (installedVersion && newVersion && installedVersion !== newVersion) {
483
+ barLn(` Installed: ${dim(installedVersion)} → ${bold(newVersion)}`);
484
+ } else if (installedVersion === newVersion) {
485
+ barLn(` Version: ${dim(installedVersion)} (files may differ)`);
486
+ }
487
+ barLn();
488
+
489
+ // Workflows
490
+ const wfChanged = changes.workflows.filter((f) => f.status === "changed");
491
+ const wfNew = changes.workflows.filter((f) => f.status === "new");
492
+ const wfUnchanged = changes.workflows.filter((f) => f.status === "unchanged");
493
+ barLn(` Workflows ${dim(`(${wfChanged.length + wfNew.length} changed, ${wfUnchanged.length} unchanged)`)}:`);
494
+ for (const f of wfChanged) barLn(` ${yellow("✦")} /${f.file.replace(".md", "")}`);
495
+ for (const f of wfNew) barLn(` ${green("+")} /${f.file.replace(".md", "")} ${dim("(new)")}`);
496
+ if (wfChanged.length === 0 && wfNew.length === 0) barLn(` ${dim("all up to date")}`);
497
+ barLn();
498
+
499
+ // Hooks
500
+ if (changes.hooks.length > 0) {
501
+ const hkChanged = changes.hooks.filter((f) => f.status === "changed");
502
+ const hkNew = changes.hooks.filter((f) => f.status === "new");
503
+ const hkUnchanged = changes.hooks.filter((f) => f.status === "unchanged");
504
+ barLn(` Hooks ${dim(`(${hkChanged.length + hkNew.length} changed, ${hkUnchanged.length} unchanged)`)}:`);
505
+ for (const f of hkChanged) barLn(` ${yellow("✦")} ${f.file}`);
506
+ for (const f of hkNew) barLn(` ${green("+")} ${f.file} ${dim("(new)")}`);
507
+ if (hkChanged.length === 0 && hkNew.length === 0) barLn(` ${dim("all up to date")}`);
508
+ barLn();
509
+ }
510
+
511
+ // Rules
512
+ barLn(` Rules: ${changes.rules === "changed" ? yellow("changed") : dim("unchanged")}`);
513
+
514
+ // Templates
515
+ const tmplChanged = changes.templates.filter((f) => f.status === "changed");
516
+ const tmplNew = changes.templates.filter((f) => f.status === "new");
517
+ if (tmplChanged.length > 0 || tmplNew.length > 0) {
518
+ barLn(` Templates ${dim(`(${tmplChanged.length + tmplNew.length} changed)`)}:`);
519
+ for (const f of tmplChanged) barLn(` ${yellow("✦")} ${f.file.replace(/\\/g, "/")}`);
520
+ for (const f of tmplNew) barLn(` ${green("+")} ${f.file.replace(/\\/g, "/")} ${dim("(new)")}`);
521
+ } else {
522
+ barLn(` Templates: ${dim("unchanged")}`);
523
+ }
524
+ barLn();
525
+ }
526
+
350
527
  // ─── Engine file detection ──────────────────────────────────────────────────
351
528
  function detectEngineFiles(vaultPath) {
352
529
  const engineDir = path.join(vaultPath, "02_Areas", "Engine");
@@ -967,13 +1144,14 @@ function installTemplateFiles(bundleDir, vaultPath) {
967
1144
  return count;
968
1145
  }
969
1146
 
970
- function installWorkflows(bundleDir, destDir) {
1147
+ function installWorkflows(bundleDir, destDir, selectedWorkflows) {
971
1148
  const srcDir = path.join(bundleDir, "src", "workflows");
972
1149
  if (!fs.existsSync(srcDir)) return 0;
973
1150
 
974
1151
  fs.mkdirSync(destDir, { recursive: true });
975
1152
  let count = 0;
976
1153
  for (const file of fs.readdirSync(srcDir).filter((f) => f.endsWith(".md"))) {
1154
+ if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
977
1155
  if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file))) count++;
978
1156
  }
979
1157
  return count;
@@ -1012,7 +1190,16 @@ function installHooksForClaude(bundleDir, vaultPath) {
1012
1190
 
1013
1191
  let count = 0;
1014
1192
  for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".sh"))) {
1015
- fs.copyFileSync(path.join(hooksSrc, file), path.join(hooksDst, file));
1193
+ const dst = path.join(hooksDst, file);
1194
+ // Read, strip \r (CRLF→LF), write — prevents "command not found" on macOS/Linux
1195
+ const content = fs.readFileSync(path.join(hooksSrc, file), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1196
+ fs.writeFileSync(dst, content, { mode: 0o755 });
1197
+ count++;
1198
+ }
1199
+ // Copy hook template files (.md)
1200
+ for (const file of fs.readdirSync(hooksSrc).filter((f) => f.endsWith(".md"))) {
1201
+ const dst = path.join(hooksDst, file);
1202
+ fs.copyFileSync(path.join(hooksSrc, file), dst);
1016
1203
  count++;
1017
1204
  }
1018
1205
 
@@ -1057,10 +1244,12 @@ function installClaudeCode(bundleDir, vaultPath, skillOpts) {
1057
1244
 
1058
1245
  const claudeDir = path.join(home, ".claude");
1059
1246
  fs.mkdirSync(claudeDir, { recursive: true });
1060
- if (installRules(bundleDir, path.join(claudeDir, "CLAUDE.md"))) steps.push("rules");
1247
+ if (!skillOpts?.skipRules) {
1248
+ if (installRules(bundleDir, path.join(claudeDir, "CLAUDE.md"))) steps.push("rules");
1249
+ }
1061
1250
 
1062
1251
  const cmdsDir = path.join(claudeDir, "commands");
1063
- const wfCount = installWorkflows(bundleDir, cmdsDir);
1252
+ const wfCount = installWorkflows(bundleDir, cmdsDir, skillOpts?.workflows);
1064
1253
  if (wfCount > 0) steps.push(`${wfCount} commands`);
1065
1254
 
1066
1255
  if (skillOpts && skillOpts.install) {
@@ -1069,7 +1258,7 @@ function installClaudeCode(bundleDir, vaultPath, skillOpts) {
1069
1258
  if (skCount > 0) steps.push(`${skCount} skills`);
1070
1259
  }
1071
1260
 
1072
- if (vaultPath) {
1261
+ if (vaultPath && !skillOpts?.skipHooks) {
1073
1262
  const hkCount = installHooksForClaude(bundleDir, vaultPath);
1074
1263
  if (hkCount > 0) steps.push(`${hkCount} hooks`);
1075
1264
  }
@@ -1105,19 +1294,21 @@ function installCursor(bundleDir, vaultPath, skillOpts) {
1105
1294
  const home = os.homedir();
1106
1295
  const steps = [];
1107
1296
 
1108
- if (vaultPath) {
1109
- if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"))) steps.push("rules");
1110
- }
1297
+ if (!skillOpts?.skipRules) {
1298
+ if (vaultPath) {
1299
+ if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"))) steps.push("rules");
1300
+ }
1111
1301
 
1112
- const cursorRulesDir = path.join(home, ".cursor", "rules");
1113
- fs.mkdirSync(cursorRulesDir, { recursive: true });
1114
- const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
1115
- if (fs.existsSync(src)) {
1116
- fs.copyFileSync(src, path.join(cursorRulesDir, "mover-os.mdc"));
1302
+ const cursorRulesDir = path.join(home, ".cursor", "rules");
1303
+ fs.mkdirSync(cursorRulesDir, { recursive: true });
1304
+ const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
1305
+ if (fs.existsSync(src)) {
1306
+ fs.copyFileSync(src, path.join(cursorRulesDir, "mover-os.mdc"));
1307
+ }
1117
1308
  }
1118
1309
 
1119
1310
  const cmdsDir = path.join(home, ".cursor", "commands");
1120
- const wfCount = installWorkflows(bundleDir, cmdsDir);
1311
+ const wfCount = installWorkflows(bundleDir, cmdsDir, skillOpts?.workflows);
1121
1312
  if (wfCount > 0) steps.push(`${wfCount} commands`);
1122
1313
 
1123
1314
  if (skillOpts && skillOpts.install) {
@@ -1253,7 +1444,7 @@ function installAntigravity(bundleDir, vaultPath, skillOpts, writtenFiles) {
1253
1444
  }
1254
1445
 
1255
1446
  const wfDir = path.join(geminiDir, "antigravity", "global_workflows");
1256
- const wfCount = installWorkflows(bundleDir, wfDir);
1447
+ const wfCount = installWorkflows(bundleDir, wfDir, skillOpts?.workflows);
1257
1448
  if (wfCount > 0) steps.push(`${wfCount} workflows`);
1258
1449
 
1259
1450
  if (skillOpts && skillOpts.install) {
@@ -1414,6 +1605,93 @@ async function main() {
1414
1605
  process.exit(1);
1415
1606
  }
1416
1607
 
1608
+ // ── Headless quick update (--update flag) ──
1609
+ if (opts.update) {
1610
+ // Auto-detect vault
1611
+ let vaultPath = opts.vault;
1612
+ if (!vaultPath) {
1613
+ const obsVaults = detectObsidianVaults();
1614
+ vaultPath = obsVaults.find((p) =>
1615
+ fs.existsSync(path.join(p, ".mover-version"))
1616
+ );
1617
+ if (!vaultPath) {
1618
+ outro(red("No Mover OS vault found. Use: npx moveros --update --vault /path"));
1619
+ process.exit(1);
1620
+ }
1621
+ }
1622
+ if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
1623
+ vaultPath = path.resolve(vaultPath);
1624
+ barLn(dim(`Vault: ${vaultPath}`));
1625
+
1626
+ // Auto-detect agents
1627
+ const detectedAgents = AGENTS.filter((a) => a.detect());
1628
+ if (detectedAgents.length === 0) {
1629
+ outro(red("No AI agents detected."));
1630
+ process.exit(1);
1631
+ }
1632
+ const selectedIds = detectedAgents.map((a) => a.id);
1633
+ barLn(dim(`Agents: ${detectedAgents.map((a) => a.name).join(", ")}`));
1634
+ barLn();
1635
+
1636
+ // Detect changes
1637
+ const changes = detectChanges(bundleDir, vaultPath, selectedIds);
1638
+ const totalChanged = countChanges(changes);
1639
+
1640
+ // Read versions
1641
+ const vfPath = path.join(vaultPath, ".mover-version");
1642
+ const installedVer = fs.existsSync(vfPath) ? fs.readFileSync(vfPath, "utf8").trim() : null;
1643
+ let newVer = `V${VERSION}`;
1644
+ try {
1645
+ const pkg = JSON.parse(fs.readFileSync(path.join(bundleDir, "package.json"), "utf8"));
1646
+ newVer = pkg.version || newVer;
1647
+ } catch {}
1648
+
1649
+ displayChangeSummary(changes, installedVer, newVer);
1650
+
1651
+ if (totalChanged === 0) {
1652
+ outro(green("Already up to date."));
1653
+ return;
1654
+ }
1655
+
1656
+ // Apply all changes
1657
+ barLn(bold("Updating..."));
1658
+ barLn();
1659
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1660
+
1661
+ // Vault structure
1662
+ createVaultStructure(vaultPath);
1663
+
1664
+ // Templates
1665
+ installTemplateFiles(bundleDir, vaultPath);
1666
+
1667
+ // Per-agent installation
1668
+ const writtenFiles = new Set();
1669
+ const skillOpts = { install: true, categories: null, workflows: null };
1670
+ for (const agent of detectedAgents) {
1671
+ const fn = AGENT_INSTALLERS[agent.id];
1672
+ if (!fn) continue;
1673
+ const sp = spinner(agent.name);
1674
+ const usesWrittenFiles = agent.id === "antigravity" || agent.id === "gemini-cli";
1675
+ const steps = usesWrittenFiles
1676
+ ? fn(bundleDir, vaultPath, skillOpts, writtenFiles)
1677
+ : fn(bundleDir, vaultPath, skillOpts);
1678
+ await sleep(200);
1679
+ if (steps.length > 0) {
1680
+ sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
1681
+ } else {
1682
+ sp.stop(`${agent.name} ${dim("configured")}`);
1683
+ }
1684
+ }
1685
+
1686
+ // Update version marker
1687
+ fs.writeFileSync(path.join(vaultPath, ".mover-version"), `V${VERSION}\n`, "utf8");
1688
+
1689
+ barLn();
1690
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1691
+ outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
1692
+ return;
1693
+ }
1694
+
1417
1695
  // ── License key ──
1418
1696
  let key = opts.key;
1419
1697
 
@@ -1459,26 +1737,45 @@ async function main() {
1459
1737
  let vaultPath = opts.vault;
1460
1738
 
1461
1739
  if (!vaultPath) {
1462
- const candidates = [
1463
- path.join(os.homedir(), "Documents", "Obsidian"),
1464
- path.join(os.homedir(), "Obsidian"),
1465
- path.join(os.homedir(), "Documents", "Mover-OS"),
1466
- ];
1740
+ const obsVaults = detectObsidianVaults();
1467
1741
 
1468
- const detected = candidates.find((p) => {
1469
- try {
1470
- return fs.existsSync(p) && (fs.existsSync(path.join(p, ".obsidian")) || fs.existsSync(path.join(p, "02_Areas")));
1471
- } catch {
1472
- return false;
1473
- }
1474
- });
1742
+ if (obsVaults.length > 0) {
1743
+ question("Select your Obsidian vault");
1744
+ barLn();
1745
+
1746
+ const vaultItems = obsVaults.map((p) => {
1747
+ const name = path.basename(p);
1748
+ const hasMover =
1749
+ fs.existsSync(path.join(p, ".mover-version")) ||
1750
+ fs.existsSync(path.join(p, "02_Areas", "Engine"));
1751
+ return {
1752
+ id: p,
1753
+ name: `${name}${hasMover ? dim(" (Mover OS)") : ""}`,
1754
+ tier: dim(p),
1755
+ };
1756
+ });
1757
+ vaultItems.push({
1758
+ id: "__manual__",
1759
+ name: "Enter path manually",
1760
+ tier: "Type or paste a custom vault path",
1761
+ });
1475
1762
 
1476
- const defaultPath = detected || path.join(os.homedir(), "Mover-OS");
1763
+ const selected = await interactiveSelect(vaultItems, { multi: false });
1477
1764
 
1478
- vaultPath = await textInput({
1479
- label: `Where is your Obsidian vault?${detected ? dim(" (detected)") : ""}`,
1480
- initial: defaultPath,
1481
- });
1765
+ if (selected === "__manual__") {
1766
+ vaultPath = await textInput({
1767
+ label: "Where is your Obsidian vault?",
1768
+ initial: path.join(os.homedir(), "Mover-OS"),
1769
+ });
1770
+ } else {
1771
+ vaultPath = selected;
1772
+ }
1773
+ } else {
1774
+ vaultPath = await textInput({
1775
+ label: "Where is your Obsidian vault?",
1776
+ initial: path.join(os.homedir(), "Mover-OS"),
1777
+ });
1778
+ }
1482
1779
  } else {
1483
1780
  barLn(dim(`Vault: ${vaultPath}`));
1484
1781
  barLn();
@@ -1706,6 +2003,102 @@ async function main() {
1706
2003
  process.exit(0);
1707
2004
  }
1708
2005
 
2006
+ // ── Change detection + selection (update mode only) ──
2007
+ let selectedWorkflows = null; // null = install all
2008
+ let skipHooks = false;
2009
+ let skipRules = false;
2010
+ let skipTemplates = false;
2011
+
2012
+ if (updateMode) {
2013
+ const changes = detectChanges(bundleDir, vaultPath, selectedIds);
2014
+ const totalChanged = countChanges(changes);
2015
+
2016
+ // Read versions for display
2017
+ const versionFilePath = path.join(vaultPath, ".mover-version");
2018
+ const installedVersion = fs.existsSync(versionFilePath)
2019
+ ? fs.readFileSync(versionFilePath, "utf8").trim()
2020
+ : null;
2021
+ let newVersion = `V${VERSION}`;
2022
+ try {
2023
+ const pkgPath = path.join(bundleDir, "package.json");
2024
+ if (fs.existsSync(pkgPath)) {
2025
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
2026
+ newVersion = pkg.version || newVersion;
2027
+ }
2028
+ } catch {}
2029
+
2030
+ barLn();
2031
+ question("Change Summary");
2032
+ barLn();
2033
+ displayChangeSummary(changes, installedVersion, newVersion);
2034
+
2035
+ if (totalChanged === 0) {
2036
+ barLn(green(" Already up to date."));
2037
+ barLn();
2038
+ } else {
2039
+ const applyChoice = await interactiveSelect(
2040
+ [
2041
+ { id: "all", name: "Yes, update all changed files", tier: "" },
2042
+ { id: "select", name: "Select individually", tier: "" },
2043
+ { id: "cancel", name: "Cancel", tier: "" },
2044
+ ],
2045
+ { multi: false, defaultIndex: 0 }
2046
+ );
2047
+
2048
+ if (applyChoice === "cancel") {
2049
+ outro("Cancelled.");
2050
+ process.exit(0);
2051
+ }
2052
+
2053
+ if (applyChoice === "select") {
2054
+ // Build list of only changed/new files for individual selection
2055
+ const changedItems = [];
2056
+ const changedPreSelected = [];
2057
+ for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
2058
+ const id = `wf:${f.file}`;
2059
+ changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new workflow" : "workflow") });
2060
+ changedPreSelected.push(id);
2061
+ }
2062
+ for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
2063
+ const id = `hook:${f.file}`;
2064
+ changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
2065
+ changedPreSelected.push(id);
2066
+ }
2067
+ if (changes.rules === "changed") {
2068
+ changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
2069
+ changedPreSelected.push("rules");
2070
+ }
2071
+ for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
2072
+ const id = `tmpl:${f.file}`;
2073
+ changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new template" : "template") });
2074
+ changedPreSelected.push(id);
2075
+ }
2076
+
2077
+ if (changedItems.length > 0) {
2078
+ question("Select files to update");
2079
+ barLn();
2080
+ const selectedFileIds = await interactiveSelect(changedItems, {
2081
+ multi: true,
2082
+ preSelected: changedPreSelected,
2083
+ });
2084
+
2085
+ // Build workflow filter Set
2086
+ const selectedWfFiles = selectedFileIds
2087
+ .filter((id) => id.startsWith("wf:"))
2088
+ .map((id) => id.slice(3));
2089
+ if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
2090
+ selectedWorkflows = new Set(selectedWfFiles);
2091
+ }
2092
+ // Check if hooks/rules/templates were deselected
2093
+ skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
2094
+ skipRules = !selectedFileIds.includes("rules");
2095
+ skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
2096
+ }
2097
+ }
2098
+ // "all" = selectedWorkflows stays null, skip flags stay false
2099
+ }
2100
+ }
2101
+
1709
2102
  // ── Skills ──
1710
2103
  const allSkills = findSkills(bundleDir);
1711
2104
  let installSkills = false;
@@ -1797,13 +2190,15 @@ async function main() {
1797
2190
  totalSteps++;
1798
2191
 
1799
2192
  // 2. Template files (runs in both modes — only creates missing files, never overwrites)
1800
- sp = spinner(updateMode ? "New template files" : "Engine templates");
1801
- const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
1802
- await sleep(200);
1803
- sp.stop(updateMode
1804
- ? `New template files${templatesInstalled > 0 ? dim(` ${templatesInstalled} added`) : dim(" all present")}`
1805
- : `Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
1806
- totalSteps++;
2193
+ if (!skipTemplates) {
2194
+ sp = spinner(updateMode ? "New template files" : "Engine templates");
2195
+ const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
2196
+ await sleep(200);
2197
+ sp.stop(updateMode
2198
+ ? `New template files${templatesInstalled > 0 ? dim(` ${templatesInstalled} added`) : dim(" all present")}`
2199
+ : `Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
2200
+ totalSteps++;
2201
+ }
1807
2202
 
1808
2203
  // 3. CLAUDE.md (skip on update)
1809
2204
  if (!updateMode) {
@@ -1820,7 +2215,7 @@ async function main() {
1820
2215
 
1821
2216
  // 4. Per-agent installation
1822
2217
  const writtenFiles = new Set(); // Track shared files to avoid double-writes (e.g. GEMINI.md)
1823
- const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine };
2218
+ const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
1824
2219
  for (const agent of selectedAgents) {
1825
2220
  const fn = AGENT_INSTALLERS[agent.id];
1826
2221
  if (!fn) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moveros",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "The self-improving OS for AI agents. Turns Obsidian into an execution engine.",
5
5
  "bin": {
6
6
  "moveros": "install.js"
@@ -0,0 +1,42 @@
1
+ You are a session logger for Mover OS. Process this session log entry.
2
+
3
+ Timestamps: current={{CURRENT_TS}}, pre-compaction={{COMPACT_TS}}.
4
+ Session backup: {{SUMMARY_PATH}}
5
+ Working directory: {{WORKING_DIR}}
6
+ Project: {{PROJECT_NAME}}
7
+
8
+ ## Steps
9
+
10
+ 0. Read the log workflow at: {{LOG_WORKFLOW}}
11
+ Use Step 4 for session log FORMAT, Step 6C for System State Snapshot FORMAT (metadata header + narrative paragraph). Follow these formats exactly.
12
+
13
+ 1. Read the session backup summary at the path above.
14
+
15
+ 2. Read the Daily Note at: {{DAILY_NOTE}}
16
+ If it does not exist, create it with a basic template.
17
+ Find the `## Session Log` section. Insert the new entry WITHIN that section (not at end of file).
18
+ Mark as [COMPACTED].
19
+ Format: `### Session Log [ATTENDED] — ~HH:MM-HH:MM [Claude Code] [{{PROJECT_NAME}}] [COMPACTED]`
20
+ Then subsections with ~timestamps and 3-4 line descriptions.
21
+
22
+ 3. If plan.md exists at: {{PLAN_MD}}
23
+ Read it, mark completed tasks [~] (NOT [x]) with [UNVERIFIED] tag, append to EXECUTION LOG.
24
+ The [~] means "code written, not yet verified." Only /morning or the user promotes to [x].
25
+
26
+ 4. If project_state.md exists at: {{PROJECT_STATE}}
27
+ Append new Solutions Ledger entries (deduplicate first), Changelog entry, and System State Snapshot.
28
+ Snapshot format: structured metadata (Mode, Focus, Risk, Next Milestone, Blocker) AND a narrative paragraph (8-12 sentences). See log.md Step 6C.
29
+
30
+ 5. Read Active_Context at: {{ACTIVE_CONTEXT}}
31
+ Prepend 1-line summary to Active Sessions (keep 5 max, remove oldest).
32
+ Update log_last_run to {{CURRENT_TS}}.
33
+
34
+ 6. Git commit changed files if inside a repo.
35
+ Git add specific files only. Never git add . at root. Never commit Dailies.
36
+
37
+ ## Constraints
38
+
39
+ - Append-only (never delete)
40
+ - Cite sources
41
+ - Mark entries [COMPACTED]
42
+ - Windows: no && in commands
@@ -58,8 +58,6 @@ if [ -f "$PENDING_FILE" ]; then
58
58
  COMPACT_TS=$(grep '^\*\*Timestamp:\*\*' "$SUMMARY_PATH" 2>/dev/null | sed 's/\*\*Timestamp:\*\* //')
59
59
  fi
60
60
  CURRENT_TS=$(date '+%Y-%m-%dT%H:%M')
61
- CURRENT_DATE=$(date '+%Y-%m-%d')
62
- CURRENT_MONTH=$(date '+%Y-%m')
63
61
  WORKING_DIR=$(pwd)
64
62
 
65
63
  # Resolve vault root: walk up from working dir looking for 02_Areas/Engine/
@@ -73,9 +71,36 @@ if [ -f "$PENDING_FILE" ]; then
73
71
  CHECK_DIR=$(dirname "$CHECK_DIR")
74
72
  done
75
73
 
74
+ # Determine which daily note to use:
75
+ # Use pre-compaction date (session date), NOT wall-clock time.
76
+ # This prevents 1am sessions from creating a new day's note.
77
+ SESSION_DATE=""
78
+ if [ -n "$COMPACT_TS" ]; then
79
+ SESSION_DATE=$(echo "$COMPACT_TS" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')
80
+ fi
81
+ # Fallback: find the latest existing daily note
82
+ if [ -z "$SESSION_DATE" ]; then
83
+ DAILIES_DIR="${VAULT_ROOT}/02_Areas/Engine/Dailies"
84
+ if [ -d "$DAILIES_DIR" ]; then
85
+ LATEST_NOTE=$(find "$DAILIES_DIR" -name "Daily - *.md" -type f 2>/dev/null | sort -r | head -1)
86
+ if [ -n "$LATEST_NOTE" ]; then
87
+ SESSION_DATE=$(echo "$LATEST_NOTE" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')
88
+ fi
89
+ fi
90
+ fi
91
+ # Final fallback: use current date
92
+ if [ -z "$SESSION_DATE" ]; then
93
+ SESSION_DATE=$(date '+%Y-%m-%d')
94
+ fi
95
+ SESSION_MONTH=$(echo "$SESSION_DATE" | cut -c1-7)
96
+
76
97
  # Derive key file paths
77
- DAILY_NOTE="${VAULT_ROOT}/02_Areas/Engine/Dailies/${CURRENT_MONTH}/Daily - ${CURRENT_DATE}.md"
98
+ DAILY_NOTE="${VAULT_ROOT}/02_Areas/Engine/Dailies/${SESSION_MONTH}/Daily - ${SESSION_DATE}.md"
78
99
  ACTIVE_CONTEXT="${VAULT_ROOT}/02_Areas/Engine/Active_Context.md"
100
+ LOG_WORKFLOW="$HOME/.claude/commands/log.md"
101
+ if [ ! -f "$LOG_WORKFLOW" ]; then
102
+ LOG_WORKFLOW="${VAULT_ROOT}/01_Projects/Mover OS Bundle/src/workflows/log.md"
103
+ fi
79
104
 
80
105
  # Detect project context (if inside 01_Projects/[Name]/)
81
106
  PROJECT_NAME=""
@@ -98,7 +123,32 @@ if [ -f "$PENDING_FILE" ]; then
98
123
  ;;
99
124
  esac
100
125
 
101
- echo "{\"decision\":\"block\",\"reason\":\"POST-COMPACTION BACKGROUND LOG: Context was just compacted. Instead of running the full /log workflow (which fills your context and causes more compactions), spawn a BACKGROUND Task agent to handle it. Do this: 1. Write a brief summary of what was worked on this session (from your compacted context — you have the conversation summary). 2. Spawn: Task tool with subagent_type='general-purpose', model='sonnet', run_in_background=true. 3. In the Task prompt, include: (a) your session context summary, (b) instructions below. 4. Mark session as logged: echo done > /tmp/mover_logdone_${SESSION_KEY}. 5. Continue with whatever you were doing. INSTRUCTIONS FOR THE BACKGROUND AGENT: You are a session logger for Mover OS. Process this session log entry. Timestamps: current=${CURRENT_TS}, pre-compaction=${COMPACT_TS:-unknown}. Session backup summary: ${SUMMARY_PATH}. Working directory: ${WORKING_DIR}. Project: ${PROJECT_NAME:-none}. STEPS: (1) Read the session backup summary at the path above. (2) Read the Daily Note at: ${DAILY_NOTE} — if it does not exist, create it with a basic template. Append a session log entry under '## Session Log' marked [COMPACTED]. Format: '### Session Log [ATTENDED] — ~HH:MM-HH:MM [Claude Code] [${PROJECT_NAME:-Unknown}] [COMPACTED]' then subsections with ~timestamps and 3-4 line descriptions. (3) If plan.md exists at: ${PLAN_MD:-N/A} — read it, mark completed tasks [x], append to EXECUTION LOG. (4) If project_state.md exists at: ${PROJECT_STATE:-N/A} — append new Solutions Ledger entries (deduplicate first), Changelog entry, System State Snapshot. (5) Read Active_Context at: ${ACTIVE_CONTEXT} — prepend 1-line summary to Active Sessions (keep 5 max, remove oldest), update log_last_run to ${CURRENT_TS}. (6) Git commit changed files if inside a repo (git add specific files only, never git add . at root, never commit Dailies). CONSTRAINTS: append-only (never delete), cite sources, mark entries [COMPACTED], Windows=no && in commands.\"}"
126
+ # Load instructions from template file (keeps this script readable)
127
+ PROMPT_TEMPLATE="$HOME/.claude/hooks/background-log-prompt.md"
128
+ if [ ! -f "$PROMPT_TEMPLATE" ]; then
129
+ PROMPT_TEMPLATE="${VAULT_ROOT}/01_Projects/Mover OS Bundle/src/hooks/background-log-prompt.md"
130
+ fi
131
+ if [ -f "$PROMPT_TEMPLATE" ]; then
132
+ AGENT_INSTRUCTIONS=$(sed \
133
+ -e "s|{{CURRENT_TS}}|${CURRENT_TS}|g" \
134
+ -e "s|{{COMPACT_TS}}|${COMPACT_TS:-unknown}|g" \
135
+ -e "s|{{SUMMARY_PATH}}|${SUMMARY_PATH}|g" \
136
+ -e "s|{{WORKING_DIR}}|${WORKING_DIR}|g" \
137
+ -e "s|{{PROJECT_NAME}}|${PROJECT_NAME:-Unknown}|g" \
138
+ -e "s|{{DAILY_NOTE}}|${DAILY_NOTE}|g" \
139
+ -e "s|{{PLAN_MD}}|${PLAN_MD:-N/A}|g" \
140
+ -e "s|{{PROJECT_STATE}}|${PROJECT_STATE:-N/A}|g" \
141
+ -e "s|{{ACTIVE_CONTEXT}}|${ACTIVE_CONTEXT}|g" \
142
+ -e "s|{{LOG_WORKFLOW}}|${LOG_WORKFLOW}|g" \
143
+ "$PROMPT_TEMPLATE" | tr '\n' ' ')
144
+ else
145
+ AGENT_INSTRUCTIONS="Session logger: read ${SUMMARY_PATH}, update Daily Note at ${DAILY_NOTE}, update project_state at ${PROJECT_STATE:-N/A}, update Active_Context at ${ACTIVE_CONTEXT}. Append-only. Mark [COMPACTED]."
146
+ fi
147
+
148
+ # Escape double quotes for JSON
149
+ AGENT_INSTRUCTIONS=$(echo "$AGENT_INSTRUCTIONS" | sed 's/"/\\"/g')
150
+
151
+ echo "{\"decision\":\"block\",\"reason\":\"POST-COMPACTION BACKGROUND LOG: Context was just compacted. Spawn a BACKGROUND Task agent (subagent_type=general-purpose, model=sonnet, run_in_background=true) with your session context summary + the instructions below. Then mark as logged: echo done > /tmp/mover_logdone_${SESSION_KEY}. Continue with your work. INSTRUCTIONS: ${AGENT_INSTRUCTIONS}\"}"
102
152
  exit 0
103
153
  fi
104
154
  fi
@@ -131,5 +181,5 @@ if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
131
181
  fi
132
182
  fi
133
183
 
134
- echo '{"decision":"block","reason":"SESSION REMINDER: Ask the user (one sentence) if they want you to run /log to capture the session. Then continue with your response."}'
184
+ echo '{"decision":"block","reason":"SESSION REMINDER: In ONE sentence, ask the user if they would like to run /log before ending. Do NOT run /log automatically. Do NOT use the Skill tool. Just ask the question and continue with your normal response."}'
135
185
  exit 0
@@ -1,19 +1,24 @@
1
- # PROJECT STATE: [Project Name]
2
- *Status: [Status e.g., Planning]*
3
-
4
- ## 1. THE BRAIN DUMP (Notes)
5
- * **Mission:** [What are we doing?]
6
- * **Current Blocker:** [What is stopping us?]
7
-
8
- ## 2. THE SOLUTIONS LEDGER
9
- * **[Topic]:** [Problem] -> [Solution]
10
-
11
- ## 3. CHANGELOG
12
- * [YYYY-MM-DD] Project Initialized -> Template copied.
13
-
14
- ## 4. SYSTEM STATE SNAPSHOTS
15
- * **Latest Snapshot:** [YYYY-MM-DD]
16
- * **Current Mode:** [e.g., Planning, Execution]
17
- * **Active Focus:** [What is the one thing we are doing?]
18
- * **Risk Profile:** [Low/High]
19
- * **Next Milestone:** [What is the next big win?]
1
+ # PROJECT STATE: [Project Name]
2
+ *Status: [Status e.g., Planning]*
3
+
4
+ ## 1. THE BRAIN DUMP (Notes)
5
+ * **Mission:** [What are we doing?]
6
+ * **Current Blocker:** [What is stopping us?]
7
+
8
+ ## 2. THE SOLUTIONS LEDGER
9
+ * **[Topic]:** [Problem] -> [Solution]
10
+
11
+ ## 3. CHANGELOG
12
+ * [YYYY-MM-DD] Project Initialized -> Template copied.
13
+
14
+ ## 4. SYSTEM STATE SNAPSHOTS
15
+
16
+ ### [YYYY-MM-DD HH:MM] Snapshot
17
+ - **Mode:** [Planning / Execution / Debugging]
18
+ - **Focus:** [Current primary task]
19
+ - **Risk:** [Low / Medium / High / Critical]
20
+ - **Next Milestone:** [What's next]
21
+ - **Blocker:** [What's blocking, or None]
22
+
23
+ **Narrative:** [8-12 sentences. What changed, why, what unblocked,
24
+ what's next. Concrete and direct.]
@@ -328,17 +328,19 @@ You are the guardian of `plan.md`.
328
328
  - **Read-First:** Your memory is `plan.md`. Read it before acting.
329
329
  - **Append-Only:** FORBIDDEN from deleting history/completed tasks.
330
330
  - **Real-Time Sync:** After task completion:
331
- 1. Mark `[x]` in `plan.md`
332
- 2. Append to `## EXECUTION LOG`: `* [YYYY-MM-DD] {Task} - {Result}`
331
+ 1. Mark `[~]` in `plan.md` with `[UNVERIFIED]` tag
332
+ 2. Append to `## EXECUTION LOG`: `* [YYYY-MM-DD] {Task} - {Result} [UNVERIFIED]`
333
333
  3. Suggest Git commit
334
334
 
335
+ `[~]` means "code written, not yet verified working." Only `/morning` or user promotes to `[x]`.
336
+
335
337
  **Passive Detection Protocol (Always Active):**
336
338
 
337
339
  These triggers fire during EVERY conversation, not just workflows. **DETECT > WRITE > CONTINUE.** Don't ask. Don't announce. Write the entry, then continue the conversation.
338
340
 
339
341
  | Trigger | What to detect | Where to write |
340
342
  |---------|---------------|---------------|
341
- | User reports action | "I did it", "just finished", "sent it", "got back from" | Daily Note `## Log` + mark `[x]` in plan.md if applicable |
343
+ | User reports action | "I did it", "just finished", "sent it", "got back from" | Daily Note `## Log` + mark `[~] [UNVERIFIED]` in plan.md if applicable |
342
344
  | Decision made | "Let's go with X", "I've decided", agreement on direction | Relevant file (plan.md, project_state.md, Active_Context) |
343
345
  | Commitment made | "I'll send", "I promised", "by Friday" | Active_Context `## 8.5 COMMITMENTS` |
344
346
  | Waiting on someone | "He said he'd", "waiting on", "they'll get back to me" | Active_Context `## 3.5 WAITING FOR` |
@@ -20,6 +20,17 @@ description: End-of-day analysis. Reads Session Log, audits strategy, updates Da
20
20
 
21
21
  Global Rules are pre-loaded as CLAUDE.md / GEMINI.md. Output `[Rules Loaded - DATE]`.
22
22
 
23
+ GET CURRENT TIME:
24
+ ```bash
25
+ date '+%Y-%m-%d %H:%M'
26
+ ```
27
+ Save as CURRENT_TIME. Extract the hour.
28
+
29
+ DETERMINE "TODAY" (session-aware date logic):
30
+ - If the hour is **before 05:00** (midnight-5am): the user is still on "yesterday." Find and analyse YESTERDAY's Daily Note, not today's. The analysis belongs to the day that was lived, not the clock date.
31
+ - If the hour is **05:00 or later**: analyse today's Daily Note as normal.
32
+ - Save the resolved date as ANALYSE_DATE. Use this for all Daily Note lookups.
33
+
23
34
  LOAD (Memory Hierarchy):
24
35
  1. Auto_Learnings.md (P0)
25
36
  2. Active_Context.md (P1)
@@ -33,7 +33,13 @@ pwd
33
33
  ```
34
34
  Save as WORKING_DIR. Extract project name: if inside `01_Projects/[Name]/`, capture `[Name]`. Otherwise use the directory basename.
35
35
 
36
- DAILY NOTE PATH (derive from SESSION_END_TIME):
36
+ DETERMINE SESSION DATE (session-aware, not wall-clock):
37
+ - Extract the hour from SESSION_END_TIME.
38
+ - If before **05:00** (midnight-5am): the session belongs to YESTERDAY. Use yesterday's date for the Daily Note.
39
+ - If **05:00 or later**: use today's date.
40
+ - Save as SESSION_DATE.
41
+
42
+ DAILY NOTE PATH (derive from SESSION_DATE):
37
43
  ```
38
44
  {VAULT_ROOT}/02_Areas/Engine/Dailies/YYYY-MM/Daily - YYYY-MM-DD.md
39
45
  ```
@@ -164,9 +170,10 @@ If no Daily Note exists: output the Session Log in chat for the user to paste.
164
170
 
165
171
  **Target:** Active project's `plan.md`
166
172
 
167
- 1. Mark completed tasks with `[x]`.
173
+ 1. Mark completed tasks with `[~]` and `[UNVERIFIED]` tag (NOT `[x]`).
174
+ `[~]` means "code written, not yet verified." Only `/morning` or user promotes to `[x]`.
168
175
  2. Append to `## EXECUTION LOG`:
169
- `* [YYYY-MM-DD] {Task} - {Result with context}`
176
+ `* [YYYY-MM-DD] {Task} - {Result with context} [UNVERIFIED]`
170
177
  Include failure context: "Failed with Error X, fixed with Y".
171
178
  3. Do NOT rewrite the roadmap. Do NOT delete any lines.
172
179
 
@@ -202,9 +209,14 @@ APPEND (never overwrite) a new snapshot:
202
209
 
203
210
  ```markdown
204
211
  ### [YYYY-MM-DD HH:MM] Snapshot
205
- **Narrative:** [8-20 sentence story of what was improved, fixed, changed.
206
- Describe the friction and the breakthrough. Be visceral. Merge all
207
- context into the narrative, not bullet points.]
212
+ - **Mode:** [Planning / Execution / Debugging]
213
+ - **Focus:** [Current primary task]
214
+ - **Risk:** [Low / Medium / High / Critical]
215
+ - **Next Milestone:** [What's next]
216
+ - **Blocker:** [What's blocking, or None]
217
+
218
+ **Narrative:** [8-12 sentences. What changed, why, what unblocked,
219
+ what's next. Concrete and direct.]
208
220
  ```
209
221
 
210
222
  If no project_state.md in scope: skip this step.
@@ -99,6 +99,23 @@ Keep this under 30 seconds. Use file modification dates, not full reads. Delegat
99
99
 
100
100
  ---
101
101
 
102
+ ## 2.6 VERIFICATION CHECK (Promote Unverified Tasks)
103
+
104
+ If a project context is active (user is inside a project directory, or today's plan references a project):
105
+ 1. Find the active project's `plan.md`
106
+ 2. Scan for tasks marked `[~]` with `[UNVERIFIED]` tag
107
+
108
+ If found:
109
+ - List them: "Last session marked these as done but unverified:"
110
+ - `[~] Task description [UNVERIFIED]`
111
+ - Ask: "Confirm these are working? (yes / list issues)"
112
+ - If confirmed: promote all to `[x]`, remove `[UNVERIFIED]` tag, append to EXECUTION LOG: `* [YYYY-MM-DD] Verified: [task list]`
113
+ - If issues listed: keep as `[~]`, add issue note to task line, surface in today's plan as priority
114
+
115
+ If no `[~]` items found: skip silently.
116
+
117
+ ---
118
+
102
119
  ## 2.7 PLAN ALIGNMENT CHECK (Escalating Friction — Level 1)
103
120
 
104
121
  If today's Daily Note exists:
@@ -18,6 +18,19 @@ description: Generates the Daily Note for the next day based on Strategy, previo
18
18
 
19
19
  Global Rules are pre-loaded as CLAUDE.md / GEMINI.md. Output `[Rules Loaded - DATE]`.
20
20
 
21
+ GET CURRENT TIME:
22
+ ```bash
23
+ date '+%Y-%m-%d %H:%M'
24
+ ```
25
+ Save as CURRENT_TIME. Extract the hour.
26
+
27
+ DETERMINE "TODAY" (session-aware date logic):
28
+ - If the hour is **before 05:00** (midnight-5am): the user is still on "yesterday" in their mind. Check if a Daily Note exists for yesterday's date. If yes, treat YESTERDAY as "today" for planning purposes. "Tomorrow" = the current calendar date.
29
+ - If the hour is **05:00 or later**: treat the current calendar date as "today." "Tomorrow" = current date + 1.
30
+ - **Rule:** Only `/plan-tomorrow` creates new Daily Notes. The note it creates is always for the NEXT waking day. Never create a note for the day after that.
31
+
32
+ Save the resolved dates as TODAY_DATE and TOMORROW_DATE.
33
+
21
34
  LOAD (Memory Hierarchy):
22
35
  1. Auto_Learnings.md (P0)
23
36
  2. Active_Context.md (P1)