@primitive.ai/prim 0.1.0-alpha.62 → 0.1.0-alpha.64

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
@@ -13,7 +13,7 @@ import {
13
13
  checkAffectedDecisions,
14
14
  daemonOrDirectGet,
15
15
  formatDecisionsWarning
16
- } from "./chunk-YITJ2H3B.js";
16
+ } from "./chunk-WS3MCNFD.js";
17
17
  import {
18
18
  JOURNAL_DIR,
19
19
  SESSIONS_DIR,
@@ -22,7 +22,7 @@ import {
22
22
  listFlushing,
23
23
  pendingJournalStats,
24
24
  readMovesFromPath
25
- } from "./chunk-S4LFRVGD.js";
25
+ } from "./chunk-HIMWM526.js";
26
26
  import {
27
27
  bindRepository,
28
28
  compareSemver,
@@ -32,12 +32,13 @@ import {
32
32
  postCommitHookBlock,
33
33
  printJson,
34
34
  registerSkillCommands,
35
+ resolveRepositoryBinding,
35
36
  uninstallPostCommitHookAtPath,
36
37
  uninstallProjectPostCommitHook
37
- } from "./chunk-LOGW54W5.js";
38
+ } from "./chunk-MCPHIB4W.js";
38
39
  import {
39
40
  fetchFeedbackCapability
40
- } from "./chunk-R4CUX6FY.js";
41
+ } from "./chunk-KHZNYEUV.js";
41
42
  import {
42
43
  inspectWorkspaceId
43
44
  } from "./chunk-UVIWZWM6.js";
