@primitive.ai/prim 0.1.0-alpha.41 → 0.1.0-alpha.42

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/dist/index.js CHANGED
@@ -4,34 +4,40 @@ import {
4
4
  color,
5
5
  colorForArea,
6
6
  dim,
7
+ requireDurableIngestAcknowledgement,
7
8
  stripAnsi,
8
9
  stripControlChars
9
- } from "./chunk-AAGJFO7C.js";
10
+ } from "./chunk-QY75BQMF.js";
10
11
  import {
11
12
  checkAffectedDecisions,
12
13
  daemonOrDirectGet,
13
14
  formatDecisionsWarning
14
- } from "./chunk-UWAJCF7P.js";
15
+ } from "./chunk-BSZGI5RL.js";
15
16
  import {
16
17
  JOURNAL_DIR,
17
18
  SESSIONS_DIR,
18
19
  bucketStats,
19
20
  listBuckets,
20
21
  listFlushing,
22
+ pendingJournalStats,
21
23
  readMovesFromPath
22
- } from "./chunk-H4OR42TJ.js";
24
+ } from "./chunk-OKUXEBPT.js";
23
25
  import {
24
- activateRepoBestEffort,
25
- gitToplevel,
26
- setRepoActive
27
- } from "./chunk-LUPD2JSH.js";
26
+ fetchFeedbackCapability
27
+ } from "./chunk-NDIK4FBF.js";
28
+ import {
29
+ inspectWorkspaceId
30
+ } from "./chunk-IMAIBPUC.js";
28
31
  import {
32
+ activateRepoBestEffort,
29
33
  binFile,
30
34
  commandMatchesBin,
31
35
  detachedHookShimCommand,
36
+ gitToplevel,
32
37
  hookShimCommand,
38
+ setRepoActive,
33
39
  warmBinCache
34
- } from "./chunk-HSZPTVKU.js";
40
+ } from "./chunk-F7O7V6ZM.js";
35
41
  import {
36
42
  HttpError,
37
43
  REFRESH_TOKEN_PATH,
@@ -42,15 +48,15 @@ import {
42
48
  getSiteUrl,
43
49
  getTokenExpiresAt,
44
50
  saveTokenExpiry
45
- } from "./chunk-26VA3ADF.js";
51
+ } from "./chunk-W6PAKEDO.js";
46
52
  import {
47
53
  daemonIsLive,
48
54
  daemonRequest
49
55
  } from "./chunk-UTKQTZHL.js";
50
56
 
51
57
  // src/index.ts
52
- import { readFileSync as readFileSync11 } from "fs";
53
- import { dirname as dirname8, resolve as resolve5 } from "path";
58
+ import { readFileSync as readFileSync12 } from "fs";
59
+ import { dirname as dirname9, resolve as resolve6 } from "path";
54
60
  import { fileURLToPath as fileURLToPath4 } from "url";
55
61
  import { Command } from "commander";
56
62
  import updateNotifier from "update-notifier";
@@ -344,8 +350,8 @@ function registerAuthCommands(program2) {
344
350
  const state = base64url(randomBytes(16));
345
351
  const redirectUri = `http://${LOCALHOST}:${CALLBACK_PORT}/callback`;
346
352
  let settle;
347
- const outcome = new Promise((resolve6) => {
348
- settle = resolve6;
353
+ const outcome = new Promise((resolve7) => {
354
+ settle = resolve7;
349
355
  });
350
356
  const server = createServer((req, res) => {
351
357
  const url = new URL(req.url ?? "/", `http://${LOCALHOST}`);
@@ -359,12 +365,12 @@ function registerAuthCommands(program2) {
359
365
  res.end(page.html, () => settle(page.result));
360
366
  });
361
367
  try {
362
- await new Promise((resolve6, reject) => {
368
+ await new Promise((resolve7, reject) => {
363
369
  const onError = (err) => reject(err);
364
370
  server.once("error", onError);
365
371
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
366
372
  server.removeListener("error", onError);
367
- resolve6();
373
+ resolve7();
368
374
  });
369
375
  });
370
376
  } catch (err) {
@@ -536,16 +542,546 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
536
542
  // src/commands/claude-install.ts
537
543
  import {
538
544
  closeSync,
539
- existsSync as existsSync2,
545
+ existsSync as existsSync3,
540
546
  fsyncSync,
541
- mkdirSync as mkdirSync2,
547
+ mkdirSync as mkdirSync3,
542
548
  openSync,
549
+ readFileSync as readFileSync3,
550
+ renameSync as renameSync2,
551
+ writeFileSync as writeFileSync3
552
+ } from "fs";
553
+ import { homedir as homedir2 } from "os";
554
+ import { dirname as dirname3, join as join2 } from "path";
555
+
556
+ // src/daemon/launchd.ts
557
+ import { spawnSync } from "child_process";
558
+ import { createHash as createHash2 } from "crypto";
559
+ import {
560
+ chmodSync,
561
+ copyFileSync,
562
+ existsSync as existsSync2,
563
+ mkdirSync as mkdirSync2,
564
+ mkdtempSync,
543
565
  readFileSync as readFileSync2,
544
566
  renameSync,
567
+ rmSync as rmSync2,
568
+ statSync,
569
+ symlinkSync,
545
570
  writeFileSync as writeFileSync2
546
571
  } from "fs";
547
572
  import { homedir } from "os";
548
- import { dirname as dirname2, join } from "path";
573
+ import { dirname as dirname2, isAbsolute, join, resolve } from "path";
574
+ var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
575
+ var RUNTIME_SCHEMA_VERSION = 1;
576
+ var RUNTIME_DIR_MODE = 448;
577
+ var RUNTIME_FILE_MODE = 384;
578
+ var RUNTIME_LAUNCHER_MODE = 448;
579
+ var PLIST_FILE_MODE = 384;
580
+ var READY_TIMEOUT_MS = 15e3;
581
+ var READY_POLL_MS = 100;
582
+ var READY_PROBE_TIMEOUT_MS = 250;
583
+ var LIFECYCLE_LOCK_TIMEOUT_MS = 3e4;
584
+ var LIFECYCLE_LOCK_POLL_MS = 50;
585
+ var LIFECYCLE_LOCK_INIT_GRACE_MS = 2e3;
586
+ function xdgDataHome(options) {
587
+ const home = options.homeDir ?? homedir();
588
+ const configured = (options.env ?? process.env).XDG_DATA_HOME;
589
+ return configured && isAbsolute(configured) ? configured : join(home, ".local", "share");
590
+ }
591
+ function runtimePaths(options = {}) {
592
+ const dataDir = join(xdgDataHome(options), "prim");
593
+ const runtimeDir = join(dataDir, "runtime");
594
+ const currentLink = join(runtimeDir, "current");
595
+ return {
596
+ dataDir,
597
+ runtimeDir,
598
+ releasesDir: join(runtimeDir, "releases"),
599
+ currentLink,
600
+ daemonEntry: join(currentLink, "prim-daemon-server"),
601
+ statuslineEntry: join(currentLink, "prim-statusline"),
602
+ statuslineLauncher: join(runtimeDir, "prim-statusline"),
603
+ manifestPath: join(currentLink, "manifest.json"),
604
+ disabledMarker: join(dataDir, "daemon.disabled"),
605
+ lifecycleLockDir: join(dataDir, "daemon.lifecycle.lock")
606
+ };
607
+ }
608
+ function findPackageVersion(entryFile) {
609
+ let dir = dirname2(resolve(entryFile));
610
+ for (let depth = 0; depth < 6; depth++) {
611
+ const packagePath = join(dir, "package.json");
612
+ if (existsSync2(packagePath)) {
613
+ try {
614
+ const manifest = JSON.parse(readFileSync2(packagePath, "utf8"));
615
+ if (manifest.name === "@primitive.ai/prim" && manifest.version) {
616
+ return manifest.version;
617
+ }
618
+ } catch {
619
+ }
620
+ }
621
+ const parent = dirname2(dir);
622
+ if (parent === dir) break;
623
+ dir = parent;
624
+ }
625
+ throw new Error(`cannot determine @primitive.ai/prim version from ${entryFile}`);
626
+ }
627
+ function readRuntimeManifest(path) {
628
+ try {
629
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
630
+ if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server") {
631
+ return null;
632
+ }
633
+ return parsed;
634
+ } catch {
635
+ return null;
636
+ }
637
+ }
638
+ function sameRuntime(current, desired, paths) {
639
+ return Boolean(
640
+ current && current.version === desired.version && current.nodePath === desired.nodePath && current.statuslineFile === desired.statuslineFile && existsSync2(paths.daemonEntry) && (desired.statuslineFile === void 0 || existsSync2(paths.statuslineEntry))
641
+ );
642
+ }
643
+ function atomicSymlink(target, linkPath) {
644
+ const tempLink = `${linkPath}.tmp-${process.pid}-${createHash2("sha256").update(`${target}\0${Date.now()}\0${Math.random()}`).digest("hex").slice(0, 12)}`;
645
+ symlinkSync(target, tempLink);
646
+ try {
647
+ renameSync(tempLink, linkPath);
648
+ } catch (error) {
649
+ rmSync2(tempLink, { force: true });
650
+ throw error;
651
+ }
652
+ }
653
+ function stageRuntime(options = {}) {
654
+ const paths = runtimePaths(options);
655
+ const daemonSource = options.daemonSource ?? binFile("prim-daemon-server");
656
+ if (!daemonSource || !existsSync2(daemonSource)) {
657
+ throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
658
+ }
659
+ const configuredStatusline = options.statuslineSource === void 0 ? binFile("prim-statusline") : options.statuslineSource;
660
+ const statuslineSource = configuredStatusline && existsSync2(configuredStatusline) ? configuredStatusline : null;
661
+ const nodePath = resolve(options.nodePath ?? process.execPath);
662
+ const version = options.version ?? findPackageVersion(daemonSource);
663
+ const desired = {
664
+ schemaVersion: RUNTIME_SCHEMA_VERSION,
665
+ version,
666
+ nodePath,
667
+ daemonFile: "prim-daemon-server",
668
+ ...statuslineSource ? { statuslineFile: "prim-statusline" } : {},
669
+ stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
670
+ };
671
+ const current = readRuntimeManifest(paths.manifestPath);
672
+ if (sameRuntime(current, desired, paths)) {
673
+ if (current?.statuslineFile) {
674
+ atomicWrite(
675
+ paths.statuslineLauncher,
676
+ statuslineLauncherContent(paths, current.nodePath),
677
+ RUNTIME_LAUNCHER_MODE
678
+ );
679
+ } else {
680
+ rmSync2(paths.statuslineLauncher, { force: true });
681
+ }
682
+ return { changed: false, manifest: current, paths };
683
+ }
684
+ mkdirSync2(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
685
+ chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
686
+ chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
687
+ const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
688
+ chmodSync(stagingDir, RUNTIME_DIR_MODE);
689
+ try {
690
+ const daemonTarget = join(stagingDir, desired.daemonFile);
691
+ copyFileSync(daemonSource, daemonTarget);
692
+ chmodSync(daemonTarget, RUNTIME_FILE_MODE);
693
+ if (statuslineSource && desired.statuslineFile) {
694
+ const statuslineTarget = join(stagingDir, desired.statuslineFile);
695
+ copyFileSync(statuslineSource, statuslineTarget);
696
+ chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
697
+ }
698
+ const manifestTarget = join(stagingDir, "manifest.json");
699
+ writeFileSync2(manifestTarget, `${JSON.stringify(desired, null, 2)}
700
+ `, {
701
+ encoding: "utf8",
702
+ mode: RUNTIME_FILE_MODE,
703
+ flag: "wx"
704
+ });
705
+ const releaseName = `release-${createHash2("sha256").update(
706
+ `${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
707
+ ).digest("hex").slice(0, 16)}`;
708
+ const releaseDir = join(paths.releasesDir, releaseName);
709
+ renameSync(stagingDir, releaseDir);
710
+ atomicSymlink(join("releases", releaseName), paths.currentLink);
711
+ if (desired.statuslineFile) {
712
+ atomicWrite(
713
+ paths.statuslineLauncher,
714
+ statuslineLauncherContent(paths, desired.nodePath),
715
+ RUNTIME_LAUNCHER_MODE
716
+ );
717
+ } else {
718
+ rmSync2(paths.statuslineLauncher, { force: true });
719
+ }
720
+ } catch (error) {
721
+ rmSync2(stagingDir, { recursive: true, force: true });
722
+ throw error;
723
+ }
724
+ return { changed: true, manifest: desired, paths };
725
+ }
726
+ function shellQuote(value) {
727
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
728
+ }
729
+ function statuslineLauncherContent(paths, nodePath) {
730
+ return `#!/bin/sh
731
+ exec ${shellQuote(nodePath)} ${shellQuote(paths.statuslineEntry)}
732
+ `;
733
+ }
734
+ function runtimeStatuslineCommand(options = {}) {
735
+ return shellQuote(runtimePaths(options).statuslineLauncher);
736
+ }
737
+ function xmlEscape(value) {
738
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
739
+ }
740
+ function generateLaunchAgentPlist(config) {
741
+ const apiUrl = config.apiUrl ? `
742
+ <key>PRIM_API_URL</key>
743
+ <string>${xmlEscape(config.apiUrl)}</string>` : "";
744
+ return `<?xml version="1.0" encoding="UTF-8"?>
745
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
746
+ <plist version="1.0">
747
+ <dict>
748
+ <key>Label</key>
749
+ <string>${LAUNCHD_LABEL}</string>
750
+ <key>ProgramArguments</key>
751
+ <array>
752
+ <string>${xmlEscape(config.nodePath)}</string>
753
+ <string>${xmlEscape(config.daemonPath)}</string>
754
+ </array>
755
+ <key>RunAtLoad</key>
756
+ <true/>
757
+ <key>KeepAlive</key>
758
+ <true/>
759
+ <key>ThrottleInterval</key>
760
+ <integer>10</integer>
761
+ <key>ProcessType</key>
762
+ <string>Background</string>
763
+ <key>Umask</key>
764
+ <integer>63</integer>
765
+ <key>WorkingDirectory</key>
766
+ <string>${xmlEscape(config.workingDirectory)}</string>
767
+ <key>StandardOutPath</key>
768
+ <string>${xmlEscape(config.logPath)}</string>
769
+ <key>StandardErrorPath</key>
770
+ <string>${xmlEscape(config.logPath)}</string>
771
+ <key>EnvironmentVariables</key>
772
+ <dict>
773
+ <key>PRIM_RUNTIME_VERSION</key>
774
+ <string>${xmlEscape(config.runtimeVersion)}</string>${apiUrl}
775
+ </dict>
776
+ </dict>
777
+ </plist>
778
+ `;
779
+ }
780
+ function launchdPaths(options = {}) {
781
+ const home = options.homeDir ?? homedir();
782
+ const uid = options.uid ?? process.getuid?.();
783
+ if (uid === void 0) throw new Error("cannot determine uid for launchd user domain");
784
+ return {
785
+ domainTarget: `gui/${uid}`,
786
+ serviceTarget: `gui/${uid}/${LAUNCHD_LABEL}`,
787
+ plistPath: join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`),
788
+ logPath: join(home, ".config", "prim", "daemon.log")
789
+ };
790
+ }
791
+ var runLaunchctl = (args) => {
792
+ const result = spawnSync("/bin/launchctl", args, { encoding: "utf8" });
793
+ return {
794
+ status: result.status,
795
+ stdout: result.stdout ?? "",
796
+ stderr: result.error?.message ?? result.stderr ?? ""
797
+ };
798
+ };
799
+ function parseLaunchdService(result) {
800
+ if (result.status !== 0) return { loaded: false };
801
+ const state = /^\s*state = (.+?)\s*$/m.exec(result.stdout)?.[1];
802
+ const rawPid = /^\s*pid = (\d+)\s*$/m.exec(result.stdout)?.[1];
803
+ const pid = rawPid ? Number(rawPid) : void 0;
804
+ return { loaded: true, state, pid, raw: result.stdout };
805
+ }
806
+ function getLaunchdService(options = {}) {
807
+ const paths = launchdPaths(options);
808
+ return parseLaunchdService((options.runner ?? runLaunchctl)(["print", paths.serviceTarget]));
809
+ }
810
+ function atomicWrite(path, content, mode) {
811
+ try {
812
+ if (readFileSync2(path, "utf8") === content) {
813
+ chmodSync(path, mode);
814
+ return false;
815
+ }
816
+ } catch {
817
+ }
818
+ mkdirSync2(dirname2(path), { recursive: true });
819
+ const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
820
+ writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
821
+ try {
822
+ renameSync(temp, path);
823
+ } catch (error) {
824
+ rmSync2(temp, { force: true });
825
+ throw error;
826
+ }
827
+ return true;
828
+ }
829
+ function launchctlOrThrow(runner, args, operation) {
830
+ const result = runner(args);
831
+ if (result.status !== 0) {
832
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit ${String(result.status)}`;
833
+ throw new Error(`launchctl ${operation} failed: ${detail}`);
834
+ }
835
+ return result;
836
+ }
837
+ function defaultSleep(ms) {
838
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
839
+ }
840
+ async function identifyDaemon(timeoutMs) {
841
+ const snapshot = await daemonRequest("status_snapshot", {}, { timeoutMs });
842
+ return typeof snapshot?.pid === "number" ? snapshot.pid : null;
843
+ }
844
+ function processIsAlive(pid) {
845
+ try {
846
+ process.kill(pid, 0);
847
+ return true;
848
+ } catch (error) {
849
+ return error.code === "EPERM";
850
+ }
851
+ }
852
+ function lifecycleOwnerPath(lockDir) {
853
+ return join(lockDir, "owner.json");
854
+ }
855
+ function readLifecycleOwner(lockDir) {
856
+ try {
857
+ const owner = JSON.parse(
858
+ readFileSync2(lifecycleOwnerPath(lockDir), "utf8")
859
+ );
860
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
861
+ return null;
862
+ }
863
+ return owner;
864
+ } catch {
865
+ return null;
866
+ }
867
+ }
868
+ function sameLifecycleOwner(a, b) {
869
+ return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
870
+ }
871
+ function tryTakeLifecycleLock(lockDir, now) {
872
+ try {
873
+ mkdirSync2(lockDir, { mode: RUNTIME_DIR_MODE });
874
+ } catch (error) {
875
+ if (error.code === "EEXIST") return null;
876
+ throw error;
877
+ }
878
+ const owner = {
879
+ pid: process.pid,
880
+ nonce: createHash2("sha256").update(`${process.pid}\0${now}\0${Math.random()}`).digest("hex"),
881
+ createdAt: now
882
+ };
883
+ try {
884
+ writeFileSync2(lifecycleOwnerPath(lockDir), `${JSON.stringify(owner)}
885
+ `, {
886
+ encoding: "utf8",
887
+ mode: RUNTIME_FILE_MODE,
888
+ flag: "wx"
889
+ });
890
+ return owner;
891
+ } catch (error) {
892
+ rmSync2(lockDir, { recursive: true, force: true });
893
+ throw error;
894
+ }
895
+ }
896
+ function recoverStaleLifecycleLock(lockDir, now, alive) {
897
+ const owner = readLifecycleOwner(lockDir);
898
+ if (owner) {
899
+ if (alive(owner.pid)) return false;
900
+ if (!sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) return false;
901
+ rmSync2(lockDir, { recursive: true, force: true });
902
+ return true;
903
+ }
904
+ try {
905
+ const before = statSync(lockDir).mtimeMs;
906
+ if (now - before < LIFECYCLE_LOCK_INIT_GRACE_MS) return false;
907
+ if (readLifecycleOwner(lockDir)) return false;
908
+ if (statSync(lockDir).mtimeMs !== before) return false;
909
+ rmSync2(lockDir, { recursive: true, force: true });
910
+ return true;
911
+ } catch {
912
+ return true;
913
+ }
914
+ }
915
+ async function withDaemonLifecycleLock(operation, options = {}) {
916
+ const lockDir = runtimePaths(options).lifecycleLockDir;
917
+ mkdirSync2(dirname2(lockDir), { recursive: true, mode: RUNTIME_DIR_MODE });
918
+ const control = options.lifecycleLock;
919
+ const nowMs = control?.nowMs ?? Date.now;
920
+ const sleepFor = control?.sleep ?? defaultSleep;
921
+ const alive = control?.processAlive ?? processIsAlive;
922
+ const deadline = nowMs() + (control?.timeoutMs ?? LIFECYCLE_LOCK_TIMEOUT_MS);
923
+ let owner = null;
924
+ while (!owner) {
925
+ const now = nowMs();
926
+ owner = tryTakeLifecycleLock(lockDir, now);
927
+ if (owner) break;
928
+ recoverStaleLifecycleLock(lockDir, now, alive);
929
+ if (now >= deadline) {
930
+ throw new Error(`timed out waiting for daemon lifecycle lock ${lockDir}`);
931
+ }
932
+ await sleepFor(control?.pollMs ?? LIFECYCLE_LOCK_POLL_MS);
933
+ }
934
+ try {
935
+ return await operation();
936
+ } finally {
937
+ if (sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) {
938
+ rmSync2(lockDir, { recursive: true, force: true });
939
+ }
940
+ }
941
+ }
942
+ async function stopVerifiedLegacyDaemon(homeDir) {
943
+ const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
944
+ let pid;
945
+ try {
946
+ pid = Number(readFileSync2(pidPath, "utf8").trim());
947
+ } catch {
948
+ throw new Error("an unsupervised daemon answered, but it has no verifiable pidfile");
949
+ }
950
+ const snapshot = await daemonRequest("status_snapshot", {}, { timeoutMs: 500 });
951
+ if (!Number.isInteger(pid) || pid <= 0 || !processIsAlive(pid) || snapshot?.pid !== pid) {
952
+ throw new Error("refusing to signal an unsupervised daemon whose pid cannot be verified");
953
+ }
954
+ process.kill(pid, "SIGTERM");
955
+ const deadline = Date.now() + 5e3;
956
+ while (Date.now() < deadline && processIsAlive(pid)) {
957
+ await defaultSleep(100);
958
+ }
959
+ if (processIsAlive(pid)) {
960
+ throw new Error(`verified legacy daemon pid=${pid} did not stop within 5000ms`);
961
+ }
962
+ return true;
963
+ }
964
+ async function waitForReady(probe, sleep2) {
965
+ const deadline = Date.now() + READY_TIMEOUT_MS;
966
+ while (Date.now() < deadline) {
967
+ if (await probe(READY_PROBE_TIMEOUT_MS)) return true;
968
+ await sleep2(READY_POLL_MS);
969
+ }
970
+ return probe(READY_PROBE_TIMEOUT_MS);
971
+ }
972
+ function daemonExplicitlyDisabled(options = {}) {
973
+ return existsSync2(runtimePaths(options).disabledMarker);
974
+ }
975
+ function setDaemonExplicitlyDisabled(disabled, options = {}) {
976
+ const marker = runtimePaths(options).disabledMarker;
977
+ if (!disabled) {
978
+ rmSync2(marker, { force: true });
979
+ return;
980
+ }
981
+ mkdirSync2(dirname2(marker), { recursive: true, mode: RUNTIME_DIR_MODE });
982
+ writeFileSync2(marker, "disabled by `prim daemon stop`\n", {
983
+ encoding: "utf8",
984
+ mode: RUNTIME_FILE_MODE
985
+ });
986
+ chmodSync(marker, RUNTIME_FILE_MODE);
987
+ }
988
+ async function ensureMacDaemon(options = {}) {
989
+ return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
990
+ }
991
+ async function ensureMacDaemonLocked(options) {
992
+ if (!options.explicitlyStarted && daemonExplicitlyDisabled(options)) {
993
+ return {
994
+ state: "disabled",
995
+ action: "none",
996
+ runtimeChanged: false,
997
+ plistChanged: false,
998
+ responding: false,
999
+ service: getLaunchdService(options)
1000
+ };
1001
+ }
1002
+ if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
1003
+ const runtime = stageRuntime(options);
1004
+ const servicePaths = launchdPaths(options);
1005
+ mkdirSync2(dirname2(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
1006
+ writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
1007
+ chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
1008
+ const plist = generateLaunchAgentPlist({
1009
+ nodePath: runtime.manifest.nodePath,
1010
+ daemonPath: runtime.paths.daemonEntry,
1011
+ runtimeVersion: runtime.manifest.version,
1012
+ logPath: servicePaths.logPath,
1013
+ workingDirectory: options.homeDir ?? homedir(),
1014
+ apiUrl: (options.env ?? process.env).PRIM_API_URL
1015
+ });
1016
+ const plistChanged = atomicWrite(servicePaths.plistPath, plist, PLIST_FILE_MODE);
1017
+ const runner = options.runner ?? runLaunchctl;
1018
+ let service = getLaunchdService({ ...options, runner });
1019
+ let action = "none";
1020
+ if (service.loaded && plistChanged) {
1021
+ launchctlOrThrow(runner, ["bootout", servicePaths.serviceTarget], "bootout");
1022
+ launchctlOrThrow(
1023
+ runner,
1024
+ ["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
1025
+ "bootstrap"
1026
+ );
1027
+ action = "reload";
1028
+ } else if (!service.loaded) {
1029
+ if (await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS)) {
1030
+ await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))();
1031
+ }
1032
+ launchctlOrThrow(
1033
+ runner,
1034
+ ["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
1035
+ "bootstrap"
1036
+ );
1037
+ action = "bootstrap";
1038
+ } else {
1039
+ const healthy = await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS);
1040
+ const socketPid2 = healthy ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
1041
+ const ownsSocket = service.pid !== void 0 && socketPid2 !== null && service.pid === socketPid2;
1042
+ if (healthy && socketPid2 !== null && !ownsSocket) {
1043
+ await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))();
1044
+ }
1045
+ if (runtime.changed || options.forceRestart || !healthy || !ownsSocket) {
1046
+ launchctlOrThrow(runner, ["kickstart", "-k", servicePaths.serviceTarget], "kickstart");
1047
+ action = "kickstart";
1048
+ }
1049
+ }
1050
+ const responding = await waitForReady(
1051
+ options.probe ?? daemonIsLive,
1052
+ options.sleep ?? defaultSleep
1053
+ );
1054
+ service = getLaunchdService({ ...options, runner });
1055
+ const socketPid = responding ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
1056
+ const running = responding && service.loaded && service.pid !== void 0 && socketPid !== null && service.pid === socketPid;
1057
+ return {
1058
+ state: running ? "running" : "unhealthy",
1059
+ action,
1060
+ runtimeChanged: runtime.changed,
1061
+ plistChanged,
1062
+ responding,
1063
+ ...socketPid === null ? {} : { socketPid },
1064
+ service,
1065
+ runtime
1066
+ };
1067
+ }
1068
+ async function bootoutMacDaemon(options = {}) {
1069
+ return withDaemonLifecycleLock(() => bootoutMacDaemonLocked(options), options);
1070
+ }
1071
+ async function bootoutMacDaemonLocked(options) {
1072
+ setDaemonExplicitlyDisabled(true, options);
1073
+ const runner = options.runner ?? runLaunchctl;
1074
+ const paths = launchdPaths(options);
1075
+ const service = getLaunchdService({ ...options, runner });
1076
+ if (service.loaded) {
1077
+ launchctlOrThrow(runner, ["bootout", paths.serviceTarget], "bootout");
1078
+ return { wasLoaded: true, legacyStopped: false, service };
1079
+ }
1080
+ const legacyStopped = await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS) ? await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))() : false;
1081
+ return { wasLoaded: false, legacyStopped, service };
1082
+ }
1083
+
1084
+ // src/commands/claude-install.ts
549
1085
  var CAPTURE_BIN = "prim-hook";
550
1086
  var GATE_BIN = "prim-pre-tool-use";
551
1087
  var POST_TOOL_USE_BIN = "prim-post-tool-use";
@@ -563,11 +1099,11 @@ var PRIM_BINS = [
563
1099
  SESSION_END_BIN
564
1100
  ];
565
1101
  var JSON_INDENT = 2;
566
- var USER_SCOPE_PATH = join(homedir(), ".claude", "settings.json");
1102
+ var USER_SCOPE_PATH = join2(homedir2(), ".claude", "settings.json");
567
1103
  function projectRoot() {
568
1104
  return gitToplevel() ?? process.cwd();
569
1105
  }
570
- var projectScopePath = () => join(projectRoot(), ".claude", "settings.json");
1106
+ var projectScopePath = () => join2(projectRoot(), ".claude", "settings.json");
571
1107
  var CAPTURE_EVENTS = [
572
1108
  "SessionStart",
573
1109
  "UserPromptSubmit",
@@ -609,10 +1145,10 @@ function settingsPathFor(scope) {
609
1145
  return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
610
1146
  }
611
1147
  function readSettings(path) {
612
- if (!existsSync2(path)) {
1148
+ if (!existsSync3(path)) {
613
1149
  return {};
614
1150
  }
615
- const raw = readFileSync2(path, "utf-8");
1151
+ const raw = readFileSync3(path, "utf-8");
616
1152
  try {
617
1153
  return JSON.parse(raw);
618
1154
  } catch (err) {
@@ -645,15 +1181,24 @@ function ensureRegistration(list, reg, force) {
645
1181
  return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
646
1182
  }
647
1183
  var LEGACY_STATUSLINE_COMMAND = "prim statusline";
1184
+ var RUNTIME_STATUSLINE_SUFFIXES = [
1185
+ "/prim/runtime/prim-statusline",
1186
+ // One pre-launcher build persisted the JS entry directly. Keep recognizing
1187
+ // it so reinstall migrates and uninstall removes that older staged form.
1188
+ "/prim/runtime/current/prim-statusline"
1189
+ ];
648
1190
  function isPrimStatusLine(settings) {
649
1191
  const s = settings.statusLine;
650
1192
  if (s?.type !== "command") {
651
1193
  return false;
652
1194
  }
653
1195
  const c = (s.command ?? "").trim();
654
- return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline");
1196
+ return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline") || RUNTIME_STATUSLINE_SUFFIXES.some((suffix) => c.includes(suffix));
655
1197
  }
656
- function applyStatusLine(settings) {
1198
+ function applyStatusLine(settings, command = STATUSLINE_COMMAND) {
1199
+ if (command === false) {
1200
+ return isPrimStatusLine(settings) ? { ...settings, statusLine: void 0 } : settings;
1201
+ }
657
1202
  if (settings.statusLine && !isPrimStatusLine(settings)) {
658
1203
  return settings;
659
1204
  }
@@ -661,7 +1206,7 @@ function applyStatusLine(settings) {
661
1206
  ...settings,
662
1207
  statusLine: {
663
1208
  type: "command",
664
- command: STATUSLINE_COMMAND,
1209
+ command,
665
1210
  refreshInterval: STATUSLINE_REFRESH_SECONDS
666
1211
  }
667
1212
  };
@@ -721,7 +1266,12 @@ function applyInstall(settings, options = {}) {
721
1266
  for (const reg of REGISTRATIONS) {
722
1267
  hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
723
1268
  }
724
- return applyPermissions(applyStatusLine({ ...settings, hooks }));
1269
+ return applyPermissions(
1270
+ applyStatusLine(
1271
+ { ...settings, hooks },
1272
+ options.installStatusline === false ? false : options.statuslineCommand ?? STATUSLINE_COMMAND
1273
+ )
1274
+ );
725
1275
  }
726
1276
  function applyUninstall(settings) {
727
1277
  const source = settings.hooks ?? {};
@@ -746,19 +1296,34 @@ function captureInstalled(settings) {
746
1296
  (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
747
1297
  );
748
1298
  }
1299
+ function feedbackInstalled(settings) {
1300
+ const stopCapture = (settings.hooks?.Stop ?? []).some(
1301
+ (entry) => entryHasCommand(entry, CAPTURE_BIN)
1302
+ );
1303
+ const sessionStart = (settings.hooks?.SessionStart ?? []).some(
1304
+ (entry) => entryHasCommand(entry, SESSION_START_BIN)
1305
+ );
1306
+ return stopCapture && sessionStart;
1307
+ }
749
1308
  function statuslineInstalled(settings) {
750
1309
  return isPrimStatusLine(settings);
751
1310
  }
1311
+ function statuslineState(settings) {
1312
+ if (isPrimStatusLine(settings)) {
1313
+ return "primitive";
1314
+ }
1315
+ return settings.statusLine ? "custom" : "empty";
1316
+ }
752
1317
  function isGateInstalled(settings) {
753
1318
  return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
754
1319
  }
755
- function atomicWrite(path, content) {
756
- const dir = dirname2(path);
757
- if (!existsSync2(dir)) {
758
- mkdirSync2(dir, { recursive: true });
1320
+ function atomicWrite2(path, content) {
1321
+ const dir = dirname3(path);
1322
+ if (!existsSync3(dir)) {
1323
+ mkdirSync3(dir, { recursive: true });
759
1324
  }
760
1325
  const tmp = `${path}.tmp.${String(Date.now())}`;
761
- writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
1326
+ writeFileSync3(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
762
1327
  `, "utf-8");
763
1328
  const fd = openSync(tmp, "r+");
764
1329
  try {
@@ -766,15 +1331,42 @@ function atomicWrite(path, content) {
766
1331
  } finally {
767
1332
  closeSync(fd);
768
1333
  }
769
- renameSync(tmp, path);
1334
+ renameSync2(tmp, path);
1335
+ }
1336
+ function userStatuslineBlocksProjectInstall(scope, userSettings) {
1337
+ return scope === "project" && statuslineState(userSettings) === "custom";
770
1338
  }
771
1339
  function performInstall(scope, force) {
772
1340
  const path = settingsPathFor(scope);
773
1341
  const before = readSettings(path);
774
- const after = applyInstall(before, { force });
1342
+ const blockedByUserStatusline = userStatuslineBlocksProjectInstall(
1343
+ scope,
1344
+ scope === "project" ? readSettings(USER_SCOPE_PATH) : {}
1345
+ );
1346
+ let statuslineCommand = STATUSLINE_COMMAND;
1347
+ if (!blockedByUserStatusline && process.platform === "darwin" && (!before.statusLine || isPrimStatusLine(before))) {
1348
+ try {
1349
+ const runtime = stageRuntime();
1350
+ if (!runtime.manifest.statuslineFile) {
1351
+ throw new Error("standalone prim-statusline entry is unavailable");
1352
+ }
1353
+ statuslineCommand = runtimeStatuslineCommand();
1354
+ } catch (error) {
1355
+ const detail = error instanceof Error ? error.message : String(error);
1356
+ process.stderr.write(
1357
+ `[prim] \u26A0 could not stage the durable statusline runtime (${detail}); using the package resolver
1358
+ `
1359
+ );
1360
+ }
1361
+ }
1362
+ const after = applyInstall(before, {
1363
+ force,
1364
+ statuslineCommand,
1365
+ installStatusline: !blockedByUserStatusline
1366
+ });
775
1367
  const changed = JSON.stringify(before) !== JSON.stringify(after);
776
1368
  if (changed) {
777
- atomicWrite(path, after);
1369
+ atomicWrite2(path, after);
778
1370
  }
779
1371
  if (scope === "project") {
780
1372
  activateRepoBestEffort(process.cwd());
@@ -784,7 +1376,9 @@ function performInstall(scope, force) {
784
1376
  path,
785
1377
  gate: isGateInstalled(after),
786
1378
  capture: captureInstalled(after),
1379
+ feedback: feedbackInstalled(after),
787
1380
  statusline: statuslineInstalled(after),
1381
+ statuslineState: statuslineState(after),
788
1382
  changed
789
1383
  };
790
1384
  }
@@ -794,14 +1388,16 @@ function performUninstall(scope) {
794
1388
  const after = applyUninstall(before);
795
1389
  const changed = JSON.stringify(before) !== JSON.stringify(after);
796
1390
  if (changed) {
797
- atomicWrite(path, after);
1391
+ atomicWrite2(path, after);
798
1392
  }
799
1393
  return {
800
1394
  scope,
801
1395
  path,
802
1396
  gate: isGateInstalled(after),
803
1397
  capture: captureInstalled(after),
1398
+ feedback: feedbackInstalled(after),
804
1399
  statusline: statuslineInstalled(after),
1400
+ statuslineState: statuslineState(after),
805
1401
  changed
806
1402
  };
807
1403
  }
@@ -811,7 +1407,7 @@ function performPermissionInstall(scope) {
811
1407
  const after = applyAutoModeTrust(applyPermissions(before));
812
1408
  const changed = JSON.stringify(before) !== JSON.stringify(after);
813
1409
  if (changed) {
814
- atomicWrite(path, after);
1410
+ atomicWrite2(path, after);
815
1411
  }
816
1412
  return {
817
1413
  scope,
@@ -828,7 +1424,9 @@ function performStatus() {
828
1424
  path,
829
1425
  gate: isGateInstalled(settings),
830
1426
  capture: captureInstalled(settings),
831
- statusline: statuslineInstalled(settings)
1427
+ feedback: feedbackInstalled(settings),
1428
+ statusline: statuslineInstalled(settings),
1429
+ statuslineState: statuslineState(settings)
832
1430
  };
833
1431
  };
834
1432
  return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
@@ -860,6 +1458,14 @@ function registerClaudeCommands(program2) {
860
1458
  `[prim] Claude Code integration already present at ${result.path} (no changes)`
861
1459
  );
862
1460
  }
1461
+ const projectState = scope === "user" ? performStatus().project.statuslineState : void 0;
1462
+ const userState = scope === "project" ? performStatus().user.statuslineState : void 0;
1463
+ if (result.statuslineState === "custom" || projectState === "custom" || userState === "custom") {
1464
+ const owner = result.statuslineState === "custom" ? result.path : projectState === "custom" ? projectScopePath() : USER_SCOPE_PATH;
1465
+ console.error(
1466
+ `[prim] \u26A0 statusline skipped: an existing custom statusLine is preserved at ${owner}; run \`prim daemon status\` for Primitive health`
1467
+ );
1468
+ }
863
1469
  console.log(JSON.stringify(result, null, JSON_INDENT));
864
1470
  });
865
1471
  claude.command("preauth").description(
@@ -889,11 +1495,11 @@ function registerClaudeCommands(program2) {
889
1495
  console.log(JSON.stringify(result, null, JSON_INDENT));
890
1496
  });
891
1497
  claude.command("status").description(
892
- "Report whether each prim surface (gate, capture, statusline) is installed per scope"
1498
+ "Report whether each prim surface (gate, capture, feedback, statusline) is installed per scope"
893
1499
  ).action(() => {
894
1500
  const result = performStatus();
895
1501
  const mark = (b) => b ? "\u2713" : "\u2717";
896
- const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 statusline ${mark(s.statusline)} (${s.path})`;
1502
+ const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 feedback ${mark(s.feedback)} \xB7 statusline ${s.statuslineState} (${s.path})`;
897
1503
  console.error(`${line("user", result.user)}
898
1504
  ${line("project", result.project)}`);
899
1505
  console.log(JSON.stringify(result, null, JSON_INDENT));
@@ -901,8 +1507,8 @@ ${line("project", result.project)}`);
901
1507
  }
902
1508
 
903
1509
  // src/commands/codex-install.ts
904
- import { homedir as homedir2 } from "os";
905
- import { join as join2 } from "path";
1510
+ import { homedir as homedir3 } from "os";
1511
+ import { join as join3 } from "path";
906
1512
  var CAPTURE_BIN2 = "prim-hook";
907
1513
  var GATE_BIN2 = "prim-pre-tool-use";
908
1514
  var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
@@ -926,8 +1532,8 @@ var CODEX_REGISTRATIONS = [
926
1532
  // refresh the bin cache the other hooks read. See lib/bin-cache.ts.
927
1533
  makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS, { cacheRead: false })
928
1534
  ];
929
- var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
930
- var projectScopePath2 = () => join2(projectRoot(), ".codex", "hooks.json");
1535
+ var USER_SCOPE_PATH2 = join3(homedir3(), ".codex", "hooks.json");
1536
+ var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
931
1537
  function settingsPathFor2(scope) {
932
1538
  return scope === "user" ? USER_SCOPE_PATH2 : projectScopePath2();
933
1539
  }
@@ -975,7 +1581,7 @@ function performInstall2(scope, force) {
975
1581
  const after = applyInstall2(before, { force });
976
1582
  const changed = JSON.stringify(before) !== JSON.stringify(after);
977
1583
  if (changed) {
978
- atomicWrite(path, after);
1584
+ atomicWrite2(path, after);
979
1585
  }
980
1586
  if (scope === "project") {
981
1587
  activateRepoBestEffort(process.cwd());
@@ -988,7 +1594,7 @@ function performUninstall2(scope) {
988
1594
  const after = applyUninstall2(before);
989
1595
  const changed = JSON.stringify(before) !== JSON.stringify(after);
990
1596
  if (changed) {
991
- atomicWrite(path, after);
1597
+ atomicWrite2(path, after);
992
1598
  }
993
1599
  return resultFor(scope, path, after, changed);
994
1600
  }
@@ -1051,9 +1657,9 @@ ${line("project", result.project)}`);
1051
1657
 
1052
1658
  // src/commands/daemon.ts
1053
1659
  import { spawn } from "child_process";
1054
- import { closeSync as closeSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
1055
- import { homedir as homedir3 } from "os";
1056
- import { join as join3 } from "path";
1660
+ import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
1661
+ import { homedir as homedir4 } from "os";
1662
+ import { join as join4 } from "path";
1057
1663
 
1058
1664
  // src/lib/presence.ts
1059
1665
  function formatLabeled(labels, cap) {
@@ -1083,33 +1689,35 @@ function formatTeammatesWithArea(teammates, cap) {
1083
1689
 
1084
1690
  // src/commands/daemon.ts
1085
1691
  var DAEMON_BIN = "prim-daemon-server";
1086
- var CONFIG_DIR = join3(homedir3(), ".config", "prim");
1087
- var PID_PATH = join3(CONFIG_DIR, "daemon.pid");
1088
- var SOCK_PATH = join3(CONFIG_DIR, "sock");
1089
- var LOG_PATH = join3(CONFIG_DIR, "daemon.log");
1692
+ var CONFIG_DIR = join4(homedir4(), ".config", "prim");
1693
+ var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
1694
+ var SOCK_PATH = join4(CONFIG_DIR, "sock");
1695
+ var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
1090
1696
  var CONFIG_DIR_MODE = 448;
1091
1697
  var LOG_FILE_MODE = 384;
1092
1698
  var STOP_TIMEOUT_MS = 5e3;
1093
1699
  var STOP_POLL_MS = 100;
1094
1700
  var STATUS_PROBE_TIMEOUT_MS = 500;
1095
- var READY_TIMEOUT_MS = 5e3;
1096
- var READY_POLL_MS = 100;
1097
- var READY_PROBE_TIMEOUT_MS = 250;
1701
+ var READY_TIMEOUT_MS2 = 5e3;
1702
+ var READY_POLL_MS2 = 100;
1703
+ var READY_PROBE_TIMEOUT_MS2 = 250;
1704
+ var HEALTHY_TIMEOUT_MS = 3e4;
1705
+ var HEALTHY_POLL_MS = 250;
1098
1706
  var EXIT_OK2 = 0;
1099
1707
  var EXIT_NOT_RUNNING = 2;
1100
1708
  var EXIT_BOOTING = 3;
1101
1709
  function readPidfile() {
1102
- if (!existsSync3(PID_PATH)) {
1710
+ if (!existsSync4(PID_PATH)) {
1103
1711
  return null;
1104
1712
  }
1105
- const raw = readFileSync3(PID_PATH, "utf-8").trim();
1713
+ const raw = readFileSync4(PID_PATH, "utf-8").trim();
1106
1714
  const pid = Number(raw);
1107
1715
  if (!Number.isInteger(pid) || pid <= 0) {
1108
1716
  return null;
1109
1717
  }
1110
- return { pid, alive: processIsAlive(pid) };
1718
+ return { pid, alive: processIsAlive2(pid) };
1111
1719
  }
1112
- function processIsAlive(pid) {
1720
+ function processIsAlive2(pid) {
1113
1721
  try {
1114
1722
  process.kill(pid, 0);
1115
1723
  return true;
@@ -1128,40 +1736,89 @@ function clearStaleArtifacts() {
1128
1736
  }
1129
1737
  }
1130
1738
  function sleep(ms) {
1131
- return new Promise((resolve6) => {
1132
- const timer = setTimeout(resolve6, ms);
1133
- timer.unref();
1134
- });
1739
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1135
1740
  }
1136
1741
  function spawnDaemon(options) {
1137
1742
  const file = binFile(DAEMON_BIN);
1138
1743
  return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
1139
1744
  }
1140
1745
  function openDaemonLog(configDir = CONFIG_DIR) {
1141
- if (!existsSync3(configDir)) {
1142
- mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
1746
+ if (!existsSync4(configDir)) {
1747
+ mkdirSync4(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
1143
1748
  }
1144
- return openSync2(join3(configDir, "daemon.log"), "a", LOG_FILE_MODE);
1749
+ return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
1145
1750
  }
1146
- async function waitForReady() {
1147
- const deadline = Date.now() + READY_TIMEOUT_MS;
1751
+ async function waitForReady2() {
1752
+ const deadline = Date.now() + READY_TIMEOUT_MS2;
1148
1753
  while (Date.now() < deadline) {
1149
- if (await daemonIsLive(READY_PROBE_TIMEOUT_MS)) {
1754
+ if (await daemonIsLive(READY_PROBE_TIMEOUT_MS2)) {
1150
1755
  return true;
1151
1756
  }
1152
- await sleep(READY_POLL_MS);
1757
+ await sleep(READY_POLL_MS2);
1153
1758
  }
1154
- return daemonIsLive(READY_PROBE_TIMEOUT_MS);
1759
+ return daemonIsLive(READY_PROBE_TIMEOUT_MS2);
1155
1760
  }
1156
- async function daemonStart(opts) {
1761
+ async function waitForHealthySnapshot(expectedVersion) {
1762
+ const deadline = Date.now() + HEALTHY_TIMEOUT_MS;
1763
+ let snapshot = null;
1764
+ while (Date.now() < deadline) {
1765
+ snapshot = await daemonRequest(
1766
+ "status_snapshot",
1767
+ {},
1768
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
1769
+ );
1770
+ if (snapshot?.healthy === true && snapshot.version === expectedVersion) return snapshot;
1771
+ await sleep(HEALTHY_POLL_MS);
1772
+ }
1773
+ return snapshot;
1774
+ }
1775
+ async function verifiedPid(existing) {
1776
+ if (!existing.alive) return null;
1777
+ const snapshot = await daemonRequest(
1778
+ "status_snapshot",
1779
+ {},
1780
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
1781
+ );
1782
+ return snapshot?.pid === existing.pid ? snapshot : null;
1783
+ }
1784
+ function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
1785
+ return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
1786
+ }
1787
+ async function detachedDaemonStart(opts) {
1157
1788
  const existing = readPidfile();
1158
1789
  if (existing?.alive) {
1159
- process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
1790
+ const snapshot = await verifiedPid(existing);
1791
+ if (snapshot) {
1792
+ process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
1160
1793
  `);
1161
- console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
1794
+ console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
1795
+ return;
1796
+ }
1797
+ process.stderr.write(
1798
+ `[prim] refusing to replace live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
1799
+ `
1800
+ );
1801
+ console.log(JSON.stringify({ started: false, pid: existing.pid, verified: false }, null, 2));
1802
+ if (!process.exitCode) process.exitCode = EXIT_BOOTING;
1162
1803
  return;
1163
1804
  }
1164
1805
  if (existing && !existing.alive) {
1806
+ const socketOwner = await daemonRequest(
1807
+ "status_snapshot",
1808
+ {},
1809
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
1810
+ );
1811
+ if (socketOwner) {
1812
+ process.stderr.write(
1813
+ `[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
1814
+ `
1815
+ );
1816
+ console.log(
1817
+ JSON.stringify({ started: false, pid: socketOwner.pid, verified: false }, null, 2)
1818
+ );
1819
+ if (!process.exitCode) process.exitCode = EXIT_BOOTING;
1820
+ return;
1821
+ }
1165
1822
  clearStaleArtifacts();
1166
1823
  }
1167
1824
  if (opts.foreground) {
@@ -1185,7 +1842,7 @@ async function daemonStart(opts) {
1185
1842
  if (logFd !== void 0) {
1186
1843
  closeSync2(logFd);
1187
1844
  }
1188
- const live = await waitForReady();
1845
+ const live = await waitForReady2();
1189
1846
  if (live) {
1190
1847
  const after = readPidfile();
1191
1848
  process.stderr.write(
@@ -1196,7 +1853,7 @@ async function daemonStart(opts) {
1196
1853
  return;
1197
1854
  }
1198
1855
  process.stderr.write(
1199
- `[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
1856
+ `[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS2}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
1200
1857
  `
1201
1858
  );
1202
1859
  console.log(JSON.stringify({ started: false }, null, 2));
@@ -1204,7 +1861,7 @@ async function daemonStart(opts) {
1204
1861
  process.exitCode = EXIT_NOT_RUNNING;
1205
1862
  }
1206
1863
  }
1207
- async function daemonStop() {
1864
+ async function detachedDaemonStop() {
1208
1865
  const existing = readPidfile();
1209
1866
  if (!existing) {
1210
1867
  process.stderr.write("[prim] daemon not running (no pidfile)\n");
@@ -1212,11 +1869,36 @@ async function daemonStop() {
1212
1869
  return;
1213
1870
  }
1214
1871
  if (!existing.alive) {
1872
+ const socketOwner = await daemonRequest(
1873
+ "status_snapshot",
1874
+ {},
1875
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
1876
+ );
1877
+ if (socketOwner) {
1878
+ process.stderr.write(
1879
+ `[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
1880
+ `
1881
+ );
1882
+ console.log(
1883
+ JSON.stringify({ stopped: false, pid: socketOwner.pid, verified: false }, null, 2)
1884
+ );
1885
+ return;
1886
+ }
1215
1887
  clearStaleArtifacts();
1216
1888
  process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
1217
1889
  console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
1218
1890
  return;
1219
1891
  }
1892
+ const snapshot = await verifiedPid(existing);
1893
+ if (!snapshot) {
1894
+ process.stderr.write(
1895
+ `[prim] refusing to signal live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
1896
+ `
1897
+ );
1898
+ console.log(JSON.stringify({ stopped: false, pid: existing.pid, verified: false }, null, 2));
1899
+ if (!process.exitCode) process.exitCode = EXIT_BOOTING;
1900
+ return;
1901
+ }
1220
1902
  try {
1221
1903
  process.kill(existing.pid, "SIGTERM");
1222
1904
  } catch (err) {
@@ -1229,7 +1911,7 @@ async function daemonStop() {
1229
1911
  }
1230
1912
  const deadline = Date.now() + STOP_TIMEOUT_MS;
1231
1913
  while (Date.now() < deadline) {
1232
- if (!processIsAlive(existing.pid)) {
1914
+ if (!processIsAlive2(existing.pid)) {
1233
1915
  clearStaleArtifacts();
1234
1916
  process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
1235
1917
  `);
@@ -1243,23 +1925,212 @@ async function daemonStop() {
1243
1925
  `
1244
1926
  );
1245
1927
  console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
1928
+ if (!process.exitCode) process.exitCode = EXIT_BOOTING;
1929
+ }
1930
+ async function macDaemonStart(forceRestart = false) {
1931
+ const result = await ensureMacDaemon({ explicitlyStarted: true, forceRestart });
1932
+ const serviceReady = result.state === "running";
1933
+ const expectedVersion = result.runtime?.manifest.version;
1934
+ const snapshot = serviceReady && expectedVersion ? await waitForHealthySnapshot(expectedVersion) : null;
1935
+ const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
1936
+ if (healthy) {
1937
+ const verb = result.action === "none" ? "already running" : "started";
1938
+ process.stderr.write(
1939
+ `[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})
1940
+ `
1941
+ );
1942
+ } else {
1943
+ process.stderr.write(
1944
+ `[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state (see ${LOG_PATH})
1945
+ `
1946
+ );
1947
+ if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
1948
+ }
1949
+ console.log(
1950
+ JSON.stringify(
1951
+ {
1952
+ started: healthy,
1953
+ supervised: true,
1954
+ action: result.action,
1955
+ pid: snapshot?.pid ?? result.service.pid,
1956
+ loaded: result.service.loaded,
1957
+ responding: result.responding,
1958
+ healthy,
1959
+ version: snapshot?.version,
1960
+ expectedVersion
1961
+ },
1962
+ null,
1963
+ 2
1964
+ )
1965
+ );
1966
+ }
1967
+ async function daemonStart(opts) {
1968
+ if (process.platform === "darwin") {
1969
+ if (!opts.foreground) {
1970
+ await macDaemonStart(false);
1971
+ return;
1972
+ }
1973
+ }
1974
+ await withDaemonLifecycleLock(async () => {
1975
+ setDaemonExplicitlyDisabled(false);
1976
+ await detachedDaemonStart(opts);
1977
+ });
1978
+ }
1979
+ async function daemonStop() {
1980
+ if (process.platform !== "darwin") {
1981
+ await withDaemonLifecycleLock(async () => {
1982
+ setDaemonExplicitlyDisabled(true);
1983
+ await detachedDaemonStop();
1984
+ });
1985
+ return;
1986
+ }
1987
+ const result = await bootoutMacDaemon();
1988
+ process.stderr.write(
1989
+ result.wasLoaded ? "[prim] daemon stopped and explicitly disabled\n" : result.legacyStopped ? "[prim] legacy daemon stopped and explicitly disabled\n" : "[prim] daemon was not loaded; explicitly disabled\n"
1990
+ );
1991
+ console.log(
1992
+ JSON.stringify(
1993
+ {
1994
+ stopped: result.wasLoaded || result.legacyStopped,
1995
+ wasRunning: result.wasLoaded || result.legacyStopped,
1996
+ supervised: true,
1997
+ disabled: true
1998
+ },
1999
+ null,
2000
+ 2
2001
+ )
2002
+ );
1246
2003
  }
1247
2004
  function classifyStatus(pidAlive, responding, snapshot, pid) {
1248
2005
  if (!pidAlive) {
1249
2006
  return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
1250
2007
  }
1251
- if (!responding) {
2008
+ if (!responding) {
2009
+ return {
2010
+ json: { running: true, responding: false, state: "starting", pid },
2011
+ exitCode: EXIT_BOOTING
2012
+ };
2013
+ }
2014
+ if (!snapshot) {
2015
+ return { json: { running: true, responding: true }, exitCode: EXIT_OK2 };
2016
+ }
2017
+ if (snapshot.healthy === false) {
2018
+ return {
2019
+ json: { running: true, responding: true, state: "degraded", ...snapshot },
2020
+ exitCode: EXIT_BOOTING
2021
+ };
2022
+ }
2023
+ return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
2024
+ }
2025
+ function classifyLaunchdStatus(service, responding, snapshot, disabled) {
2026
+ if (!service.loaded) {
2027
+ if (responding) {
2028
+ return {
2029
+ json: {
2030
+ running: true,
2031
+ responding: true,
2032
+ supervised: false,
2033
+ state: "unsupervised",
2034
+ disabled,
2035
+ ...snapshot
2036
+ },
2037
+ exitCode: EXIT_BOOTING
2038
+ };
2039
+ }
2040
+ return {
2041
+ json: { running: false, supervised: true, loaded: false, disabled },
2042
+ exitCode: EXIT_NOT_RUNNING
2043
+ };
2044
+ }
2045
+ if (!responding) {
2046
+ return {
2047
+ json: {
2048
+ running: true,
2049
+ responding: false,
2050
+ supervised: true,
2051
+ state: service.state ?? "starting",
2052
+ pid: service.pid,
2053
+ disabled
2054
+ },
2055
+ exitCode: EXIT_BOOTING
2056
+ };
2057
+ }
2058
+ if (!snapshot || service.pid === void 0) {
1252
2059
  return {
1253
- json: { running: true, responding: false, state: "starting", pid },
2060
+ json: {
2061
+ running: true,
2062
+ responding: true,
2063
+ supervised: true,
2064
+ state: "ownership_unverified",
2065
+ pid: service.pid,
2066
+ socketPid: snapshot?.pid,
2067
+ disabled
2068
+ },
2069
+ exitCode: EXIT_BOOTING
2070
+ };
2071
+ }
2072
+ if (service.pid !== snapshot.pid) {
2073
+ return {
2074
+ json: {
2075
+ running: true,
2076
+ responding: true,
2077
+ supervised: true,
2078
+ state: "pid_mismatch",
2079
+ pid: service.pid,
2080
+ socketPid: snapshot.pid,
2081
+ disabled
2082
+ },
1254
2083
  exitCode: EXIT_BOOTING
1255
2084
  };
1256
2085
  }
2086
+ if (snapshot?.healthy === false) {
2087
+ return {
2088
+ json: {
2089
+ running: true,
2090
+ responding: true,
2091
+ supervised: true,
2092
+ state: "degraded",
2093
+ disabled,
2094
+ ...snapshot
2095
+ },
2096
+ exitCode: EXIT_BOOTING
2097
+ };
2098
+ }
2099
+ return {
2100
+ json: {
2101
+ running: true,
2102
+ responding: true,
2103
+ supervised: true,
2104
+ state: service.state ?? "running",
2105
+ disabled,
2106
+ ...snapshot
2107
+ },
2108
+ exitCode: EXIT_OK2
2109
+ };
2110
+ }
2111
+ function writeLiveSnapshot(snapshot, supervised = false) {
1257
2112
  if (!snapshot) {
1258
- return { json: { running: true, responding: true }, exitCode: EXIT_OK2 };
2113
+ process.stderr.write(
2114
+ supervised ? "[prim] \u2713 daemon live under launchd (no snapshot)\n" : "[prim] \u2713 daemon live\n"
2115
+ );
2116
+ return;
1259
2117
  }
1260
- return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
2118
+ const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
2119
+ if (snapshot.healthy === false) {
2120
+ process.stderr.write(
2121
+ `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}
2122
+ `
2123
+ );
2124
+ return;
2125
+ }
2126
+ process.stderr.write(
2127
+ `[prim] \u2713 daemon live${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
2128
+ snapshot.uptimeMs / 1e3
2129
+ )}s \xB7 session=${snapshot.sessionId}${team}
2130
+ `
2131
+ );
1261
2132
  }
1262
- async function daemonStatus() {
2133
+ async function detachedDaemonStatus() {
1263
2134
  const pid = readPidfile();
1264
2135
  const pidAlive = pid?.alive ?? false;
1265
2136
  const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
@@ -1274,38 +2145,127 @@ async function daemonStatus() {
1274
2145
  } else if (!responding) {
1275
2146
  process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
1276
2147
  `);
1277
- } else if (!snapshot) {
1278
- process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
1279
2148
  } else {
1280
- const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
2149
+ writeLiveSnapshot(snapshot);
2150
+ }
2151
+ console.log(JSON.stringify(json, null, 2));
2152
+ if (exitCode !== EXIT_OK2 && !process.exitCode) {
2153
+ process.exitCode = exitCode;
2154
+ }
2155
+ }
2156
+ async function macDaemonStatus() {
2157
+ const service = getLaunchdService();
2158
+ const responding = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
2159
+ const snapshot = responding ? await daemonRequest(
2160
+ "status_snapshot",
2161
+ {},
2162
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
2163
+ ) : null;
2164
+ const disabled = daemonExplicitlyDisabled();
2165
+ const { json, exitCode } = classifyLaunchdStatus(service, responding, snapshot, disabled);
2166
+ if (!service.loaded && responding) {
2167
+ process.stderr.write(
2168
+ `[prim] \u25CC daemon pid=${snapshot?.pid ?? "?"} is live but not supervised by launchd
2169
+ `
2170
+ );
2171
+ } else if (!service.loaded) {
2172
+ process.stderr.write(`[prim] \u2717 daemon down${disabled ? " (explicitly disabled)" : ""}
2173
+ `);
2174
+ } else if (!responding) {
2175
+ process.stderr.write(
2176
+ `[prim] \u25CC launchd service ${service.state ?? "loaded"}; socket is not responding yet
2177
+ `
2178
+ );
2179
+ } else if (!snapshot || service.pid === void 0) {
2180
+ process.stderr.write("[prim] \u2717 launchd daemon socket ownership could not be verified\n");
2181
+ } else if (service.pid !== snapshot.pid) {
1281
2182
  process.stderr.write(
1282
- `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}${team}
2183
+ `[prim] \u2717 launchd pid=${service.pid} does not own the daemon socket (pid=${snapshot.pid})
1283
2184
  `
1284
2185
  );
2186
+ } else {
2187
+ writeLiveSnapshot(snapshot, true);
1285
2188
  }
1286
2189
  console.log(JSON.stringify(json, null, 2));
1287
- if (exitCode !== EXIT_OK2 && !process.exitCode) {
1288
- process.exitCode = exitCode;
2190
+ if (exitCode !== EXIT_OK2 && !process.exitCode) process.exitCode = exitCode;
2191
+ }
2192
+ async function daemonStatus() {
2193
+ if (process.platform === "darwin") {
2194
+ await macDaemonStatus();
2195
+ return;
1289
2196
  }
2197
+ await detachedDaemonStatus();
1290
2198
  }
1291
2199
  async function daemonRestart(opts) {
2200
+ if (process.platform === "darwin" && !opts.foreground) {
2201
+ await macDaemonStart(true);
2202
+ return;
2203
+ }
2204
+ if (process.platform !== "darwin") {
2205
+ await withDaemonLifecycleLock(async () => {
2206
+ setDaemonExplicitlyDisabled(false);
2207
+ await detachedDaemonStop();
2208
+ await detachedDaemonStart(opts);
2209
+ });
2210
+ return;
2211
+ }
1292
2212
  await daemonStop();
1293
2213
  await daemonStart(opts);
1294
2214
  }
2215
+ async function daemonEnsure() {
2216
+ if (process.platform !== "darwin") {
2217
+ const disabled = await withDaemonLifecycleLock(async () => {
2218
+ if (daemonExplicitlyDisabled()) return true;
2219
+ await detachedDaemonStart({});
2220
+ return false;
2221
+ });
2222
+ if (disabled) {
2223
+ process.stderr.write("[prim] daemon remains explicitly disabled\n");
2224
+ console.log(JSON.stringify({ ensured: false, disabled: true, supervised: false }, null, 2));
2225
+ }
2226
+ return;
2227
+ }
2228
+ const result = await ensureMacDaemon();
2229
+ if (result.state === "disabled") {
2230
+ process.stderr.write("[prim] daemon remains explicitly disabled\n");
2231
+ } else if (result.state === "running") {
2232
+ process.stderr.write(`[prim] \u2713 daemon ensured under launchd (${result.action})
2233
+ `);
2234
+ } else {
2235
+ process.stderr.write(`[prim] \u2717 daemon ensure failed; see ${LOG_PATH}
2236
+ `);
2237
+ if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
2238
+ }
2239
+ console.log(
2240
+ JSON.stringify(
2241
+ {
2242
+ ensured: result.state === "running",
2243
+ disabled: result.state === "disabled",
2244
+ supervised: true,
2245
+ action: result.action
2246
+ },
2247
+ null,
2248
+ 2
2249
+ )
2250
+ );
2251
+ }
1295
2252
  function registerDaemonCommands(program2) {
1296
2253
  const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
1297
- daemon.command("start").description("Spawn the prim-daemon-server in the background").option("--foreground", "Run in the foreground (inherit stdio); use under launchd / systemd").action(async (opts) => {
2254
+ daemon.command("start").description("Start the daemon (installs a supervised LaunchAgent on macOS)").option("--foreground", "Run in the foreground (inherit stdio); use under launchd / systemd").action(async (opts) => {
1298
2255
  await daemonStart(opts);
1299
2256
  });
1300
- daemon.command("stop").description("Send SIGTERM to the running daemon and clean up the socket").action(async () => {
2257
+ daemon.command("stop").description("Stop and explicitly disable the daemon").action(async () => {
1301
2258
  await daemonStop();
1302
2259
  });
1303
2260
  daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
1304
2261
  await daemonStatus();
1305
2262
  });
1306
- daemon.command("restart").description("Stop, then start (preserves no state today)").option("--foreground", "Restart in the foreground").action(async (opts) => {
2263
+ daemon.command("restart").description("Restart the daemon and clear any explicit disable marker").option("--foreground", "Restart in the foreground").action(async (opts) => {
1307
2264
  await daemonRestart(opts);
1308
2265
  });
2266
+ daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").action(async () => {
2267
+ await daemonEnsure();
2268
+ });
1309
2269
  }
1310
2270
 
1311
2271
  // src/decisions/cascade-renderer.ts
@@ -2153,11 +3113,11 @@ function registerDecisionsCommands(program2) {
2153
3113
  }
2154
3114
 
2155
3115
  // src/commands/doctor.ts
2156
- import { existsSync as existsSync4 } from "fs";
3116
+ import { existsSync as existsSync5 } from "fs";
2157
3117
  var DAEMON_PROBE_TIMEOUT_MS = 500;
2158
3118
  var CONNECTIVITY_TIMEOUT_MS = 3e3;
2159
3119
  var MS_PER_SECOND = 1e3;
2160
- var STALE_PENDING_MS = 6e4;
3120
+ var STALE_PENDING_MS = 3e4;
2161
3121
  var EXIT_UNHEALTHY = 1;
2162
3122
  function classifyDoctor(checks) {
2163
3123
  const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
@@ -2171,7 +3131,7 @@ function checkAuth() {
2171
3131
  return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
2172
3132
  }
2173
3133
  const expiresAt = getTokenExpiresAt();
2174
- const hasRefresh = existsSync4(REFRESH_TOKEN_PATH);
3134
+ const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
2175
3135
  if (expiresAt !== void 0 && Date.now() >= expiresAt) {
2176
3136
  return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
2177
3137
  name: "auth",
@@ -2185,62 +3145,269 @@ function checkAuth() {
2185
3145
  const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
2186
3146
  return { name: "auth", status: "ok", detail };
2187
3147
  }
2188
- async function checkDaemon() {
2189
- const live = await daemonIsLive(DAEMON_PROBE_TIMEOUT_MS);
2190
- return live ? { name: "daemon", status: "ok", detail: "live" } : {
3148
+ function classifyDaemonHealth(snapshot, options = {}) {
3149
+ if (options.disabled) {
3150
+ return {
3151
+ name: "daemon",
3152
+ status: "fail",
3153
+ detail: "explicitly stopped \u2014 run `prim daemon start`"
3154
+ };
3155
+ }
3156
+ if (options.service && !options.service.loaded) {
3157
+ return {
3158
+ name: "daemon",
3159
+ status: "fail",
3160
+ detail: "launchd service is not loaded \u2014 run `prim daemon start`"
3161
+ };
3162
+ }
3163
+ if (!snapshot) {
3164
+ return {
3165
+ name: "daemon",
3166
+ status: "fail",
3167
+ detail: "socket unavailable \u2014 run `prim daemon start`"
3168
+ };
3169
+ }
3170
+ if (options.service && (!Number.isInteger(options.service.pid) || (options.service.pid ?? 0) <= 0 || !Number.isInteger(snapshot.pid) || (snapshot.pid ?? 0) <= 0 || options.service.pid !== snapshot.pid)) {
3171
+ return {
3172
+ name: "daemon",
3173
+ status: "fail",
3174
+ detail: `launchd does not own the daemon socket (launchd ${String(options.service.pid ?? "none")} \xB7 socket ${String(snapshot.pid ?? "none")})`
3175
+ };
3176
+ }
3177
+ if (!snapshot.heartbeat?.healthy) {
3178
+ return {
3179
+ name: "daemon",
3180
+ status: "fail",
3181
+ detail: `heartbeat unhealthy${snapshot.heartbeat?.lastError ? ` \u2014 ${snapshot.heartbeat.lastError}` : ""}`
3182
+ };
3183
+ }
3184
+ if (!snapshot.ingestion?.healthy) {
3185
+ const pending = snapshot.ingestion?.pendingCount ?? 0;
3186
+ const pendingLabel = snapshot.ingestion?.pendingSampled ? `at least ${String(pending)}` : String(pending);
3187
+ return {
3188
+ name: "daemon",
3189
+ status: "fail",
3190
+ detail: `ingestion unhealthy \xB7 ${pendingLabel} pending${snapshot.ingestion?.lastError ? ` \u2014 ${snapshot.ingestion.lastError}` : ""}`
3191
+ };
3192
+ }
3193
+ if (snapshot.healthy !== true) {
3194
+ return { name: "daemon", status: "fail", detail: "health state is not ready" };
3195
+ }
3196
+ return {
2191
3197
  name: "daemon",
2192
- status: "warn",
2193
- detail: "down \u2014 capture still journals; drains on next `prim` invocation"
3198
+ status: "ok",
3199
+ detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""}`
2194
3200
  };
2195
3201
  }
3202
+ async function checkDaemon() {
3203
+ let service;
3204
+ if (process.platform === "darwin") {
3205
+ try {
3206
+ service = getLaunchdService();
3207
+ } catch {
3208
+ service = { loaded: false };
3209
+ }
3210
+ }
3211
+ const snapshot = await daemonRequest(
3212
+ "status_snapshot",
3213
+ {},
3214
+ { timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
3215
+ );
3216
+ return classifyDaemonHealth(snapshot, {
3217
+ disabled: daemonExplicitlyDisabled(),
3218
+ service
3219
+ });
3220
+ }
2196
3221
  function checkJournal() {
2197
- const stats = bucketStats();
2198
- const pending = stats.reduce((n, s) => n + s.lineCount, 0);
3222
+ const stats = pendingJournalStats();
3223
+ const pending = stats.pendingCount;
3224
+ const pendingLabel = stats.sampled ? `at least ${String(pending)}` : String(pending);
2199
3225
  if (pending === 0) {
3226
+ if (stats.sampled) {
3227
+ return {
3228
+ name: "journal",
3229
+ status: "fail",
3230
+ detail: "bounded journal sample could not prove the queue is empty"
3231
+ };
3232
+ }
2200
3233
  return { name: "journal", status: "ok", detail: "no pending moves" };
2201
3234
  }
2202
- const oldestMs = Math.max(...stats.map((s) => Date.now() - s.mtimeMs));
3235
+ if (stats.oldestPendingAt === void 0) {
3236
+ return {
3237
+ name: "journal",
3238
+ status: "fail",
3239
+ detail: `${pendingLabel} pending with no readable capture timestamp`
3240
+ };
3241
+ }
3242
+ const oldestMs = Date.now() - stats.oldestPendingAt;
2203
3243
  const oldestS = Math.round(oldestMs / MS_PER_SECOND);
2204
3244
  if (oldestMs > STALE_PENDING_MS) {
2205
3245
  return {
2206
3246
  name: "journal",
2207
- status: "warn",
2208
- detail: `${String(pending)} pending, oldest ${String(oldestS)}s \u2014 drain may be stalled`
3247
+ status: "fail",
3248
+ detail: `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
3249
+ };
3250
+ }
3251
+ if (stats.sampled) {
3252
+ return {
3253
+ name: "journal",
3254
+ status: "fail",
3255
+ detail: `${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`
2209
3256
  };
2210
3257
  }
2211
3258
  return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
2212
3259
  }
2213
3260
  function checkStranded() {
2214
- const stranded = listFlushing();
2215
- if (stranded.length === 0) {
3261
+ const stats = pendingJournalStats();
3262
+ if (stats.strandedFileCount === 0 && !stats.strandedSampled) {
2216
3263
  return { name: "stranded", status: "ok", detail: "none" };
2217
3264
  }
2218
- const moves = stranded.reduce((n, f) => n + f.lineCount, 0);
3265
+ const qualifier = stats.strandedSampled ? "at least " : "";
2219
3266
  return {
2220
3267
  name: "stranded",
2221
3268
  status: "warn",
2222
- detail: `${String(moves)} move(s) in ${String(stranded.length)} file(s) \u2014 run \`prim moves flush\``
3269
+ detail: `${qualifier}${String(stats.strandedCount)} move(s) in ${qualifier}${String(stats.strandedFileCount)} file(s) \u2014 run \`prim moves flush\``
3270
+ };
3271
+ }
3272
+ function checkWorkspaceIdentity() {
3273
+ const identity = inspectWorkspaceId();
3274
+ switch (identity.status) {
3275
+ case "ready":
3276
+ return { name: "feedback-id", status: "ok", detail: "stable worktree identity ready" };
3277
+ case "missing":
3278
+ return {
3279
+ name: "feedback-id",
3280
+ status: "warn",
3281
+ detail: "not initialized \u2014 the next active hook will create it"
3282
+ };
3283
+ case "not_git":
3284
+ return {
3285
+ name: "feedback-id",
3286
+ status: "warn",
3287
+ detail: "not in a Git worktree \u2014 capture falls back to legacy V1"
3288
+ };
3289
+ case "corrupt":
3290
+ return {
3291
+ name: "feedback-id",
3292
+ status: "warn",
3293
+ detail: "identity is corrupt \u2014 not rotated; capture falls back to legacy V1"
3294
+ };
3295
+ case "unavailable":
3296
+ return {
3297
+ name: "feedback-id",
3298
+ status: "warn",
3299
+ detail: `identity unavailable during ${identity.operation} \u2014 capture falls back to legacy V1`
3300
+ };
3301
+ }
3302
+ }
3303
+ function checkFeedbackHooks() {
3304
+ try {
3305
+ const status = performStatus();
3306
+ if (status.project.feedback || status.user.feedback) {
3307
+ return {
3308
+ name: "feedback-hooks",
3309
+ status: "ok",
3310
+ detail: status.project.feedback ? "project handlers ready" : "user handlers ready"
3311
+ };
3312
+ }
3313
+ return {
3314
+ name: "feedback-hooks",
3315
+ status: "warn",
3316
+ detail: "Stop + SessionStart handlers missing \u2014 run `prim claude install`"
3317
+ };
3318
+ } catch (error) {
3319
+ const message = error instanceof Error ? error.message : String(error);
3320
+ return { name: "feedback-hooks", status: "warn", detail: message.slice(0, 80) };
3321
+ }
3322
+ }
3323
+ function parseMovesStatus(value) {
3324
+ if (!value || typeof value !== "object") {
3325
+ throw new Error("moves status returned a non-object response");
3326
+ }
3327
+ const status = value;
3328
+ const nullableNumber = (candidate) => candidate === null || typeof candidate === "number" && Number.isFinite(candidate);
3329
+ if (status.captureState !== "enabled" && status.captureState !== "disabled" || !nullableNumber(status.latestIngestAt) || !nullableNumber(status.latestClassificationAt) || !nullableNumber(status.highWaterMark) || typeof status.pendingSessionCount !== "number" || !Number.isInteger(status.pendingSessionCount) || status.pendingSessionCount < 0 || typeof status.sampled !== "boolean") {
3330
+ throw new Error("moves status returned an invalid response");
3331
+ }
3332
+ return status;
3333
+ }
3334
+ function classifyMovesStatus(status) {
3335
+ const capture = status.captureState === "enabled" ? { name: "capture", status: "ok", detail: "enabled; ingest endpoint durable" } : {
3336
+ name: "capture",
3337
+ status: "fail",
3338
+ detail: "disabled for the current organization; local Moves are retained"
2223
3339
  };
3340
+ let classification;
3341
+ if (status.pendingSessionCount > 0) {
3342
+ classification = {
3343
+ name: "classification",
3344
+ status: "warn",
3345
+ detail: `${String(status.pendingSessionCount)} session(s) pending${status.sampled ? " in a bounded sample" : ""}`
3346
+ };
3347
+ } else if (status.sampled) {
3348
+ classification = {
3349
+ name: "classification",
3350
+ status: "warn",
3351
+ detail: "caught up in the bounded sample; older sessions were not inspected"
3352
+ };
3353
+ } else {
3354
+ classification = {
3355
+ name: "classification",
3356
+ status: "ok",
3357
+ detail: status.latestClassificationAt === null ? "no sessions awaiting classification" : `caught up \xB7 last ${new Date(status.latestClassificationAt).toISOString()}`
3358
+ };
3359
+ }
3360
+ return [capture, classification];
2224
3361
  }
2225
- async function checkConnectivity() {
3362
+ async function checkBackend() {
2226
3363
  try {
2227
- await getClient().get("/api/cli/decisions/recent?limit=1", {
3364
+ const response = await getClient().get("/api/cli/moves/status", {
2228
3365
  signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
2229
3366
  });
2230
- return { name: "connectivity", status: "ok", detail: "server reachable" };
3367
+ const status = parseMovesStatus(response);
3368
+ return [
3369
+ { name: "connectivity", status: "ok", detail: "server reachable and authenticated" },
3370
+ ...classifyMovesStatus(status)
3371
+ ];
2231
3372
  } catch (err) {
2232
3373
  const message = err instanceof Error ? err.message : String(err);
2233
- const status = message.includes("Authentication") ? "fail" : "warn";
2234
- return { name: "connectivity", status, detail: message.slice(0, 80) };
3374
+ return [{ name: "connectivity", status: "fail", detail: message.slice(0, 120) }];
3375
+ }
3376
+ }
3377
+ async function checkFeedbackCapability() {
3378
+ try {
3379
+ const capability = await fetchFeedbackCapability(AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS));
3380
+ return capability.status === "available" ? { name: "feedback-api", status: "ok", detail: "server supports decision feedback" } : {
3381
+ name: "feedback-api",
3382
+ status: "warn",
3383
+ detail: "available after binding this CLI to an organization"
3384
+ };
3385
+ } catch (error) {
3386
+ if (error instanceof HttpError && error.status === 404) {
3387
+ return {
3388
+ name: "feedback-api",
3389
+ status: "warn",
3390
+ detail: "server does not support decision feedback yet"
3391
+ };
3392
+ }
3393
+ if (error instanceof HttpError && error.status === 401) {
3394
+ return { name: "feedback-api", status: "fail", detail: error.message.slice(0, 80) };
3395
+ }
3396
+ const message = error instanceof Error ? error.message : String(error);
3397
+ return { name: "feedback-api", status: "warn", detail: message.slice(0, 80) };
2235
3398
  }
2236
3399
  }
2237
3400
  async function collectChecks() {
3401
+ const backend = await checkBackend();
2238
3402
  return [
2239
3403
  checkAuth(),
2240
3404
  await checkDaemon(),
2241
3405
  checkJournal(),
2242
3406
  checkStranded(),
2243
- await checkConnectivity()
3407
+ checkFeedbackHooks(),
3408
+ checkWorkspaceIdentity(),
3409
+ ...backend,
3410
+ await checkFeedbackCapability()
2244
3411
  ];
2245
3412
  }
2246
3413
  function icon(status) {
@@ -2262,26 +3429,28 @@ async function runDoctor() {
2262
3429
  }
2263
3430
  }
2264
3431
  function registerDoctorCommands(program2) {
2265
- program2.command("doctor").description("Check capture-pipeline health end to end (auth, daemon, journal, server)").action(async () => {
3432
+ program2.command("doctor").description(
3433
+ "Check capture and feedback health end to end (auth, supervisor, delivery, worktree, server)"
3434
+ ).action(async () => {
2266
3435
  await runDoctor();
2267
3436
  });
2268
3437
  }
2269
3438
 
2270
3439
  // src/commands/hermes-install.ts
2271
3440
  import {
2272
- chmodSync,
3441
+ chmodSync as chmodSync2,
2273
3442
  closeSync as closeSync3,
2274
- existsSync as existsSync5,
3443
+ existsSync as existsSync6,
2275
3444
  fsyncSync as fsyncSync2,
2276
- mkdirSync as mkdirSync4,
3445
+ mkdirSync as mkdirSync5,
2277
3446
  openSync as openSync3,
2278
- readFileSync as readFileSync4,
2279
- renameSync as renameSync2,
2280
- rmSync as rmSync2,
2281
- writeFileSync as writeFileSync3
3447
+ readFileSync as readFileSync5,
3448
+ renameSync as renameSync3,
3449
+ rmSync as rmSync3,
3450
+ writeFileSync as writeFileSync4
2282
3451
  } from "fs";
2283
- import { homedir as homedir4 } from "os";
2284
- import { dirname as dirname3, join as join4 } from "path";
3452
+ import { homedir as homedir5 } from "os";
3453
+ import { dirname as dirname4, join as join5 } from "path";
2285
3454
  import { Document, parseDocument, stringify } from "yaml";
2286
3455
  var CAPTURE_BIN3 = "prim-hook";
2287
3456
  var GATE_BIN3 = "prim-pre-tool-use";
@@ -2300,13 +3469,13 @@ var PRIM_BINS3 = [
2300
3469
  SESSION_END_BIN2
2301
3470
  ];
2302
3471
  function hermesHome() {
2303
- return process.env.HERMES_HOME ?? join4(homedir4(), ".hermes");
3472
+ return process.env.HERMES_HOME ?? join5(homedir5(), ".hermes");
2304
3473
  }
2305
3474
  function configPath() {
2306
- return join4(hermesHome(), "config.yaml");
3475
+ return join5(hermesHome(), "config.yaml");
2307
3476
  }
2308
3477
  function shimPath() {
2309
- return join4(hermesHome(), "agent-hooks", "prim-shim.sh");
3478
+ return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
2310
3479
  }
2311
3480
  var SHIM_SCRIPT = `#!/bin/sh
2312
3481
  # prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
@@ -2395,10 +3564,10 @@ function isCaptureInstalled(hooks) {
2395
3564
  );
2396
3565
  }
2397
3566
  function readDoc(path) {
2398
- if (!existsSync5(path)) {
3567
+ if (!existsSync6(path)) {
2399
3568
  return new Document();
2400
3569
  }
2401
- const raw = readFileSync4(path, "utf-8");
3570
+ const raw = readFileSync5(path, "utf-8");
2402
3571
  return raw.trim().length === 0 ? new Document() : parseDocument(raw);
2403
3572
  }
2404
3573
  function readHooks(doc) {
@@ -2470,19 +3639,19 @@ function setAutoAccept(raw) {
2470
3639
  `;
2471
3640
  }
2472
3641
  function atomicWriteFile(path, content) {
2473
- const dir = dirname3(path);
2474
- if (!existsSync5(dir)) {
2475
- mkdirSync4(dir, { recursive: true });
3642
+ const dir = dirname4(path);
3643
+ if (!existsSync6(dir)) {
3644
+ mkdirSync5(dir, { recursive: true });
2476
3645
  }
2477
3646
  const tmp = `${path}.tmp.${String(process.pid)}`;
2478
- writeFileSync3(tmp, content, "utf-8");
3647
+ writeFileSync4(tmp, content, "utf-8");
2479
3648
  const fd = openSync3(tmp, "r+");
2480
3649
  try {
2481
3650
  fsyncSync2(fd);
2482
3651
  } finally {
2483
3652
  closeSync3(fd);
2484
3653
  }
2485
- renameSync2(tmp, path);
3654
+ renameSync3(tmp, path);
2486
3655
  }
2487
3656
  function mergeKeepsYamlValid(before, after) {
2488
3657
  return parseDocument(after).errors.length <= parseDocument(before).errors.length;
@@ -2495,15 +3664,15 @@ function assertMergeValid(before, after) {
2495
3664
  }
2496
3665
  function writeShim() {
2497
3666
  const path = shimPath();
2498
- const dir = dirname3(path);
2499
- if (!existsSync5(dir)) {
2500
- mkdirSync4(dir, { recursive: true });
3667
+ const dir = dirname4(path);
3668
+ if (!existsSync6(dir)) {
3669
+ mkdirSync5(dir, { recursive: true });
2501
3670
  }
2502
- writeFileSync3(path, SHIM_SCRIPT, "utf-8");
2503
- chmodSync(path, SHIM_MODE);
3671
+ writeFileSync4(path, SHIM_SCRIPT, "utf-8");
3672
+ chmodSync2(path, SHIM_MODE);
2504
3673
  }
2505
3674
  function removeShim() {
2506
- rmSync2(shimPath(), { force: true });
3675
+ rmSync3(shimPath(), { force: true });
2507
3676
  }
2508
3677
  function autoAcceptOf(raw) {
2509
3678
  if (raw.trim().length === 0) {
@@ -2516,7 +3685,7 @@ function readHooksFromRaw(raw) {
2516
3685
  }
2517
3686
  function performInstall3(opts) {
2518
3687
  const path = configPath();
2519
- const raw = existsSync5(path) ? readFileSync4(path, "utf-8") : "";
3688
+ const raw = existsSync6(path) ? readFileSync5(path, "utf-8") : "";
2520
3689
  const existing = readHooksFromRaw(raw);
2521
3690
  const desired = applyInstall3(existing, opts.force);
2522
3691
  let next = raw;
@@ -2542,11 +3711,11 @@ function performInstall3(opts) {
2542
3711
  }
2543
3712
  function performUninstall3() {
2544
3713
  const path = configPath();
2545
- if (!existsSync5(path)) {
3714
+ if (!existsSync6(path)) {
2546
3715
  removeShim();
2547
3716
  return { path, gate: false, capture: false, autoAccept: false, changed: false };
2548
3717
  }
2549
- const raw = readFileSync4(path, "utf-8");
3718
+ const raw = readFileSync5(path, "utf-8");
2550
3719
  const existing = readHooksFromRaw(raw);
2551
3720
  const remaining = applyUninstall3(existing);
2552
3721
  const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
@@ -2566,7 +3735,7 @@ function performUninstall3() {
2566
3735
  }
2567
3736
  function performStatus3() {
2568
3737
  const path = configPath();
2569
- const hooks = existsSync5(path) ? readHooks(readDoc(path)) : {};
3738
+ const hooks = existsSync6(path) ? readHooks(readDoc(path)) : {};
2570
3739
  return { path, gate: isGateInstalled3(hooks), capture: isCaptureInstalled(hooks) };
2571
3740
  }
2572
3741
  var TRUST_NOTICE2 = "[prim] Hermes requires hook consent: it prompts once per (event, command) on a TTY and records approval in ~/.hermes/shell-hooks-allowlist.json. To pre-approve non-interactively, re-run with --auto-accept (sets hooks_auto_accept: true), export HERMES_ACCEPT_HOOKS=1, or start Hermes with `hermes --accept-hooks chat`. Until approved, the hooks will not fire.";
@@ -2611,9 +3780,9 @@ function registerHermesCommands(program2) {
2611
3780
 
2612
3781
  // src/commands/hooks.ts
2613
3782
  import { execFileSync } from "child_process";
2614
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2615
- import { homedir as homedir5 } from "os";
2616
- import { dirname as dirname4, join as join5, resolve } from "path";
3783
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3784
+ import { homedir as homedir6 } from "os";
3785
+ import { dirname as dirname5, join as join6, resolve as resolve2 } from "path";
2617
3786
  import { Option } from "commander";
2618
3787
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
2619
3788
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
@@ -2662,16 +3831,16 @@ ${gatedShim(spec.binName)}
2662
3831
  ${end}`;
2663
3832
  }
2664
3833
  function mergePrimBlock(hookPath, block, binName) {
2665
- if (existsSync6(hookPath)) {
2666
- const existing = readFileSync5(hookPath, "utf-8");
3834
+ if (existsSync7(hookPath)) {
3835
+ const existing = readFileSync6(hookPath, "utf-8");
2667
3836
  if (containsPrimHook(existing, binName)) return false;
2668
3837
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
2669
- writeFileSync4(hookPath, `${existing}${separator}${block}
3838
+ writeFileSync5(hookPath, `${existing}${separator}${block}
2670
3839
  `, { mode: 493 });
2671
3840
  return true;
2672
3841
  }
2673
- mkdirSync5(dirname4(hookPath), { recursive: true });
2674
- writeFileSync4(hookPath, `#!/bin/sh
3842
+ mkdirSync6(dirname5(hookPath), { recursive: true });
3843
+ writeFileSync5(hookPath, `#!/bin/sh
2675
3844
  # ${PRIM_CREATED_MARK}
2676
3845
 
2677
3846
  ${block}
@@ -2686,14 +3855,14 @@ function getGitRoot() {
2686
3855
  return root;
2687
3856
  }
2688
3857
  function detectHusky(gitRoot) {
2689
- const huskyDir = resolve(gitRoot, ".husky");
2690
- if (!existsSync6(huskyDir)) return false;
2691
- if (existsSync6(resolve(huskyDir, "_"))) return true;
2692
- if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
2693
- const pkgPath = resolve(gitRoot, "package.json");
2694
- if (existsSync6(pkgPath)) {
3858
+ const huskyDir = resolve2(gitRoot, ".husky");
3859
+ if (!existsSync7(huskyDir)) return false;
3860
+ if (existsSync7(resolve2(huskyDir, "_"))) return true;
3861
+ if (existsSync7(resolve2(huskyDir, "pre-commit"))) return true;
3862
+ const pkgPath = resolve2(gitRoot, "package.json");
3863
+ if (existsSync7(pkgPath)) {
2695
3864
  try {
2696
- const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
3865
+ const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
2697
3866
  const scripts = pkg2.scripts ?? {};
2698
3867
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
2699
3868
  return true;
@@ -2708,8 +3877,8 @@ function containsPrimHook(content, binName = PRE_COMMIT.binName) {
2708
3877
  }
2709
3878
  async function askConfirmation(question) {
2710
3879
  if (!process.stdin.isTTY) return false;
2711
- const { createInterface } = await import("readline/promises");
2712
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3880
+ const { createInterface: createInterface2 } = await import("readline/promises");
3881
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2713
3882
  try {
2714
3883
  const answer = await rl.question(`${question} [y/N] `);
2715
3884
  const normalized = answer.trim().toLowerCase();
@@ -2719,8 +3888,8 @@ async function askConfirmation(question) {
2719
3888
  }
2720
3889
  }
2721
3890
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
2722
- const hookPath = resolve(gitRoot, ".husky", spec.hookName);
2723
- const existed = existsSync6(hookPath);
3891
+ const hookPath = resolve2(gitRoot, ".husky", spec.hookName);
3892
+ const existed = existsSync7(hookPath);
2724
3893
  const wrote = mergePrimBlock(hookPath, huskyBlock(spec), spec.binName);
2725
3894
  if (!wrote) {
2726
3895
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
@@ -2731,13 +3900,13 @@ function installToHusky(gitRoot, spec = PRE_COMMIT) {
2731
3900
  }
2732
3901
  }
2733
3902
  function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2734
- const hooksDir = resolve(gitRoot, ".git", "hooks");
2735
- const hookPath = resolve(hooksDir, spec.hookName);
2736
- if (!existsSync6(hooksDir)) {
2737
- mkdirSync5(hooksDir, { recursive: true });
3903
+ const hooksDir = resolve2(gitRoot, ".git", "hooks");
3904
+ const hookPath = resolve2(hooksDir, spec.hookName);
3905
+ if (!existsSync7(hooksDir)) {
3906
+ mkdirSync6(hooksDir, { recursive: true });
2738
3907
  }
2739
- if (existsSync6(hookPath)) {
2740
- const existing = readFileSync5(hookPath, "utf-8");
3908
+ if (existsSync7(hookPath)) {
3909
+ const existing = readFileSync6(hookPath, "utf-8");
2741
3910
  if (containsPrimHook(existing, spec.binName)) {
2742
3911
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
2743
3912
  return;
@@ -2746,12 +3915,12 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2746
3915
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
2747
3916
  return;
2748
3917
  }
2749
- writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
3918
+ writeFileSync5(hookPath, dotGitScript(spec), { mode: 493 });
2750
3919
  console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
2751
3920
  }
2752
- var PRIM_GIT_HOOKS_DIR = join5(homedir5(), ".config", "prim", "git-hooks");
3921
+ var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
2753
3922
  function expandTilde(p) {
2754
- return p.startsWith("~/") ? join5(homedir5(), p.slice(2)) : p;
3923
+ return p.startsWith("~/") ? join6(homedir6(), p.slice(2)) : p;
2755
3924
  }
2756
3925
  function isOurHooksDir(value) {
2757
3926
  return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
@@ -2816,24 +3985,24 @@ function ownedHookNames() {
2816
3985
  return [...HOOKS.map((s) => s.hookName), ...PASSTHROUGH_HOOKS];
2817
3986
  }
2818
3987
  function writeOwnHooks() {
2819
- if (!existsSync6(PRIM_GIT_HOOKS_DIR)) {
2820
- mkdirSync5(PRIM_GIT_HOOKS_DIR, { recursive: true });
3988
+ if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
3989
+ mkdirSync6(PRIM_GIT_HOOKS_DIR, { recursive: true });
2821
3990
  }
2822
3991
  for (const spec of HOOKS) {
2823
- writeFileSync4(resolve(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
3992
+ writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
2824
3993
  mode: 493
2825
3994
  });
2826
3995
  }
2827
3996
  for (const name of PASSTHROUGH_HOOKS) {
2828
- writeFileSync4(resolve(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
3997
+ writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
2829
3998
  }
2830
3999
  }
2831
4000
  function appendPrimBlock(hookPath, spec) {
2832
4001
  mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
2833
4002
  }
2834
4003
  function stripPrimBlock(hookPath, spec) {
2835
- if (!existsSync6(hookPath)) return;
2836
- const existing = readFileSync5(hookPath, "utf-8");
4004
+ if (!existsSync7(hookPath)) return;
4005
+ const existing = readFileSync6(hookPath, "utf-8");
2837
4006
  const primCreated = existing.includes(PRIM_CREATED_MARK);
2838
4007
  const { start, end } = blockMarkers(spec);
2839
4008
  const s = existing.indexOf(start);
@@ -2845,7 +4014,7 @@ function stripPrimBlock(hookPath, spec) {
2845
4014
  unlinkSync2(hookPath);
2846
4015
  return;
2847
4016
  }
2848
- writeFileSync4(hookPath, out, { mode: 493 });
4017
+ writeFileSync5(hookPath, out, { mode: 493 });
2849
4018
  }
2850
4019
  function installGlobalHooks(opts = {}) {
2851
4020
  const global = gitConfigGet("--global");
@@ -2876,7 +4045,7 @@ function installGlobalHooks(opts = {}) {
2876
4045
  }
2877
4046
  const dir = expandTilde(global);
2878
4047
  for (const spec of HOOKS) {
2879
- appendPrimBlock(resolve(dir, spec.hookName), spec);
4048
+ appendPrimBlock(resolve2(dir, spec.hookName), spec);
2880
4049
  }
2881
4050
  console.log(
2882
4051
  `Appended prim hooks into existing core.hooksPath dir ${global} (pointer unchanged).`
@@ -2887,8 +4056,8 @@ function uninstallGlobalHooks() {
2887
4056
  const global = gitConfigGet("--global");
2888
4057
  if (isOurHooksDir(global)) {
2889
4058
  for (const name of ownedHookNames()) {
2890
- const p = resolve(PRIM_GIT_HOOKS_DIR, name);
2891
- if (existsSync6(p)) unlinkSync2(p);
4059
+ const p = resolve2(PRIM_GIT_HOOKS_DIR, name);
4060
+ if (existsSync7(p)) unlinkSync2(p);
2892
4061
  }
2893
4062
  execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"]);
2894
4063
  console.log("Removed prim global git hooks and unset core.hooksPath.");
@@ -2897,7 +4066,7 @@ function uninstallGlobalHooks() {
2897
4066
  if (global !== "") {
2898
4067
  const dir = expandTilde(global);
2899
4068
  for (const spec of HOOKS) {
2900
- stripPrimBlock(resolve(dir, spec.hookName), spec);
4069
+ stripPrimBlock(resolve2(dir, spec.hookName), spec);
2901
4070
  }
2902
4071
  console.log(`Removed the prim block from ${global} (left the dir and core.hooksPath intact).`);
2903
4072
  return;
@@ -2977,12 +4146,12 @@ function registerHooksCommands(program2) {
2977
4146
  }
2978
4147
  const gitRoot = getGitRoot();
2979
4148
  for (const spec of HOOKS) {
2980
- const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
2981
- if (!existsSync6(hookPath)) {
4149
+ const hookPath = resolve2(gitRoot, ".git", "hooks", spec.hookName);
4150
+ if (!existsSync7(hookPath)) {
2982
4151
  console.log(`No ${spec.hookName} hook found.`);
2983
4152
  continue;
2984
4153
  }
2985
- if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
4154
+ if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
2986
4155
  unlinkSync2(hookPath);
2987
4156
  console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
2988
4157
  } else {
@@ -2993,43 +4162,60 @@ function registerHooksCommands(program2) {
2993
4162
  }
2994
4163
 
2995
4164
  // src/commands/moves.ts
2996
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
2997
- import { join as join6 } from "path";
4165
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
4166
+ import { join as join7 } from "path";
2998
4167
 
2999
4168
  // src/flusher.ts
3000
- import { renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
4169
+ import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
4170
+ import { createInterface } from "readline";
3001
4171
  var BATCH_SIZE = 500;
3002
4172
  var HTTP_TIMEOUT_MS = 1e4;
3003
4173
  var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
3004
4174
  var ORPHAN_QUARANTINE_MS = 6e4;
3005
- function batchMoves(moves, size = BATCH_SIZE) {
3006
- const batches = [];
3007
- for (let i = 0; i < moves.length; i += size) {
3008
- batches.push(moves.slice(i, i + size));
3009
- }
3010
- return batches;
3011
- }
3012
- async function drainFlushingPath(flushingPath) {
3013
- const moves = readMovesFromPath(flushingPath);
3014
- if (moves.length === 0) {
3015
- unlinkSync3(flushingPath);
3016
- return 0;
3017
- }
3018
- const client = getClient();
3019
- for (const batch of batchMoves(moves)) {
3020
- await client.post(
4175
+ async function drainFlushingPath(flushingPath, client = getClient()) {
4176
+ const postBatch = async (batch2) => {
4177
+ const response = await client.post(
3021
4178
  "/api/cli/moves/ingest",
3022
- { batch },
4179
+ { batch: batch2 },
3023
4180
  { signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
3024
4181
  );
4182
+ requireDurableIngestAcknowledgement(response, batch2.length);
4183
+ };
4184
+ const input = createReadStream(flushingPath, { encoding: "utf-8" });
4185
+ const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
4186
+ let batch = [];
4187
+ let total = 0;
4188
+ try {
4189
+ for await (const line of lines) {
4190
+ if (line.length === 0) {
4191
+ continue;
4192
+ }
4193
+ try {
4194
+ batch.push(JSON.parse(line));
4195
+ } catch {
4196
+ continue;
4197
+ }
4198
+ if (batch.length === BATCH_SIZE) {
4199
+ await postBatch(batch);
4200
+ total += batch.length;
4201
+ batch = [];
4202
+ }
4203
+ }
4204
+ if (batch.length > 0) {
4205
+ await postBatch(batch);
4206
+ total += batch.length;
4207
+ }
4208
+ } finally {
4209
+ lines.close();
4210
+ input.destroy();
3025
4211
  }
3026
4212
  unlinkSync3(flushingPath);
3027
- return moves.length;
4213
+ return total;
3028
4214
  }
3029
4215
  async function drainPath(path) {
3030
4216
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
3031
4217
  try {
3032
- renameSync3(path, tmpPath);
4218
+ renameSync4(path, tmpPath);
3033
4219
  } catch (err) {
3034
4220
  if (err.code === "ENOENT") {
3035
4221
  return 0;
@@ -3038,7 +4224,7 @@ async function drainPath(path) {
3038
4224
  }
3039
4225
  return drainFlushingPath(tmpPath);
3040
4226
  }
3041
- function processIsAlive2(pid) {
4227
+ function processIsAlive3(pid) {
3042
4228
  try {
3043
4229
  process.kill(pid, 0);
3044
4230
  return true;
@@ -3048,36 +4234,84 @@ function processIsAlive2(pid) {
3048
4234
  }
3049
4235
  function selectRecoverable(files, now, opts = {}) {
3050
4236
  const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
3051
- const isAlive = opts.isAlive ?? processIsAlive2;
3052
- return files.filter(
3053
- (f) => f.pid === void 0 ? now - f.mtimeMs > quarantineMs : !isAlive(f.pid)
3054
- );
4237
+ const isAlive = opts.isAlive ?? processIsAlive3;
4238
+ return files.filter((f) => {
4239
+ if (f.pid === void 0) {
4240
+ return now - f.mtimeMs > quarantineMs;
4241
+ }
4242
+ return f.pid === opts.ownerPid || !isAlive(f.pid);
4243
+ });
3055
4244
  }
3056
- async function recoverOrphans() {
3057
- let total = 0;
3058
- for (const file of selectRecoverable(listFlushing(), Date.now())) {
4245
+ async function recoverOrphans(candidates = listFlushing({ sampleBytes: 0 }), options = {}) {
4246
+ const summary = { flushed: 0, errors: [], failedBuckets: /* @__PURE__ */ new Set() };
4247
+ const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
4248
+ ownerPid: options.ownerPid ?? process.pid,
4249
+ isAlive: options.isAlive
4250
+ }).sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
4251
+ const drain = options.drain ?? drainFlushingPath;
4252
+ for (const file of recoverable) {
4253
+ if (summary.failedBuckets.has(file.bucket)) {
4254
+ continue;
4255
+ }
3059
4256
  try {
3060
- total += await drainFlushingPath(file.path);
3061
- } catch {
4257
+ summary.flushed += await drain(file.path);
4258
+ } catch (err) {
4259
+ summary.errors.push(err);
4260
+ summary.failedBuckets.add(file.bucket);
3062
4261
  }
3063
4262
  }
3064
- return total;
4263
+ return summary;
3065
4264
  }
3066
- async function flush() {
3067
- let total = await recoverOrphans();
3068
- for (const { path } of listBuckets()) {
3069
- total += await drainPath(path);
4265
+ var FlushError = class extends Error {
4266
+ flushed;
4267
+ constructor(cause, flushed) {
4268
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
4269
+ this.name = "FlushError";
4270
+ this.flushed = flushed;
4271
+ }
4272
+ };
4273
+ async function flushOnce() {
4274
+ const recovered = await recoverOrphans();
4275
+ let total = recovered.flushed;
4276
+ const errors = recovered.errors;
4277
+ for (const { bucket, path } of listBuckets()) {
4278
+ if (recovered.failedBuckets.has(bucket)) {
4279
+ continue;
4280
+ }
4281
+ try {
4282
+ total += await drainPath(path);
4283
+ } catch (err) {
4284
+ errors.push(err);
4285
+ }
4286
+ }
4287
+ if (errors.length > 0) {
4288
+ throw new FlushError(errors[0], total);
3070
4289
  }
3071
4290
  return { flushed: total };
3072
4291
  }
4292
+ var flushInFlight;
4293
+ function flush() {
4294
+ if (flushInFlight) {
4295
+ return flushInFlight;
4296
+ }
4297
+ const attempt = flushOnce().finally(() => {
4298
+ if (flushInFlight === attempt) {
4299
+ flushInFlight = void 0;
4300
+ }
4301
+ });
4302
+ flushInFlight = attempt;
4303
+ return attempt;
4304
+ }
4305
+ function shouldFlushPending(stats, now, thresholdMs = OPPORTUNISTIC_FLUSH_AFTER_MS) {
4306
+ if (stats.sampled) {
4307
+ return true;
4308
+ }
4309
+ return stats.pendingCount > 0 && (stats.oldestPendingAt === void 0 || now - stats.oldestPendingAt > thresholdMs);
4310
+ }
3073
4311
  async function flushIfNeeded() {
3074
4312
  try {
3075
- const stats = bucketStats();
3076
- if (stats.length === 0) {
3077
- return;
3078
- }
3079
- const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
3080
- if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
4313
+ const stats = pendingJournalStats();
4314
+ if (shouldFlushPending(stats, Date.now())) {
3081
4315
  await flush();
3082
4316
  }
3083
4317
  } catch {
@@ -3111,21 +4345,25 @@ function registerMovesCommands(program2) {
3111
4345
  console.log(`[prim] root: ${JOURNAL_DIR}`);
3112
4346
  for (const s of stats) {
3113
4347
  const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
4348
+ const count = `${String(s.lineCount)}${s.sampled ? "+" : ""}`;
3114
4349
  console.log(
3115
- ` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
4350
+ ` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
3116
4351
  );
3117
4352
  }
3118
4353
  if (stranded.length > 0) {
3119
4354
  const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
3120
4355
  const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
4356
+ const sampled = stranded.some((file) => file.sampled);
4357
+ const qualifier = sampled ? "at least " : "";
3121
4358
  console.log(
3122
- `[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
4359
+ `[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${qualifier}${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
3123
4360
  );
3124
4361
  for (const f of stranded) {
3125
4362
  const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
3126
4363
  const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
4364
+ const count = `${String(f.lineCount)}${f.sampled ? "+" : ""}`;
3127
4365
  console.log(
3128
- ` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(f.lineCount).padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
4366
+ ` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
3129
4367
  );
3130
4368
  }
3131
4369
  }
@@ -3153,19 +4391,19 @@ function registerMovesCommands(program2) {
3153
4391
  }
3154
4392
  });
3155
4393
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
3156
- const dir = join6(process.cwd(), ".prim");
3157
- if (!existsSync7(dir)) {
3158
- mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
4394
+ const dir = join7(process.cwd(), ".prim");
4395
+ if (!existsSync8(dir)) {
4396
+ mkdirSync7(dir, { recursive: true, mode: DIR_MODE });
3159
4397
  }
3160
- const file = join6(process.cwd(), WORKSPACE_FILE);
3161
- writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
4398
+ const file = join7(process.cwd(), WORKSPACE_FILE);
4399
+ writeFileSync6(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
3162
4400
  mode: FILE_MODE2
3163
4401
  });
3164
4402
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
3165
4403
  });
3166
4404
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
3167
- const file = join6(process.cwd(), WORKSPACE_FILE);
3168
- if (!existsSync7(file)) {
4405
+ const file = join7(process.cwd(), WORKSPACE_FILE);
4406
+ if (!existsSync8(file)) {
3169
4407
  console.log("[prim] no workspace binding in cwd");
3170
4408
  return;
3171
4409
  }
@@ -3258,23 +4496,23 @@ function registerReconcileCommands(program2) {
3258
4496
 
3259
4497
  // src/commands/session.ts
3260
4498
  import {
3261
- existsSync as existsSync8,
3262
- mkdirSync as mkdirSync7,
3263
- readFileSync as readFileSync6,
4499
+ existsSync as existsSync9,
4500
+ mkdirSync as mkdirSync8,
4501
+ readFileSync as readFileSync7,
3264
4502
  readdirSync,
3265
4503
  unlinkSync as unlinkSync5,
3266
- writeFileSync as writeFileSync6
4504
+ writeFileSync as writeFileSync7
3267
4505
  } from "fs";
3268
- import { join as join7 } from "path";
4506
+ import { join as join8 } from "path";
3269
4507
  var DIR_MODE2 = 448;
3270
4508
  var FILE_MODE3 = 384;
3271
4509
  function ensureDir() {
3272
- if (!existsSync8(SESSIONS_DIR)) {
3273
- mkdirSync7(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
4510
+ if (!existsSync9(SESSIONS_DIR)) {
4511
+ mkdirSync8(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
3274
4512
  }
3275
4513
  }
3276
4514
  function markerPath(sessionId) {
3277
- return join7(SESSIONS_DIR, `${sessionId}.json`);
4515
+ return join8(SESSIONS_DIR, `${sessionId}.json`);
3278
4516
  }
3279
4517
  function registerSessionCommands(program2) {
3280
4518
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -3284,13 +4522,13 @@ function registerSessionCommands(program2) {
3284
4522
  orgId: opts.orgId,
3285
4523
  startedAt: Date.now()
3286
4524
  };
3287
- writeFileSync6(markerPath(sessionId), JSON.stringify(marker, null, 2), {
4525
+ writeFileSync7(markerPath(sessionId), JSON.stringify(marker, null, 2), {
3288
4526
  mode: FILE_MODE3
3289
4527
  });
3290
4528
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
3291
4529
  });
3292
4530
  session.command("list").description("List active session markers").action(() => {
3293
- if (!existsSync8(SESSIONS_DIR)) {
4531
+ if (!existsSync9(SESSIONS_DIR)) {
3294
4532
  console.log("[prim] no session markers");
3295
4533
  return;
3296
4534
  }
@@ -3302,7 +4540,7 @@ function registerSessionCommands(program2) {
3302
4540
  for (const f of files) {
3303
4541
  const sessionId = f.replace(/\.json$/, "");
3304
4542
  try {
3305
- const m = JSON.parse(readFileSync6(join7(SESSIONS_DIR, f), "utf-8"));
4543
+ const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
3306
4544
  console.log(`${sessionId} org=${m.orgId}`);
3307
4545
  } catch {
3308
4546
  }
@@ -3310,7 +4548,7 @@ function registerSessionCommands(program2) {
3310
4548
  });
3311
4549
  session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
3312
4550
  const p = markerPath(sessionId);
3313
- if (!existsSync8(p)) {
4551
+ if (!existsSync9(p)) {
3314
4552
  console.log(`[prim] no marker for session ${sessionId}`);
3315
4553
  return;
3316
4554
  }
@@ -3320,9 +4558,9 @@ function registerSessionCommands(program2) {
3320
4558
  }
3321
4559
 
3322
4560
  // src/commands/setup.ts
3323
- import { spawnSync } from "child_process";
3324
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3325
- import { join as join8 } from "path";
4561
+ import { spawnSync as spawnSync2 } from "child_process";
4562
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
4563
+ import { join as join9 } from "path";
3326
4564
  var EXIT_INCOMPLETE = 1;
3327
4565
  var EXIT_USAGE3 = 2;
3328
4566
  var SESSION_LABELS = {
@@ -3346,7 +4584,20 @@ function planSetupSteps(opts) {
3346
4584
  key: "daemon",
3347
4585
  label: "Companion daemon",
3348
4586
  args: ["daemon", "start"],
3349
- required: false
4587
+ required: true
4588
+ });
4589
+ steps.push({
4590
+ key: "health",
4591
+ label: "Capture health",
4592
+ args: ["doctor"],
4593
+ required: true
4594
+ });
4595
+ } else {
4596
+ steps.push({
4597
+ key: "daemon-opt-out",
4598
+ label: "Daemon opt-out",
4599
+ args: ["daemon", "stop"],
4600
+ required: true
3350
4601
  });
3351
4602
  }
3352
4603
  steps.push({
@@ -3385,8 +4636,8 @@ function detectProjectConflicts(agent, run) {
3385
4636
  }
3386
4637
  try {
3387
4638
  const root = gitToplevel();
3388
- const preCommit = root && join8(root, ".git", "hooks", "pre-commit");
3389
- if (preCommit && existsSync9(preCommit) && readFileSync7(preCommit, "utf-8").includes("prim-pre-commit")) {
4639
+ const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
4640
+ if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
3390
4641
  conflicts.push(CONFLICT_HOOKS);
3391
4642
  }
3392
4643
  } catch {
@@ -3422,7 +4673,7 @@ function registerSetupCommand(program2) {
3422
4673
  ).option(
3423
4674
  "--migrate",
3424
4675
  "with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
3425
- ).option("--no-daemon", "skip starting the companion daemon").action((opts) => {
4676
+ ).option("--no-daemon", "stop and disable the companion daemon").action((opts) => {
3426
4677
  const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
3427
4678
  if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
3428
4679
  process.stderr.write(
@@ -3440,7 +4691,7 @@ function registerSetupCommand(program2) {
3440
4691
  const scope = opts.scope;
3441
4692
  const self = process.argv[1];
3442
4693
  const run = (args, capture = false) => {
3443
- const r = spawnSync(process.execPath, [self, ...args], {
4694
+ const r = spawnSync2(process.execPath, [self, ...args], {
3444
4695
  stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
3445
4696
  encoding: "utf-8"
3446
4697
  });
@@ -3504,39 +4755,39 @@ function registerSetupCommand(program2) {
3504
4755
  // src/commands/skill.ts
3505
4756
  import {
3506
4757
  closeSync as closeSync4,
3507
- existsSync as existsSync11,
4758
+ existsSync as existsSync12,
3508
4759
  fsyncSync as fsyncSync3,
3509
4760
  openSync as openSync4,
3510
- readFileSync as readFileSync9,
3511
- renameSync as renameSync4,
3512
- writeFileSync as writeFileSync7
4761
+ readFileSync as readFileSync10,
4762
+ renameSync as renameSync5,
4763
+ writeFileSync as writeFileSync8
3513
4764
  } from "fs";
3514
- import { homedir as homedir7 } from "os";
3515
- import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
4765
+ import { homedir as homedir8 } from "os";
4766
+ import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
3516
4767
  import { fileURLToPath as fileURLToPath2 } from "url";
3517
4768
  import { createPatch } from "diff";
3518
4769
 
3519
4770
  // src/commands/claude-plugin.ts
3520
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync8, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
3521
- import { homedir as homedir6 } from "os";
3522
- import { dirname as dirname5, join as join9, resolve as resolve2 } from "path";
4771
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync4, rmdirSync } from "fs";
4772
+ import { homedir as homedir7 } from "os";
4773
+ import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
3523
4774
  import { fileURLToPath } from "url";
3524
- var __dirname = dirname5(fileURLToPath(import.meta.url));
4775
+ var __dirname = dirname6(fileURLToPath(import.meta.url));
3525
4776
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
3526
4777
  function resolvePluginDir(cwd, scope) {
3527
4778
  if (scope && scope !== "user" && scope !== "project") {
3528
4779
  console.error(`Unknown --scope "${scope}" (expected user or project)`);
3529
4780
  return null;
3530
4781
  }
3531
- const base = scope === "user" ? join9(homedir6(), ".claude") : join9(gitToplevel(cwd) ?? cwd, ".claude");
3532
- return join9(base, "skills", "prim");
4782
+ const base = scope === "user" ? join10(homedir7(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
4783
+ return join10(base, "skills", "prim");
3533
4784
  }
3534
4785
  function packageVersion() {
3535
4786
  let dir = __dirname;
3536
- while (dir !== dirname5(dir)) {
3537
- const p = resolve2(dir, "package.json");
3538
- if (existsSync10(p)) return JSON.parse(readFileSync8(p, "utf-8")).version;
3539
- dir = dirname5(dir);
4787
+ while (dir !== dirname6(dir)) {
4788
+ const p = resolve3(dir, "package.json");
4789
+ if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
4790
+ dir = dirname6(dir);
3540
4791
  }
3541
4792
  return "0.0.0";
3542
4793
  }
@@ -3546,12 +4797,12 @@ function pluginManifest() {
3546
4797
  `;
3547
4798
  }
3548
4799
  function removeDirIfEmpty(dir) {
3549
- if (existsSync10(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
4800
+ if (existsSync11(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
3550
4801
  }
3551
4802
  function pluginPaths(dir) {
3552
4803
  return {
3553
- manifestPath: join9(dir, ".claude-plugin", "plugin.json"),
3554
- skillPath: join9(dir, "SKILL.md")
4804
+ manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
4805
+ skillPath: join10(dir, "SKILL.md")
3555
4806
  };
3556
4807
  }
3557
4808
  function installClaudePlugin(cwd, opts) {
@@ -3560,8 +4811,8 @@ function installClaudePlugin(cwd, opts) {
3560
4811
  const { manifestPath, skillPath } = pluginPaths(dir);
3561
4812
  const manifest = pluginManifest();
3562
4813
  const skill = loadSkill();
3563
- const manifestCurrent = existsSync10(manifestPath) ? readFileSync8(manifestPath, "utf-8") : null;
3564
- const skillCurrent = existsSync10(skillPath) ? readFileSync8(skillPath, "utf-8") : null;
4814
+ const manifestCurrent = existsSync11(manifestPath) ? readFileSync9(manifestPath, "utf-8") : null;
4815
+ const skillCurrent = existsSync11(skillPath) ? readFileSync9(skillPath, "utf-8") : null;
3565
4816
  if (manifestCurrent === manifest && skillCurrent === skill) {
3566
4817
  console.log("No changes \u2014 prim skill plugin already up to date.");
3567
4818
  return 0;
@@ -3570,9 +4821,9 @@ function installClaudePlugin(cwd, opts) {
3570
4821
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
3571
4822
  return 0;
3572
4823
  }
3573
- mkdirSync8(join9(dir, ".claude-plugin"), { recursive: true });
3574
- atomicWrite2(manifestPath, manifest);
3575
- atomicWrite2(skillPath, skill);
4824
+ mkdirSync9(join10(dir, ".claude-plugin"), { recursive: true });
4825
+ atomicWrite3(manifestPath, manifest);
4826
+ atomicWrite3(skillPath, skill);
3576
4827
  console.log(`Installed prim skill plugin at ${dir}`);
3577
4828
  console.log("Restart Claude Code or run /reload-plugins to load it.");
3578
4829
  if (opts.scope !== "user") {
@@ -3584,13 +4835,13 @@ function uninstallClaudePlugin(cwd, opts) {
3584
4835
  const dir = resolvePluginDir(cwd, opts.scope);
3585
4836
  if (dir === null) return 1;
3586
4837
  const { manifestPath, skillPath } = pluginPaths(dir);
3587
- if (!existsSync10(manifestPath) && !existsSync10(skillPath)) {
4838
+ if (!existsSync11(manifestPath) && !existsSync11(skillPath)) {
3588
4839
  console.log(`prim skill plugin not present at ${dir}`);
3589
4840
  return 0;
3590
4841
  }
3591
- rmSync3(manifestPath, { force: true });
3592
- rmSync3(skillPath, { force: true });
3593
- removeDirIfEmpty(join9(dir, ".claude-plugin"));
4842
+ rmSync4(manifestPath, { force: true });
4843
+ rmSync4(skillPath, { force: true });
4844
+ removeDirIfEmpty(join10(dir, ".claude-plugin"));
3594
4845
  removeDirIfEmpty(dir);
3595
4846
  console.log(`Removed prim skill plugin from ${dir}`);
3596
4847
  return 0;
@@ -3599,7 +4850,7 @@ function statusClaudePlugin(cwd, opts) {
3599
4850
  const dir = resolvePluginDir(cwd, opts.scope);
3600
4851
  if (dir === null) return 1;
3601
4852
  const { skillPath } = pluginPaths(dir);
3602
- const installed = existsSync10(skillPath);
4853
+ const installed = existsSync11(skillPath);
3603
4854
  if (opts.json) {
3604
4855
  printJson({ installed, target: dir });
3605
4856
  return installed ? 0 : 1;
@@ -3613,7 +4864,7 @@ function statusClaudePlugin(cwd, opts) {
3613
4864
  }
3614
4865
 
3615
4866
  // src/commands/skill.ts
3616
- var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
4867
+ var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
3617
4868
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
3618
4869
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
3619
4870
  var TARGET_CANDIDATES = [
@@ -3631,24 +4882,24 @@ var AGENT_TARGET = {
3631
4882
  hermes: ".hermes.md"
3632
4883
  };
3633
4884
  function userTargetFor(agent) {
3634
- if (agent === "claude") return join10(homedir7(), ".claude", "CLAUDE.md");
3635
- if (agent === "codex") return join10(homedir7(), ".codex", "AGENTS.md");
4885
+ if (agent === "claude") return join11(homedir8(), ".claude", "CLAUDE.md");
4886
+ if (agent === "codex") return join11(homedir8(), ".codex", "AGENTS.md");
3636
4887
  if (agent === "hermes") {
3637
- return join10(process.env.HERMES_HOME ?? join10(homedir7(), ".hermes"), ".hermes.md");
4888
+ return join11(process.env.HERMES_HOME ?? join11(homedir8(), ".hermes"), ".hermes.md");
3638
4889
  }
3639
4890
  return null;
3640
4891
  }
3641
4892
  function loadSkill() {
3642
4893
  let dir = __dirname2;
3643
- while (dir !== dirname6(dir)) {
3644
- const p = resolve3(dir, "SKILL.md");
3645
- if (existsSync11(p)) return readFileSync9(p, "utf-8");
3646
- dir = dirname6(dir);
4894
+ while (dir !== dirname7(dir)) {
4895
+ const p = resolve4(dir, "SKILL.md");
4896
+ if (existsSync12(p)) return readFileSync10(p, "utf-8");
4897
+ dir = dirname7(dir);
3647
4898
  }
3648
4899
  throw new Error("SKILL.md not found in package");
3649
4900
  }
3650
4901
  function detectTargets(cwd) {
3651
- return TARGET_CANDIDATES.filter((p) => existsSync11(resolve3(cwd, p)));
4902
+ return TARGET_CANDIDATES.filter((p) => existsSync12(resolve4(cwd, p)));
3652
4903
  }
3653
4904
  function detectNewline(content) {
3654
4905
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -3674,23 +4925,23 @@ function removeBlock(existing) {
3674
4925
  const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
3675
4926
  return out.replace(/(\r?\n){2,}$/, "$1");
3676
4927
  }
3677
- function atomicWrite2(target, content) {
4928
+ function atomicWrite3(target, content) {
3678
4929
  const tmp = `${target}.tmp`;
3679
- writeFileSync7(tmp, content);
4930
+ writeFileSync8(tmp, content);
3680
4931
  const fd = openSync4(tmp, "r+");
3681
4932
  try {
3682
4933
  fsyncSync3(fd);
3683
4934
  } finally {
3684
4935
  closeSync4(fd);
3685
4936
  }
3686
- renameSync4(tmp, target);
4937
+ renameSync5(tmp, target);
3687
4938
  }
3688
4939
  function resolveTarget(cwd, opts) {
3689
4940
  if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
3690
4941
  console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
3691
4942
  return null;
3692
4943
  }
3693
- if (opts.target) return resolve3(cwd, opts.target);
4944
+ if (opts.target) return resolve4(cwd, opts.target);
3694
4945
  if (opts.scope === "user") {
3695
4946
  if (!opts.agent) {
3696
4947
  console.error("--scope user requires --agent (claude, codex, or hermes)");
@@ -3703,13 +4954,13 @@ function resolveTarget(cwd, opts) {
3703
4954
  }
3704
4955
  if (opts.agent) {
3705
4956
  const mapped = AGENT_TARGET[opts.agent];
3706
- if (typeof mapped === "string") return resolve3(cwd, mapped);
4957
+ if (typeof mapped === "string") return resolve4(cwd, mapped);
3707
4958
  console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
3708
4959
  return null;
3709
4960
  }
3710
4961
  const matches = detectTargets(cwd);
3711
- if (matches.length === 0) return resolve3(cwd, DEFAULT_TARGET);
3712
- if (matches.length === 1) return resolve3(cwd, matches[0]);
4962
+ if (matches.length === 0) return resolve4(cwd, DEFAULT_TARGET);
4963
+ if (matches.length === 1) return resolve4(cwd, matches[0]);
3713
4964
  console.error("Multiple rules files detected. Use --target to disambiguate:");
3714
4965
  for (const m of matches) console.error(` ${m}`);
3715
4966
  return null;
@@ -3718,7 +4969,7 @@ function runInstall(cwd, opts) {
3718
4969
  if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
3719
4970
  const target = resolveTarget(cwd, opts);
3720
4971
  if (target === null) return 1;
3721
- const existing = existsSync11(target) ? readFileSync9(target, "utf-8") : "";
4972
+ const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
3722
4973
  const eol = existing ? detectNewline(existing) : "\n";
3723
4974
  const block = composeBlock(loadSkill(), eol);
3724
4975
  const next = applyBlock(existing, block, eol);
@@ -3730,7 +4981,7 @@ function runInstall(cwd, opts) {
3730
4981
  process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
3731
4982
  return 0;
3732
4983
  }
3733
- atomicWrite2(target, next);
4984
+ atomicWrite3(target, next);
3734
4985
  console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
3735
4986
  return 0;
3736
4987
  }
@@ -3738,17 +4989,17 @@ function runUninstall(cwd, opts) {
3738
4989
  if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
3739
4990
  const target = resolveTarget(cwd, opts);
3740
4991
  if (target === null) return 1;
3741
- if (!existsSync11(target)) {
4992
+ if (!existsSync12(target)) {
3742
4993
  console.log(`Skill block not present at ${target}`);
3743
4994
  return 0;
3744
4995
  }
3745
- const existing = readFileSync9(target, "utf-8");
4996
+ const existing = readFileSync10(target, "utf-8");
3746
4997
  const next = removeBlock(existing);
3747
4998
  if (next === null) {
3748
4999
  console.log(`Skill block not present at ${target}`);
3749
5000
  return 0;
3750
5001
  }
3751
- atomicWrite2(target, next);
5002
+ atomicWrite3(target, next);
3752
5003
  console.log(`Removed skill block from ${target}`);
3753
5004
  return 0;
3754
5005
  }
@@ -3756,10 +5007,10 @@ function runStatus(cwd, opts) {
3756
5007
  if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
3757
5008
  const target = resolveTarget(cwd, opts);
3758
5009
  if (target === null) return 1;
3759
- const fileExists = existsSync11(target);
5010
+ const fileExists = existsSync12(target);
3760
5011
  let installed = false;
3761
5012
  if (fileExists) {
3762
- const content = readFileSync9(target, "utf-8");
5013
+ const content = readFileSync10(target, "utf-8");
3763
5014
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
3764
5015
  }
3765
5016
  if (opts.json) {
@@ -3796,18 +5047,24 @@ function registerSkillCommands(program2) {
3796
5047
  }
3797
5048
 
3798
5049
  // src/commands/statusline.ts
3799
- import { readFileSync as readFileSync10 } from "fs";
3800
- import { dirname as dirname7, resolve as resolve4 } from "path";
5050
+ import { readFileSync as readFileSync11 } from "fs";
5051
+ import { dirname as dirname8, resolve as resolve5 } from "path";
3801
5052
  import { fileURLToPath as fileURLToPath3 } from "url";
3802
5053
  var STATUSLINE_TIMEOUT_MS = 200;
3803
5054
  var STATUSLINE_NAME_CAP = 3;
3804
5055
  function readPackageVersion() {
3805
5056
  try {
3806
- const here = dirname7(fileURLToPath3(import.meta.url));
3807
- const candidates = [resolve4(here, "../../package.json"), resolve4(here, "../package.json")];
5057
+ const here = dirname8(fileURLToPath3(import.meta.url));
5058
+ const candidates = [
5059
+ // The supervised runtime stages a tiny manifest beside the standalone
5060
+ // statusline bundle, so it remains versioned without loading the package.
5061
+ resolve5(here, "manifest.json"),
5062
+ resolve5(here, "../../package.json"),
5063
+ resolve5(here, "../package.json")
5064
+ ];
3808
5065
  for (const path of candidates) {
3809
5066
  try {
3810
- const pkg2 = JSON.parse(readFileSync10(path, "utf-8"));
5067
+ const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
3811
5068
  if (pkg2.version) {
3812
5069
  return pkg2.version;
3813
5070
  }
@@ -3837,6 +5094,17 @@ async function renderStatusline() {
3837
5094
  debug("daemon snapshot missing");
3838
5095
  return `primitive ${version} (daemon: down)`;
3839
5096
  }
5097
+ if (snapshot.healthy === false) {
5098
+ if (snapshot.ingestion?.healthy === false) {
5099
+ const pending = snapshot.ingestion.pendingCount;
5100
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
5101
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
5102
+ }
5103
+ if (snapshot.heartbeat?.healthy === false) {
5104
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
5105
+ }
5106
+ return `primitive ${version} (daemon: starting)`;
5107
+ }
3840
5108
  if (snapshot.envMismatch) {
3841
5109
  return `primitive ${version} (daemon: live \xB7 presence: other env)`;
3842
5110
  }
@@ -3862,6 +5130,7 @@ function registerStatuslineCommands(program2) {
3862
5130
  const line = await renderStatusline();
3863
5131
  process.stdout.write(line);
3864
5132
  } catch {
5133
+ process.stdout.write(`primitive ${readPackageVersion()} (daemon: unavailable)`);
3865
5134
  }
3866
5135
  });
3867
5136
  }
@@ -3978,8 +5247,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
3978
5247
  }
3979
5248
 
3980
5249
  // src/index.ts
3981
- var __dirname3 = dirname8(fileURLToPath4(import.meta.url));
3982
- var pkg = JSON.parse(readFileSync11(resolve5(__dirname3, "../package.json"), "utf-8"));
5250
+ var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
5251
+ var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
3983
5252
  updateNotifier({ pkg }).notify();
3984
5253
  var program = new Command();
3985
5254
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(