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.
- package/install.js +763 -70
- 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: [
|
|
2557
2715
|
{
|
|
2716
|
+
matcher: "startup|clear|resume",
|
|
2558
2717
|
hooks: [
|
|
2559
|
-
{ type: "command", command:
|
|
2560
|
-
{ type: "command", command:
|
|
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:
|
|
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: [
|
|
2598
2783
|
{
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
|
|
2604
|
-
|
|
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:
|
|
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,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
|
-
|
|
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
|
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3319
|
-
//
|
|
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
|
-
"
|
|
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
|
-
|
|
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 \"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6383
|
-
|
|
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")}
|
|
7147
|
-
` ${dim("
|
|
7822
|
+
` ${cyan("1")} Run ${bold("moveros")}`,
|
|
7823
|
+
` ${dim("Opens your dashboard; setup starts there, a few plain questions")}`,
|
|
7148
7824
|
"",
|
|
7149
|
-
` ${cyan("2")} Open
|
|
7150
|
-
` ${dim("
|
|
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("
|
|
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
|
|
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")}
|
|
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) => {
|