@@ -48,7 +49,7 @@ import {
48
49
  packageVersion,
49
50
  pinnedHookCommand,
50
51
  warmBinCache
51
- } from "./chunk-KBFFVYBC.js";
52
+ } from "./chunk-LNU44OJV.js";
52
53
  import {
53
54
  canonicalGitRoot,
54
55
  canonicalRepositoryPath,
@@ -73,7 +74,7 @@ import {
73
74
  resolveAuthCredential,
74
75
  setStoredToken,
75
76
  withFileLock
76
- } from "./chunk-I3HIPAIR.js";
77
+ } from "./chunk-CPX3BUOC.js";
77
78
  import {
78
79
  daemonIsLive,
79
80
  daemonRequest
@@ -104,6 +105,7 @@ async function applyActivation(active) {
104
105
  bound = await bindRepository(root);
105
106
  }
106
107
  setRepoActive(root, active);
108
+ await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
107
109
  process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
108
110
  `);
109
111
  printJson({ active, repo: root, ...bound, ...postCommitHook ? { postCommitHook } : {} });
@@ -671,7 +673,7 @@ function normalizeApiUrl(url) {
671
673
 
672
674
  // src/daemon/launchd.ts
673
675
  var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
674
- var RUNTIME_SCHEMA_VERSION = 2;
676
+ var RUNTIME_SCHEMA_VERSION = 3;
675
677
  var RUNTIME_DIR_MODE = 448;
676
678
  var RUNTIME_FILE_MODE = 384;
677
679
  var RUNTIME_LAUNCHER_MODE = 448;
@@ -751,7 +753,7 @@ function findPackageVersion(entryFile) {
751
753
  function readRuntimeManifest(path) {
752
754
  try {
753
755
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
754
- if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server" || typeof parsed.daemonSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.daemonSha256) || parsed.statuslineFile !== void 0 && parsed.statuslineFile !== "prim-statusline" || parsed.statuslineFile === void 0 !== (parsed.statuslineSha256 === void 0) || parsed.statuslineSha256 !== void 0 && !/^[a-f0-9]{64}$/u.test(parsed.statuslineSha256)) {
756
+ if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server" || typeof parsed.daemonSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.daemonSha256)) {
755
757
  return null;
756
758
  }
757
759
  return parsed;
@@ -759,12 +761,12 @@ function readRuntimeManifest(path) {
759
761
  return null;
760
762
  }
761
763
  }
762
- function sameRuntime(current, desired, daemonPath, statuslinePath) {
763
- if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256 || current.statuslineFile !== desired.statuslineFile || current.statuslineSha256 !== desired.statuslineSha256) {
764
+ function sameRuntime(current, desired, daemonPath) {
765
+ if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256) {
764
766
  return false;
765
767
  }
766
768
  try {
767
- return sha256File(daemonPath) === desired.daemonSha256 && (desired.statuslineSha256 === void 0 || sha256File(statuslinePath) === desired.statuslineSha256);
769
+ return sha256File(daemonPath) === desired.daemonSha256;
768
770
  } catch {
769
771
  return false;
770
772
  }
@@ -788,8 +790,6 @@ function stageRuntime(options = {}) {
788
790
  if (!daemonSource || !existsSync2(daemonSource)) {
789
791
  throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
790
792
  }
791
- const configuredStatusline = options.statuslineSource === void 0 ? binFile("prim-statusline") : options.statuslineSource;
792
- const statuslineSource = configuredStatusline && existsSync2(configuredStatusline) ? configuredStatusline : null;
793
793
  const nodePath = resolve(options.nodePath ?? process.execPath);
794
794
  const version = options.version ?? findPackageVersion(daemonSource);
795
795
  const desired = {
@@ -798,10 +798,6 @@ function stageRuntime(options = {}) {
798
798
  nodePath,
799
799
  daemonFile: "prim-daemon-server",
800
800
  daemonSha256: sha256File(daemonSource),
801
- ...statuslineSource ? {
802
- statuslineFile: "prim-statusline",
803
- statuslineSha256: sha256File(statuslineSource)
804
- } : {},
805
801
  stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
806
802
  };
807
803
  let currentRelease = null;
@@ -811,19 +807,13 @@ function stageRuntime(options = {}) {
811
807
  }
812
808
  const current = currentRelease ? readRuntimeManifest(join(currentRelease, "manifest.json")) : null;
813
809
  const currentDaemon = currentRelease ? join(currentRelease, desired.daemonFile) : "";
814
- const currentStatusline = currentRelease ? join(currentRelease, "prim-statusline") : "";
815
- if (currentRelease && sameRuntime(current, desired, currentDaemon, currentStatusline)) {
810
+ if (currentRelease && sameRuntime(current, desired, currentDaemon)) {
816
811
  const manifest = current;
817
- const statuslinePath = manifest.statuslineFile ? currentStatusline : void 0;
818
- if (statuslinePath) {
819
- atomicWrite(
820
- paths.statuslineLauncher,
821
- statuslineLauncherContent(statuslinePath, manifest.nodePath),
822
- RUNTIME_LAUNCHER_MODE
823
- );
824
- } else {
825
- rmSync(paths.statuslineLauncher, { force: true });
826
- }
812
+ atomicWrite(
813
+ paths.statuslineLauncher,
814
+ statuslineLauncherContent(manifest.version, options.homeDir),
815
+ RUNTIME_LAUNCHER_MODE
816
+ );
827
817
  return {
828
818
  changed: false,
829
819
  manifest,
@@ -844,14 +834,6 @@ function stageRuntime(options = {}) {
844
834
  if (sha256File(daemonTarget) !== desired.daemonSha256) {
845
835
  throw new Error("cannot stage runtime: daemon bundle changed while it was copied");
846
836
  }
847
- if (statuslineSource && desired.statuslineFile) {
848
- const statuslineTarget = join(stagingDir, desired.statuslineFile);
849
- copyFileSync(statuslineSource, statuslineTarget);
850
- chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
851
- if (sha256File(statuslineTarget) !== desired.statuslineSha256) {
852
- throw new Error("cannot stage runtime: statusline bundle changed while it was copied");
853
- }
854
- }
855
837
  const manifestTarget = join(stagingDir, "manifest.json");
856
838
  writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
857
839
  `, {
@@ -859,23 +841,16 @@ function stageRuntime(options = {}) {
859
841
  mode: RUNTIME_FILE_MODE,
860
842
  flag: "wx"
861
843
  });
862
- const releaseName = `release-${createHash2("sha256").update(
863
- `${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
864
- ).digest("hex").slice(0, 16)}`;
844
+ const releaseName = `release-${createHash2("sha256").update(`${version}\0${nodePath}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`).digest("hex").slice(0, 16)}`;
865
845
  releaseDir = join(paths.releasesDir, releaseName);
866
846
  renameSync(stagingDir, releaseDir);
867
847
  releaseDir = realpathSync(releaseDir);
868
848
  atomicSymlink(join("releases", releaseName), paths.currentLink);
869
- if (desired.statuslineFile) {
870
- const statuslinePath = join(releaseDir, desired.statuslineFile);
871
- atomicWrite(
872
- paths.statuslineLauncher,
873
- statuslineLauncherContent(statuslinePath, desired.nodePath),
874
- RUNTIME_LAUNCHER_MODE
875
- );
876
- } else {
877
- rmSync(paths.statuslineLauncher, { force: true });
878
- }
849
+ atomicWrite(
850
+ paths.statuslineLauncher,
851
+ statuslineLauncherContent(desired.version, options.homeDir),
852
+ RUNTIME_LAUNCHER_MODE
853
+ );
879
854
  } catch (error) {
880
855
  rmSync(stagingDir, { recursive: true, force: true });
881
856
  throw error;
@@ -890,9 +865,20 @@ function stageRuntime(options = {}) {
890
865
  function shellQuote(value) {
891
866
  return `'${value.replaceAll("'", `'"'"'`)}'`;
