@primitive.ai/prim 0.1.0-alpha.34 → 0.1.0-alpha.36

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
@@ -1,13 +1,19 @@
1
1
  # @primitive.ai/prim
2
2
 
3
3
  The official CLI for [Primitive](https://getprimitive.ai)'s **decision graph**. It
4
- passively captures the decisions your team makes while coding, gates edits that
5
- conflict with prior team decisions, and reports team presence from the command
6
- line and via session + git hooks.
4
+ passively captures the decisions your team makes while coding and reports team
5
+ presence from the command line and via session + git hooks.
7
6
 
8
7
  > [!WARNING]
9
8
  > This project is in **alpha**. Commands and APIs may change between releases.
10
9
 
10
+ > [!NOTE]
11
+ > **Conflict Gates** check each edit against the decision graph and surface any
12
+ > load-bearing decision it conflicts with; their **Enforcement** tier blocks or pauses
13
+ > a conflicting edit until you reconcile and retry. Conflict Gates are **not currently
14
+ > enabled** — automatic decision capture runs regardless. To enable them for your team,
15
+ > contact **support@getprimitive.ai**.
16
+
11
17
  ## Installation
12
18
 
13
19
  Requires Node.js 20+.
@@ -37,7 +43,7 @@ Or run the steps individually:
37
43
  # 1. Authenticate via browser (WorkOS OAuth)
38
44
  prim auth login
39
45
 
40
- # 2. Wire the session hooks (decision capture + conflict gate + presence)
46
+ # 2. Wire the session hooks (decision capture + presence)
41
47
  prim claude install # or: prim codex install / prim hermes install
42
48
 
43
49
  # 3. Start the companion daemon (latency + team presence)
@@ -94,7 +100,7 @@ prim auth status # Check authentication status
94
100
  ### Session integration
95
101
 
96
102
  Wires the agent's session hooks so the decisions you make are captured into the
97
- graph, conflicting edits are gated, and presence is reported. Each hook
103
+ graph, and presence is reported. Each hook
98
104
  self-resolves the CLI at run time (PATH, then a local install, then
99
105
  `npx --yes @latest`), so it keeps working with no global install.
100
106
 
@@ -147,9 +153,12 @@ would create a cycle (exit 2); an unresolved id exits 4.
147
153
  ### Reconcile
148
154
 
149
155
  ```bash
150
- prim reconcile <id> # Mint a single-use bypass for a decision a gate flagged
156
+ prim reconcile <id> # Mint a single-use bypass for a decision Conflict Gates flagged
151
157
  ```
152
158
 
159
+ Part of Conflict Gates **Enforcement**, which is not currently enabled (see the note
160
+ at the top). Contact support@getprimitive.ai to enable it.
161
+
153
162
  ### Hooks
154
163
 
155
164
  ```bash
@@ -192,13 +201,22 @@ prim moves flush # Drain the local journals to the server (also runs fr
192
201
  ### Skill
193
202
 
194
203
  ```bash
195
- prim skill install # Install the decision-graph agent guide into your rules file
196
- prim skill uninstall # Remove the managed block
197
- prim skill status # Report whether the block is installed
204
+ prim skill install --agent claude # Install the decision-graph guide for Claude Code
205
+ prim skill install --agent codex # …or write the guide into another agent's rules file
206
+ prim skill uninstall --agent claude # Remove it
207
+ prim skill status --agent claude # Report whether it's installed
198
208
  ```
199
209
 
200
- Writes a managed block teaching your agent how to work with the decision graph
201
- into the rules file it reads (CLAUDE.md, AGENTS.md, .cursor/rules, …).
210
+ Teaches your agent how to work with the decision graph. For **Claude Code**
211
+ (`--agent claude`) this installs a skills-directory plugin at
212
+ `<repo>/.claude/skills/prim/` (or `~/.claude/skills/prim/` with `--scope user`)
213
+ — a `.claude-plugin/plugin.json` + `SKILL.md` that auto-loads as the model-invoked
214
+ `prim@skills-dir` skill, no marketplace step; restart Claude Code or run
215
+ `/reload-plugins` after installing. For every other agent it writes a managed
216
+ block into the rules file that agent reads (`--agent codex` → AGENTS.md,
217
+ `--agent hermes` → .hermes.md, or an auto-detected .cursor/rules, …). A bare
218
+ `prim skill install` (no `--agent`) auto-detects a rules file and writes the
219
+ block; pass `--target <path>` for an explicit file.
202
220
 
203
221
  ## Development
204
222
 
package/SKILL.md CHANGED
@@ -9,9 +9,9 @@ description: Use the prim CLI for Primitive's decision graph — passive decisio
9
9
 
10
10
  ## Mental model
11
11
 
12
- As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
12
+ As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. **Conflict Gates** can check a later change against that graph and surface any load-bearing decision it conflicts with; with **Enforcement**, prim blocks the edit until you reconcile the decision and retry. Conflict Gates are **not currently enabled** — capture runs regardless. To enable them for your team, contact support@getprimitive.ai.
13
13
 
14
- You never invoke capture. It runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code), `npx --yes @primitive.ai/prim codex install` (Codex), or `npx --yes @primitive.ai/prim hermes install` (Hermes). Your job is to **respond** to the gate, **read** the graph before load-bearing edits, and **answer** the occasional rationale confirmation.
14
+ You never invoke capture. It runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code), `npx --yes @primitive.ai/prim codex install` (Codex), or `npx --yes @primitive.ai/prim hermes install` (Hermes). Your job is to **read** the graph before load-bearing edits and **answer** the occasional rationale confirmation. (Responding to Conflict Gates applies only once Enforcement is enabled — see below.)
15
15
 
16
16
  ## Auth
17
17
 
@@ -29,17 +29,19 @@ The CLI auto-refreshes a still-valid session from the stored refresh token (proa
29
29
 
30
30
  1. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
31
31
  2. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If auth-related, re-check `auth status`.
32
- 3. `<idOrShortId>` arguments accept either a full decision ID or the short ID shown in feeds and gate reasons.
32
+ 3. `<idOrShortId>` arguments accept either a full decision ID or the short ID shown in feeds (and gate reasons, when Conflict Gates are enabled).
33
33
 
34
- ## Heed the conflict gate
34
+ ## Conflict Gates & Enforcement (not currently enabled)
35
35
 
36
- Before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch; Hermes: write_file/patch) a PreToolUse hook scores the target file against the graph:
36
+ **Conflict Gates** check each edit against the decision graph before it lands and surface any load-bearing decision it conflicts with. Their **Enforcement** tier goes further -- a conflicting edit is blocked (or paused for confirmation) until you reconcile the decision and retry. **Conflict Gates are not currently enabled** — automatic decision capture (above) runs regardless. To enable Conflict Gates and Enforcement for your team, contact support@getprimitive.ai.
37
+
38
+ When Enforcement is enabled, before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch; Hermes: write_file/patch) a PreToolUse hook scores the target file against the graph:
37
39
 
38
40
  - **deny** -- the edit is blocked: it conflicts with a load-bearing prior decision. Don't fight it. Read the reason line; it names the decision id. If you genuinely intend to override that decision, run `npx --yes @primitive.ai/prim reconcile dec_<shortId>`, then retry the edit once. Otherwise choose an approach that respects the decision.
39
41
  - **warn / additional context** -- the edit proceeds, but a relevant prior decision is surfaced. Read it. On Codex a would-be `ask` is delivered as allow-plus-context (Codex can't pause mid-tool), so that context is your only signal -- read it before continuing. Hermes has no soft-confirm tier, so a would-be `ask` arrives as a **deny** carrying the same reconcile directive: reconcile and retry, or set `PRIM_HOOK_MODE=warn` to downgrade it to context-only.
40
42
  - **"decision check skipped / not verified" or "... partial / truncated"** -- the check could not fully run. Treat constraints as UNKNOWN, not clear; never read silence as approval.
41
43
 
42
- The gate fail-opens on its *own* infrastructure errors (no daemon, network blip, org-unbound token) -- a setup problem never blocks your edit. That is exactly why an "unavailable" note matters: it is the honest signal that the check, not your edit, is what failed.
44
+ When enabled, the gate fail-opens on its *own* infrastructure errors (no daemon, network blip, org-unbound token) -- a setup problem would not block your edit. That is why an "unavailable" note would matter: it is the honest signal that the check, not your edit, is what failed.
43
45
 
44
46
  ## Read the graph before large or load-bearing edits
45
47
 
@@ -49,6 +51,8 @@ The gate fail-opens on its *own* infrastructure errors (no daemon, network blip,
49
51
 
50
52
  ## Reconcile and the verdict footer
51
53
 
54
+ Reconcile and the verdict footer are part of Conflict Gates **Enforcement**, which is **not currently enabled** (contact support@getprimitive.ai to turn it on); the `reconcile` command stays available regardless. When Enforcement is on:
55
+
52
56
  `npx --yes @primitive.ai/prim reconcile <idOrShortId>` mints a single-use bypass for the named decision -- it prints `[prim] reconcile bypass issued for dec_<short> (expires in ...)` to STDERR, with the bypass JSON on STDOUT. Your *next* edit to the governed file then goes through, and on that edit prim prints a verdict footer to STDERR -- confirmation the override was recorded, not silently dropped:
53
57
 
54
58
  ```
@@ -75,7 +79,7 @@ Capture is automatic for the decisions you *make while coding*. When the user in
75
79
  npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
76
80
  ```
77
81
 
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).
82
+ 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 — these are the files Conflict Gates would check on later edits, same path form as `decisions check`; Conflict Gates are not currently enabled). 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
83
 
80
84
  ## Relate decisions (link / unlink)
81
85
 
@@ -116,14 +120,14 @@ npx --yes @primitive.ai/prim hooks uninstall
116
120
 
117
121
  Under `CI=1` (or with `--non-interactive`), `hooks install` fails fast in a Husky repo unless `--yes` or `--target` is set; the error names both escapes. `hooks uninstall` only removes the `.git/hooks` copies — if a hook was installed into `.husky/`, remove the prim block from that file manually. To suppress the hooks for one commit, use `git commit --no-verify`.
118
122
 
119
- These git hooks are separate from the **session hooks** (`claude install` / `codex install` / `hermes install`) that drive in-session capture and the conflict gate.
123
+ These git hooks are separate from the **session hooks** (`claude install` / `codex install` / `hermes install`) that drive in-session capture (and Conflict Gates, when enabled).
120
124
 
121
125
  ## Output formats
122
126
 
123
127
  The CLI keeps STDOUT machine-readable and STDERR human-readable. The `decisions` and `reconcile` commands **always** emit a single JSON document on STDOUT — no flag needed; pipe straight to `jq`. The `decisions` commands have **no** `--json` flag and reject one; `reconcile` accepts a reserved no-op `--json`. `auth status` and `skill status` default to human-readable STDOUT and take `--json` to switch to JSON.
124
128
 
125
129
  - **STDOUT is machine-readable** — JSON (one document per invocation). `decisions` reads project lean shapes, not raw rows.
126
- - **STDERR is human-readable** — a verdict-first line, plus the gate/verdict-footer/presence notes.
130
+ - **STDERR is human-readable** — a verdict-first line, plus presence notes (and, when Conflict Gates are enabled, the gate/verdict-footer notes).
127
131
  - **Exit code is authoritative** where it carries meaning — `auth status` exits 0 when authenticated; `decisions show`/`cascade`/`confirm` exit non-zero (e.g. 4 not-found) on a missing or unauthorized id.
128
132
 
129
133
  Examples:
@@ -135,12 +139,12 @@ Examples:
135
139
 
136
140
  ## Pitfalls
137
141
 
138
- - **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.
139
- - **A `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
142
+ - **An "unavailable" / "not verified" decision check is not an all-clear.** Treat constraints as UNKNOWN and proceed deliberately; never read the silence as approval — the same holds for Conflict Gates once enabled.
143
+ - **When Enforcement is enabled, a `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
140
144
  - **Reconcile bypasses are single-use and short-lived.** One bypass clears your *next* edit to the governed file; it is not a standing override.
141
145
  - **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` / `hermes 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.)
142
146
  - **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
143
147
 
144
148
  ## After each task
145
149
 
146
- If the conflict gate denied or warned you, report which decision(s) it named and whether you reconciled. If you read the graph before a load-bearing change, note what you found so the user can verify in the dashboard.
150
+ If Conflict Gates are enabled and one denied or warned you, report which decision(s) it named and whether you reconciled. If you read the graph before a load-bearing change, note what you found so the user can verify in the dashboard.
@@ -54,6 +54,16 @@ function colorForArea(area) {
54
54
  }
55
55
  return AREA_COLORS[area] ?? "gray";
56
56
  }
57
+ function stripControlChars(text) {
58
+ let out = "";
59
+ for (const ch of text) {
60
+ const code = ch.codePointAt(0) ?? 0;
61
+ if (code > 31 && !(code >= 127 && code <= 159)) {
62
+ out += ch;
63
+ }
64
+ }
65
+ return out;
66
+ }
57
67
  function stripAnsi(text) {
58
68
  return text.replace(/\u001b\[[0-9;]*m/g, "");
59
69
  }
@@ -63,5 +73,6 @@ export {
63
73
  dim,
64
74
  bold,
65
75
  colorForArea,
76
+ stripControlChars,
66
77
  stripAnsi
67
78
  };
@@ -0,0 +1,78 @@
1
+ // src/lib/activation.ts
2
+ import { execFileSync as execFileSync2 } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+
7
+ // src/lib/git.ts
8
+ import { execFileSync } from "child_process";
9
+ function gitToplevel(cwd) {
10
+ try {
11
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
+ cwd,
13
+ encoding: "utf-8",
14
+ stdio: ["ignore", "pipe", "ignore"]
15
+ }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ // src/lib/activation.ts
22
+ var PRIM_ACTIVE_KEY = "prim.active";
23
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
24
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
25
+ function repoActiveFlag(cwd) {
26
+ try {
27
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
28
+ cwd,
29
+ encoding: "utf-8",
30
+ stdio: ["ignore", "pipe", "ignore"]
31
+ }).trim();
32
+ if (value === "true") return "true";
33
+ if (value === "false") return "false";
34
+ return void 0;
35
+ } catch {
36
+ return void 0;
37
+ }
38
+ }
39
+ function setRepoActive(cwd, active) {
40
+ execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
41
+ cwd,
42
+ stdio: ["ignore", "ignore", "pipe"]
43
+ });
44
+ }
45
+ function activateRepoBestEffort(cwd) {
46
+ try {
47
+ setRepoActive(cwd, true);
48
+ } catch {
49
+ }
50
+ }
51
+ function hasProjectPrimInstall(cwd) {
52
+ const root = gitToplevel(cwd);
53
+ if (root === null || root === homedir()) return false;
54
+ for (const rel of PROJECT_INSTALL_FILES) {
55
+ const path = join(root, rel);
56
+ if (existsSync(path)) {
57
+ try {
58
+ const content = readFileSync(path, "utf-8");
59
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
60
+ } catch {
61
+ }
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ function isRepoActiveForCapture(cwd) {
67
+ const flag = repoActiveFlag(cwd);
68
+ if (flag === "true") return true;
69
+ if (flag === "false") return false;
70
+ return hasProjectPrimInstall(cwd);
71
+ }
72
+
73
+ export {
74
+ gitToplevel,
75
+ setRepoActive,
76
+ activateRepoBestEffort,
77
+ isRepoActiveForCapture
78
+ };
@@ -2,13 +2,16 @@
2
2
  import {
3
3
  bold,
4
4
  color
5
- } from "../chunk-4QJOQIY6.js";
5
+ } from "../chunk-AAGJFO7C.js";
6
6
  import {
7
7
  scrubFromCwd
8
8
  } from "../chunk-6LAQVM26.js";
9
9
  import {
10
10
  toMove
11
11
  } from "../chunk-S2O4P4A3.js";
12
+ import {
13
+ isRepoActiveForCapture
14
+ } from "../chunk-LUPD2JSH.js";
12
15
  import {
13
16
  getClient
14
17
  } from "../chunk-26VA3ADF.js";
@@ -128,6 +131,10 @@ async function main() {
128
131
  return;
129
132
  }
130
133
  const cwd = parsed.cwd ?? process.cwd();
134
+ if (!isRepoActiveForCapture(cwd)) {
135
+ emit();
136
+ return;
137
+ }
131
138
  const base = toMove(parsed, resolveCliVersion(), agent);
132
139
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
133
140
  try {
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ isRepoActiveForCapture
4
+ } from "../chunk-LUPD2JSH.js";
2
5
  import {
3
6
  getClient,
4
7
  getSiteUrl
@@ -295,6 +298,10 @@ async function main() {
295
298
  emit(failOpen());
296
299
  return;
297
300
  }
301
+ if (!isRepoActiveForCapture(cwd)) {
302
+ emit(failOpen());
303
+ return;
304
+ }
298
305
  let results;
299
306
  try {
300
307
  results = await Promise.all(files.map((f) => checkOneFile(f)));
@@ -10,6 +10,9 @@ import {
10
10
  shouldFlushAfter,
11
11
  toMove
12
12
  } from "../chunk-S2O4P4A3.js";
13
+ import {
14
+ isRepoActiveForCapture
15
+ } from "../chunk-LUPD2JSH.js";
13
16
  import "../chunk-26VA3ADF.js";
14
17
  import {
15
18
  normalizeEnvelope,
@@ -42,12 +45,14 @@ try {
42
45
  const raw = readFileSync(0, "utf-8");
43
46
  const parsed = normalizeEnvelope(JSON.parse(raw), agent);
44
47
  const cwd = parsed.cwd ?? process.cwd();
45
- const base = toMove(parsed, resolveCliVersion(), agent);
46
- const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
47
- const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
48
- appendMove(move, orgId);
49
- if (shouldFlushAfter(move.eventType)) {
50
- spawnBackgroundFlush();
48
+ if (isRepoActiveForCapture(cwd)) {
49
+ const base = toMove(parsed, resolveCliVersion(), agent);
50
+ const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
51
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
52
+ appendMove(move, orgId);
53
+ if (shouldFlushAfter(move.eventType)) {
54
+ spawnBackgroundFlush();
55
+ }
51
56
  }
52
57
  } catch (err) {
53
58
  if (process.env.PRIM_HOOK_DEBUG) {