mover-os 4.7.6 → 4.7.8

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 (107) hide show
  1. package/install.js +763 -70
  2. package/package.json +15 -3
  3. package/src/dashboard/build.js +1503 -0
  4. package/src/dashboard/dashboard.js +276 -0
  5. package/src/dashboard/index.js +319 -0
  6. package/src/dashboard/lib/activation-log.js +297 -0
  7. package/src/dashboard/lib/active-context-parser.js +177 -0
  8. package/src/dashboard/lib/agent-command.js +93 -0
  9. package/src/dashboard/lib/agent-detect.js +255 -0
  10. package/src/dashboard/lib/agent-detector.js +92 -0
  11. package/src/dashboard/lib/agent-session.js +462 -0
  12. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  13. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  14. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  15. package/src/dashboard/lib/config-parser.js +109 -0
  16. package/src/dashboard/lib/connect-recommender.js +131 -0
  17. package/src/dashboard/lib/correlations-parser.js +231 -0
  18. package/src/dashboard/lib/daily-note-resolver.js +211 -0
  19. package/src/dashboard/lib/date-utils.js +35 -0
  20. package/src/dashboard/lib/distribution-parser.js +78 -0
  21. package/src/dashboard/lib/dossier-parser.js +64 -0
  22. package/src/dashboard/lib/drift-history.js +83 -0
  23. package/src/dashboard/lib/drift-score.js +119 -0
  24. package/src/dashboard/lib/engine-health.js +170 -0
  25. package/src/dashboard/lib/engine-writer.js +1685 -0
  26. package/src/dashboard/lib/execution-plan.js +125 -0
  27. package/src/dashboard/lib/experiments-parser.js +429 -0
  28. package/src/dashboard/lib/feed-parser.js +294 -0
  29. package/src/dashboard/lib/forked-future.js +60 -0
  30. package/src/dashboard/lib/goal-forecast.js +304 -0
  31. package/src/dashboard/lib/goals-parser.js +67 -0
  32. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  33. package/src/dashboard/lib/hook-activity.js +48 -0
  34. package/src/dashboard/lib/hook-indexer.js +169 -0
  35. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  36. package/src/dashboard/lib/identity-parser.js +85 -0
  37. package/src/dashboard/lib/ingestion.js +418 -0
  38. package/src/dashboard/lib/library-indexer-v2.js +212 -0
  39. package/src/dashboard/lib/library-indexer.js +105 -0
  40. package/src/dashboard/lib/log-activation.sh +61 -0
  41. package/src/dashboard/lib/memory-curator.js +97 -0
  42. package/src/dashboard/lib/memory-gardener.js +177 -0
  43. package/src/dashboard/lib/memory-gepa.js +102 -0
  44. package/src/dashboard/lib/memory-index.js +470 -0
  45. package/src/dashboard/lib/memory-rerank.js +72 -0
  46. package/src/dashboard/lib/memory-text.js +136 -0
  47. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  48. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  49. package/src/dashboard/lib/onboarding-forge.js +70 -0
  50. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  51. package/src/dashboard/lib/override-summary.js +73 -0
  52. package/src/dashboard/lib/paths.js +192 -0
  53. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  54. package/src/dashboard/lib/phantom-strategy.js +129 -0
  55. package/src/dashboard/lib/project-scanner.js +121 -0
  56. package/src/dashboard/lib/promise-wall.js +88 -0
  57. package/src/dashboard/lib/record-score.js +173 -0
  58. package/src/dashboard/lib/redaction.js +140 -0
  59. package/src/dashboard/lib/refusal-parser.js +44 -0
  60. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  61. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  62. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  63. package/src/dashboard/lib/run-registry.js +226 -0
  64. package/src/dashboard/lib/safe-write.js +63 -0
  65. package/src/dashboard/lib/session-log-parser.js +145 -0
  66. package/src/dashboard/lib/session-time-parser.js +158 -0
  67. package/src/dashboard/lib/skill-index.js +171 -0
  68. package/src/dashboard/lib/skill-indexer.js +118 -0
  69. package/src/dashboard/lib/skill-recommender.js +689 -0
  70. package/src/dashboard/lib/strategy-parser.js +245 -0
  71. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  72. package/src/dashboard/lib/streak-parser.js +95 -0
  73. package/src/dashboard/lib/suggested-now.js +254 -0
  74. package/src/dashboard/lib/tool-awareness.js +125 -0
  75. package/src/dashboard/lib/transcript-parser.js +331 -0
  76. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  77. package/src/dashboard/lib/view-generator.js +163 -0
  78. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  79. package/src/dashboard/lib/walkthrough-script.js +140 -0
  80. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  81. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  82. package/src/dashboard/server.js +2024 -0
  83. package/src/dashboard/shortcut.js +0 -0
  84. package/src/dashboard/static/setup-poc.html +306 -0
  85. package/src/dashboard/static/walkthrough-poc.html +580 -0
  86. package/src/dashboard/styles.css +1201 -0
  87. package/src/dashboard/templates/index.html +278 -0
  88. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  89. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  90. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  91. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  92. package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
  93. package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
  94. package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
  95. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  96. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  97. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  98. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  99. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  100. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  101. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  102. package/src/dashboard/ui/dist/icon.svg +4 -0
  103. package/src/dashboard/ui/dist/index.html +18 -0
  104. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  105. package/src/dashboard/ui/dist/registerSW.js +1 -0
  106. package/src/dashboard/ui/dist/sw.js +1 -0
  107. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
