@primitive.ai/prim 0.1.0-alpha.48 → 0.1.0-alpha.50

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
@@ -12,7 +12,7 @@ import {
12
12
  checkAffectedDecisions,
13
13
  daemonOrDirectGet,
14
14
  formatDecisionsWarning
15
- } from "./chunk-WRPKAPVE.js";
15
+ } from "./chunk-VDLAAUWV.js";
16
16
  import {
17
17
  JOURNAL_DIR,
18
18
  SESSIONS_DIR,
@@ -21,23 +21,27 @@ import {
21
21
  listFlushing,
22
22
  pendingJournalStats,
23
23
  readMovesFromPath
24
- } from "./chunk-5K725K35.js";
24
+ } from "./chunk-BOTKPLTI.js";
25
25
  import {
26
26
  fetchFeedbackCapability
27
- } from "./chunk-CK75QBCB.js";
27
+ } from "./chunk-WELBNODJ.js";
28
28
  import {
29
29
  inspectWorkspaceId
30
30
  } from "./chunk-IMAIBPUC.js";
31
31
  import {
32
- activateRepoBestEffort,
33
32
  binFile,
34
33
  commandMatchesBin,
35
34
  detachedHookShimCommand,
36
- gitToplevel,
37
35
  hookShimCommand,
38
- setRepoActive,
39
36
  warmBinCache
40
- } from "./chunk-F7O7V6ZM.js";
37
+ } from "./chunk-HSZPTVKU.js";
38
+ import {
39
+ activateRepoBestEffort,
40
+ decisionIngestionStatus,
41
+ gitToplevel,
42
+ isRepoActiveForCapture,
43
+ setRepoActive
44
+ } from "./chunk-R5ZJRSLV.js";
41
45
  import {
42
46
  HttpError,
43
47
  REFRESH_TOKEN_PATH,
@@ -52,7 +56,7 @@ import {
52
56
  resolveAuthCredential,
53
57
  setStoredToken,
54
58
  withFileLock
55
- } from "./chunk-TPROV45L.js";
59
+ } from "./chunk-DWDEQRWN.js";
56
60
  import {
57
61
  daemonIsLive,
58
62
  daemonRequest
@@ -60,7 +64,7 @@ import {
60
64
 
61
65
  // src/index.ts
62
66
  import { readFileSync as readFileSync12 } from "fs";
63
- import { dirname as dirname8, resolve as resolve6 } from "path";
67
+ import { dirname as dirname9, resolve as resolve6 } from "path";
64
68
  import { fileURLToPath as fileURLToPath4 } from "url";
65
69
  import { Command } from "commander";
66
70
  import updateNotifier from "update-notifier";
@@ -1703,6 +1707,12 @@ var HEALTHY_POLL_MS = 250;
1703
1707
  var EXIT_OK2 = 0;
1704
1708
  var EXIT_NOT_RUNNING = 2;
1705
1709
  var EXIT_BOOTING = 3;
1710
+ function formatDaemonLifecycleMessage(message, decisionIngestion) {
1711
+ return `${message} \xB7 Decision ingestion ${decisionIngestion}`;
1712
+ }
1713
+ function formatCurrentDaemonLifecycleMessage(message) {
1714
+ return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
1715
+ }
1706
1716
  var MAX_HEALTH_ERROR_LENGTH = 240;
1707
1717
  function boundedHealthError(value) {
1708
1718
  if (!value) return void 0;
@@ -1819,8 +1829,10 @@ async function detachedDaemonStart(opts) {
1819
1829
  if (existing?.alive) {
1820
1830
  const snapshot = await verifiedPid(existing);
1821
1831
  if (snapshot) {
1822
- process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
1823
- `);
1832
+ process.stderr.write(
1833
+ `${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})`)}
1834
+ `
1835
+ );
1824
1836
  console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
1825
1837
  return;
1826
1838
  }
@@ -1876,7 +1888,7 @@ async function detachedDaemonStart(opts) {
1876
1888
  if (live) {
1877
1889
  const after = readPidfile();
1878
1890
  process.stderr.write(
1879
- `[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
1891
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})`)}
1880
1892
  `
1881
1893
  );
1882
1894
  console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
@@ -1966,7 +1978,7 @@ async function macDaemonStart(forceRestart = false) {
1966
1978
  if (healthy) {
1967
1979
  const verb = result.action === "none" ? "already running" : "started";
1968
1980
  process.stderr.write(
1969
- `[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})
1981
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})`)}
1970
1982
  `
1971
1983
  );
