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

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
@@ -113,6 +113,12 @@ and offers to install into `.husky/`.
113
113
  prim statusline # Render the team-presence statusline (reads the daemon)
114
114
  ```
115
115
 
116
+ ### Welcome
117
+
118
+ ```bash
119
+ prim welcome # Brief orientation to the decision graph (shown after setup)
120
+ ```
121
+
116
122
  ### Session & journal
117
123
 
118
124
  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,
@@ -751,6 +753,22 @@ import { spawn } from "child_process";
751
753
  import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
752
754
  import { homedir as homedir3 } from "os";
753
755
  import { join as join4 } from "path";
756
+
757
+ // src/lib/presence.ts
758
+ function formatTeammates(names, cap) {
759
+ if (names === void 0) {
760
+ return "\u2014";
761
+ }
762
+ if (names.length === 0) {
763
+ return "just you";
764
+ }
765
+ if (names.length <= cap) {
766
+ return names.join(", ");
767
+ }
768
+ return `${names.slice(0, cap).join(", ")} +${String(names.length - cap)}`;
769
+ }
770
+
771
+ // src/commands/daemon.ts
754
772
  var DAEMON_BIN = "prim-daemon-server";
755
773
  var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
756
774
  var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
@@ -924,8 +942,9 @@ async function daemonStatus() {
924
942
  } else if (!snapshot) {
925
943
  process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
926
944
  } else {
945
+ const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
927
946
  process.stderr.write(
928
- `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
947
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}${team}
929
948
  `
930
949
  );
931
950
  }
@@ -1342,6 +1361,44 @@ function formatConfirmJson(result) {
1342
1361
  return JSON.stringify(result.outcome, null, 2);
1343
1362
  }
1344
1363
 
1364
+ // src/decisions/create.ts
1365
+ var CREATE_TIMEOUT_MS = 1e4;
1366
+ var defaultDeps4 = { getClient };
1367
+ function toRequestBody(request) {
1368
+ const candidate = {
1369
+ intent: request.intent,
1370
+ kind: request.kind,
1371
+ rationale: request.rationale,
1372
+ area: request.area,
1373
+ decided: request.decided,
1374
+ alternatives: request.alternatives,
1375
+ confidence: request.confidence,
1376
+ reversibility: request.reversibility,
1377
+ files: request.files
1378
+ };
1379
+ const body = {};
1380
+ for (const [key, value] of Object.entries(candidate)) {
1381
+ const isEmpty = value === void 0 || Array.isArray(value) && value.length === 0;
1382
+ if (!isEmpty) {
1383
+ body[key] = value;
1384
+ }
1385
+ }
1386
+ return body;
1387
+ }
1388
+ async function fetchCreate(request, deps = defaultDeps4) {
1389
+ const client = deps.getClient();
1390
+ return await client.post("/api/cli/decisions/create", toRequestBody(request), {
1391
+ signal: AbortSignal.timeout(CREATE_TIMEOUT_MS)
1392
+ });
1393
+ }
1394
+ function formatCreateHuman(outcome) {
1395
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1396
+ return `[prim] created ${id}.`;
1397
+ }
1398
+ function formatCreateJson(outcome) {
1399
+ return JSON.stringify(outcome, null, 2);
1400
+ }
1401
+
1345
1402
  // src/decisions/show.ts
1346
1403
  var NOT_FOUND_RE3 = /not found/i;
1347
1404
  function colorStatus(status) {
@@ -1354,14 +1411,14 @@ function colorStatus(status) {
1354
1411
  return color(status, "gray");
1355
1412
  }
1356
1413
  var SHOW_TIMEOUT_MS = 1e4;
1357
- var defaultDeps4 = { getClient };
1414
+ var defaultDeps5 = { getClient };
1358
1415
  var DecisionNotFoundError = class extends Error {
1359
1416
  constructor(idOrShortId) {
1360
1417
  super(`Decision not found: ${idOrShortId}`);
1361
1418
  this.name = "DecisionNotFoundError";
1362
1419
  }
1363
1420
  };
1364
- async function fetchShow(idOrShortId, deps = defaultDeps4) {
1421
+ async function fetchShow(idOrShortId, deps = defaultDeps5) {
1365
1422
  const params = new URLSearchParams({ id: idOrShortId });
1366
1423
  const client = deps.getClient();
1367
1424
  try {
@@ -1478,13 +1535,15 @@ function formatShowJson(result) {
1478
1535
 
1479
1536
  // src/commands/decisions.ts
1480
1537
  var EXIT_NOT_FOUND = 4;
1538
+ var EXIT_USAGE = 2;
1539
+ var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1481
1540
  function registerDecisionsCommands(program2) {
1482
1541
  const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
1483
1542
  decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
1484
1543
  "--files <files>",
1485
1544
  "Comma-separated file paths to check against the Decision Graph"
1486
1545
  ).action(async (opts) => {
1487
- const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
1546
+ const filePaths = splitList(opts.files);
1488
1547
  const result = await checkAffectedDecisions(filePaths);
1489
1548
  const warning = formatDecisionsWarning(result);
1490
1549
  if (warning) {
@@ -1546,6 +1605,40 @@ function registerDecisionsCommands(program2) {
1546
1605
  throw err;
1547
1606
  }
1548
1607
  });
1608
+ 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(
1609
+ "--area <area>",
1610
+ "Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
1611
+ ).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(
1612
+ "--files <paths>",
1613
+ "Comma-separated repo-relative paths this decision governs (gates edits to them)"
1614
+ ).action(async (opts) => {
1615
+ const request = {
1616
+ intent: opts.intent,
1617
+ kind: opts.kind,
1618
+ rationale: opts.rationale,
1619
+ area: opts.area,
1620
+ decided: splitList(opts.decided),
1621
+ alternatives: splitList(opts.alternatives),
1622
+ confidence: opts.confidence,
1623
+ reversibility: opts.reversibility,
1624
+ files: splitList(opts.files)
1625
+ };
1626
+ try {
1627
+ const outcome = await fetchCreate(request);
1628
+ console.error(formatCreateHuman(outcome));
1629
+ console.log(formatCreateJson(outcome));
1630
+ } catch (err) {
1631
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
1632
+ console.error(`[prim] create rejected: ${err.message}`);
1633
+ console.log(
1634
+ JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2)
1635
+ );
1636
+ process.exitCode = EXIT_USAGE;
1637
+ return;
1638
+ }
1639
+ throw err;
1640
+ }
1641
+ });
1549
1642
  }
1550
1643
 
1551
1644
  // src/commands/hooks.ts
@@ -1866,7 +1959,7 @@ function registerMovesCommands(program2) {
1866
1959
 
1867
1960
  // src/commands/reconcile.ts
1868
1961
  var EXIT_OK2 = 0;
1869
- var EXIT_USAGE = 2;
1962
+ var EXIT_USAGE2 = 2;
1870
1963
  var EXIT_SERVER = 3;
1871
1964
  var HTTP_CLIENT_ERROR_MIN = 400;
1872
1965
  var HTTP_SERVER_ERROR_MIN = 500;
@@ -1910,7 +2003,7 @@ async function performReconcile(idOrShortId, opts = {}) {
1910
2003
  process.stderr.write(`[prim] reconcile rejected: ${err.message}
