mover-os 4.7.7 → 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.
- package/install.js +688 -68
- package/package.json +15 -3
- package/src/dashboard/build.js +1503 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +177 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +462 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +211 -0
- package/src/dashboard/lib/date-utils.js +35 -0
- package/src/dashboard/lib/distribution-parser.js +78 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +83 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-health.js +170 -0
- package/src/dashboard/lib/engine-writer.js +1685 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +304 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +212 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +470 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +226 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/strategy-parser.js +245 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +331 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2024 -0
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- 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
|
-
|
|
948
|
-
|
|
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: "
|
|
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
|
|
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.
|
|
2385
|
-
//
|
|
2386
|
-
// —
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
2715
|
+
{
|
|
2716
|
+
matcher: "startup|clear|resume",
|
|
2717
|
+
hooks: [
|
|
2718
|
+
{ type: "command", command: `bash "${hooksRoot}/session-start.sh" full`, timeout: 30 },
|
|
2719
|
+
{ type: "command", command: `bash "${hooksRoot}/engine-health.sh" full`, timeout: 20 },
|
|
2720
|
+
],
|
|
2721
|
+
},
|
|
2557
2722
|
{
|
|
2723
|
+
matcher: "compact",
|
|
2558
2724
|
hooks: [
|
|
2559
|
-
{ type: "command", command:
|
|
2560
|
-
{ type: "command", command:
|
|
2561
|
-
{ type: "command", command: 'bash "$HOME/.claude/hooks/context-staleness.sh"', timeout: 5 },
|
|
2725
|
+
{ type: "command", command: `bash "${hooksRoot}/session-start.sh" volatile`, timeout: 15 },
|
|
2726
|
+
{ type: "command", command: `bash "${hooksRoot}/engine-health.sh" full`, timeout: 20 },
|
|
2562
2727
|
],
|
|
2563
2728
|
},
|
|
2564
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
|
+
},
|
|
2740
|
+
],
|
|
2565
2741
|
PreToolUse: [
|
|
2566
2742
|
{
|
|
2567
2743
|
matcher: "Write|Edit",
|
|
2568
|
-
hooks: [{ type: "command", command:
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
2760
|
+
Stop: [
|
|
2582
2761
|
{
|
|
2583
|
-
hooks: [
|
|
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
|
-
|
|
2772
|
+
SubagentStart: [
|
|
2587
2773
|
{
|
|
2588
|
-
hooks: [{ type: "command", command:
|
|
2774
|
+
hooks: [{ type: "command", command: `bash "${hooksRoot}/subagent-context.sh"`, timeout: 5 }],
|
|
2589
2775
|
},
|
|
2590
2776
|
],
|
|
2591
|
-
|
|
2777
|
+
SubagentStop: [
|
|
2592
2778
|
{
|
|
2593
|
-
|
|
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
|
-
|
|
2782
|
+
PreCompact: [
|
|
2783
|
+
{
|
|
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
|
+
],
|
|
2788
|
+
},
|
|
2789
|
+
],
|
|
2790
|
+
// SessionEnd: memory-index-session.js DELIBERATELY omitted (dormant memory engine).
|
|
2791
|
+
SessionEnd: [
|
|
2598
2792
|
{
|
|
2599
|
-
|
|
2600
|
-
hooks: [{ type: "command", command: 'bash "$HOME/.claude/hooks/session-start.sh" full', timeout: 5 }],
|
|
2793
|
+
hooks: [{ type: "command", command: `bash "${hooksRoot}/session-end.sh"`, timeout: 10 }],
|
|
2601
2794
|
},
|
|
2795
|
+
],
|
|
2796
|
+
PermissionRequest: [
|
|
2602
2797
|
{
|
|
2603
|
-
matcher: "
|
|
2604
|
-
hooks: [{ type: "command", command:
|
|
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:
|
|
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
|
-
|
|
2896
|
-
|
|
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,23 +3591,105 @@ 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
|
-
|
|
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
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
(
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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
|
}
|
|
@@ -3339,16 +3733,40 @@ function installHooksForClaude(bundleDir, vaultPath) {
|
|
|
3339
3733
|
return count;
|
|
3340
3734
|
}
|
|
3341
3735
|
|
|
3342
|
-
// ─── Multi-agent hook installer (v4.7.5)
|
|
3343
|
-
// Copies the MVP hook set
|
|
3344
|
-
//
|
|
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.
|
|
3345
3748
|
const MVP_HOOK_SCRIPTS = [
|
|
3749
|
+
// SessionStart (2)
|
|
3346
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)
|
|
3347
3755
|
"engine-protection.sh",
|
|
3348
3756
|
"git-safety.sh",
|
|
3757
|
+
// PermissionRequest (1) — v4.7.8 NEW: nested decision-shape via adapter
|
|
3758
|
+
"engine-permission-request.sh",
|
|
3759
|
+
// PostToolUse (2)
|
|
3349
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
|
|
3350
3763
|
"dirty-tree-guard.sh",
|
|
3351
|
-
"
|
|
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",
|
|
3352
3770
|
];
|
|
3353
3771
|
|
|
3354
3772
|
// Section-aware TOML upsert. Sets [section] key = value, preserving comments,
|
|
@@ -3493,6 +3911,21 @@ function installHooksForCodex(bundleDir) {
|
|
|
3493
3911
|
const configToml = path.join(codexDir, "config.toml");
|
|
3494
3912
|
upsertTomlKey(configToml, "features", "codex_hooks", "true");
|
|
3495
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
|
+
|
|
3496
3929
|
return count;
|
|
3497
3930
|
}
|
|
3498
3931
|
|
|
@@ -4093,6 +4526,8 @@ function preflight() {
|
|
|
4093
4526
|
// ─── CLI Command Handlers (stubs — implemented progressively) ────────────────
|
|
4094
4527
|
const CLI_HANDLERS = {
|
|
4095
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); },
|
|
4096
4531
|
// warm removed
|
|
4097
4532
|
capture: async (opts) => { await cmdCapture(opts); },
|
|
4098
4533
|
who: async (opts) => { await cmdWho(opts); },
|
|
@@ -4273,6 +4708,41 @@ async function cmdDoctor(opts) {
|
|
|
4273
4708
|
);
|
|
4274
4709
|
}
|
|
4275
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
|
+
|
|
4276
4746
|
// Engine Git
|
|
4277
4747
|
barLn();
|
|
4278
4748
|
const engineGit = fs.existsSync(path.join(engineDir, ".git"));
|
|
@@ -4295,8 +4765,58 @@ async function cmdDoctor(opts) {
|
|
|
4295
4765
|
barLn(dim(" Fix any issues above, then run /mover-check in your AI agent for deeper validation."));
|
|
4296
4766
|
}
|
|
4297
4767
|
|
|
4298
|
-
// ─── moveros pulse
|
|
4768
|
+
// ─── moveros pulse / dashboard ───────────────────────────────────────────────
|
|
4769
|
+
// Default: opens browser dashboard at http://127.0.0.1:3737. Use `--terminal` for legacy TUI.
|
|
4299
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) {
|
|
4300
4820
|
const vault = resolveVaultPath(opts.vault);
|
|
4301
4821
|
if (!vault) {
|
|
4302
4822
|
// handled by requireVault()
|
|
@@ -4630,7 +5150,32 @@ async function cmdSync(opts) {
|
|
|
4630
5150
|
for (const c of candidates) {
|
|
4631
5151
|
if (fs.existsSync(path.join(c, "src", "workflows"))) { bundleDir = c; break; }
|
|
4632
5152
|
}
|
|
4633
|
-
|
|
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
|
+
}
|
|
4634
5179
|
|
|
4635
5180
|
// Compare source hash vs installed for each agent
|
|
4636
5181
|
const srcRulesHash = (() => {
|
|
@@ -5593,7 +6138,7 @@ async function cmdHelp(opts) {
|
|
|
5593
6138
|
` ${dim("moveros capture --task \"Fix the login bug\"")}`,
|
|
5594
6139
|
"",
|
|
5595
6140
|
` ${cyan("who")} Entity lookup — search People, Orgs, Places`,
|
|
5596
|
-
` ${dim("moveros who \"
|
|
6141
|
+
` ${dim("moveros who \"Sarah\" → everything you know about them")}`,
|
|
5597
6142
|
"",
|
|
5598
6143
|
` ${cyan("prayer")} Mosque timetable — next prayer in your status line`,
|
|
5599
6144
|
` ${dim("Paste or fetch. Shows countdown. Optional.")}`,
|
|
@@ -5829,7 +6374,7 @@ async function cmdMainMenu() {
|
|
|
5829
6374
|
|
|
5830
6375
|
// Context filter: hide irrelevant commands
|
|
5831
6376
|
const contextFilter = (cmd) => {
|
|
5832
|
-
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;
|
|
5833
6378
|
if (!hasPrayer && cmd === "prayer") return false;
|
|
5834
6379
|
return true;
|
|
5835
6380
|
};
|
|
@@ -6043,6 +6588,19 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6043
6588
|
barLn();
|
|
6044
6589
|
if (totalChanged > 0) {
|
|
6045
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 {}
|
|
6046
6604
|
barLn();
|
|
6047
6605
|
}
|
|
6048
6606
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -6134,7 +6692,14 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6134
6692
|
for (const dir of cmdDirs) {
|
|
6135
6693
|
const dest = path.join(dir, "update.md");
|
|
6136
6694
|
if (fs.existsSync(dir)) {
|
|
6137
|
-
|
|
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
|
+
}
|
|
6138
6703
|
}
|
|
6139
6704
|
}
|
|
6140
6705
|
statusLine("ok", "/update", "workflow synced");
|
|
@@ -6174,7 +6739,12 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6174
6739
|
switch (result.action) {
|
|
6175
6740
|
case "install":
|
|
6176
6741
|
case "safe-overwrite":
|
|
6177
|
-
|
|
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);
|
|
6178
6748
|
savePristineCopy("workflows", srcFile);
|
|
6179
6749
|
updated.push(file);
|
|
6180
6750
|
break;
|
|
@@ -6188,7 +6758,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6188
6758
|
const backupDir = path.join(home, ".mover", "backups", `pre-update-${new Date().toISOString().slice(0, 10)}`);
|
|
6189
6759
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
6190
6760
|
if (fs.existsSync(destFile)) fs.copyFileSync(destFile, path.join(backupDir, file));
|
|
6191
|
-
linkOrCopy(srcFile, destFile);
|
|
6761
|
+
if (!writeWorkflowTransformed(srcFile, destFile)) linkOrCopy(srcFile, destFile);
|
|
6192
6762
|
savePristineCopy("workflows", srcFile);
|
|
6193
6763
|
updated.push(file);
|
|
6194
6764
|
break;
|
|
@@ -6196,7 +6766,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6196
6766
|
case "conflict": {
|
|
6197
6767
|
const mergeResult = tryAutoMerge("workflows", srcFile, destFile);
|
|
6198
6768
|
if (mergeResult.success) {
|
|
6199
|
-
|
|
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);
|
|
6200
6773
|
savePristineCopy("workflows", srcFile);
|
|
6201
6774
|
autoMerged.push(file);
|
|
6202
6775
|
} else {
|
|
@@ -6395,6 +6968,17 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
|
|
|
6395
6968
|
barLn();
|
|
6396
6969
|
if (updated.length > 0 || autoMerged.length > 0) {
|
|
6397
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 {}
|
|
6398
6982
|
barLn();
|
|
6399
6983
|
}
|
|
6400
6984
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -6443,17 +7027,34 @@ async function main() {
|
|
|
6443
7027
|
let bundleDir = path.resolve(__dirname);
|
|
6444
7028
|
const startTime = Date.now();
|
|
6445
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
|
+
|
|
6446
7047
|
// ── TUI: Enter alternate screen ──
|
|
6447
7048
|
enterAltScreen();
|
|
6448
7049
|
|
|
6449
7050
|
// ── Intro: Logo plays once ──
|
|
6450
7051
|
await printHeader();
|
|
6451
7052
|
|
|
6452
|
-
|
|
6453
|
-
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"];
|
|
6454
7054
|
|
|
6455
|
-
|
|
6456
|
-
|
|
7055
|
+
// ── `moveros menu` → classic interactive launcher (persistent loop) ──
|
|
7056
|
+
if (opts.command === "menu") {
|
|
7057
|
+
opts.command = "";
|
|
6457
7058
|
while (true) {
|
|
6458
7059
|
clearContent();
|
|
6459
7060
|
ln(compactHeader());
|
|
@@ -7213,21 +7814,23 @@ async function main() {
|
|
|
7213
7814
|
// ── Next steps (animated slide-in) ──
|
|
7214
7815
|
ln(gray(" ─────────────────────────────────────────────"));
|
|
7215
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.
|
|
7216
7819
|
await slideIn([
|
|
7217
7820
|
` ${bold("Next steps")}`,
|
|
7218
7821
|
"",
|
|
7219
|
-
` ${cyan("1")}
|
|
7220
|
-
` ${dim("
|
|
7822
|
+
` ${cyan("1")} Run ${bold("moveros")}`,
|
|
7823
|
+
` ${dim("Opens your dashboard; setup starts there, a few plain questions")}`,
|
|
7221
7824
|
"",
|
|
7222
|
-
` ${cyan("2")} Open
|
|
7223
|
-
` ${dim("
|
|
7825
|
+
` ${cyan("2")} Open your vault in ${bold("Obsidian")} ${dim("(optional)")}`,
|
|
7826
|
+
` ${dim("For viewing and browsing your files. Not required to start.")}`,
|
|
7224
7827
|
"",
|
|
7225
7828
|
]);
|
|
7226
|
-
ln(` ${cyan("3")} Enable the Obsidian theme`);
|
|
7829
|
+
ln(` ${cyan("3")} Enable the Obsidian theme ${dim("(if using Obsidian)")}`);
|
|
7227
7830
|
ln(` ${dim("Settings → Appearance → CSS snippets → minimal-theme")}`);
|
|
7228
7831
|
ln();
|
|
7229
|
-
ln(` ${cyan("4")} Run ${bold("/setup")}`);
|
|
7230
|
-
ln(` ${dim("
|
|
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")}`);
|
|
7231
7834
|
ln();
|
|
7232
7835
|
ln(gray(" ─────────────────────────────────────────────"));
|
|
7233
7836
|
ln();
|
|
@@ -7240,13 +7843,30 @@ async function main() {
|
|
|
7240
7843
|
ln();
|
|
7241
7844
|
ln(` ${bold("CLI commands")} ${dim("(run from any terminal)")}`);
|
|
7242
7845
|
ln();
|
|
7243
|
-
ln(` ${green("▸")} ${bold("moveros
|
|
7846
|
+
ln(` ${green("▸")} ${bold("moveros")} ${dim("Open your dashboard")}`);
|
|
7244
7847
|
ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
|
|
7245
7848
|
ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
|
|
7246
7849
|
ln(` ${green("▸")} ${bold("moveros prayer")} ${dim("Set up prayer time reminders")}`);
|
|
7247
7850
|
ln(` ${green("▸")} ${bold("moveros update")} ${dim("Update agents, rules, and skills")}`);
|
|
7248
|
-
ln(` ${green("▸")} ${bold("moveros")}
|
|
7851
|
+
ln(` ${green("▸")} ${bold("moveros menu")} ${dim("All commands (classic launcher)")}`);
|
|
7249
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
|
+
}
|
|
7250
7870
|
}
|
|
7251
7871
|
|
|
7252
7872
|
main().catch((err) => {
|