package/install.js CHANGED
@@ -944,8 +944,30 @@ async function downloadPayload(key) {
944
944
  // The ordering of -tvzf and -tzf output is identical (tar walks the
945
945
  // archive in the same order), so we zip them by line index for the
946
946
  // type/path correlation.
947
- const pathsListing = execSync(`tar -tzf "${tarPath}"`, { encoding: 'utf8' });
948
- const verboseListing = execSync(`tar -tvzf "${tarPath}"`, { encoding: 'utf8' });
947
+ // v4.7.9 (S4 ship-blocker): tar preflight. Both listing calls were outside
948
+ // any try/catch, so on a Windows box without tar on PATH the raw spawn threw
949
+ // `Error: spawnSync /bin/sh ENOENT` (or `'tar' is not recognized`) with no
950
+ // hint about what the user must install. Check tar is present first, then
951
+ // wrap the listings so a genuinely corrupt payload reports differently from a
952
+ // missing-tar host. cmdExists() is the same probe used elsewhere in install.
953
+ if (!cmdExists('tar')) {
954
+ throw new Error(
955
+ "tar is required to install Mover OS but was not found on PATH. " +
956
+ "Install tar and re-run (Git Bash and WSL both ship it; on bare Windows run `choco install tartool` " +
957
+ "or use the bsdtar bundled with Windows 10+ by adding C:\\\\Windows\\\\System32 to PATH)."
958
+ );
959
+ }
960
+ let pathsListing, verboseListing;
961
+ try {
962
+ pathsListing = execSync(`tar -tzf "${tarPath}"`, { encoding: 'utf8' });
963
+ verboseListing = execSync(`tar -tvzf "${tarPath}"`, { encoding: 'utf8' });
964
+ } catch (e) {
965
+ throw new Error(
966
+ `tar could not read the downloaded payload at ${tarPath}. ` +
967
+ "The download is likely corrupt or incomplete — delete that file and re-run. " +
968
+ `(tar reported: ${e.message})`
969
+ );
970
+ }
949
971
  const pathLines = pathsListing.split('\n').filter(Boolean);
950
972
  const verboseLines = verboseListing.split('\n').filter(Boolean);
951
973
  const badPaths = [];
@@ -988,6 +1010,41 @@ async function downloadPayload(key) {
988
1010
  // Clean up tarball
989
1011
  try { fs.unlinkSync(tarPath); } catch {}
990
1012
 
1013
+ // v4.7.9 (S5 ship-blocker): post-extract assert. A silently-truncated tarball
1014
+ // can pass the listing/type checks above yet extract to nothing useful. If the
1015
+ // canonical payload root (~/.mover/src/workflows) is absent after extraction,
1016
+ // every downstream installer would copy from nonexistent dirs and the install
1017
+ // would "succeed" while shipping no workflows. Fail loudly here instead.
1018
+ const extractedWorkflows = path.join(moverDir, "src", "workflows");
1019
+ if (!fs.existsSync(extractedWorkflows)) {
1020
+ throw new Error(
1021
+ `Payload extracted but ${extractedWorkflows} is missing — the download is corrupt or incomplete. ` +
1022
+ `Delete ${path.join(moverDir, "payload.tar.gz")} and re-run.`
1023
+ );
1024
+ }
1025
+ // Existence alone is too weak: a tarball carrying just an empty src/workflows/
1026
+ // dir passes the check above, then installWorkflows() finds zero .md files and
1027
+ // returns 0 commands while the install reports success — the exact silent-empty
1028
+ // failure S5 exists to stop. Require at least one workflow .md to be present.
1029
+ let extractedWfCount = 0;
1030
+ try {
1031
+ // Require a real .md FILE — not just any entry ending in ".md". A corrupt
1032
+ // payload containing a directory literally named "x.md" would otherwise pass.
1033
+ extractedWfCount = fs.readdirSync(extractedWorkflows, { withFileTypes: true })
1034
+ .filter((d) => d.isFile() && d.name.endsWith(".md")).length;
1035
+ } catch (e) {
1036
+ throw new Error(
1037
+ `Payload extracted but ${extractedWorkflows} is unreadable (${e.message}) — ` +
1038
+ `the download is corrupt or incomplete. Delete ${path.join(moverDir, "payload.tar.gz")} and re-run.`
1039
+ );
1040
+ }
1041
+ if (extractedWfCount === 0) {
1042
+ throw new Error(
1043
+ `Payload extracted but ${extractedWorkflows} contains no workflow (.md) files — ` +
1044
+ `the download is corrupt or incomplete. Delete ${path.join(moverDir, "payload.tar.gz")} and re-run.`
1045
+ );
1046
+ }
1047
+
991
1048
  return moverDir;
992
1049
  }
993
1050
 
@@ -1052,7 +1109,9 @@ const CLI_COMMANDS = {
1052
1109
  update: { desc: "Comprehensive update — agents, rules, skills", alias: ["-u"] },
1053
1110
  uninstall: { desc: "Remove Mover OS from vault and agents", alias: [] },
1054
1111
  doctor: { desc: "Health check across all installed agents", alias: [] },
1055
- pulse: { desc: "Terminal dashboard — energy, tasks, streaks",alias: [] },
1112
+ pulse: { desc: "Browser dashboard — Suggested Now, patterns, drift (use --terminal for TUI)",alias: ["dashboard"] },
1113
+ open: { desc: "Open the dashboard in your browser (starts it in the background if needed)", alias: [] },
1114
+ menu: { desc: "Classic terminal launcher (install, update, doctor…)", alias: [] },
1056
1115
  // warm removed — hooks + rules + /morning already handle session priming
1057
1116
  capture: { desc: "Quick capture — tasks, links, ideas", alias: [] },
1058
1117
  who: { desc: "Entity memory lookup", alias: [] },
@@ -1085,6 +1144,7 @@ function parseArgs() {
1085
1144
  if (a === "--json") { opts.json = true; continue; }
1086
1145
  if (a === "--quiet" || a === "-q") { opts.quiet = true; continue; }
1087
1146
  if (a === "--dry-run") { opts.dryRun = true; continue; }
1147
+ if (a === "--no-open") { opts.noOpen = true; continue; }
1088
1148
  if (a === "--help" || a === "-h") {
1089
1149
  ln();
1090
1150
  ln(` ${bold("moveros")} ${dim("— the Mover OS companion CLI")}`);
@@ -1104,9 +1164,10 @@ function parseArgs() {
1104
1164
  ln(` --json Structured JSON output (for status command)`);
1105
1165
  ln(` --quiet, -q Minimal output, one value per line`);
1106
1166
  ln(` --dry-run Show what would change without doing it`);
1167
+ ln(` --no-open Skip the browser launch (automation / headless)`);
1107
1168
  ln(` --update, -u Quick update (backward compat)`);
1108
1169
  ln();
1109
- ln(` ${dim("Run")} moveros ${dim("with no args for interactive menu")}`);
1170
+ ln(` ${dim("Run")} moveros ${dim("with no args to open the dashboard — installs on first run")}`);
1110
1171
  ln();
1111
1172
  process.exit(0);
1112
1173
  }
@@ -2381,9 +2442,11 @@ Stuck: /debug-resistance
2381
2442
  // ─── Claude Code hooks (settings.json) ──────────────────────────────────────
2382
2443
  // ─── Codex hook config generator ────────────────────────────────────────────
2383
2444
  // v4.7.5: Codex hooks invoked through mover-hook-adapter.js for schema
2384
- // translation. MVP scope: session-start, engine-protection, git-safety,
2385
- // plan-sync-reminder, dirty-tree-guard (no session-log-reminder under Codex
2386
- // — that script is Claude-transcript-specific).
2445
+ // translation.
2446
+ //
2447
+ // v4.7.8: expanded coverage was 5 hooks across 4 events, now 13 hooks across
2448
+ // 5 events. Brings Codex to Claude Code parity on every event Codex supports.
2449
+ // PermissionRequest deferred until adapter handles nested decision schema.
2387
2450
  //
2388
2451
  // IMPORTANT: Codex runs hook commands through cmd.exe on Windows, which does
2389
2452
  // NOT expand $HOME. We resolve absolute paths at install time so the same
@@ -2413,6 +2476,28 @@ function generateCodexHooks() {
2413
2476
  command: `node ${adapter} codex SessionStart ${hookDir}/session-start.sh" resume`,
2414
2477
  timeout: 15,
2415
2478
  },
2479
+ {
2480
+ // v4.7.8: Engine Health surface on session boot — matches the
2481
+ // PreCompact-equivalent hook Claude Code runs.
2482
+ type: "command",
2483
+ command: `node ${adapter} codex SessionStart ${hookDir}/engine-health.sh" full`,
2484
+ timeout: 15,
2485
+ },
2486
+ ],
2487
+ },
2488
+ ],
2489
+ // v4.7.8 NEW: per-prompt enforcement (Single Test reminder, friction floor)
2490
+ UserPromptSubmit: [
2491
+ {
2492
+ hooks: [
2493
+ {
2494
+ type: "command",
2495
+ command: `node ${adapter} codex UserPromptSubmit ${hookDir}/user-prompt-enforce.sh"`,
2496
+ // v4.7.8 audit fix: 4s undercut adapter's 4500ms fallback (mover-hook-adapter.js:65),
2497
+ // host would SIGTERM adapter before its internal timeout fired. 5s gives the
2498
+ // adapter room to surface its own timeout error cleanly.
2499
+ timeout: 5,
2500
+ },
2416
2501
  ],
2417
2502
  },
2418
2503
  ],
@@ -2438,6 +2523,25 @@ function generateCodexHooks() {
2438
2523
  ],
2439
2524
  },
2440
2525
  ],
2526
+ // v4.7.8: PermissionRequest event. Schema verified against
2527
+ // https://developers.openai.com/codex/hooks — Codex expects nested
2528
+ // {decision:{behavior,message?}} shape, which engine-permission-request.sh
2529
+ // already emits. Adapter (mover-hook-adapter.js v4.7.8) recognizes the
2530
+ // shape via isPermissionBehavior and passes through with allowlisting.
2531
+ // Strategy.md and Identity_Prime.md pass through to manual dialog;
2532
+ // Daily Notes and non-Engine files auto-allow.
2533
+ PermissionRequest: [
2534
+ {
2535
+ matcher: "Edit|Write|apply_patch",
2536
+ hooks: [
2537
+ {
2538
+ type: "command",
2539
+ command: `node ${adapter} codex PermissionRequest ${hookDir}/engine-permission-request.sh"`,
2540
+ timeout: 5,
2541
+ },
2542
+ ],
2543
+ },
2544
+ ],
2441
2545
  PostToolUse: [
2442
2546
  {
2443
2547
  matcher: "Edit|Write|apply_patch",
@@ -2447,16 +2551,50 @@ function generateCodexHooks() {
2447
2551
  command: `node ${adapter} codex PostToolUse ${hookDir}/plan-sync-reminder.sh"`,
2448
2552
  timeout: 5,
2449
2553
  },
2554
+ {
2555
+ // v4.7.8: auto-commit Engine edits. Multi-agent safe via flock
2556
+ // (see engine-auto-commit.sh:62-89). Silent on non-Engine files.
2557
+ type: "command",
2558
+ command: `node ${adapter} codex PostToolUse ${hookDir}/engine-auto-commit.sh"`,
2559
+ timeout: 10,
2560
+ },
2450
2561
  ],
2451
2562
  },
2452
2563
  ],