1911
2004
  `);
1912
2005
  console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
1913
- process.exitCode = EXIT_USAGE;
2006
+ process.exitCode = EXIT_USAGE2;
1914
2007
  return;
1915
2008
  }
1916
2009
  const message = err instanceof Error ? err.message : String(err);
@@ -2172,6 +2265,7 @@ import { readFileSync as readFileSync8 } from "fs";
2172
2265
  import { dirname as dirname5, resolve as resolve3 } from "path";
2173
2266
  import { fileURLToPath as fileURLToPath3 } from "url";
2174
2267
  var STATUSLINE_TIMEOUT_MS = 200;
2268
+ var STATUSLINE_NAME_CAP = 3;
2175
2269
  function readPackageVersion() {
2176
2270
  try {
2177
2271
  const here = dirname5(fileURLToPath3(import.meta.url));
@@ -2209,7 +2303,14 @@ async function renderStatusline() {
2209
2303
  if (snapshot.presenceStale) {
2210
2304
  return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2211
2305
  }
2212
- const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
2306
+ let team;
2307
+ if (snapshot.onlineNames !== void 0) {
2308
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
2309
+ } else if (typeof snapshot.onlineCount === "number") {
2310
+ team = `team: ${String(snapshot.onlineCount)} online`;
2311
+ } else {
2312
+ team = "team: \u2014";
2313
+ }
2213
2314
  return `primitive ${version} (daemon: live \xB7 ${team})`;
2214
2315
  }
2215
2316
  function registerStatuslineCommands(program2) {
@@ -2222,6 +2323,42 @@ function registerStatuslineCommands(program2) {
2222
2323
  });
2223
2324
  }
2224
2325
 
2326
+ // src/commands/welcome.ts
2327
+ var CMD_GUTTER = 38;
2328
+ function formatWelcome() {
2329
+ const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
2330
+ const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
2331
+ return [
2332
+ bold(color("Welcome to Primitive", "green")),
2333
+ "",
2334
+ "Primitive captures the decisions your team makes while coding into a",
2335
+ "shared graph \u2014 and flags edits that conflict with earlier ones before",
2336
+ "they land.",
2337
+ "",
2338
+ bold("How it works"),
2339
+ bullet("Capture is automatic \u2014 keep coding; your decisions are recorded for you."),
2340
+ bullet("The conflict gate has your back: when an edit conflicts with a"),
2341
+ " load-bearing decision, prim surfaces it. Run `prim reconcile dec_<id>` to clear",
2342
+ " that decision and retry.",
2343
+ bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
2344
+ " answering keeps the graph trustworthy.",
2345
+ "",
2346
+ bold("Get started"),
2347
+ cmd("prim decisions recent", "what your team has decided lately"),
2348
+ cmd("prim decisions check --files <files>", "what governs files you're about to change"),
2349
+ cmd("prim --help", "everything else"),
2350
+ "",
2351
+ dim("App: https://app.getprimitive.ai")
2352
+ ].join("\n");
2353
+ }
2354
+ function registerWelcomeCommand(program2) {
2355
+ program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(() => {
2356
+ process.stderr.write(`${formatWelcome()}
2357
+ `);
2358
+ printJson({ welcomed: true });
2359
+ });
2360
+ }
2361
+
2225
2362
  // src/index.ts
2226
2363
  var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2227
2364
  var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
@@ -2242,6 +2379,7 @@ registerCodexCommands(program);
2242
2379
  registerDaemonCommands(program);
2243
2380
  registerReconcileCommands(program);
2244
2381
  registerStatuslineCommands(program);
2382
+ registerWelcomeCommand(program);
2245
2383
  process.on("unhandledRejection", (err) => {
2246
2384
  const msg = err instanceof Error ? err.message : String(err);
2247
2385
  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.21",
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",