@primitive.ai/prim 0.1.0-alpha.20 → 0.1.0-alpha.22

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/README.md CHANGED
@@ -58,9 +58,14 @@ graph, conflicting edits are gated, and presence is reported. Each hook
58
58
  self-resolves the CLI at run time (PATH, then a local install, then
59
59
  `npx --yes @latest`), so it keeps working with no global install.
60
60
 
61
+ Installs into the current project by default — the repo's `.claude/settings.json`
62
+ / `.codex/hooks.json`, resolved from the git root (so any subdirectory works);
63
+ pass `--scope user` to install machine-wide.
64
+
61
65
  ```bash
62
- prim claude install # Install Claude Code hooks (uninstall / status)
63
- prim codex install # Install OpenAI Codex hooks (uninstall / status)
66
+ prim claude install # Install Claude Code hooks (project scope; uninstall / status)
67
+ prim claude install --scope user # Install machine-wide instead
68
+ prim codex install # Install OpenAI Codex hooks (project scope)
64
69
  ```
65
70
 
66
71
  ### Daemon
@@ -113,6 +118,12 @@ and offers to install into `.husky/`.
113
118
  prim statusline # Render the team-presence statusline (reads the daemon)
114
119
  ```
115
120
 
121
+ ### Welcome
122
+
123
+ ```bash
124
+ prim welcome # Brief orientation to the decision graph (shown after setup)
125
+ ```
126
+
116
127
  ### Session & journal
117
128
 
118
129
  Lower-level plumbing for the capture pipeline — org binding and the local move
package/SKILL.md CHANGED
@@ -67,6 +67,16 @@ npx --yes @primitive.ai/prim decisions confirm <idOrShortId>
67
67
 
68
68
  Confirmations are author-targeted and rare by design; answering keeps the graph's rationale trustworthy. Don't manufacture rationale — if you don't know why a decision was made, say so.
69
69
 
70
+ ## Author a decision deliberately
71
+
72
+ Capture is automatic for the decisions you *make while coding*. When the user instead asks you to **record a decision explicitly** — one that didn't fall out of an edit (a design call, a convention, a choice settled in discussion) — author it directly:
73
+
74
+ ```
75
+ npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
76
+ ```
77
+
78
+ Only `--intent` is required. Optional: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale`, `--area`, `--decided`, `--alternatives` (comma-separated), `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — pass these to make the conflict gate fire on later edits to those files, same path form as `decisions check`). STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`. Author on the user's behalf only when they ask for a decision to be recorded; don't narrate your own routine edits into the graph (the hooks already do that).
79
+
70
80
  ## Presence
71
81
 
72
82
  With the daemon running (`npx --yes @primitive.ai/prim daemon start`), `npx --yes @primitive.ai/prim daemon status` includes the live online count in its STDOUT JSON (when presence is fresh); Claude Code surfaces it in the statusline as `team: N online`. Your captured decisions are attributed to your agent automatically -- no flag required.
@@ -108,7 +118,7 @@ Examples:
108
118
  - **An "unavailable" / "not verified" gate or check is not an all-clear.** Treat constraints as UNKNOWN and proceed deliberately; never read the silence as approval.
109
119
  - **A `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
110
120
  - **Reconcile bypasses are single-use and short-lived.** One bypass clears your *next* edit to the governed file; it is not a standing override.
111
- - **Capture is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status`) and the daemon is running — don't try to inject moves by hand.
121
+ - **Capture of your coding activity is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status`) and the daemon is running — don't try to inject moves by hand. (Deliberately *authoring* a decision the user asks you to record is a separate, supported path — `decisions create`, above.)
112
122
  - **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
113
123
 
114
124
  ## After each task
@@ -11,6 +11,7 @@ var ANSI_CODES = {
11
11
  gray: "\x1B[90m"
12
12
  };
13
13
  var ANSI_RESET = "\x1B[0m";
14
+ var ANSI_DIM = "\x1B[2m";
14
15
  var ANSI_BOLD = "\x1B[1m";
15
16
  function supportsColor() {
16
17
  if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") {
@@ -24,6 +25,12 @@ function color(text, c) {
24
25
  }
25
26
  return `${ANSI_CODES[c]}${text}${ANSI_RESET}`;
26
27
  }
28
+ function dim(text) {
29
+ if (!supportsColor()) {
30
+ return text;
31
+ }
32
+ return `${ANSI_DIM}${text}${ANSI_RESET}`;
33
+ }
27
34
  function bold(text) {
28
35
  if (!supportsColor()) {
29
36
  return text;
@@ -53,6 +60,7 @@ function stripAnsi(text) {
53
60
 
54
61
  export {
55
62
  color,
63
+ dim,
56
64
  bold,
57
65
  colorForArea,
58
66
  stripAnsi
@@ -27,6 +27,7 @@ var client = getClient();
27
27
  var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
28
28
  var lastHeartbeatAt;
29
29
  var lastOnlineCount;
30
+ var lastOnlineNames;
30
31
  var lastOkAtLocal;
31
32
  var heartbeatTimer;
32
33
  var tokenCheckTimer;
@@ -70,9 +71,8 @@ async function sendHeartbeat() {
70
71
  if (typeof result.lastHeartbeatAt === "number") {
71
72
  lastHeartbeatAt = result.lastHeartbeatAt;
72
73
  }
73
- if (typeof result.onlineCount === "number") {
74
- lastOnlineCount = result.onlineCount;
75
- }
74
+ lastOnlineCount = typeof result.onlineCount === "number" ? result.onlineCount : void 0;
75
+ lastOnlineNames = Array.isArray(result.onlineNames) ? result.onlineNames : void 0;
76
76
  }
77
77
  } catch (err) {
78
78
  process.stderr.write(
@@ -123,9 +123,10 @@ function handleStatusSnapshot() {
123
123
  uptimeMs: Date.now() - startedAt,
124
124
  sessionId: activeSessionId,
125
125
  lastHeartbeatAt,
126
- // Withhold a frozen count once it's no longer fresh; the statusline shows
127
- // "presence: stale" rather than a confident, wrong "team: N".
126
+ // Withhold a frozen count/names once they're no longer fresh; the
127
+ // statusline shows "presence: stale" rather than a confident, wrong list.
128
128
  onlineCount: presenceFresh ? lastOnlineCount : void 0,
129
+ onlineNames: presenceFresh ? lastOnlineNames : void 0,
129
130
  presenceStale
130
131
  };
131
132
  }
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  bold,
4
4
  color
5
- } from "../chunk-BEEGFDGU.js";
5
+ } from "../chunk-4QJOQIY6.js";
6
6
  import {
7
7
  getClient
8
8
  } from "../chunk-26VA3ADF.js";
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ bold,
3
4
  color,
4
5
  colorForArea,
6
+ dim,
5
7
  stripAnsi
6
- } from "./chunk-BEEGFDGU.js";
8
+ } from "./chunk-4QJOQIY6.js";
7
9
  import {
8
10
  checkAffectedDecisions,
9
11
  daemonOrDirectGet,
@@ -279,6 +281,7 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
279
281
  }
280
282
 
281
283
  // src/commands/claude-install.ts
284
+ import { execSync } from "child_process";
282
285
  import {
283
286
  closeSync,
284
287
  existsSync as existsSync3,
@@ -368,7 +371,17 @@ var PRIM_BINS = [
368
371
  ];
369
372
  var JSON_INDENT = 2;
370
373
  var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
371
- var PROJECT_SCOPE_PATH = join2(process.cwd(), ".claude", "settings.json");
374
+ function projectRoot() {
375
+ try {
376
+ return execSync("git rev-parse --show-toplevel", {
377
+ encoding: "utf-8",
378
+ stdio: ["ignore", "pipe", "ignore"]
379
+ }).trim();
380
+ } catch {
381
+ return process.cwd();
382
+ }
383
+ }
384
+ var projectScopePath = () => join2(projectRoot(), ".claude", "settings.json");
372
385
  var CAPTURE_EVENTS = [
373
386
  "SessionStart",
374
387
  "UserPromptSubmit",
@@ -389,7 +402,7 @@ var REGISTRATIONS = [
389
402
  makeRegistration("SessionEnd", "*", SESSION_END_BIN)
390
403
  ];
391
404
  function settingsPathFor(scope) {
392
- return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
405
+ return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
393
406
  }
394
407
  function readSettings(path) {
395
408
  if (!existsSync3(path)) {
@@ -546,15 +559,15 @@ function performStatus() {
546
559
  statusline: statuslineInstalled(settings)
547
560
  };
548
561
  };
549
- return { user: statusFor(USER_SCOPE_PATH), project: statusFor(PROJECT_SCOPE_PATH) };
562
+ return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
550
563
  }
551
564
  function resolveScope(input) {
552
- if (input === void 0 || input === "user") {
553
- return "user";
554
- }
555
- if (input === "project") {
565
+ if (input === void 0 || input === "project") {
556
566
  return "project";
557
567
  }
568
+ if (input === "user") {
569
+ return "user";
570
+ }
558
571
  console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
559
572
  process.exit(1);
560
573
  }
@@ -562,7 +575,7 @@ function registerClaudeCommands(program2) {
562
575
  const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
563
576
  claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
564
577
  "--scope <scope>",
565
- "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
578
+ "project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
566
579
  ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
567
580
  const scope = resolveScope(opts.scope);
568
581
  const result = performInstall(scope, opts.force ?? false);
@@ -579,7 +592,7 @@ function registerClaudeCommands(program2) {
579
592
  });
580
593
  claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
581
594
  "--scope <scope>",
582
- "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
595
+ "project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
583
596
  ).action((opts) => {
584
597
  const scope = resolveScope(opts.scope);
585
598
  const result = performUninstall(scope);
@@ -627,9 +640,9 @@ var CODEX_REGISTRATIONS = [
627
640
  makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
628
641
  ];
629
642
  var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
630
- var PROJECT_SCOPE_PATH2 = join3(process.cwd(), ".codex", "hooks.json");
643
+ var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
631
644
  function settingsPathFor2(scope) {
632
- return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
645
+ return scope === "user" ? USER_SCOPE_PATH2 : projectScopePath2();
633
646
  }
634
647
  function applyInstall2(settings, options = {}) {
635
648
  const hooks = { ...settings.hooks ?? {} };
@@ -694,24 +707,24 @@ function performStatus2() {
694
707
  const settings = readSettings(path);
695
708
  return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
696
709
  };
697
- return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(PROJECT_SCOPE_PATH2) };
710
+ return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(projectScopePath2()) };
698
711
  }
699
712
  function resolveScope2(input) {
700
- if (input === void 0 || input === "user") {
701
- return "user";
702
- }
703
- if (input === "project") {
713
+ if (input === void 0 || input === "project") {
704
714
  return "project";
705
715
  }
716
+ if (input === "user") {
717
+ return "user";
718
+ }
706
719
  console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
707
720
  process.exit(1);
708
721
  }
709
722
  var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
710
723
  function registerCodexCommands(program2) {
711
724
  const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
712
- codex.command("install").description("Register the prim hooks in Codex's ~/.codex/hooks.json").option(
725
+ codex.command("install").description("Register the prim hooks in Codex's hooks.json (project scope by default)").option(
713
726
  "--scope <scope>",
714
- "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
727
+ "project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
715
728
  ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
716
729
  const scope = resolveScope2(opts.scope);
717
730
  const result = performInstall2(scope, opts.force ?? false);
@@ -723,9 +736,9 @@ function registerCodexCommands(program2) {
723
736
  console.error(TRUST_NOTICE);
724
737
  console.log(JSON.stringify(result, null, JSON_INDENT2));
725
738
  });
726
- codex.command("uninstall").description("Remove all prim hooks from ~/.codex/hooks.json").option(
739
+ codex.command("uninstall").description("Remove all prim hooks from Codex's hooks.json").option(
727
740
  "--scope <scope>",
728
- "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
741
+ "project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
729
742
  ).action((opts) => {
730
743
  const scope = resolveScope2(opts.scope);
731
744
  const result = performUninstall2(scope);
@@ -751,6 +764,22 @@ import { spawn } from "child_process";
751
764
  import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
752
765
  import { homedir as homedir3 } from "os";
753
766
  import { join as join4 } from "path";
767
+
768
+ // src/lib/presence.ts
769
+ function formatTeammates(names, cap) {
770
+ if (names === void 0) {
771
+ return "\u2014";
772
+ }
773
+ if (names.length === 0) {
774
+ return "just you";
775
+ }
776
+ if (names.length <= cap) {
777
+ return names.join(", ");
778
+ }
779
+ return `${names.slice(0, cap).join(", ")} +${String(names.length - cap)}`;
780
+ }
781
+
782
+ // src/commands/daemon.ts
754
783
  var DAEMON_BIN = "prim-daemon-server";
755
784
  var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
756
785
  var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
@@ -924,8 +953,9 @@ async function daemonStatus() {
924
953
  } else if (!snapshot) {
925
954
  process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
926
955
  } else {
956
+ const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
927
957
  process.stderr.write(
928
- `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
958
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}${team}
929
959
  `
930
960
  );
931
961
  }
@@ -1189,8 +1219,8 @@ async function fetchRecent(args, deps = defaultDeps2) {
1189
1219
  if (args.since !== void 0) {
1190
1220
  params.set("since", args.since);
1191
1221
  }
1192
- const client = deps.getClient();
1193
1222
  try {
1223
+ const client = deps.getClient();
1194
1224
  const res = await daemonOrDirectGet(
1195
1225
  "decisions_recent",
1196
1226
  `/api/cli/decisions/recent?${params.toString()}`,
@@ -1236,9 +1266,18 @@ function authorLabel(row) {
1236
1266
  }
1237
1267
  }
1238
1268
  var AUTHOR_WIDTH = 18;
1269
+ var AREA_WIDTH = 12;
1239
1270
  function padRight(s, width) {
1240
1271
  return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
1241
1272
  }
1273
+ function formatRecentRow(row) {
1274
+ const clock = formatClock(row.classifiedAt);
1275
+ const author = padRight(authorLabel(row), AUTHOR_WIDTH);
1276
+ const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
1277
+ const areaPlain = padRight(areaText, AREA_WIDTH);
1278
+ const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
1279
+ return ` ${clock} ${author}${areaCol}${row.intent}`;
1280
+ }
1242
1281
  function formatRecentHuman(result) {
1243
1282
  if (result.unavailable !== void 0) {
1244
1283
  return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
@@ -1248,12 +1287,7 @@ function formatRecentHuman(result) {
1248
1287
  }
1249
1288
  const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
1250
1289
  for (const row of result.decisions) {
1251
- const clock = formatClock(row.classifiedAt);
1252
- const author = padRight(authorLabel(row), AUTHOR_WIDTH);
1253
- const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
1254
- const areaPlain = padRight(areaText, 12);
1255
- const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
1256
- lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
1290
+ lines.push(formatRecentRow(row));
1257
1291
  }
1258
1292
  return lines.join("\n");
1259
1293
  }
@@ -1342,6 +1376,44 @@ function formatConfirmJson(result) {
1342
1376
  return JSON.stringify(result.outcome, null, 2);
1343
1377
  }
1344
1378
 
1379
+ // src/decisions/create.ts
1380
+ var CREATE_TIMEOUT_MS = 1e4;
1381
+ var defaultDeps4 = { getClient };
1382
+ function toRequestBody(request) {
1383
+ const candidate = {
1384
+ intent: request.intent,
1385
+ kind: request.kind,
1386
+ rationale: request.rationale,
1387
+ area: request.area,
1388
+ decided: request.decided,
1389
+ alternatives: request.alternatives,
1390
+ confidence: request.confidence,
1391
+ reversibility: request.reversibility,
1392
+ files: request.files
1393
+ };
1394
+ const body = {};
1395
+ for (const [key, value] of Object.entries(candidate)) {
1396
+ const isEmpty = value === void 0 || Array.isArray(value) && value.length === 0;
1397
+ if (!isEmpty) {
1398
+ body[key] = value;
1399
+ }
1400
+ }
1401
+ return body;
1402
+ }
1403
+ async function fetchCreate(request, deps = defaultDeps4) {
1404
+ const client = deps.getClient();
1405
+ return await client.post("/api/cli/decisions/create", toRequestBody(request), {
1406
+ signal: AbortSignal.timeout(CREATE_TIMEOUT_MS)
1407
+ });
1408
+ }
1409
+ function formatCreateHuman(outcome) {
1410
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1411
+ return `[prim] created ${id}.`;
1412
+ }
1413
+ function formatCreateJson(outcome) {
1414
+ return JSON.stringify(outcome, null, 2);
1415
+ }
1416
+
1345
1417
  // src/decisions/show.ts
1346
1418
  var NOT_FOUND_RE3 = /not found/i;
1347
1419
  function colorStatus(status) {
@@ -1354,14 +1426,14 @@ function colorStatus(status) {
1354
1426
  return color(status, "gray");
1355
1427
  }
1356
1428
  var SHOW_TIMEOUT_MS = 1e4;
1357
- var defaultDeps4 = { getClient };
1429
+ var defaultDeps5 = { getClient };
1358
1430
  var DecisionNotFoundError = class extends Error {
1359
1431
  constructor(idOrShortId) {
1360
1432
  super(`Decision not found: ${idOrShortId}`);
1361
1433
  this.name = "DecisionNotFoundError";
1362
1434
  }
1363
1435
  };
1364
- async function fetchShow(idOrShortId, deps = defaultDeps4) {
1436
+ async function fetchShow(idOrShortId, deps = defaultDeps5) {
1365
1437
  const params = new URLSearchParams({ id: idOrShortId });
1366
1438
  const client = deps.getClient();
1367
1439
  try {
@@ -1478,13 +1550,15 @@ function formatShowJson(result) {
1478
1550
 
1479
1551
  // src/commands/decisions.ts
1480
1552
  var EXIT_NOT_FOUND = 4;
1553
+ var EXIT_USAGE = 2;
1554
+ var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1481
1555
  function registerDecisionsCommands(program2) {
1482
1556
  const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
1483
1557
  decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
1484
1558
  "--files <files>",
1485
1559
  "Comma-separated file paths to check against the Decision Graph"
1486
1560
  ).action(async (opts) => {
1487
- const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
1561
+ const filePaths = splitList(opts.files);
1488
1562
  const result = await checkAffectedDecisions(filePaths);
1489
1563
  const warning = formatDecisionsWarning(result);
1490
1564
  if (warning) {
@@ -1546,10 +1620,44 @@ function registerDecisionsCommands(program2) {
1546
1620
  throw err;
1547
1621
  }
1548
1622
  });
1623
+ decisions.command("create").description("Author a decision directly \u2014 the deliberate manual path around automatic capture").requiredOption("--intent <text>", "What was decided (required)").option("--kind <kind>", "change | exploration | task_execution | unclear (default change)").option("--rationale <text>", "Why the decision was made").option(
1624
+ "--area <area>",
1625
+ "Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
1626
+ ).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(
1627
+ "--files <paths>",
1628
+ "Comma-separated repo-relative paths this decision governs (gates edits to them)"
1629
+ ).action(async (opts) => {
1630
+ const request = {
1631
+ intent: opts.intent,
1632
+ kind: opts.kind,
1633
+ rationale: opts.rationale,
1634
+ area: opts.area,
1635
+ decided: splitList(opts.decided),
1636
+ alternatives: splitList(opts.alternatives),
1637
+ confidence: opts.confidence,
1638
+ reversibility: opts.reversibility,
1639
+ files: splitList(opts.files)
1640
+ };
1641
+ try {
1642
+ const outcome = await fetchCreate(request);
1643
+ console.error(formatCreateHuman(outcome));
1644
+ console.log(formatCreateJson(outcome));
1645
+ } catch (err) {
1646
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
1647
+ console.error(`[prim] create rejected: ${err.message}`);
1648
+ console.log(
1649
+ JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2)
1650
+ );
1651
+ process.exitCode = EXIT_USAGE;
1652
+ return;
1653
+ }
1654
+ throw err;
1655
+ }
1656
+ });
1549
1657
  }
1550
1658
 
1551
1659
  // src/commands/hooks.ts
1552
- import { execSync } from "child_process";
1660
+ import { execSync as execSync2 } from "child_process";
1553
1661
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1554
1662
  import { resolve } from "path";
1555
1663
  import { Option } from "commander";
@@ -1587,7 +1695,7 @@ ${hookShim(spec.binName)}
1587
1695
  ${end}`;