1972
1984
  } else {
@@ -2140,28 +2152,23 @@ function classifyLaunchdStatus(service, responding, snapshot, disabled) {
2140
2152
  exitCode: EXIT_OK2
2141
2153
  };
2142
2154
  }
2143
- function writeLiveSnapshot(snapshot, supervised = false) {
2155
+ function formatDaemonSnapshotMessage(snapshot, supervised, decisionIngestion) {
2144
2156
  if (!snapshot) {
2145
- process.stderr.write(
2146
- supervised ? "[prim] \u2713 daemon live under launchd (no snapshot)\n" : "[prim] \u2713 daemon live\n"
2147
- );
2148
- return;
2157
+ return supervised ? `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion} under launchd (no snapshot)` : `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion}`;
2149
2158
  }
2150
2159
  const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
2151
2160
  if (snapshot.healthy === false) {
2152
2161
  const reason = daemonDegradedReason(snapshot);
2153
- process.stderr.write(
2154
- `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}
2155
- `
2156
- );
2157
- return;
2162
+ return `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}`;
2158
2163
  }
2159
- process.stderr.write(
2160
- `[prim] \u2713 daemon live${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
2161
- snapshot.uptimeMs / 1e3
2162
- )}s \xB7 session=${snapshot.sessionId}${team}
2163
- `
2164
- );
2164
+ return `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion}${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
2165
+ snapshot.uptimeMs / 1e3
2166
+ )}s \xB7 session=${snapshot.sessionId}${team}`;
2167
+ }
2168
+ function writeLiveSnapshot(snapshot, supervised = false) {
2169
+ const decisionIngestion = snapshot?.healthy === false ? "disabled" : decisionIngestionStatus(process.cwd());
2170
+ process.stderr.write(`${formatDaemonSnapshotMessage(snapshot, supervised, decisionIngestion)}
2171
+ `);
2165
2172
  }
2166
2173
  async function detachedDaemonStatus() {
2167
2174
  const pid = readPidfile();
@@ -2262,8 +2269,10 @@ async function daemonEnsure() {
2262
2269
  if (result.state === "disabled") {
2263
2270
  process.stderr.write("[prim] daemon remains explicitly disabled\n");
2264
2271
  } else if (result.state === "running") {
2265
- process.stderr.write(`[prim] \u2713 daemon ensured under launchd (${result.action})
2266
- `);
2272
+ process.stderr.write(
2273
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})`)}
2274
+ `
2275
+ );
2267
2276
  } else {
2268
2277
  process.stderr.write(`[prim] \u2717 daemon ensure failed; see ${LOG_PATH}
2269
2278
  `);
@@ -2999,9 +3008,29 @@ function formatShowJson(result) {
2999
3008
  return JSON.stringify(result, null, 2);
3000
3009
  }
3001
3010
 
3011
+ // src/lib/confirmation.ts
3012
+ function isNonInteractive(globals) {
3013
+ return Boolean(globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE);
3014
+ }
3015
+ async function askConfirmation(question, output = process.stdout) {
3016
+ if (!process.stdin.isTTY) return false;
3017
+ const { createInterface: createInterface2 } = await import("readline/promises");
3018
+ const rl = createInterface2({ input: process.stdin, output });
3019
+ try {
3020
+ const answer = await rl.question(`${question} [y/N] `);
3021
+ const normalized = answer.trim().toLowerCase();
3022
+ return normalized === "y" || normalized === "yes";
3023
+ } finally {
3024
+ rl.close();
3025
+ }
3026
+ }
3027
+
3002
3028
  // src/commands/decisions.ts
3003
3029
  var EXIT_NOT_FOUND = 4;
3004
3030
  var EXIT_USAGE = 2;
3031
+ var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create this one Decision without enabling passive ingestion?";
3032
+ var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
3033
+ var CREATE_INACTIVE_REJECTED = "[prim] decision not created: Decision ingestion is disabled here; rerun with prim's --yes to approve this one Decision, or run `prim enable` in a Git project";
3005
3034
  var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
3006
3035
  function registerDecisionsCommands(program2) {
3007
3036
  const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
@@ -3081,7 +3110,19 @@ function registerDecisionsCommands(program2) {
3081
3110
  ).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option(
3082
3111
  "--files <paths>",
3083
3112
  "Comma-separated repo-relative paths this decision governs (the files Conflict Gates would check \u2014 not currently enabled)"
3084
- ).action(async (opts) => {
3113
+ ).action(async (opts, command) => {
3114
+ if (!isRepoActiveForCapture(process.cwd())) {
3115
+ const globals = command.optsWithGlobals();
3116
+ const nonInteractive = isNonInteractive(globals);
3117
+ const approved = Boolean(globals.yes) || !nonInteractive && await askConfirmation(CREATE_INACTIVE_PROMPT, process.stderr);
3118
+ if (!approved) {
3119
+ console.error(CREATE_INACTIVE_REJECTED);
3120
+ console.log(JSON.stringify({ ok: false, error: "prim_inactive" }, null, 2));
3121
+ process.exitCode = EXIT_USAGE;
3122
+ return;
3123
+ }
3124
+ console.error(CREATE_INACTIVE_APPROVED);
3125
+ }
3085
3126
  const request = {
3086
3127
  intent: opts.intent,
3087
3128
  kind: opts.kind,
@@ -3243,10 +3284,11 @@ function classifyDaemonHealth(snapshot, options = {}) {
3243
3284
  if (snapshot.healthy !== true) {
3244
3285
  return { name: "daemon", status: "fail", detail: "health state is not ready" };
3245
3286
  }
3287
+ const ingestionStatus = options.ingestionStatus ?? decisionIngestionStatus(process.cwd());
3246
3288
  return {
3247
3289
  name: "daemon",
3248
3290
  status: "ok",
3249
- detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""}`
3291
+ detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus}`
3250
3292
  };
3251
3293
  }
3252
3294
  async function checkDaemon() {
@@ -3925,18 +3967,6 @@ function detectHusky(gitRoot) {
3925
3967
  function containsPrimHook(content, binName = PRE_COMMIT.binName) {
3926
3968
  return content.includes(binName);
3927
3969
  }
3928
- async function askConfirmation(question) {
3929
- if (!process.stdin.isTTY) return false;
3930
- const { createInterface: createInterface2 } = await import("readline/promises");
3931
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
3932
- try {
3933
- const answer = await rl.question(`${question} [y/N] `);
3934
- const normalized = answer.trim().toLowerCase();
3935
- return normalized === "y" || normalized === "yes";
3936
- } finally {
3937
- rl.close();
3938
- }
3939
- }
3940
3970
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
3941
3971
  const hookPath = resolve2(gitRoot, ".husky", spec.hookName);
3942
3972
  const existed = existsSync7(hookPath);
@@ -4154,9 +4184,7 @@ function registerHooksCommands(program2) {
4154
4184
  return;
4155
4185
  }
4156
4186
  const globals = command.optsWithGlobals();
4157
- const nonInteractive = Boolean(
4158
- globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
4159
- );
4187
+ const nonInteractive = isNonInteractive(globals);
4160
4188
  const gitRoot = getGitRoot();
4161
4189
  if (opts.target === "husky") return installHooks(gitRoot, "husky");
4162
4190
  if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
@@ -4213,10 +4241,11 @@ function registerHooksCommands(program2) {
4213
4241
 
4214
4242
  // src/commands/moves.ts
4215
4243
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
4216
- import { join as join7 } from "path";
4244
+ import { join as join8 } from "path";
4217
4245
 
4218
4246
  // src/flusher.ts
4219
4247
  import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
4248
+ import { dirname as dirname5, join as join7 } from "path";
4220
4249
  import { createInterface } from "readline";
4221
4250
  var BATCH_SIZE = 500;
4222
4251
  var HTTP_TIMEOUT_MS = 1e4;
@@ -4340,11 +4369,23 @@ async function flushOnce() {
4340
4369
  return { flushed: total };
4341
4370
  }
4342
4371
  var flushInFlight;
4372
+ var FLUSH_LOCK_PATH = join7(dirname5(JOURNAL_DIR), ".flush.lock");
4373
+ var FLUSH_LOCK_TIMEOUT_MS = 250;
4374
+ function isFlushLockContended(error) {
4375
+ return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
4376
+ }
4343
4377
  function flush() {
4344
4378
  if (flushInFlight) {
4345
4379
  return flushInFlight;
4346
4380
  }
4347
- const attempt = flushOnce().finally(() => {
4381
+ const attempt = withFileLock(FLUSH_LOCK_PATH, flushOnce, {
4382
+ timeoutMs: FLUSH_LOCK_TIMEOUT_MS
4383
+ }).catch((error) => {
4384
+ if (isFlushLockContended(error)) {
4385
+ return { flushed: 0, skipped: true };
4386
+ }
4387
+ throw error;
4388
+ }).finally(() => {
4348
4389
  if (flushInFlight === attempt) {
4349
4390
  flushInFlight = void 0;
4350
4391
  }
@@ -4382,7 +4423,11 @@ var WORKSPACE_FILE = ".prim/workspace.json";
4382
4423
  function registerMovesCommands(program2) {
4383
4424
  const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
4384
4425
  moves.command("flush").description("Drain all local move journals to the server").action(async () => {
4385
- const { flushed } = await flush();
4426
+ const { flushed, skipped } = await flush();
4427
+ if (skipped) {
4428
+ console.log("[prim] another flush is already draining; moves left journaled");
4429
+ return;
4430
+ }
4386
4431
  console.log(`[prim] flushed ${String(flushed)} move${flushed === 1 ? "" : "s"}`);
4387
4432
  });
4388
4433
  moves.command("status").description("Show per-bucket pending stats").action(() => {
@@ -4441,18 +4486,18 @@ function registerMovesCommands(program2) {
4441
4486
  }
4442
4487
  });
4443
4488
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
4444
- const dir = join7(process.cwd(), ".prim");
4489
+ const dir = join8(process.cwd(), ".prim");
4445
4490
  if (!existsSync8(dir)) {
4446
4491
  mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
4447
4492
  }
4448
- const file = join7(process.cwd(), WORKSPACE_FILE);
4493
+ const file = join8(process.cwd(), WORKSPACE_FILE);
4449
4494
  writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
4450
4495
  mode: FILE_MODE
4451
4496
  });
4452
4497
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
4453
4498
  });
4454
4499
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
4455
- const file = join7(process.cwd(), WORKSPACE_FILE);
4500
+ const file = join8(process.cwd(), WORKSPACE_FILE);
4456
4501
  if (!existsSync8(file)) {
4457
4502
  console.log("[prim] no workspace binding in cwd");
4458
4503
  return;
@@ -4553,7 +4598,7 @@ import {
4553
4598
  unlinkSync as unlinkSync5,
4554
4599
  writeFileSync as writeFileSync6
4555
4600
  } from "fs";
4556
- import { join as join8 } from "path";
4601
+ import { join as join9 } from "path";
4557
4602
  var DIR_MODE2 = 448;
4558
4603
  var FILE_MODE2 = 384;
4559
4604
  function ensureDir() {
@@ -4562,7 +4607,7 @@ function ensureDir() {
4562
4607
  }
4563
4608
  }
4564
4609
  function markerPath(sessionId) {
4565
- return join8(SESSIONS_DIR, `${sessionId}.json`);
4610
+ return join9(SESSIONS_DIR, `${sessionId}.json`);
4566
4611
  }
4567
4612
  function registerSessionCommands(program2) {
4568
4613
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -4590,7 +4635,7 @@ function registerSessionCommands(program2) {
4590
4635
  for (const f of files) {
4591
4636
  const sessionId = f.replace(/\.json$/, "");
4592
4637
  try {
4593
- const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
4638
+ const m = JSON.parse(readFileSync7(join9(SESSIONS_DIR, f), "utf-8"));
4594
4639
  console.log(`${sessionId} org=${m.orgId}`);
4595
4640
  } catch {
4596
4641
  }
@@ -4610,7 +4655,7 @@ function registerSessionCommands(program2) {
4610
4655
  // src/commands/setup.ts
4611
4656
  import { spawnSync as spawnSync2 } from "child_process";
4612
4657
  import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
4613
- import { join as join9 } from "path";
4658
+ import { join as join10 } from "path";
4614
4659
  var EXIT_INCOMPLETE = 1;
4615
4660
  var EXIT_USAGE3 = 2;
4616
4661
  var SESSION_LABELS = {
@@ -4686,7 +4731,7 @@ function detectProjectConflicts(agent, run) {
4686
4731
  }
4687
4732
  try {
4688
4733
  const root = gitToplevel();
4689
- const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
4734
+ const preCommit = root && join10(root, ".git", "hooks", "pre-commit");
4690
4735
  if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
4691
4736
  conflicts.push(CONFLICT_HOOKS);
4692
4737
  }
@@ -4837,31 +4882,31 @@ import {
4837
4882
  writeFileSync as writeFileSync7
4838
4883
  } from "fs";
4839
4884
  import { homedir as homedir8 } from "os";
4840
- import { dirname as dirname6, join as join11, resolve as resolve4 } from "path";
4885
+ import { dirname as dirname7, join as join12, resolve as resolve4 } from "path";
4841
4886
  import { fileURLToPath as fileURLToPath2 } from "url";
4842
4887
  import { createPatch } from "diff";
4843
4888
 
4844
4889
  // src/commands/claude-plugin.ts
4845
4890
  import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
4846
4891
  import { homedir as homedir7 } from "os";
4847
- import { dirname as dirname5, join as join10, resolve as resolve3 } from "path";
4892
+ import { dirname as dirname6, join as join11, resolve as resolve3 } from "path";
4848
4893
  import { fileURLToPath } from "url";
4849
- var __dirname = dirname5(fileURLToPath(import.meta.url));
4894
+ var __dirname = dirname6(fileURLToPath(import.meta.url));
4850
4895
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
4851
4896
  function resolvePluginDir(cwd, scope) {
4852
4897
  if (scope && scope !== "user" && scope !== "project") {
4853
4898
  console.error(`Unknown --scope "${scope}" (expected user or project)`);
4854
4899
  return null;
4855
4900
  }
4856
- const base = scope === "user" ? join10(homedir7(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
4857
- return join10(base, "skills", "prim");
4901
+ const base = scope === "user" ? join11(homedir7(), ".claude") : join11(gitToplevel(cwd) ?? cwd, ".claude");
4902
+ return join11(base, "skills", "prim");
4858
4903
  }
4859
4904
  function packageVersion() {
4860
4905
  let dir = __dirname;
4861
- while (dir !== dirname5(dir)) {
4906
+ while (dir !== dirname6(dir)) {
4862
4907
  const p = resolve3(dir, "package.json");
4863
4908
  if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
4864
- dir = dirname5(dir);
4909
+ dir = dirname6(dir);
4865
4910
  }
4866
4911
  return "0.0.0";
4867
4912
  }
@@ -4875,8 +4920,8 @@ function removeDirIfEmpty(dir) {
4875
4920
  }
4876
4921
  function pluginPaths(dir) {
4877
4922
  return {
4878
- manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
4879
- skillPath: join10(dir, "SKILL.md")
4923
+ manifestPath: join11(dir, ".claude-plugin", "plugin.json"),
4924
+ skillPath: join11(dir, "SKILL.md")
4880
4925
  };
4881
4926
  }
4882
4927
  function installClaudePlugin(cwd, opts) {
@@ -4895,7 +4940,7 @@ function installClaudePlugin(cwd, opts) {
4895
4940
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
4896
4941
  return 0;
4897
4942
  }
4898
- mkdirSync8(join10(dir, ".claude-plugin"), { recursive: true });
4943
+ mkdirSync8(join11(dir, ".claude-plugin"), { recursive: true });
4899
4944
  atomicWrite3(manifestPath, manifest);
4900
4945
  atomicWrite3(skillPath, skill);
4901
4946
  console.log(`Installed prim skill plugin at ${dir}`);
@@ -4915,7 +4960,7 @@ function uninstallClaudePlugin(cwd, opts) {
4915
4960
  }
4916
4961
  rmSync3(manifestPath, { force: true });
4917
4962
  rmSync3(skillPath, { force: true });
4918
- removeDirIfEmpty(join10(dir, ".claude-plugin"));
4963
+ removeDirIfEmpty(join11(dir, ".claude-plugin"));
4919
4964
  removeDirIfEmpty(dir);
4920
4965
  console.log(`Removed prim skill plugin from ${dir}`);
4921
4966
  return 0;
@@ -4938,7 +4983,7 @@ function statusClaudePlugin(cwd, opts) {
4938
4983
  }
4939
4984
 
4940
4985
  // src/commands/skill.ts
4941
- var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
4986
+ var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
4942
4987
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
4943
4988
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
4944
4989
  var TARGET_CANDIDATES = [
@@ -4956,19 +5001,19 @@ var AGENT_TARGET = {
4956
5001
  hermes: ".hermes.md"
4957
5002
  };
4958
5003
  function userTargetFor(agent) {
4959
- if (agent === "claude") return join11(homedir8(), ".claude", "CLAUDE.md");
4960
- if (agent === "codex") return join11(homedir8(), ".codex", "AGENTS.md");
5004
+ if (agent === "claude") return join12(homedir8(), ".claude", "CLAUDE.md");
5005
+ if (agent === "codex") return join12(homedir8(), ".codex", "AGENTS.md");
4961
5006
  if (agent === "hermes") {
4962
- return join11(process.env.HERMES_HOME ?? join11(homedir8(), ".hermes"), ".hermes.md");
5007
+ return join12(process.env.HERMES_HOME ?? join12(homedir8(), ".hermes"), ".hermes.md");
4963
5008
  }
4964
5009
  return null;
4965
5010
  }
4966
5011
  function loadSkill() {
4967
5012
  let dir = __dirname2;
4968
- while (dir !== dirname6(dir)) {
5013
+ while (dir !== dirname7(dir)) {
4969
5014
  const p = resolve4(dir, "SKILL.md");
4970
5015
  if (existsSync12(p)) return readFileSync10(p, "utf-8");
4971
- dir = dirname6(dir);
5016
+ dir = dirname7(dir);
4972
5017
  }
4973
5018
  throw new Error("SKILL.md not found in package");
4974
5019
  }
@@ -5122,13 +5167,13 @@ function registerSkillCommands(program2) {
5122
5167
 
5123
5168
  // src/commands/statusline.ts
5124
5169
  import { readFileSync as readFileSync11 } from "fs";
5125
- import { dirname as dirname7, resolve as resolve5 } from "path";
5170
+ import { dirname as dirname8, resolve as resolve5 } from "path";
5126
5171
  import { fileURLToPath as fileURLToPath3 } from "url";
5127
5172
  var STATUSLINE_TIMEOUT_MS = 200;
5128
5173
  var STATUSLINE_NAME_CAP = 3;
5129
5174
  function readPackageVersion() {
5130
5175
  try {
5131
- const here = dirname7(fileURLToPath3(import.meta.url));
5176
+ const here = dirname8(fileURLToPath3(import.meta.url));
5132
5177
  const candidates = [
5133
5178
  // The supervised runtime stages a tiny manifest beside the standalone
5134
5179
  // statusline bundle, so it remains versioned without loading the package.
@@ -5169,6 +5214,9 @@ async function renderStatusline() {
5169
5214
  return `primitive ${version} (daemon: down)`;
5170
5215
  }
5171
5216
  if (snapshot.healthy === false) {
5217
+ if (snapshot.needsReauth) {
5218
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
5219
+ }
5172
5220
  if (snapshot.ingestion?.healthy === false) {
5173
5221
  const pending = snapshot.ingestion.pendingCount;
5174
5222
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
@@ -5179,11 +5227,12 @@ async function renderStatusline() {
5179
5227
  }
5180
5228
  return `primitive ${version} (daemon: starting)`;
5181
5229
  }
5230
+ const ingestionStatus = decisionIngestionStatus(process.cwd());
5182
5231
  if (snapshot.envMismatch) {
5183
- return `primitive ${version} (daemon: live \xB7 presence: other env)`;
5232
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
5184
5233
  }
5185
5234
  if (snapshot.presenceStale) {
5186
- return `primitive ${version} (daemon: live \xB7 presence: stale)`;
5235
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
5187
5236
  }
5188
5237
  let team;
5189
5238
  if (snapshot.onlineTeammates !== void 0) {
@@ -5195,7 +5244,7 @@ async function renderStatusline() {
5195
5244
  } else {
5196
5245
  team = "team: \u2014";
5197
5246
  }
5198
- return `primitive ${version} (daemon: live \xB7 ${team})`;
5247
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
5199
5248
  }
5200
5249
  function registerStatuslineCommands(program2) {
5201
5250
  program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
@@ -5321,7 +5370,7 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
5321
5370
  }
5322
5371
 
5323
5372
  // src/index.ts
5324
- var __dirname3 = dirname8(fileURLToPath4(import.meta.url));
5373
+ var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
5325
5374
  var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
5326
5375
  updateNotifier({ pkg }).notify();
5327
5376
  var program = new Command();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/statusline.ts
4
- import { readFileSync as readFileSync4 } from "fs";
4
+ import { readFileSync as readFileSync5 } from "fs";
5
5
  import { dirname as dirname4, resolve as resolve2 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
@@ -132,14 +132,77 @@ function daemonRequest(method, params = {}, opts = {}) {
132
132
  });
133
133
  }
134
134
 
135
+ // src/lib/activation.ts
136
+ import { execFileSync as execFileSync2 } from "child_process";
137
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
138
+ import { homedir as homedir3 } from "os";
139
+ import { join as join4 } from "path";
140
+
141
+ // src/lib/git.ts
142
+ import { execFileSync } from "child_process";
143
+ function gitToplevel(cwd) {
144
+ try {
145
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
146
+ cwd,
147
+ encoding: "utf-8",
148
+ stdio: ["ignore", "pipe", "ignore"]
149
+ }).trim();
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ // src/lib/activation.ts
156
+ var PRIM_ACTIVE_KEY = "prim.active";
157
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
158
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
159
+ function repoActiveFlag(cwd) {
160
+ try {
161
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
162
+ cwd,
163
+ encoding: "utf-8",
164
+ stdio: ["ignore", "pipe", "ignore"]
165
+ }).trim();
166
+ if (value === "true") return "true";
167
+ if (value === "false") return "false";
168
+ return void 0;
169
+ } catch {
170
+ return void 0;
171
+ }
172
+ }
173
+ function hasProjectPrimInstall(cwd) {
174
+ const root = gitToplevel(cwd);
175
+ if (root === null || root === homedir3()) return false;
176
+ for (const rel of PROJECT_INSTALL_FILES) {
177
+ const path = join4(root, rel);
178
+ if (existsSync3(path)) {
179
+ try {
180
+ const content = readFileSync3(path, "utf-8");
181
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
182
+ } catch {
183
+ }
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+ function isRepoActiveForCapture(cwd) {
189
+ const flag = repoActiveFlag(cwd);
190
+ if (flag === "true") return true;
191
+ if (flag === "false") return false;
192
+ return hasProjectPrimInstall(cwd);
193
+ }
194
+ function decisionIngestionStatus(cwd) {
195
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
196
+ }
197
+
135
198
  // src/lib/bin-cache.ts
136
199
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
137
- import { homedir as homedir3 } from "os";
138
- import { join as join5 } from "path";
200
+ import { homedir as homedir4 } from "os";
201
+ import { join as join6 } from "path";
139
202
 
140
203
  // src/lib/bin-path.ts
141
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
142
- import { dirname as dirname3, isAbsolute, join as join4 } from "path";
204
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
205
+ import { dirname as dirname3, isAbsolute, join as join5 } from "path";
143
206
  import { fileURLToPath } from "url";
144
207
  var PKG_NAME = "@primitive.ai/prim";
145
208
  var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
@@ -218,7 +281,7 @@ function readPackageVersion() {
218
281
  ];
219
282
  for (const path of candidates) {
220
283
  try {
221
- const pkg = JSON.parse(readFileSync4(path, "utf-8"));
284
+ const pkg = JSON.parse(readFileSync5(path, "utf-8"));
222
285
  if (pkg.version) {
223
286
  return pkg.version;
224
287
  }
@@ -249,6 +312,9 @@ async function renderStatusline() {
249
312
  return `primitive ${version} (daemon: down)`;
250
313
  }
251
314
  if (snapshot.healthy === false) {
315
+ if (snapshot.needsReauth) {
316
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
317
+ }
252
318
  if (snapshot.ingestion?.healthy === false) {
253
319
  const pending = snapshot.ingestion.pendingCount;
254
320
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
@@ -259,11 +325,12 @@ async function renderStatusline() {
259
325
  }
260
326
  return `primitive ${version} (daemon: starting)`;
261
327
  }
328
+ const ingestionStatus = decisionIngestionStatus(process.cwd());
262
329
  if (snapshot.envMismatch) {
263
- return `primitive ${version} (daemon: live \xB7 presence: other env)`;
330
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
264
331
  }
265
332
  if (snapshot.presenceStale) {
266
- return `primitive ${version} (daemon: live \xB7 presence: stale)`;
333
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
267
334
  }
268
335
  let team;
269
336
  if (snapshot.onlineTeammates !== void 0) {
@@ -275,7 +342,7 @@ async function renderStatusline() {
275
342
  } else {
276
343
  team = "team: \u2014";
277
344
  }
278
- return `primitive ${version} (daemon: live \xB7 ${team})`;
345
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
279
346
  }
280
347
 
281
348
  // src/statusline-main.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.48",
3
+ "version": "0.1.0-alpha.50",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",