892
867
  }
893
- function statuslineLauncherContent(statuslinePath, nodePath) {
868
+ function statuslineLauncherContent(version, homeDir = homedir()) {
869
+ const socketPath = join(homeDir, ".config", "prim", "sock");
870
+ const fallback = `primitive ${version} (daemon: down)`;
894
871
  return `#!/bin/sh
895
- exec ${shellQuote(nodePath)} ${shellQuote(statuslinePath)}
872
+ response=$(
873
+ printf 'prim-statusline-v1\\000%s\\000%s\\000' "\${PWD:-/}" "\${PRIM_API_URL:-}" |
874
+ /usr/bin/nc -U -w 1 ${shellQuote(socketPath)} 2>/dev/null
875
+ )
876
+ status=$?
877
+ if [ "$status" -eq 0 ] && [ -n "$response" ]; then
878
+ printf '%s' "$response"
879
+ else
880
+ printf '%s' ${shellQuote(fallback)}
881
+ fi
896
882
  `;
897
883
  }
898
884
  function runtimeStatuslineCommand(options = {}) {
@@ -1488,6 +1474,7 @@ var CAPTURE_EVENTS = [
1488
1474
  "UserPromptSubmit",
1489
1475
  "PreToolUse",
1490
1476
  "PostToolUse",
1477
+ "PostToolUseFailure",
1491
1478
  "Stop",
1492
1479
  "SessionEnd",
1493
1480
  "SubagentStop"
@@ -1509,6 +1496,11 @@ var REGISTRATIONS = [
1509
1496
  ),
1510
1497
  makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
1511
1498
  makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
1499
+ makeRegistration(
1500
+ "PostToolUseFailure",
1501
+ "Edit|Write|MultiEdit|NotebookEdit|Bash",
1502
+ POST_TOOL_USE_BIN
1503
+ ),
1512
1504
  makeRegistration("SessionStart", "*", SESSION_START_BIN),
1513
1505
  makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
1514
1506
  ];
@@ -1728,10 +1720,7 @@ function performInstall(scope, force) {
1728
1720
  let statuslineCommand = STATUSLINE_COMMAND;
1729
1721
  if (!blockedByUserStatusline && process.platform === "darwin" && (!before.statusLine || isPrimStatusLine(before))) {
1730
1722
  try {
1731
- const runtime = stageRuntime();
1732
- if (!runtime.manifest.statuslineFile) {
1733
- throw new Error("standalone prim-statusline entry is unavailable");
1734
- }
1723
+ stageRuntime();
1735
1724
  statuslineCommand = runtimeStatuslineCommand();
1736
1725
  } catch (error) {
1737
1726
  const detail = error instanceof Error ? error.message : String(error);
@@ -1893,6 +1882,8 @@ var GATE_BIN2 = "prim-pre-tool-use";
1893
1882
  var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
1894
1883
  var SESSION_START_BIN2 = "prim-session-start";
1895
1884
  var CODEX_ARGS = "--agent codex";
1885
+ var CODEX_EDIT_MATCHER = "apply_patch";
1886
+ var CODEX_POST_TOOL_MATCHER = "apply_patch|Bash";
1896
1887
  var JSON_INDENT2 = 2;
1897
1888
  var CODEX_CAPTURE_EVENTS = [
1898
1889
  "SessionStart",
@@ -1905,8 +1896,8 @@ var CODEX_CAPTURE_EVENTS = [
1905
1896
  var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_USE_BIN2, SESSION_START_BIN2];
1906
1897
  var CODEX_REGISTRATIONS = [
1907
1898
  ...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN2, CODEX_ARGS)),
1908
- makeRegistration("PreToolUse", "apply_patch", GATE_BIN2, CODEX_ARGS),
1909
- makeRegistration("PostToolUse", "apply_patch", POST_TOOL_USE_BIN2, CODEX_ARGS),
1899
+ makeRegistration("PreToolUse", CODEX_EDIT_MATCHER, GATE_BIN2, CODEX_ARGS),
1900
+ makeRegistration("PostToolUse", CODEX_POST_TOOL_MATCHER, POST_TOOL_USE_BIN2, CODEX_ARGS),
1910
1901
  makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
1911
1902
  ];
1912
1903
  var USER_SCOPE_PATH2 = join3(homedir3(), ".codex", "hooks.json");
@@ -2030,10 +2021,74 @@ ${line("project", result.project)}`);
2030
2021
  }
2031
2022
 
2032
2023
  // src/commands/daemon.ts
2033
- import { spawn } from "child_process";
2024
+ import { spawn as spawn2 } from "child_process";
2034
2025
  import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
2026
+ import { homedir as homedir5 } from "os";
2027
+ import { join as join5 } from "path";
2028
+ import { Option } from "commander";
2029
+
2030
+ // src/daemon/latest-bootstrap.ts
2031
+ import { spawn } from "child_process";
2035
2032
  import { homedir as homedir4 } from "os";
2036
2033
  import { join as join4 } from "path";
2034
+ var LATEST_BOOTSTRAP_TIMEOUT_MS = 45e3;
2035
+ var LATEST_BOOTSTRAP_LOCK_NAME = "daemon-latest.lock";
2036
+ async function runLatestEnsure(options) {
2037
+ const env = {
2038
+ ...options.env ?? process.env,
2039
+ npm_config_prefer_online: "true",
2040
+ npm_config_fetch_retries: "1",
2041
+ npm_config_fetch_retry_mintimeout: "1000",
2042
+ npm_config_fetch_retry_maxtimeout: "5000",
2043
+ npm_config_fetch_timeout: "15000"
2044
+ };
2045
+ return await new Promise((resolve5) => {
2046
+ let settled = false;
2047
+ let child;
2048
+ try {
2049
+ child = (options.spawnProcess ?? spawn)(
2050
+ "npx",
2051
+ ["--yes", "--prefer-online", "-p", "@primitive.ai/prim@latest", "prim", "daemon", "ensure"],
2052
+ { env, stdio: "ignore" }
2053
+ );
2054
+ } catch {
2055
+ resolve5(false);
2056
+ return;
2057
+ }
2058
+ const timer = setTimeout(() => {
2059
+ child.kill("SIGTERM");
2060
+ finish(false);
2061
+ }, options.timeoutMs ?? LATEST_BOOTSTRAP_TIMEOUT_MS);
2062
+ const finish = (result) => {
2063
+ if (settled) return;
2064
+ settled = true;
2065
+ clearTimeout(timer);
2066
+ resolve5(result);
2067
+ };
2068
+ timer.unref();
2069
+ child.once("error", () => finish(false));
2070
+ child.once("exit", (code) => finish(code === 0));
2071
+ });
2072
+ }
2073
+ async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
2074
+ const lockPath = options.lockPath ?? join4(options.homeDir ?? homedir4(), ".config", "prim", LATEST_BOOTSTRAP_LOCK_NAME);
2075
+ try {
2076
+ return await withFileLock(
2077
+ lockPath,
2078
+ async () => {
2079
+ const current = await ensureCurrent();
2080
+ if (current.disabled) return true;
2081
+ return await runLatestEnsure(options);
2082
+ },
2083
+ // A concurrent SessionStart already owns the complete bootstrap. Do not
2084
+ // queue another npm lookup behind it; the short window only permits one
2085
+ // retry after recovering a stale lock.
2086
+ { timeoutMs: 100, pollMs: 25 }
2087
+ );
2088
+ } catch {
2089
+ return false;
2090
+ }
2091
+ }
2037
2092
 
2038
2093
  // src/lib/presence.ts
2039
2094
  var DECISION_ORIGIN = "https://app.getprimitive.ai";
@@ -2084,10 +2139,10 @@ function formatTeammatesWithArea(teammates, cap) {
2084
2139
 
2085
2140
  // src/commands/daemon.ts
2086
2141
  var DAEMON_BIN = "prim-daemon-server";
2087
- var CONFIG_DIR = join4(homedir4(), ".config", "prim");
2088
- var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
2089
- var SOCK_PATH = join4(CONFIG_DIR, "sock");
2090
- var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
2142
+ var CONFIG_DIR = join5(homedir5(), ".config", "prim");
2143
+ var PID_PATH = join5(CONFIG_DIR, "daemon.pid");
2144
+ var SOCK_PATH = join5(CONFIG_DIR, "sock");
2145
+ var LOG_PATH = join5(CONFIG_DIR, "daemon.log");
2091
2146
  var CONFIG_DIR_MODE = 448;
2092
2147
  var LOG_FILE_MODE = 384;
2093
2148
  var STOP_TIMEOUT_MS = 5e3;
@@ -2158,13 +2213,13 @@ function sleep(ms) {
2158
2213
  }
2159
2214
  function spawnDaemon(options) {
2160
2215
  const file = binFile(DAEMON_BIN);
2161
- return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
2216
+ return file ? spawn2(process.execPath, [file], options) : spawn2(DAEMON_BIN, [], options);
2162
2217
  }
2163
2218
  function openDaemonLog(configDir = CONFIG_DIR) {
2164
2219
  if (!existsSync4(configDir)) {
2165
2220
  mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
2166
2221
  }
2167
- return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
2222
+ return openSync2(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
2168
2223
  }
2169
2224
  async function waitForReady() {
2170
2225
  const deadline = Date.now() + READY_TIMEOUT_MS;
@@ -2676,7 +2731,7 @@ async function daemonEnsure() {
2676
2731
  process.stderr.write("[prim] daemon remains explicitly disabled\n");
2677
2732
  console.log(JSON.stringify({ ensured: false, disabled: true, supervised: false }, null, 2));
2678
2733
  }
2679
- return;
2734
+ return { disabled };
2680
2735
  }
2681
2736
  const result = await ensureMacDaemon();
2682
2737
  if (result.state === "disabled") {
@@ -2703,6 +2758,7 @@ async function daemonEnsure() {
2703
2758
  2
2704
2759
  )
2705
2760
  );
2761
+ return { disabled: result.state === "disabled" };
2706
2762
  }
2707
2763
  function registerDaemonCommands(program2) {
2708
2764
  const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
@@ -2718,13 +2774,17 @@ function registerDaemonCommands(program2) {
2718
2774
  daemon.command("restart").description("Restart the daemon and clear any explicit disable marker").option("--foreground", "Restart in the foreground").action(async (opts) => {
2719
2775
  await daemonRestart(opts);
2720
2776
  });
2721
- daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").action(async () => {
2777
+ daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").addOption(new Option("--latest-bootstrap").hideHelp()).action(async (opts) => {
2778
+ if (opts.latestBootstrap) {
2779
+ await runLatestDaemonBootstrap(daemonEnsure);
2780
+ return;
2781
+ }
2722
2782
  await daemonEnsure();
2723
2783
  });
2724
2784
  }
2725
2785
 
2726
2786
  // src/commands/decisions.ts
2727
- import { Option } from "commander";
2787
+ import { Option as Option2 } from "commander";
2728
2788
 
2729
2789
  // src/decisions/cascade-renderer.ts
2730
2790
  var DEPENDENTS_INLINE_LIMIT = 5;
@@ -3526,7 +3586,7 @@ function registerDecisionsCommands(program2) {
3526
3586
  }
3527
3587
  });
3528
3588
  decisions.command("create").description("Record a decision directly with its explicit user or agent origin").requiredOption("--intent <text>", "What was decided (required)").addOption(
3529
- new Option(
3589
+ new Option2(
3530
3590
  "--attribution <origin>",
3531
3591
  "Who originated the exact choice: user | agent (required)"
3532
3592
  ).choices(["user", "agent"]).makeOptionMandatory()
@@ -3857,19 +3917,46 @@ function checkWorkspaceIdentity() {
3857
3917
  };
3858
3918
  }
3859
3919
  }
3860
- function classifyRepositoryBinding(value) {
3861
- return isValidRepoSyncId(value) ? {
3920
+ function classifyRepositoryBinding(value, currentServerValue) {
3921
+ if (!isValidRepoSyncId(value)) {
3922
+ return {
3923
+ name: "repo-binding",
3924
+ status: "fail",
3925
+ detail: "missing or invalid local prim.repoSyncId \u2014 run `prim enable`"
3926
+ };
3927
+ }
3928
+ if (currentServerValue !== void 0 && value !== currentServerValue) {
3929
+ return {
3930
+ name: "repo-binding",
3931
+ status: "fail",
3932
+ detail: "local repository binding is stale for the current origin \u2014 run `prim enable`"
3933
+ };
3934
+ }
3935
+ return {
3862
3936
  name: "repo-binding",
3863
3937
  status: "ok",
3864
- detail: "valid local repository binding ready"
3865
- } : {
3866
- name: "repo-binding",
3867
- status: "fail",
3868
- detail: "missing or invalid local prim.repoSyncId \u2014 run `prim enable`"
3938
+ detail: currentServerValue === void 0 ? "valid local repository binding ready" : "local repository binding matches the current server binding"
3869
3939
  };
3870
3940
  }
3871
- function checkRepositoryBinding() {
3872
- return classifyRepositoryBinding(repoSyncId(process.cwd()));
3941
+ async function checkRepositoryBinding() {
3942
+ const root = process.cwd();
3943
+ const local = repoSyncId(root);
3944
+ const localCheck = classifyRepositoryBinding(local);
3945
+ if (localCheck.status === "fail") return localCheck;
3946
+ try {
3947
+ const current = await resolveRepositoryBinding(root, {
3948
+ signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS),
3949
+ quietRefresh: true
3950
+ });
3951
+ return classifyRepositoryBinding(local, current.repoSyncId);
3952
+ } catch (error) {
3953
+ const detail = error instanceof Error ? error.message : String(error);
3954
+ return {
3955
+ name: "repo-binding",
3956
+ status: "fail",
3957
+ detail: `could not verify the current repository binding \u2014 ${detail.slice(0, 80)}`
3958
+ };
3959
+ }
3873
3960
  }
3874
3961
  function classifyPostCommitHook(inspection) {
3875
3962
  if (inspection.covered) {
@@ -4016,7 +4103,7 @@ async function collectChecks() {
4016
4103
  checkStranded(),
4017
4104
  checkFeedbackHooks(),
4018
4105
  checkWorkspaceIdentity(),
4019
- checkRepositoryBinding(),
4106
+ await checkRepositoryBinding(),
4020
4107
  checkPostCommitHook(),
4021
4108
  ...backend,
4022
4109
  await checkFeedbackCapability()
@@ -4061,8 +4148,8 @@ import {
4061
4148
  rmSync as rmSync2,
4062
4149
  writeFileSync as writeFileSync3
4063
4150
  } from "fs";
4064
- import { homedir as homedir5 } from "os";
4065
- import { dirname as dirname3, join as join5 } from "path";
4151
+ import { homedir as homedir6 } from "os";
4152
+ import { dirname as dirname3, join as join6 } from "path";
4066
4153
  import { Document, parseDocument, stringify } from "yaml";
4067
4154
  var CAPTURE_BIN3 = "prim-hook";
4068
4155
  var GATE_BIN3 = "prim-pre-tool-use";
@@ -4087,13 +4174,13 @@ var PINNED_CASES = PRIM_BINS3.map((bin) => {
4087
4174
  ;;`;
4088
4175
  }).join("\n");
4089
4176
  function hermesHome() {
4090
- return process.env.HERMES_HOME ?? join5(homedir5(), ".hermes");
4177
+ return process.env.HERMES_HOME ?? join6(homedir6(), ".hermes");
4091
4178
  }
4092
4179
  function configPath() {
4093
- return join5(hermesHome(), "config.yaml");
4180
+ return join6(hermesHome(), "config.yaml");
4094
4181
  }
4095
4182
  function shimPath() {
4096
- return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
4183
+ return join6(hermesHome(), "agent-hooks", "prim-shim.sh");
4097
4184
  }
4098
4185
  var SHIM_SCRIPT = `#!/bin/sh
4099
4186
  # prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
@@ -4125,6 +4212,7 @@ var REGISTRATIONS2 = [
4125
4212
  ...CAPTURE_EVENTS2.map((event) => ({ event, bin: CAPTURE_BIN3 })),
4126
4213
  { event: "pre_tool_call", matcher: EDIT_MATCHER, bin: GATE_BIN3, timeout: GATE_TIMEOUT_SECONDS },
4127
4214
  { event: "post_tool_call", matcher: EDIT_MATCHER, bin: POST_TOOL_USE_BIN3 },
4215
+ { event: "post_approval_response", bin: POST_TOOL_USE_BIN3 },
4128
4216
  { event: "on_session_start", bin: SESSION_START_BIN3 },
4129
4217
  { event: "on_session_end", bin: SESSION_END_BIN2 }
4130
4218
  ];
@@ -4395,9 +4483,9 @@ function registerHermesCommands(program2) {
4395
4483
  // src/commands/hooks.ts
4396
4484
  import { execFileSync } from "child_process";
4397
4485
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
4398
- import { homedir as homedir6 } from "os";
4399
- import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
4400
- import { Option as Option2 } from "commander";
4486
+ import { homedir as homedir7 } from "os";
4487
+ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
4488
+ import { Option as Option3 } from "commander";
4401
4489
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
4402
4490
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
4403
4491
  var HOOKS = [PRE_COMMIT, POST_COMMIT];
@@ -4544,9 +4632,9 @@ function projectHooksDir(gitRoot) {
4544
4632
  }
4545
4633
  return resolve2(gitRoot, commonDir, "hooks");
4546
4634
  }
4547
- var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
4635
+ var PRIM_GIT_HOOKS_DIR = join7(homedir7(), ".config", "prim", "git-hooks");
4548
4636
  function expandTilde(p) {
4549
- return p.startsWith("~/") ? join6(homedir6(), p.slice(2)) : p;
4637
+ return p.startsWith("~/") ? join7(homedir7(), p.slice(2)) : p;
4550
4638
  }
4551
4639
  function isOurHooksDir(value) {
4552
4640
  return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
@@ -4739,12 +4827,12 @@ function registerHooksCommands(program2) {
4739
4827
  hooks.command("install").description(
4740
4828
  "Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
4741
4829
  ).addOption(
4742
- new Option2("--target <where>", "install destination; bypasses Husky detection").choices([
4830
+ new Option3("--target <where>", "install destination; bypasses Husky detection").choices([
4743
4831
  "husky",
4744
4832
  "git-hooks"
4745
4833
  ])
4746
4834
  ).addOption(
4747
- new Option2(
4835
+ new Option3(
4748
4836
  "--scope <scope>",
4749
4837
  "project (default, this repo) or user (a global core.hooksPath capturing every repo)"
4750
4838
  ).choices(["project", "user"])
@@ -4784,7 +4872,7 @@ function registerHooksCommands(program2) {
4784
4872
  hooks.command("uninstall").description(
4785
4873
  "Remove the prim git hooks (.git/hooks, or the global core.hooksPath with --scope user)"
4786
4874
  ).addOption(
4787
- new Option2(
4875
+ new Option3(
4788
4876
  "--scope <scope>",
4789
4877
  "project (default, this repo) or user (global core.hooksPath)"
4790
4878
  ).choices(["project", "user"])
@@ -4821,11 +4909,11 @@ function registerHooksCommands(program2) {
4821
4909
 
4822
4910
  // src/commands/moves.ts
4823
4911
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
4824
- import { join as join8 } from "path";
4912
+ import { join as join9 } from "path";
4825
4913
 
4826
4914
  // src/flusher.ts
4827
4915
  import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
4828
- import { dirname as dirname5, join as join7 } from "path";
4916
+ import { dirname as dirname5, join as join8 } from "path";
4829
4917
  import { createInterface } from "readline";
4830
4918
  var BATCH_SIZE = 500;
4831
4919
  var HTTP_TIMEOUT_MS = 1e4;
@@ -4949,7 +5037,7 @@ async function flushOnce() {
4949
5037
  return { flushed: total };
4950
5038
  }
4951
5039
  var flushInFlight;
4952
- var FLUSH_LOCK_PATH = join7(dirname5(JOURNAL_DIR), ".flush.lock");
5040
+ var FLUSH_LOCK_PATH = join8(dirname5(JOURNAL_DIR), ".flush.lock");
4953
5041
  var FLUSH_LOCK_TIMEOUT_MS = 250;
4954
5042
  function isFlushLockContended(error) {
4955
5043
  return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
@@ -5066,18 +5154,18 @@ function registerMovesCommands(program2) {
5066
5154
  }
5067
5155
  });
5068
5156
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
5069
- const dir = join8(process.cwd(), ".prim");
5157
+ const dir = join9(process.cwd(), ".prim");
5070
5158
  if (!existsSync8(dir)) {
5071
5159
  mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
5072
5160
  }
5073
- const file = join8(process.cwd(), WORKSPACE_FILE);
5161
+ const file = join9(process.cwd(), WORKSPACE_FILE);
5074
5162
  writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
5075
5163
  mode: FILE_MODE
5076
5164
  });
5077
5165
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
5078
5166
  });
5079
5167
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
5080
- const file = join8(process.cwd(), WORKSPACE_FILE);
5168
+ const file = join9(process.cwd(), WORKSPACE_FILE);
5081
5169
  if (!existsSync8(file)) {
5082
5170
  console.log("[prim] no workspace binding in cwd");
5083
5171
  return;
@@ -5176,7 +5264,7 @@ import {
5176
5264
  unlinkSync as unlinkSync5,
5177
5265
  writeFileSync as writeFileSync6
5178
5266
  } from "fs";
5179
- import { join as join9 } from "path";
5267
+ import { join as join10 } from "path";
5180
5268
  var DIR_MODE2 = 448;
5181
5269
  var FILE_MODE2 = 384;
5182
5270
  function ensureDir() {
@@ -5185,7 +5273,7 @@ function ensureDir() {
5185
5273
  }
5186
5274
  }
5187
5275
  function markerPath(sessionId) {
5188
- return join9(SESSIONS_DIR, `${sessionId}.json`);
5276
+ return join10(SESSIONS_DIR, `${sessionId}.json`);
5189
5277
  }
5190
5278
  function registerSessionCommands(program2) {
5191
5279
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -5213,7 +5301,7 @@ function registerSessionCommands(program2) {
5213
5301
  for (const f of files) {
5214
5302
  const sessionId = f.replace(/\.json$/, "");
5215
5303
  try {
5216
- const m = JSON.parse(readFileSync7(join9(SESSIONS_DIR, f), "utf-8"));
5304
+ const m = JSON.parse(readFileSync7(join10(SESSIONS_DIR, f), "utf-8"));
5217
5305
  console.log(`${sessionId} org=${m.orgId}`);
5218
5306
  } catch {
5219
5307
  }
@@ -5233,7 +5321,7 @@ function registerSessionCommands(program2) {
5233
5321
  // src/commands/setup.ts
5234
5322
  import { spawnSync as spawnSync2 } from "child_process";
5235
5323
  import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
5236
- import { join as join10 } from "path";
5324
+ import { join as join11 } from "path";
5237
5325
  var EXIT_INCOMPLETE = 1;
5238
5326
  var EXIT_USAGE3 = 2;
5239
5327
  var SESSION_LABELS = {
@@ -5314,7 +5402,7 @@ function detectProjectConflicts(agent, run) {
5314
5402
  }
5315
5403
  try {
5316
5404
  const root = gitToplevel();
5317
- const preCommit = root && join10(root, ".git", "hooks", "pre-commit");
5405
+ const preCommit = root && join11(root, ".git", "hooks", "pre-commit");
5318
5406
  if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
5319
5407
  conflicts.push(CONFLICT_HOOKS);
5320
5408
  }
@@ -5473,14 +5561,55 @@ function registerSetupCommand(program2, dependencies = {}) {
5473
5561
  import { readFileSync as readFileSync9 } from "fs";
5474
5562
  import { dirname as dirname6, resolve as resolve3 } from "path";
5475
5563
  import { fileURLToPath } from "url";
5476
- var STATUSLINE_TIMEOUT_MS = 200;
5564
+
5565
+ // src/lib/statusline-render.ts
5477
5566
  var STATUSLINE_NAME_CAP = 3;
5567
+ function formatStatusline(version, snapshot, resolveIngestionStatus) {
5568
+ if (!snapshot) {
5569
+ return `primitive ${version} (daemon: down)`;
5570
+ }
5571
+ if (snapshot.healthy === false) {
5572
+ if (snapshot.needsReauth) {
5573
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
5574
+ }
5575
+ if (snapshot.ingestion?.healthy === false) {
5576
+ const pending = snapshot.ingestion.pendingCount;
5577
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
5578
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
5579
+ }
5580
+ if (snapshot.heartbeat?.healthy === false) {
5581
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
5582
+ }
5583
+ return `primitive ${version} (daemon: starting)`;
5584
+ }
5585
+ const ingestionStatus = resolveIngestionStatus();
5586
+ if (snapshot.envMismatch) {
5587
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
5588
+ }
5589
+ if (snapshot.presenceStale) {
5590
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
5591
+ }
5592
+ let team;
5593
+ if (snapshot.onlineTeammates !== void 0) {
5594
+ team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
5595
+ } else if (snapshot.onlineNames !== void 0) {
5596
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
5597
+ } else if (typeof snapshot.onlineCount === "number") {
5598
+ team = `team: ${String(snapshot.onlineCount)} online`;
5599
+ } else {
5600
+ team = "team: \u2014";
5601
+ }
5602
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
5603
+ }
5604
+
5605
+ // src/commands/statusline.ts
5606
+ var STATUSLINE_TIMEOUT_MS = 200;
5478
5607
  function readPackageVersion() {
5479
5608
  try {
5480
5609
  const here = dirname6(fileURLToPath(import.meta.url));
5481
5610
  const candidates = [
5482
- // The supervised runtime stages a tiny manifest beside the standalone
5483
- // statusline bundle, so it remains versioned without loading the package.
5611
+ // Schema-v2 runtimes staged a manifest beside this standalone bundle.
5612
+ // Keep resolving it while those legacy launchers transition to schema v3.
5484
5613
  resolve3(here, "manifest.json"),
5485
5614
  resolve3(here, "../../package.json"),
5486
5615
  resolve3(here, "../package.json")
@@ -5515,40 +5644,8 @@ async function renderStatusline() {
5515
5644
  );
5516
5645
  if (!snapshot) {
5517
5646
  debug("daemon snapshot missing");
5518
- return `primitive ${version} (daemon: down)`;
5519
- }
5520
- if (snapshot.healthy === false) {
5521
- if (snapshot.needsReauth) {
5522
- return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
5523
- }
5524
- if (snapshot.ingestion?.healthy === false) {
5525
- const pending = snapshot.ingestion.pendingCount;
5526
- const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
5527
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
5528
- }
5529
- if (snapshot.heartbeat?.healthy === false) {
5530
- return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
5531
- }
5532
- return `primitive ${version} (daemon: starting)`;
5533
- }
5534
- const ingestionStatus = decisionIngestionStatus(process.cwd());
5535
- if (snapshot.envMismatch) {
5536
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
5537
- }
5538
- if (snapshot.presenceStale) {
5539
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
5540
- }
5541
- let team;
5542
- if (snapshot.onlineTeammates !== void 0) {
5543
- team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
5544
- } else if (snapshot.onlineNames !== void 0) {
5545
- team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
5546
- } else if (typeof snapshot.onlineCount === "number") {
5547
- team = `team: ${String(snapshot.onlineCount)} online`;
5548
- } else {
5549
- team = "team: \u2014";
5550
5647
  }
5551
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
5648
+ return formatStatusline(version, snapshot, () => decisionIngestionStatus(process.cwd()));
5552
5649
  }
5553
5650
  function registerStatuslineCommands(program2) {
5554
5651
  program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {