moveros 4.1.1 → 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;
@@ -1018,6 +1196,12 @@ function installHooksForClaude(bundleDir, vaultPath) {
1018
1196
  fs.writeFileSync(dst, content, { mode: 0o755 });
1019
1197
  count++;
1020
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);
1203
+ count++;
1204
+ }
1021
1205
 
1022
1206
  // Deep-merge hooks into existing settings.json (preserve user config)
1023
1207
  const settingsPath = path.join(settingsDir, "settings.json");
@@ -1060,10 +1244,12 @@ function installClaudeCode(bundleDir, vaultPath, skillOpts) {
1060
1244
 
1061
1245
  const claudeDir = path.join(home, ".claude");
1062
1246
  fs.mkdirSync(claudeDir, { recursive: true });
1063
- 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
+ }
1064
1250
 
1065
1251
  const cmdsDir = path.join(claudeDir, "commands");
1066
- const wfCount = installWorkflows(bundleDir, cmdsDir);
1252
+ const wfCount = installWorkflows(bundleDir, cmdsDir, skillOpts?.workflows);
1067
1253
  if (wfCount > 0) steps.push(`${wfCount} commands`);
1068
1254
 
1069
1255
  if (skillOpts && skillOpts.install) {
@@ -1072,7 +1258,7 @@ function installClaudeCode(bundleDir, vaultPath, skillOpts) {
1072
1258
  if (skCount > 0) steps.push(`${skCount} skills`);
1073
1259
  }
1074
1260
 
1075
- if (vaultPath) {
1261
+ if (vaultPath && !skillOpts?.skipHooks) {
1076
1262
  const hkCount = installHooksForClaude(bundleDir, vaultPath);
1077
1263
  if (hkCount > 0) steps.push(`${hkCount} hooks`);
1078
1264
  }
@@ -1108,19 +1294,21 @@ function installCursor(bundleDir, vaultPath, skillOpts) {
1108
1294
  const home = os.homedir();
1109
1295
  const steps = [];
1110
1296
 
1111
- if (vaultPath) {
1112
- if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"))) steps.push("rules");
1113
- }
1297
+ if (!skillOpts?.skipRules) {
1298
+ if (vaultPath) {
1299
+ if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"))) steps.push("rules");
1300
+ }
1114
1301
 
1115
- const cursorRulesDir = path.join(home, ".cursor", "rules");
1116
- fs.mkdirSync(cursorRulesDir, { recursive: true });
1117
- const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
1118
- if (fs.existsSync(src)) {
1119
- 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
+ }
1120
1308
  }
1121
1309
 
1122
1310
  const cmdsDir = path.join(home, ".cursor", "commands");
1123
- const wfCount = installWorkflows(bundleDir, cmdsDir);
1311
+ const wfCount = installWorkflows(bundleDir, cmdsDir, skillOpts?.workflows);
1124
1312
  if (wfCount > 0) steps.push(`${wfCount} commands`);
1125
1313
 
1126
1314
  if (skillOpts && skillOpts.install) {
@@ -1256,7 +1444,7 @@ function installAntigravity(bundleDir, vaultPath, skillOpts, writtenFiles) {
1256
1444
  }
1257
1445
 
1258
1446
  const wfDir = path.join(geminiDir, "antigravity", "global_workflows");
1259
- const wfCount = installWorkflows(bundleDir, wfDir);
1447
+ const wfCount = installWorkflows(bundleDir, wfDir, skillOpts?.workflows);
1260
1448
  if (wfCount > 0) steps.push(`${wfCount} workflows`);
1261
1449
 
1262
1450
  if (skillOpts && skillOpts.install) {
@@ -1417,6 +1605,93 @@ async function main() {
1417
1605
  process.exit(1);
1418
1606
  }
1419
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
+
1420
1695
  // ── License key ──
1421
1696
  let key = opts.key;
1422
1697
 
@@ -1462,26 +1737,45 @@ async function main() {
1462
1737
  let vaultPath = opts.vault;
1463
1738
 
1464
1739
  if (!vaultPath) {
1465
- const candidates = [
1466
- path.join(os.homedir(), "Documents", "Obsidian"),
1467
- path.join(os.homedir(), "Obsidian"),
1468
- path.join(os.homedir(), "Documents", "Mover-OS"),
1469
- ];
1740
+ const obsVaults = detectObsidianVaults();
1470
1741
 
1471
- const detected = candidates.find((p) => {
1472
- try {
1473
- return fs.existsSync(p) && (fs.existsSync(path.join(p, ".obsidian")) || fs.existsSync(path.join(p, "02_Areas")));
1474
- } catch {
1475
- return false;
1476
- }
1477
- });
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
+ });
1478
1762
 
1479
- const defaultPath = detected || path.join(os.homedir(), "Mover-OS");
1763
+ const selected = await interactiveSelect(vaultItems, { multi: false });
1480
1764
 
1481
- vaultPath = await textInput({
1482
- label: `Where is your Obsidian vault?${detected ? dim(" (detected)") : ""}`,
1483
- initial: defaultPath,
1484
- });
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
+ }
1485
1779
  } else {
1486
1780
  barLn(dim(`Vault: ${vaultPath}`));
1487
1781
  barLn();
@@ -1709,6 +2003,102 @@ async function main() {
1709
2003
  process.exit(0);
1710
2004
  }
1711
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
+
1712
2102
  // ── Skills ──
1713
2103
  const allSkills = findSkills(bundleDir);
1714
2104
  let installSkills = false;
@@ -1800,13 +2190,15 @@ async function main() {
1800
2190
  totalSteps++;
1801
2191
 
1802
2192
  // 2. Template files (runs in both modes — only creates missing files, never overwrites)
1803
- sp = spinner(updateMode ? "New template files" : "Engine templates");
1804
- const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
1805
- await sleep(200);
1806
- sp.stop(updateMode
1807
- ? `New template files${templatesInstalled > 0 ? dim(` ${templatesInstalled} added`) : dim(" all present")}`
1808
- : `Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
1809
- 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
+ }
1810
2202
 
1811
2203
  // 3. CLAUDE.md (skip on update)
1812
2204
  if (!updateMode) {
@@ -1823,7 +2215,7 @@ async function main() {
1823
2215
 
1824
2216
  // 4. Per-agent installation
1825
2217
  const writtenFiles = new Set(); // Track shared files to avoid double-writes (e.g. GEMINI.md)
1826
- const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine };
2218
+ const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
1827
2219
  for (const agent of selectedAgents) {
1828
2220
  const fn = AGENT_INSTALLERS[agent.id];
1829
2221
  if (!fn) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moveros",
3
- "version": "4.1.1",
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)