1588
1696
  }
1589
1697
  function getGitRoot() {
1590
- return execSync("git rev-parse --show-toplevel", {
1698
+ return execSync2("git rev-parse --show-toplevel", {
1591
1699
  encoding: "utf-8"
1592
1700
  }).trim();
1593
1701
  }
@@ -1866,7 +1974,7 @@ function registerMovesCommands(program2) {
1866
1974
 
1867
1975
  // src/commands/reconcile.ts
1868
1976
  var EXIT_OK2 = 0;
1869
- var EXIT_USAGE = 2;
1977
+ var EXIT_USAGE2 = 2;
1870
1978
  var EXIT_SERVER = 3;
1871
1979
  var HTTP_CLIENT_ERROR_MIN = 400;
1872
1980
  var HTTP_SERVER_ERROR_MIN = 500;
@@ -1910,7 +2018,7 @@ async function performReconcile(idOrShortId, opts = {}) {
1910
2018
  process.stderr.write(`[prim] reconcile rejected: ${err.message}
1911
2019
  `);
1912
2020
  console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
1913
- process.exitCode = EXIT_USAGE;
2021
+ process.exitCode = EXIT_USAGE2;
1914
2022
  return;
1915
2023
  }
1916
2024
  const message = err instanceof Error ? err.message : String(err);
@@ -2172,6 +2280,7 @@ import { readFileSync as readFileSync8 } from "fs";
2172
2280
  import { dirname as dirname5, resolve as resolve3 } from "path";
2173
2281
  import { fileURLToPath as fileURLToPath3 } from "url";
2174
2282
  var STATUSLINE_TIMEOUT_MS = 200;
2283
+ var STATUSLINE_NAME_CAP = 3;
2175
2284
  function readPackageVersion() {
2176
2285
  try {
2177
2286
  const here = dirname5(fileURLToPath3(import.meta.url));
@@ -2209,7 +2318,14 @@ async function renderStatusline() {
2209
2318
  if (snapshot.presenceStale) {
2210
2319
  return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2211
2320
  }
2212
- const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
2321
+ let team;
2322
+ if (snapshot.onlineNames !== void 0) {
2323
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
2324
+ } else if (typeof snapshot.onlineCount === "number") {
2325
+ team = `team: ${String(snapshot.onlineCount)} online`;
2326
+ } else {
2327
+ team = "team: \u2014";
2328
+ }
2213
2329
  return `primitive ${version} (daemon: live \xB7 ${team})`;
2214
2330
  }
2215
2331
  function registerStatuslineCommands(program2) {
@@ -2222,6 +2338,91 @@ function registerStatuslineCommands(program2) {
2222
2338
  });
2223
2339
  }
2224
2340
 
2341
+ // src/commands/welcome.ts
2342
+ var CMD_GUTTER = 38;
2343
+ var RECENT_LIMIT = 5;
2344
+ var REVERSE_PROMPT_LINES = [
2345
+ "What are the most important goals in your organization that you're",
2346
+ "responsible for, right now? What are you not focusing on, in order",
2347
+ "to focus on those goals?"
2348
+ ];
2349
+ var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
2350
+ function welcomeStateFromRecent(result) {
2351
+ if (result.unavailable !== void 0) {
2352
+ return { org: "unknown" };
2353
+ }
2354
+ if (result.decisions.length === 0) {
2355
+ return { org: "empty" };
2356
+ }
2357
+ return { org: "active", recent: result.decisions.slice(0, RECENT_LIMIT) };
2358
+ }
2359
+ function formatWelcome(state) {
2360
+ const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
2361
+ const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
2362
+ const head = [
2363
+ bold(color("Welcome to Primitive", "green")),
2364
+ "",
2365
+ "Primitive captures the decisions your team makes while coding into a",
2366
+ "shared graph \u2014 and flags edits that conflict with earlier ones before",
2367
+ "they land.",
2368
+ "",
2369
+ bold("How it works"),
2370
+ bullet("Capture is automatic \u2014 keep coding; your decisions are recorded for you."),
2371
+ bullet("The conflict gate has your back: when an edit conflicts with a"),
2372
+ " load-bearing decision, prim surfaces it. Run `prim reconcile dec_<id>` to clear",
2373
+ " that decision and retry.",
2374
+ bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
2375
+ " answering keeps the graph trustworthy.",
2376
+ ""
2377
+ ];
2378
+ let body;
2379
+ if (state.org === "active") {
2380
+ body = [
2381
+ bold("Recent team decisions"),
2382
+ ...state.recent.map(formatRecentRow),
2383
+ "",
2384
+ bold("Get started"),
2385
+ cmd("prim decisions check --files <files>", "what governs files you're about to change"),
2386
+ cmd("prim --help", "everything else")
2387
+ ];
2388
+ } else if (state.org === "empty") {
2389
+ body = [
2390
+ bold("Let's seed your decision graph"),
2391
+ "Your team has no decisions recorded yet. Tell me, in your own words:",
2392
+ "",
2393
+ ...REVERSE_PROMPT_LINES.map((line) => ` ${line}`),
2394
+ "",
2395
+ "Share your answer and I'll record each goal as a decision."
2396
+ ];
2397
+ } else {
2398
+ body = [
2399
+ bold("Get started"),
2400
+ cmd("prim decisions recent", "what your team has decided lately"),
2401
+ cmd("prim decisions check --files <files>", "what governs files you're about to change"),
2402
+ cmd("prim --help", "everything else")
2403
+ ];
2404
+ }
2405
+ return [...head, ...body, "", dim("App: https://app.getprimitive.ai")].join("\n");
2406
+ }
2407
+ function welcomeJson(state) {
2408
+ if (state.org === "active") {
2409
+ return { welcomed: true, org: "active", recent: state.recent };
2410
+ }
2411
+ if (state.org === "empty") {
2412
+ return { welcomed: true, org: "empty", reversePrompt: REVERSE_PROMPT };
2413
+ }
2414
+ return { welcomed: true, org: "unknown" };
2415
+ }
2416
+ function registerWelcomeCommand(program2, deps = { getClient }) {
2417
+ program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(async () => {
2418
+ const result = await fetchRecent({ limit: RECENT_LIMIT }, deps);
2419
+ const state = welcomeStateFromRecent(result);
2420
+ process.stderr.write(`${formatWelcome(state)}
2421
+ `);
2422
+ printJson(welcomeJson(state));
2423
+ });
2424
+ }
2425
+
2225
2426
  // src/index.ts
2226
2427
  var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2227
2428
  var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
@@ -2242,6 +2443,7 @@ registerCodexCommands(program);
2242
2443
  registerDaemonCommands(program);
2243
2444
  registerReconcileCommands(program);
2244
2445
  registerStatuslineCommands(program);
2446
+ registerWelcomeCommand(program);
2245
2447
  process.on("unhandledRejection", (err) => {
2246
2448
  const msg = err instanceof Error ? err.message : String(err);
2247
2449
  console.error(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.22",
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",