2453
2564
  Stop: [
2454
2565
  {
2566
+ // v4.7.8: 6 sequential hooks. Order matters — cheap/important first
2567
+ // so they complete before any per-event budget exhausts.
2455
2568
  hooks: [
2456
2569
  {
2457
2570
  type: "command",
2458
2571
  command: `node ${adapter} codex Stop ${hookDir}/dirty-tree-guard.sh"`,
2459
- timeout: 10,
2572
+ timeout: 8,
2573
+ },
2574
+ {
2575
+ type: "command",
2576
+ command: `node ${adapter} codex Stop ${hookDir}/session-log-reminder.sh"`,
2577
+ timeout: 8,
2578
+ },
2579
+ {
2580
+ type: "command",
2581
+ command: `node ${adapter} codex Stop ${hookDir}/context-staleness.sh"`,
2582
+ timeout: 5,
2583
+ },
2584
+ {
2585
+ type: "command",
2586
+ command: `node ${adapter} codex Stop ${hookDir}/iteration-check.sh"`,
2587
+ timeout: 5,
2588
+ },
2589
+ {
2590
+ type: "command",
2591
+ command: `node ${adapter} codex Stop ${hookDir}/engine-health.sh" summary`,
2592
+ timeout: 8,
2593
+ },
2594
+ {
2595
+ type: "command",
2596
+ command: `node ${adapter} codex Stop ${hookDir}/clock.sh"`,
2597
+ timeout: 2,
2460
2598
  },
2461
2599
  ],
2462
2600
  },
@@ -2550,64 +2688,121 @@ function generateGeminiHooks() {
2550
2688
  }
2551
2689
 
2552
2690
  function generateClaudeSettings() {
2691
+ // v4.7.9 (S3 ship-blocker): resolve absolute, forward-slash hook paths at
2692
+ // install time instead of the literal `$HOME`. On Windows, Claude Code runs
2693
+ // Stop/PreToolUse hooks through cmd.exe, which does NOT expand `$HOME` — so
2694
+ // `bash "$HOME/.claude/hooks/X.sh"` resolved to a nonexistent literal path
2695
+ // and EVERY guardrail hook (engine-protection, git-safety, session-start…)
2696
+ // was silently dead on Windows. This is the SAME fix already proven in
2697
+ // generateCodexHooks/generateGeminiHooks (see comment above generateCodexHooks):
2698
+ // forward slashes work for `bash` on macOS/Linux/Windows, and an absolute
2699
+ // path removes the cmd.exe `$HOME` dependency. The settings-merge dedup
2700
+ // (basename-based, see line ~3372) is unaffected because it compares the
2701
+ // hook script basename, which is identical for `$HOME/...` and absolute paths.
2702
+ const home = os.homedir();
2703
+ const fwd = (p) => p.split(path.sep).join("/");
2704
+ const hooksRoot = fwd(path.join(home, ".claude", "hooks"));
2553
2705
  return JSON.stringify(
2554
2706
  {
2555
2707
  hooks: {
2556
- Stop: [
2708
+ // ── SessionStart ── full P0-P5 load on startup|clear|resume, volatile reload on
2709
+ // compact. engine-health runs AFTER session-start and self-dedups: on `full`,
2710
+ // session-start.sh emits the staleness summary INLINE and drops a fresh
2711
+ // `mover_health_emitted` marker, so the trailing engine-health.sh exits 0; on
2712
+ // compact/precompact there is no fresh full-marker so it emits. (See the dedup
2713
+ // guard in engine-health.sh — added with the inline emission in 95ed8b40.)
2714
+ SessionStart: [
2557
2715
  {
2716
+ matcher: "startup|clear|resume",
2558
2717
  hooks: [
2559
- { type: "command", command: 'bash "$HOME/.claude/hooks/session-log-reminder.sh"', timeout: 10 },
2560
- { type: "command", command: 'bash "$HOME/.claude/hooks/dirty-tree-guard.sh"', timeout: 10 },
2561
- { type: "command", command: 'bash "$HOME/.claude/hooks/context-staleness.sh"', timeout: 5 },
2718
+ { type: "command", command: `bash "${hooksRoot}/session-start.sh" full`, timeout: 30 },
2719
+ { type: "command", command: `bash "${hooksRoot}/engine-health.sh" full`, timeout: 20 },
2562
2720
  ],
2563
2721
  },
2722
+ {
2723
+ matcher: "compact",
2724
+ hooks: [
2725
+ { type: "command", command: `bash "${hooksRoot}/session-start.sh" volatile`, timeout: 15 },
2726
+ { type: "command", command: `bash "${hooksRoot}/engine-health.sh" full`, timeout: 20 },
2727
+ ],
2728
+ },
2729
+ ],
2730
+ // ── UserPromptSubmit ── per-prompt anti-drift enforcement (the [MOVER] status line).
2731
+ // memory-recall.sh / skill-recall.sh are DELIBERATELY NOT wired here: the memory
2732
+ // & recall engine ships DORMANT for new installs (opt-in). Do not "fix" this by
2733
+ // adding them — that absence is intentional, unlike user-prompt-enforce.sh which
2734
+ // was an oversight this generator missed when v4.7.8 added it (wired for Codex +
2735
+ // Gemini but not Claude). Reconciled in v4.8.0.
2736
+ UserPromptSubmit: [
2737
+ {
2738
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/user-prompt-enforce.sh"`, timeout: 5 }],
2739
+ },
2564
2740
  ],
2565
2741
  PreToolUse: [
2566
2742
  {
2567
2743
  matcher: "Write|Edit",
2568
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/engine-protection.sh"', timeout: 5 }],
2744
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/engine-protection.sh"`, timeout: 5 }],
2569
2745
  },
2570
2746
  {
2571
2747
  matcher: "Bash",
2572
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/git-safety.sh"', timeout: 5 }],
2748
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/git-safety.sh"`, timeout: 5 }],
2573
2749
  },
2574
2750
  ],
2575
2751
  PostToolUse: [
2576
2752
  {
2577
2753
  matcher: "Write|Edit",
2578
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/plan-sync-reminder.sh"', timeout: 5 }],
2754
+ hooks: [
2755
+ { type: "command", command: `bash "${hooksRoot}/plan-sync-reminder.sh"`, timeout: 5 },
2756
+ { type: "command", command: `bash "${hooksRoot}/engine-auto-commit.sh"`, timeout: 10 },
2757
+ ],
2579
2758
  },
2580
2759
  ],
2581
- PreCompact: [
2760
+ Stop: [
2582
2761
  {
2583
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/pre-compact-backup.sh"', timeout: 15 }],
2762
+ hooks: [
2763
+ { type: "command", command: `bash "${hooksRoot}/session-log-reminder.sh"`, timeout: 10 },
2764
+ { type: "command", command: `bash "${hooksRoot}/dirty-tree-guard.sh"`, timeout: 10 },
2765
+ { type: "command", command: `bash "${hooksRoot}/context-staleness.sh"`, timeout: 10 },
2766
+ { type: "command", command: `bash "${hooksRoot}/iteration-check.sh"`, timeout: 10 },
2767
+ { type: "command", command: `bash "${hooksRoot}/engine-health.sh" summary`, timeout: 10 },
2768
+ { type: "command", command: `bash "${hooksRoot}/clock.sh"`, timeout: 2 },
2769
+ ],
2584
2770
  },
2585
2771
  ],
2586
- SessionEnd: [
2772
+ SubagentStart: [
2587
2773
  {
2588
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/session-end.sh"', timeout: 10 }],
2774
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/subagent-context.sh"`, timeout: 5 }],
2589
2775
  },
2590
2776
  ],
2591
- PermissionRequest: [
2777
+ SubagentStop: [
2592
2778
  {
2593
- matcher: "Write|Edit",
2594
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/engine-permission-request.sh"', timeout: 5 }],
2779
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/subagent-validate.sh"`, timeout: 5 }],
2595
2780
  },
2596
2781
  ],
2597
- SessionStart: [
2782
+ PreCompact: [
2598
2783
  {
2599
- matcher: "startup|clear",
2600
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/session-start.sh" full', timeout: 5 }],
2784
+ hooks: [
2785
+ { type: "command", command: `bash "${hooksRoot}/pre-compact-backup.sh"`, timeout: 30 },
2786
+ { type: "command", command: `bash "${hooksRoot}/engine-health.sh" full`, timeout: 10 },
2787
+ ],
2601
2788
  },
2789
+ ],
2790
+ // SessionEnd: memory-index-session.js DELIBERATELY omitted (dormant memory engine).
2791
+ SessionEnd: [
2602
2792
  {
2603
- matcher: "compact",
2604
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/session-start.sh" volatile', timeout: 5 }],
2793
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/session-end.sh"`, timeout: 10 }],
2794
+ },
2795
+ ],
2796
+ PermissionRequest: [
2797
+ {
2798
+ matcher: "Write|Edit",
2799
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/engine-permission-request.sh"`, timeout: 5 }],
2605
2800
  },
2606
2801
  ],
2607
2802
  ConfigChange: [
2608
2803
  {
2609
2804
  matcher: "user_settings",
2610
- hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/config-guard.sh"', timeout: 5 }],
2805
+ hooks: [{ type: "command", command: `bash "${hooksRoot}/config-guard.sh"`, timeout: 5 }],
2611
2806
  },
2612
2807
  ],
2613
2808
  },
@@ -2883,6 +3078,70 @@ function installTemplateFiles(bundleDir, vaultPath) {
2883
3078
  return { count, hashes };
2884
3079
  }
2885
3080
 
3081
+ // S1: install a single Markdown workflow to a Claude-style destination with the
3082
+ // config-first vault-resolution transform applied. Claude Code normally
3083
+ // hard-links workflows so editing src/ propagates live to ~/.claude/commands,
3084
+ // but the bare-git-rev-parse Pre-Flight block (see fixVaultResolution) must be
3085
+ // rewritten to a config-first resolver, and we MUST NOT mutate the shared inode.
3086
+ // So: if the body contains the bare block, write a transformed COPY (live-edit
3087
+ // hard-link is traded for a correct Pre-Flight on those files only); every other
3088
+ // workflow keeps the hard link. The transform is idempotent, so re-running is
3089
+ // safe. Shared by the fresh-install path (installWorkflows) AND the /update path
3090
+ // (direct update.md sync + smart-update loop) so updated workflows get the same
3091
+ // correct Pre-Flight as fresh installs.
3092
+ //
3093
+ // `presetContent` (optional): write/transform THIS string instead of reading
3094
+ // srcPath from disk — used by the smart-update conflict path, where the body to
3095
+ // install is an in-memory auto-merge result that may differ from the source file.
3096
+ //
3097
+ // Gap-3 fail-loud contract: if the body MATCHED the bare block (i.e. it NEEDS
3098
+ // the transform) we MUST emit the corrected copy. If reading/transforming throws
3099
+ // for a body that needs the fix, we DO NOT silently fall back to linkOrCopy
3100
+ // (which would ship the known-broken Pre-Flight) — the error propagates. Bodies
3101
+ // that don't match fall through safely: return false so the caller linkOrCopies.
3102
+ //
3103
+ // Returns true when a transformed copy was written; false when the file does not
3104
+ // need the transform and should be hard-linked/copied verbatim by the caller.
3105
+ function writeWorkflowTransformed(srcPath, destPath, presetContent) {
3106
+ let raw;
3107
+ if (presetContent !== undefined) {
3108
+ raw = presetContent;
3109
+ } else {
3110
+ // A read failure on a real source file is a hard error: we cannot prove the
3111
+ // file doesn't need the transform, so we must not silently linkOrCopy it.
3112
+ raw = fs.readFileSync(srcPath, "utf8");
3113
+ }
3114
+ let fixed;
3115
+ try {
3116
+ fixed = fixVaultResolution(raw);
3117
+ } catch (e) {
3118
+ // Transform itself threw. If the body needs the fix, fail loud; else no-op.
3119
+ if (workflowNeedsVaultFix(raw)) {
3120
+ throw new Error(`Vault-resolution transform failed for ${path.basename(destPath)}: ${e.message}`);
3121
+ }
3122
+ return false;
3123
+ }
3124
+ if (fixed === raw) {
3125
+ // No bare block (or already fixed). If preset content was supplied we still
3126
+ // own the write; otherwise the caller hard-links/copies the source verbatim.
3127
+ if (presetContent !== undefined) {
3128
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
3129
+ fs.writeFileSync(destPath, fixed, "utf8");
3130
+ return true;
3131
+ }
3132
+ return false;
3133
+ }
3134
+ // Defensive: if the transform claims to have changed the body but the bare
3135
+ // block somehow survives, refuse to ship it rather than emit a broken file.
3136
+ if (workflowNeedsVaultFix(fixed)) {
3137
+ throw new Error(`Vault-resolution transform did not remove the bare git-rev-parse block in ${path.basename(destPath)}`);
3138
+ }
3139
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
3140
+ if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
3141
+ fs.writeFileSync(destPath, fixed, "utf8");
3142
+ return true;
3143
+ }
3144
+
2886
3145
  function installWorkflows(bundleDir, destDir, selectedWorkflows) {
2887
3146
  const srcDir = path.join(bundleDir, "src", "workflows");
2888
3147
  if (!fs.existsSync(srcDir)) return 0;
@@ -2892,8 +3151,14 @@ function installWorkflows(bundleDir, destDir, selectedWorkflows) {
2892
3151
  let count = 0;
2893
3152
  for (const file of srcFiles) {
2894
3153
  if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
2895
- if (linkOrCopy(path.join(srcDir, file), path.join(destDir, file))) {
2896
- savePristineCopy("workflows", path.join(srcDir, file));
3154
+ const srcPath = path.join(srcDir, file);
3155
+ const destPath = path.join(destDir, file);
3156
+ // Gap-3: writeWorkflowTransformed throws if a workflow that NEEDS the
3157
+ // Pre-Flight fix fails to transform — we surface that instead of shipping
3158
+ // the broken source via linkOrCopy. Only unmatched workflows fall through.
3159
+ const wrote = writeWorkflowTransformed(srcPath, destPath);
3160
+ if (wrote || linkOrCopy(srcPath, destPath)) {
3161
+ savePristineCopy("workflows", srcPath);
2897
3162
  count++;
2898
3163
  }
2899
3164
  }
@@ -3059,6 +3324,49 @@ function parseWorkflowMd(filePath) {
3059
3324
  return { name, description, body, filePath };
3060
3325
  }
3061
3326
 
3327
+ // v4.7.9 (S1 ship-blocker): workflow Pre-Flight blocks resolve the vault root
3328
+ // with a bare `git rev-parse --show-toplevel`. A LIVE vault is NOT a git repo,
3329
+ // so that command exits non-zero and the agent never learns VAULT_ROOT — every
3330
+ // subsequent `{VAULT_ROOT}/...` read silently fails. The canonical resolution
3331
+ // (mirrored from resolveVaultPath) reads ~/.mover/config.json `vaultPath`
3332
+ // FIRST, then falls back to git, then walks up for 02_Areas/Engine. This
3333
+ // transform rewrites the bare fenced block at INSTALL time so we never mutate
3334
+ // the shared-inode source .md (Claude path uses copy-with-transform; the
3335
+ // converted agents already write copies). Idempotent: returns body unchanged
3336
+ // if the bare block isn't present (already fixed, or a workflow that doesn't
3337
+ // resolve the vault this way).
3338
+ const VAULT_RESOLVE_SNIPPET = [
3339
+ "```bash",
3340
+ "# Resolve VAULT_ROOT — config first (a live vault is NOT a git repo).",
3341
+ 'VAULT_ROOT="$(node -e \'try{console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.mover/config.json","utf8")).vaultPath||"")}catch(e){}\' 2>/dev/null)"',
3342
+ '[ -z "$VAULT_ROOT" ] && VAULT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"',
3343
+ 'if [ -z "$VAULT_ROOT" ]; then d="$PWD"; while [ "$d" != "/" ]; do [ -d "$d/02_Areas/Engine" ] && VAULT_ROOT="$d" && break; d="$(dirname "$d")"; done; fi',
3344
+ 'echo "$VAULT_ROOT"',
3345
+ "```",
3346
+ ].join("\n");
3347
+
3348
+ // Match the exact bare Pre-Flight block: ```bash\n git rev-parse --show-toplevel \n```
3349
+ // Tolerate surrounding blank lines / trailing whitespace inside the fence. Built
3350
+ // fresh on each call (no shared lastIndex) so .test() is stateless across callers.
3351
+ function bareVaultBlockRe() {
3352
+ return /```bash\s*\n[ \t]*git rev-parse --show-toplevel[ \t]*\n```/;
3353
+ }
3354
+
3355
+ // True when `body` still ships the bare git-rev-parse Pre-Flight block — i.e. it
3356
+ // NEEDS the config-first rewrite. Used by the install paths to decide between a
3357
+ // transformed copy and a verbatim hard-link, and to enforce the Gap-3 fail-loud
3358
+ // contract (a body that needs the fix must never ship untransformed).
3359
+ function workflowNeedsVaultFix(body) {
3360
+ if (typeof body !== "string") return false;
3361
+ return bareVaultBlockRe().test(body);
3362
+ }
3363
+
3364
+ function fixVaultResolution(body) {
3365
+ if (typeof body !== "string") return body;
3366
+ if (!workflowNeedsVaultFix(body)) return body;
3367
+ return body.replace(bareVaultBlockRe(), VAULT_RESOLVE_SNIPPET);
3368
+ }
3369
+
3062
3370
  // Gemini CLI: .md → .toml
3063
3371
  function mdToToml(wf) {
3064
3372
  const desc = wf.description.replace(/"/g, '\\"');
@@ -3125,6 +3433,10 @@ function installWorkflowsConverted(bundleDir, destDir, format, selectedWorkflows
3125
3433
  for (const file of fs.readdirSync(srcDir).filter((f) => f.endsWith(".md"))) {
3126
3434
  if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
3127
3435
  const wf = parseWorkflowMd(path.join(srcDir, file));
3436
+ // S1: config-first vault resolution. Safe — these agents always get a
3437
+ // freshly-written copy (no shared inode), so transforming the body here
3438
+ // only changes the emitted file, never the source .md.
3439
+ wf.body = fixVaultResolution(wf.body);
3128
3440
  const converted = conv.fn(wf);
3129
3441
  if (conv.dir) {
3130
3442
  // Directory-based output (e.g., Codex: morning/SKILL.md)
@@ -3279,51 +3591,182 @@ function installHooksForClaude(bundleDir, vaultPath) {
3279
3591
  try {
3280
3592
  const existing = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
3281
3593
  if (!existing.hooks) existing.hooks = {};
3282
- // Merge each hook event (Stop, PreToolUse, PreCompact)
3594
+
3595
+ // Gap-3 (S3 update path): repair existing Windows-broken hook commands.
3596
+ // A pre-v4.7.9 install wrote `bash "$HOME/.claude/hooks/X.sh" [args]` (the
3597
+ // form cmd.exe can't expand). The basename-only dedup below treats that as
3598
+ // "already present" and would neither add the corrected command NOR fix the
3599
+ // old one — leaving the hook dead on Windows forever. So FIRST rewrite any
3600
+ // existing command that targets one of OUR hook scripts via a $HOME/~/
3601
+ // relative path into the exact absolute-forward-slash form generateClaudeSettings
3602
+ // now emits (matched on the script + trailing-arg tail so the two
3603
+ // session-start variants map to their correct full/volatile replacements).
3604
+ // After rewriting in place, the dedup sees the new form and won't duplicate.
3605
+ const canonicalCmds = [];
3606
+ for (const entries of Object.values(newHooks)) {
3607
+ for (const grp of entries) for (const h of grp.hooks) canonicalCmds.push(h.command);
3608
+ }
3609
+ // Normalized tail signature: `<script>.sh<rest>` with quotes stripped and
3610
+ // backslashes normalized, so a QUOTED canonical command and an UNQUOTED (or
3611
+ // backslash-pathed) old command map to the SAME key. `<rest>` keeps the
3612
+ // trailing arg (` full` / ` volatile`) so the two session-start variants
3613
+ // stay distinct. Matching on the basename (after the last slash, either
3614
+ // direction) means $HOME / %USERPROFILE% / ~ / absolute prefixes all collapse
3615
+ // to the same signature.
3616
+ const tailSig = (cmd) => {
3617
+ if (typeof cmd !== "string") return null;
3618
+ const norm = cmd.replace(/["']/g, "").replace(/\\/g, "/");
3619
+ const m = norm.match(/([^\/\s]+\.sh)(.*)$/);
3620
+ return m ? (m[1] + m[2]).trim() : null;
3621
+ };
3622
+ const canonicalByTail = new Map();
3623
+ for (const cmd of canonicalCmds) {
3624
+ const t = tailSig(cmd);
3625
+ if (t) canonicalByTail.set(t, cmd);
3626
+ }
3627
+ // An "old form" is a command pointing at a .claude/hooks/<script>.sh via a
3628
+ // HOME-relative prefix ($HOME, %USERPROFILE%, or ~), in either slash
3629
+ // direction, quoted or not. Absolute paths and non-Mover commands are NOT
3630
+ // old forms (so they're left untouched).
3631
+ const isOldHomeForm = (cmd) =>
3632
+ typeof cmd === "string" &&
3633
+ /\.claude[\\/]+hooks[\\/]+[^"\\/]*\.sh/.test(cmd) &&
3634
+ /(\$HOME|\$\{HOME\}|%USERPROFILE%|(^|["'\s])~[\\/])/.test(cmd);
3635
+ const rewriteCmd = (cmd) => {
3636
+ if (!isOldHomeForm(cmd)) return cmd;
3637
+ const t = tailSig(cmd);
3638
+ const canon = t && canonicalByTail.get(t);
3639
+ return canon || cmd; // only rewrite scripts we actually own/emit
3640
+ };
3641
+ for (const event of Object.keys(existing.hooks)) {
3642
+ const groups = existing.hooks[event];
3643
+ if (!Array.isArray(groups)) continue;
3644
+ for (const grp of groups) {
3645
+ if (!grp || !Array.isArray(grp.hooks)) continue;
3646
+ for (const h of grp.hooks) {
3647
+ if (h && typeof h.command === "string") h.command = rewriteCmd(h.command);
3648
+ }
3649
+ }
3650
+ }
3651
+
3652
+ // Merge each hook event (Stop, PreToolUse, PreCompact).
3653
+ // Dedup runs AFTER the old-form rewrite above, so a repaired command is now
3654
+ // seen as present and is not re-added.
3655
+ //
3656
+ // v4.7.9: HOOK-LEVEL dedup keyed on a signature = script basename + trailing
3657
+ // args (quotes stripped, slash direction normalized). Three fixes over the
3658
+ // old basename-only check:
3659
+ // • Dedup per individual hook, not per group: a new group is appended with
3660
+ // only the hooks the event is MISSING. The old code (and an interim
3661
+ // group-level version) re-added an entire multi-hook group — e.g. Stop's
3662
+ // 3 hooks — whenever any single hook was absent, duplicating the ones
3663
+ // already present.
3664
+ // • Include the trailing arg in the signature so `session-start.sh full`
3665
+ // and `session-start.sh volatile` are distinct (multi-group event safe).
3666
+ // • Strip ALL quotes from both sides so `…session-start.sh" full` matches
3667
+ // `session-start.sh full` (the old single-quote .replace left the needle
3668
+ // unmatchable → duplicates).
3669
+ const cmdSig = (cmd) => {
3670
+ if (typeof cmd !== "string") return "";
3671
+ const m = cmd.match(/([^\\/"]+\.sh)("?.*)$/);
3672
+ const tail = m ? m[1] + m[2] : cmd;
3673
+ return tail.replace(/["']/g, "").replace(/\\/g, "/").trim();
3674
+ };
3283
3675
  for (const [event, entries] of Object.entries(newHooks)) {
3284
3676
  if (!existing.hooks[event]) {
3285
3677
  existing.hooks[event] = entries;
3286
3678
  } else {
3287
- // Check if our hooks are already registered (by command substring).
3288
- // v4.7.6: split on both / and \\ so the basename extraction works on
3289
- // Windows where command paths use backslashes. The forward-slash-only
3290
- // split returned the entire command string on Windows, which never
3291
- // matched the substring check, leading to duplicate hooks accumulating
3292
- // on every install.
3293
- const existingCmds = JSON.stringify(existing.hooks[event]);
3294
- const alreadyHas = entries[0].hooks.every(
3295
- (h) => existingCmds.includes(h.command.split(/[\\/]/).pop().replace('"', ""))
3296
- );
3297
- if (!alreadyHas) {
3298
- existing.hooks[event].push(...entries);
3679
+ const existingSigs = new Set();
3680
+ for (const grp of existing.hooks[event]) {
3681
+ if (grp && Array.isArray(grp.hooks)) for (const h of grp.hooks) existingSigs.add(cmdSig(h.command));
3682
+ }
3683
+ for (const grp of entries) {
3684
+ // Keep only hooks whose signature isn't already registered for this event.
3685
+ const missing = (grp.hooks || []).filter((h) => !existingSigs.has(cmdSig(h.command)));
3686
+ if (missing.length === 0) continue; // every hook in this group already present
3687
+ // Append a group carrying only the missing hooks (preserve matcher if any).
3688
+ const toAdd = grp.matcher !== undefined
3689
+ ? { matcher: grp.matcher, hooks: missing }
3690
+ : { hooks: missing };
3691
+ existing.hooks[event].push(toAdd);
3692
+ for (const h of missing) existingSigs.add(cmdSig(h.command));
3299
3693
  }
3300
3694
  }
3301
3695
  }
3696
+ // v4.7.7: set skillListingBudgetFraction so Mover OS users don't see
3697
+ // skill descriptions truncated by Claude Code's default 1% budget.
3698
+ // Per Claude Code's own /doctor output, Mover's ~63 skills need 2.5%
3699
+ // of context; 0.03 (3%) gives that exact size + small headroom for
3700
+ // user-added skills, without doubling the cost like 0.05 would.
3701
+ //
3702
+ // TRADEOFF: each session uses ~4,000 more system-prompt tokens than
3703
+ // Claude Code's default 1%. That marginally accelerates autocompaction
3704
+ // (~2% sooner) and rate-limit ticker. The benefit (full skill
3705
+ // descriptions available, no truncation message) outweighs the cost
3706
+ // for users who installed Mover OS specifically for the skill packs.
3707
+ //
3708
+ // Only set if not already configured — respect explicit user choice.
3709
+ // To opt out: manually set "skillListingBudgetFraction": 0.01 in your
3710
+ // ~/.claude/settings.json after install.
3711
+ if (existing.skillListingBudgetFraction === undefined) {
3712
+ existing.skillListingBudgetFraction = 0.03;
3713
+ }
3302
3714
  fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf8");
3303
3715
  } catch {
3304
3716
  // Corrupt settings — backup before overwrite
3305
3717
  const backupPath = settingsPath + ".bak";
3306
3718
  try { fs.copyFileSync(settingsPath, backupPath); } catch {}
3307
3719
  ln(` ${yellow("!")} Settings file corrupt — backed up to settings.json.bak, writing fresh`);
3308
- fs.writeFileSync(settingsPath, generateClaudeSettings(), "utf8");
3720
+ // Fresh write also sets the budget — combine generated hooks with the
3721
+ // skill budget setting so a fresh install doesn't trigger truncation.
3722
+ const fresh = JSON.parse(generateClaudeSettings());
3723
+ fresh.skillListingBudgetFraction = 0.03;
3724
+ fs.writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf8");
3309
3725
  }
3310
3726
  } else {
3311
- fs.writeFileSync(settingsPath, generateClaudeSettings(), "utf8");
3727
+ // Fresh install — write hooks AND the skill listing budget together.
3728
+ const fresh = JSON.parse(generateClaudeSettings());
3729
+ fresh.skillListingBudgetFraction = 0.03;
3730
+ fs.writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf8");
3312
3731
  }
3313
3732
 
3314
3733
  return count;
3315
3734
  }
3316
3735
 
3317
- // ─── Multi-agent hook installer (v4.7.5) ────────────────────────────────────
3318
- // Copies the MVP hook set (5 enforcement hooks + adapter + shared lib) to the
3319
- // agent's hook directory and writes its hook config. Used by Codex and Gemini.
3736
+ // ─── Multi-agent hook installer (v4.7.5 + v4.7.8 expansion) ─────────────────
3737
+ // Copies the MVP hook set + adapter + shared lib to the agent's hook directory
3738
+ // and writes its hook config. Used by Codex and Gemini.
3739
+ //
3740
+ // v4.7.8: expanded from 5 → 12 hook scripts to bring Codex to Claude Code
3741
+ // parity on UserPromptSubmit (per-prompt anti-drift), PostToolUse
3742
+ // engine-auto-commit, and Stop multi-script coverage (session-log-reminder,
3743
+ // context-staleness, iteration-check, engine-health, clock).
3744
+ //
3745
+ // PermissionRequest deferred: engine-permission-request.sh emits a nested
3746
+ // decision shape that mover-hook-adapter.js:isValidOutput rejects. Wiring it
3747
+ // would silently drop the decision. Adapter extension required first.
3320
3748
  const MVP_HOOK_SCRIPTS = [
3749
+ // SessionStart (2)
3321
3750
  "session-start.sh",
3751
+ "engine-health.sh", // also fires under Stop
3752
+ // UserPromptSubmit (1) — v4.7.8 NEW: per-prompt enforcement injection
3753
+ "user-prompt-enforce.sh",
3754
+ // PreToolUse (2)
3322
3755
  "engine-protection.sh",
3323
3756
  "git-safety.sh",
3757
+ // PermissionRequest (1) — v4.7.8 NEW: nested decision-shape via adapter
3758
+ "engine-permission-request.sh",
3759
+ // PostToolUse (2)
3324
3760
  "plan-sync-reminder.sh",
3761
+ "engine-auto-commit.sh", // v4.7.8 NEW: micro-commit Engine edits
3762
+ // Stop (6) — v4.7.8 NEW: session-log-reminder, context-staleness, iteration-check, clock
3325
3763
  "dirty-tree-guard.sh",
3326
- "mover-lib.sh", // sourced by the others
3764
+ "session-log-reminder.sh",
3765
+ "context-staleness.sh",
3766
+ "iteration-check.sh",
3767
+ "clock.sh",
3768
+ // Shared library (sourced by the others)
3769
+ "mover-lib.sh",
3327
3770
  ];
3328
3771
 
3329
3772
  // Section-aware TOML upsert. Sets [section] key = value, preserving comments,
@@ -3468,6 +3911,21 @@ function installHooksForCodex(bundleDir) {
3468
3911
  const configToml = path.join(codexDir, "config.toml");
3469
3912
  upsertTomlKey(configToml, "features", "codex_hooks", "true");
3470
3913
 
3914
+ // v4.7.8: Stash a marker so the post-install summary can show the
3915
+ // one-time "trust hooks in Codex IDE" hint. Codex IDE prompts users to
3916
+ // review/trust newly installed or modified hooks (security feature —
3917
+ // hooks run outside the sandbox). There is NO config-level bypass per
3918
+ // https://developers.openai.com/codex/hooks. CLI uses hooks.json
3919
+ // directly without the trust gate, so Mover OS works fine via CLI.
3920
+ // The trust prompt only affects users running Codex IDE.
3921
+ try {
3922
+ fs.writeFileSync(
3923
+ path.join(codexDir, ".mover-hook-install-stamp"),
3924
+ new Date().toISOString() + "\n",
3925
+ "utf8"
3926
+ );
3927
+ } catch {}
3928
+
3471
3929
  return count;
3472
3930
  }
3473
3931
 
@@ -4068,6 +4526,8 @@ function preflight() {
4068
4526
  // ─── CLI Command Handlers (stubs — implemented progressively) ────────────────
4069
4527
  const CLI_HANDLERS = {
4070
4528
  pulse: async (opts) => { await cmdPulse(opts); },
4529
+ dashboard: async (opts) => { await cmdPulse(opts); }, // alias — opens browser dashboard
4530
+ open: async (opts) => { await cmdOpen(opts); },
4071
4531
  // warm removed
4072
4532
  capture: async (opts) => { await cmdCapture(opts); },
4073
4533
  who: async (opts) => { await cmdWho(opts); },
@@ -4200,6 +4660,89 @@ async function cmdDoctor(opts) {
4200
4660
  statusLine(allOk ? "ok" : "warn", ` ${reg.name}`, checks.join(", "));
4201
4661
  }
4202
4662
 
4663
+ // ── Claude Code skill listing budget (v4.7.7) ────────────────────────
4664
+ // Claude Code 2.1.x truncates skill descriptions when the total exceeds
4665
+ // skillListingBudgetFraction (default 0.01 = 1% of context, ~2K tokens).
4666
+ // Users with 50+ skills installed see the "X descriptions dropped" message.
4667
+ // This is a Claude Code feature, not a Mover OS bug — but Mover users hit
4668
+ // it because we ship many skills. Help the user diagnose + tune.
4669
+ barLn();
4670
+ barLn(dim(" Claude Code:"));
4671
+ const claudeSkillsDir = path.join(home, ".claude", "skills");
4672
+ if (fs.existsSync(claudeSkillsDir)) {
4673
+ const skillCount = fs.readdirSync(claudeSkillsDir).filter(n => !n.startsWith(".")).length;
4674
+ statusLine(
4675
+ skillCount > 70 ? "warn" : "ok",
4676
+ " Skill count",
4677
+ `${skillCount} in ~/.claude/skills/`
4678
+ );
4679
+ if (skillCount > 50) {
4680
+ // Try to read settings.json to report current value.
4681
+ let currentFraction = null;
4682
+ try {
4683
+ const s = JSON.parse(fs.readFileSync(path.join(home, ".claude", "settings.json"), "utf8"));
4684
+ if (typeof s.skillListingBudgetFraction === "number") currentFraction = s.skillListingBudgetFraction;
4685
+ } catch {}
4686
+ const fracDisplay = currentFraction === null ? "unset (Claude Code default 0.01)" : String(currentFraction);
4687
+ barLn(dim(` Skill listing budget: ${fracDisplay}`));
4688
+ if (currentFraction === null || currentFraction < 0.025) {
4689
+ barLn(dim(` With ${skillCount} skills, Claude Code's default 1% budget truncates`));
4690
+ barLn(dim(` less-used skill descriptions ("X descriptions dropped" message).`));
4691
+ barLn(dim(` 'moveros install' sets skillListingBudgetFraction: 0.03 (3%) to fit`));
4692
+ barLn(dim(` Mover's manifest. Trade: ~4K extra tokens per session, all skills`));
4693
+ barLn(dim(` visible. To opt back to 1% default: edit ~/.claude/settings.json.`));
4694
+ } else {
4695
+ barLn(dim(` Budget covers ~${Math.floor(currentFraction * 100 * 25)} skills before truncation. Run /skills`));
4696
+ barLn(dim(` in Claude Code to disable specific skills.`));
4697
+ }
4698
+ }
4699
+ }
4700
+ // CLAUDE.md size check — Claude Code flags > 40K as "Large CLAUDE.md".
4701
+ const claudeMd = path.join(home, ".claude", "CLAUDE.md");
4702
+ if (fs.existsSync(claudeMd)) {
4703
+ const claudeMdSize = fs.statSync(claudeMd).size;
4704
+ statusLine(
4705
+ claudeMdSize > 40000 ? "warn" : "ok",
4706
+ " CLAUDE.md size",
4707
+ `${claudeMdSize.toLocaleString()} chars` + (claudeMdSize > 40000 ? " (Claude Code flags >40K)" : "")
4708
+ );
4709
+ }
4710
+
4711
+ // ── Codex CLI / MCP plugin conflict check (v4.7.8) ────────────────────
4712
+ // Unauthenticated MCP plugins in ~/.codex/config.toml cause codex exec to
4713
+ // hang or exit empty on complex prompts (the Vercel "claude-plugins-official"
4714
+ // plugin is the most common offender — it requires Bearer auth that's never
4715
+ // set, so the MCP transport errors and blocks the LLM response path).
4716
+ // We don't auto-disable user plugins, just surface the issue.
4717
+ const codexConfig = path.join(home, ".codex", "config.toml");
4718
+ if (fs.existsSync(codexConfig)) {
4719
+ barLn();
4720
+ barLn(dim(" Codex:"));
4721
+ const codexToml = fs.readFileSync(codexConfig, "utf8");
4722
+ // Find all [plugins."NAME"] sections with enabled = true
4723
+ const pluginRe = /\[plugins\."([^"]+)"\]\s*\nenabled\s*=\s*true/g;
4724
+ const enabledPlugins = [];
4725
+ let m;
4726
+ while ((m = pluginRe.exec(codexToml)) !== null) enabledPlugins.push(m[1]);
4727
+ // Known plugins that require auth and break Codex CLI when unauthenticated.
4728
+ // Add more here as users report similar issues.
4729
+ const KNOWN_BROKEN_AUTH = new Set([
4730
+ "vercel@claude-plugins-official",
4731
+ ]);
4732
+ const broken = enabledPlugins.filter(p => KNOWN_BROKEN_AUTH.has(p));
4733
+ if (broken.length > 0) {
4734
+ statusLine("warn", " MCP plugin auth", `${broken.length} plugin(s) likely unauthenticated`);
4735
+ for (const p of broken) {
4736
+ barLn(dim(` [plugins."${p}"] enabled=true — auth not configured`));
4737
+ }
4738
+ barLn(dim(` Symptom: codex exec hangs or returns empty on complex prompts.`));
4739
+ barLn(dim(` Fix: edit ~/.codex/config.toml, flip enabled=false for the above plugins`));
4740
+ barLn(dim(` (or configure the missing auth). Mover OS does NOT auto-disable plugins.`));
4741
+ } else if (enabledPlugins.length > 0) {
4742
+ statusLine("ok", " MCP plugin auth", `${enabledPlugins.length} plugin(s), none flagged`);
4743
+ }
4744
+ }
4745
+
4203
4746
  // Engine Git
4204
4747
  barLn();
4205
4748
  const engineGit = fs.existsSync(path.join(engineDir, ".git"));
@@ -4222,8 +4765,58 @@ async function cmdDoctor(opts) {
4222
4765
  barLn(dim(" Fix any issues above, then run /mover-check in your AI agent for deeper validation."));
4223
4766
  }
4224
4767
 
4225
- // ─── moveros pulse ──────────────────────────────────────────────────────────
4768
+ // ─── moveros pulse / dashboard ───────────────────────────────────────────────
4769
+ // Default: opens browser dashboard at http://127.0.0.1:3737. Use `--terminal` for legacy TUI.
4226
4770
  async function cmdPulse(opts) {
4771
+ const rest = opts.rest || [];
4772
+ const wantTerminal = rest.includes("--terminal");
4773
+ if (!wantTerminal) {
4774
+ const vault = resolveVaultPath(opts.vault);
4775
+ if (!vault) return;
4776
+ try {
4777
+ const dash = require("./src/dashboard");
4778
+ await dash.run({
4779
+ vault,
4780
+ version: (() => { try { return require("./package.json").version; } catch { return "unknown"; } })(),
4781
+ rest
4782
+ });
4783
+ return;
4784
+ } catch (err) {
4785
+ console.error(dim(` dashboard module error: ${err.message}`));
4786
+ console.error(dim(" falling back to terminal pulse — fix or run with --terminal explicitly"));
4787
+ }
4788
+ }
4789
+ await cmdPulseTerminal(opts);
4790
+ }
4791
+
4792
+ // ─── moveros open ────────────────────────────────────────────────────────────
4793
+ // Ensure the dashboard is running in the background, then open it in the
4794
+ // browser and exit. Used by the desktop launcher (shortcut.js → `node
4795
+ // install.js open`) and by anyone who wants the dashboard without holding a
4796
+ // terminal open. `pulse` is the foreground equivalent.
4797
+ async function cmdOpen(opts, { soft = false, route = "" } = {}) {
4798
+ const vault = resolveVaultPath(opts.vault);
4799
+ if (!vault) {
4800
+ console.error(dim(" Could not resolve vault. Run `moveros doctor` to diagnose."));
4801
+ return;
4802
+ }
4803
+ try {
4804
+ const dash = require("./src/dashboard");
4805
+ await dash.open({
4806
+ vault,
4807
+ version: (() => { try { return require("./package.json").version; } catch { return "unknown"; } })(),
4808
+ rest: opts.rest || [],
4809
+ soft,
4810
+ noOpen: !!opts.noOpen,
4811
+ route,
4812
+ });
4813
+ } catch (err) {
4814
+ console.error(dim(` dashboard module error: ${err.message}`));
4815
+ console.error(dim(" open it manually with: moveros pulse"));
4816
+ }
4817
+ }
4818
+
4819
+ async function cmdPulseTerminal(opts) {
4227
4820
  const vault = resolveVaultPath(opts.vault);
4228
4821
  if (!vault) {
4229
4822
  // handled by requireVault()
@@ -4557,7 +5150,32 @@ async function cmdSync(opts) {
4557
5150
  for (const c of candidates) {
4558
5151
  if (fs.existsSync(path.join(c, "src", "workflows"))) { bundleDir = c; break; }
4559
5152
  }
4560
- if (!bundleDir) { barLn(yellow(" Can't find Mover OS source. Run from the bundle directory.")); return; }
5153
+ // v4.7.9 (S6 ship-blocker): download fallback. `moveros sync` was disk-only
5154
+ // it resolved bundleDir from cwd / vault-bundle / __dirname and bailed if none
5155
+ // had src/. npm-global installs have NO src/ on disk (the CLI is just the
5156
+ // published install.js), so every npm-global user hit "Can't find Mover OS
5157
+ // source" and could never sync. Mirror cmdUpdateComprehensive: if a license
5158
+ // key is available, download the payload to ~/.mover and use that as the
5159
+ // source. Only bail when there is no disk source AND no key to download with.
5160
+ if (!bundleDir) {
5161
+ const key = opts.key || cfg.licenseKey;
5162
+ if (key) {
5163
+ const dlSp = spinner("Downloading Mover OS");
5164
+ try {
5165
+ bundleDir = await downloadPayload(key);
5166
+ dlSp.stop(green("Downloaded"));
5167
+ barLn();
5168
+ } catch (err) {
5169
+ dlSp.stop(red("Download failed"));
5170
+ barLn(yellow(` ${err.message}`));
5171
+ return;
5172
+ }
5173
+ } else {
5174
+ barLn(yellow(" Can't find Mover OS source on disk, and no license key to download it."));
5175
+ barLn(dim(" Run from the bundle directory, or set a license key (moveros install) so sync can fetch the payload."));
5176
+ return;
5177
+ }
5178
+ }
4561
5179
 
4562
5180
  // Compare source hash vs installed for each agent
4563
5181
  const srcRulesHash = (() => {
@@ -5520,7 +6138,7 @@ async function cmdHelp(opts) {
5520
6138
  ` ${dim("moveros capture --task \"Fix the login bug\"")}`,
5521
6139
  "",
5522
6140
  ` ${cyan("who")} Entity lookup — search People, Orgs, Places`,
5523
- ` ${dim("moveros who \"Ishaaq\" → everything you know about them")}`,
6141
+ ` ${dim("moveros who \"Sarah\" → everything you know about them")}`,
5524
6142
  "",
5525
6143
  ` ${cyan("prayer")} Mosque timetable — next prayer in your status line`,
5526
6144
  ` ${dim("Paste or fetch. Shows countdown. Optional.")}`,
@@ -5756,7 +6374,7 @@ async function cmdMainMenu() {
5756
6374
 
5757
6375
  // Context filter: hide irrelevant commands
5758
6376
  const contextFilter = (cmd) => {
5759
- if (!hasVault && ["pulse", "replay", "diff", "sync", "capture", "who", "context", "backup", "restore", "doctor"].includes(cmd)) return false;
6377
+ if (!hasVault && ["pulse", "open", "replay", "diff", "sync", "capture", "who", "context", "backup", "restore", "doctor"].includes(cmd)) return false;
5760
6378
  if (!hasPrayer && cmd === "prayer") return false;
5761
6379
  return true;
5762
6380
  };
@@ -5970,6 +6588,19 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5970
6588
  barLn();
5971
6589
  if (totalChanged > 0) {
5972
6590
  barLn(dim(" Restart your AI session to load updated rules and skills."));
6591
+ // v4.7.8: If Codex hooks were just installed/updated, point users at
6592
+ // the IDE trust step. Codex IDE shows new/modified hooks as "needs
6593
+ // review" — a security feature. CLI fires hooks fine without trust.
6594
+ try {
6595
+ const codexStamp = path.join(os.homedir(), ".codex", ".mover-hook-install-stamp");
6596
+ if (selectedIds.includes("codex") && fs.existsSync(codexStamp)) {
6597
+ const stampedAt = new Date(fs.readFileSync(codexStamp, "utf8").trim()).getTime();
6598
+ if (Date.now() - stampedAt < 5 * 60 * 1000) {
6599
+ barLn(dim(" Codex IDE: open Settings → Hooks, click Trust on the new entries."));
6600
+ barLn(dim(" (One-time per hook change. CLI fires them without the trust gate.)"));
6601
+ }
6602
+ }
6603
+ } catch {}
5973
6604
  barLn();
5974
6605
  }
5975
6606
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -6061,7 +6692,14 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
6061
6692
  for (const dir of cmdDirs) {
6062
6693
  const dest = path.join(dir, "update.md");
6063
6694
  if (fs.existsSync(dir)) {
6064
- fs.copyFileSync(updateSrc, dest);
6695
+ // Gap-1: apply the config-first vault-resolution transform here too —
6696
+ // update.md itself ships the bare git-rev-parse Pre-Flight, and this
6697
+ // direct sync would otherwise copy the known-broken block verbatim.
6698
+ // writeWorkflowTransformed returns false only for bodies that DON'T need
6699
+ // the fix; update.md always does, so the copy fallback never runs for it.
6700
+ if (!writeWorkflowTransformed(updateSrc, dest)) {
6701
+ fs.copyFileSync(updateSrc, dest);
6702
+ }
6065
6703
  }
6066
6704
  }
6067
6705
  statusLine("ok", "/update", "workflow synced");
@@ -6101,7 +6739,12 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
6101
6739
  switch (result.action) {
6102
6740
  case "install":
6103
6741
  case "safe-overwrite":
6104
- linkOrCopy(srcFile, destFile);
6742
+ // Gap-1: same config-first transform as the fresh-install path. The
6743
+ // smart-update loop previously linkOrCopy'd the raw source, shipping the
6744
+ // bare git-rev-parse Pre-Flight on update. writeWorkflowTransformed
6745
+ // emits a corrected copy for matched files and returns false (→ linkOrCopy)
6746
+ // for the rest, preserving the live-edit hard link where it's safe.
6747
+ if (!writeWorkflowTransformed(srcFile, destFile)) linkOrCopy(srcFile, destFile);
6105
6748
  savePristineCopy("workflows", srcFile);
6106
6749
  updated.push(file);
6107
6750
  break;
@@ -6115,7 +6758,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
6115
6758
  const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
6116
6759
  fs.mkdirSync(backupDir, { recursive: true });
6117
6760
  if (fs.existsSync(destFile)) fs.copyFileSync(destFile, path.join(backupDir, file));
6118
- linkOrCopy(srcFile, destFile);
6761
+ if (!writeWorkflowTransformed(srcFile, destFile)) linkOrCopy(srcFile, destFile);
6119
6762
  savePristineCopy("workflows", srcFile);
6120
6763
  updated.push(file);
6121
6764
  break;
@@ -6123,7 +6766,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
6123
6766
  case "conflict": {
6124
6767
  const mergeResult = tryAutoMerge("workflows", srcFile, destFile);
6125
6768
  if (mergeResult.success) {
6126
- fs.writeFileSync(destFile, mergeResult.content, "utf8");
6769
+ // Transform the MERGED body before writing: an auto-merge can carry the
6770
+ // bare block over from the user's old copy, so a verbatim write would
6771
+ // re-ship the broken Pre-Flight. presetContent feeds the in-memory merge.
6772
+ writeWorkflowTransformed(srcFile, destFile, mergeResult.content);
6127
6773
  savePristineCopy("workflows", srcFile);
6128
6774
  autoMerged.push(file);
6129
6775
  } else {
@@ -6322,6 +6968,17 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
6322
6968
  barLn();
6323
6969
  if (updated.length > 0 || autoMerged.length > 0) {
6324
6970
  barLn(dim(" Restart your AI session to load updated rules and skills."));
6971
+ // v4.7.8: Codex IDE hook-trust hint (same logic as install path above).
6972
+ try {
6973
+ const codexStamp = path.join(os.homedir(), ".codex", ".mover-hook-install-stamp");
6974
+ if (selectedIds.includes("codex") && fs.existsSync(codexStamp)) {
6975
+ const stampedAt = new Date(fs.readFileSync(codexStamp, "utf8").trim()).getTime();
6976
+ if (Date.now() - stampedAt < 5 * 60 * 1000) {
6977
+ barLn(dim(" Codex IDE: open Settings → Hooks, click Trust on the new entries."));
6978
+ barLn(dim(" (One-time per hook change. CLI fires them without the trust gate.)"));
6979
+ }
6980
+ }
6981
+ } catch {}
6325
6982
  barLn();
6326
6983
  }
6327
6984
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
@@ -6370,17 +7027,34 @@ async function main() {
6370
7027
  let bundleDir = path.resolve(__dirname);
6371
7028
  const startTime = Date.now();
6372
7029
 
7030
+ // ── Bare `moveros` — smart default (decided before the branded header so
7031
+ // launching the dashboard is instant, with no logo animation in the way).
7032
+ // Configured machine → open Mover Studio (soft: won't stack tabs on a
7033
+ // reflexive re-run). Fresh machine → fall through to the installer, which
7034
+ // lands the user on the dashboard's onboarding route when it finishes.
7035
+ // `moveros menu` restores the classic interactive launcher. ──
7036
+ if (!opts.command) {
7037
+ const configuredVault = resolveVaultPath(opts.vault);
7038
+ if (configuredVault) {
7039
+ recordCliUsage("open");
7040
+ await cmdOpen(opts, { soft: true });
7041
+ if (!opts.noOpen) ln(dim(" Tools: moveros menu · moveros doctor · moveros update"));
7042
+ return;
7043
+ }
7044
+ opts.command = "install"; // no vault yet → first run goes straight to install
7045
+ }
7046
+
6373
7047
  // ── TUI: Enter alternate screen ──
6374
7048
  enterAltScreen();
6375
7049
 
6376
7050
  // ── Intro: Logo plays once ──
6377
7051
  await printHeader();
6378
7052
 
6379
- // ── Route: no command interactive menu (persistent loop) ──
6380
- const lightCommands = ["pulse", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "uninstall", "test"];
7053
+ const lightCommands = ["pulse", "dashboard", "open", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "uninstall", "test"];
6381
7054
 
6382
- if (!opts.command) {
6383
- // Interactive loop stay open like a real app
7055
+ // ── `moveros menu` → classic interactive launcher (persistent loop) ──
7056
+ if (opts.command === "menu") {
7057
+ opts.command = "";
6384
7058
  while (true) {
6385
7059
  clearContent();
6386
7060
  ln(compactHeader());
@@ -7140,21 +7814,23 @@ async function main() {
7140
7814
  // ── Next steps (animated slide-in) ──
7141
7815
  ln(gray(" ─────────────────────────────────────────────"));
7142
7816
  ln();
7817
+ // Funnel v2 (owner spec): the dashboard IS the front door. Lead with
7818
+ // moveros; Obsidian is the file viewer, /setup the terminal alternative.
7143
7819
  await slideIn([
7144
7820
  ` ${bold("Next steps")}`,
7145
7821
  "",
7146
- ` ${cyan("1")} Open your vault in ${bold("Obsidian")}`,
7147
- ` ${dim("This is where you view and browse your files")}`,
7822
+ ` ${cyan("1")} Run ${bold("moveros")}`,
7823
+ ` ${dim("Opens your dashboard; setup starts there, a few plain questions")}`,
7148
7824
  "",
7149
- ` ${cyan("2")} Open the vault folder in your AI agent`,
7150
- ` ${dim("Installed: " + agentNames.join(", "))}`,
7825
+ ` ${cyan("2")} Open your vault in ${bold("Obsidian")} ${dim("(optional)")}`,
7826
+ ` ${dim("For viewing and browsing your files. Not required to start.")}`,
7151
7827
  "",
7152
7828
  ]);
7153
- ln(` ${cyan("3")} Enable the Obsidian theme`);
7829
+ ln(` ${cyan("3")} Enable the Obsidian theme ${dim("(if using Obsidian)")}`);
7154
7830
  ln(` ${dim("Settings → Appearance → CSS snippets → minimal-theme")}`);
7155
7831
  ln();
7156
- ln(` ${cyan("4")} Run ${bold("/setup")}`);
7157
- ln(` ${dim("Builds your Identity, Strategy, and Goals")}`);
7832
+ ln(` ${cyan("4")} Prefer the terminal? Run ${bold("/setup")} in your agent instead`);
7833
+ ln(` ${dim("The same setup, full depth: Identity, Strategy, Goals, projects")}`);
7158
7834
  ln();
7159
7835
  ln(gray(" ─────────────────────────────────────────────"));
7160
7836
  ln();
@@ -7167,13 +7843,30 @@ async function main() {
7167
7843
  ln();
7168
7844
  ln(` ${bold("CLI commands")} ${dim("(run from any terminal)")}`);
7169
7845
  ln();
7170
- ln(` ${green("▸")} ${bold("moveros pulse")} ${dim("Dashboard energy, tasks, streaks")}`);
7846
+ ln(` ${green("▸")} ${bold("moveros")} ${dim("Open your dashboard")}`);
7171
7847
  ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
7172
7848
  ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
7173
7849
  ln(` ${green("▸")} ${bold("moveros prayer")} ${dim("Set up prayer time reminders")}`);
7174
7850
  ln(` ${green("▸")} ${bold("moveros update")} ${dim("Update agents, rules, and skills")}`);
7175
- ln(` ${green("▸")} ${bold("moveros")} ${dim("Full menu with all commands")}`);
7851
+ ln(` ${green("▸")} ${bold("moveros menu")} ${dim("All commands (classic launcher)")}`);
7176
7852
  ln();
7853
+
7854
+ // ── Land on the dashboard's onboarding route (the payoff) ──
7855
+ // Engine is still templates pre-/setup, so force the cold-start Onboarding
7856
+ // cascade explicitly (?onboarding=1). TTY-only + best-effort: a dashboard
7857
+ // hiccup never fails or blocks the install. `--no-open` opts out (automation).
7858
+ if (!opts.noOpen && process.stdout.isTTY) {
7859
+ ln(gray(" ─────────────────────────────────────────────"));
7860
+ ln();
7861
+ try {
7862
+ const dash = require("./src/dashboard");
7863
+ await dash.open({ vault: vaultPath, version: VERSION, rest: [], soft: false, route: "/?onboarding=1" });
7864
+ ln(` ${dim("Opening onboarding in your browser. Next: run")} ${bold("/setup")} ${dim("in your AI agent.")}`);
7865
+ } catch (err) {
7866
+ ln(` ${dim("Your dashboard is ready — open it anytime with")} ${bold("moveros")}.`);
7867
+ }
7868
+ ln();
7869
+ }
7177
7870
  }
7178
7871
 
7179
7872
  main().catch((err) => {