@primitive.ai/prim 0.1.0-alpha.16 → 0.1.0-alpha.18

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/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prim
3
- description: Use the prim CLI for managing Primitive specs, contexts, projects, and pre-commit hooks. TRIGGER when the user mentions Primitive, prim, "specs" (in the Primitive sense), or "contexts" (in the Primitive sense); when the repo's package.json depends on @primitive.ai/prim; when the user asks to sync, map, update, or auto-map a spec; when configuring Primitive pre-commit hooks. SKIP when "spec" means test specs (vitest, jest, rspec), when "context" means React context or LLM context window, or for unrelated CLIs.
3
+ description: Use the prim CLI for Primitive specs, contexts, projects, pre-commit hooks, and the decision graph (passive decision capture, the conflict gate, reconcile, and team presence). TRIGGER when the user mentions Primitive, prim, "specs" or "contexts" (in the Primitive sense), or decisions / the decision graph / a conflict gate / reconcile; when the repo's package.json depends on @primitive.ai/prim; when the user asks to sync, map, update, or auto-map a spec; when an edit is denied or warned by a prior decision; when configuring Primitive hooks. SKIP when "spec" means test specs (vitest, jest, rspec), when "context" means React context or an LLM context window, or for unrelated CLIs.
4
4
  ---
5
5
 
6
6
  # Working with the prim CLI
@@ -35,6 +35,36 @@ The CLI auto-refreshes expired tokens. On unrecoverable expiry it throws `Authen
35
35
  2. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
36
36
  3. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If a command fails with an unrecognized error, re-run with `--help` to check your flags. If auth-related, re-check `auth status`.
37
37
 
38
+ ## Working with the decision graph
39
+
40
+ Separate from specs, prim passively captures the decisions you make during a coding session -- which library, which pattern, which config value -- into a queryable decision graph, and actively **gates** edits that would conflict with a load-bearing prior decision. Capture and the gate run automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code) or `npx --yes @primitive.ai/prim codex install` (Codex). You never invoke capture; you *respond* to the gate and *read* the graph.
41
+
42
+ ### Heed the conflict gate
43
+ Before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch) a PreToolUse hook scores the target file against the graph:
44
+
45
+ - **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.
46
+ - **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.
47
+ - **"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.
48
+
49
+ 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.
50
+
51
+ ### Read the graph before large or load-bearing edits
52
+ - `npx --yes @primitive.ai/prim decisions check --files "src/a.ts,src/b.ts"` -- which active decisions reference the files you're about to touch (comma-separated paths, one `--files` value). Run it before a big change.
53
+ - `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex`); `--limit <n>` and `--since <dur>` narrow it.
54
+ - `npx --yes @primitive.ai/prim decisions show <idOrShortId>` and `npx --yes @primitive.ai/prim decisions cascade <idOrShortId>` -- full detail, and the downstream blast radius a change would disturb.
55
+
56
+ ### Reconcile and the verdict footer
57
+ `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:
58
+
59
+ ```
60
+ ✓ Conflict caught before merge · N decisions saved · <author>'s intent preserved
61
+ ```
62
+
63
+ `N` is the reconciled decision's downstream live-dependent count, shown as `N+` when the server caps it.
64
+
65
+ ### Presence
66
+ 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.
67
+
38
68
  ## Common workflows
39
69
 
40
70
  ### Read a spec's current text (do this before any partial edit)
@@ -1,30 +1,3 @@
1
- // src/hooks/prim-hook-core.ts
2
- import { randomUUID } from "crypto";
3
- import { platform } from "os";
4
-
5
- // src/protocol/move.ts
6
- var ENVELOPE_VERSION = 1;
7
-
8
- // src/hooks/prim-hook-core.ts
9
- function toMove(parsed, cliVersion) {
10
- return {
11
- moveId: randomUUID(),
12
- capturedAt: Date.now(),
13
- sessionId: parsed.session_id ?? "",
14
- eventType: parsed.hook_event_name ?? "unknown",
15
- payload: parsed,
16
- env: {
17
- cwd: parsed.cwd ?? process.cwd(),
18
- cliVersion,
19
- osPlatform: platform()
20
- },
21
- envelopeVersion: ENVELOPE_VERSION
22
- };
23
- }
24
- function shouldFlushAfter(eventType) {
25
- return eventType === "SessionEnd";
26
- }
27
-
28
1
  // src/hooks/redact.ts
29
2
  import { existsSync, readFileSync } from "fs";
30
3
  import { join } from "path";
@@ -105,7 +78,5 @@ function scrubFromCwd(value, cwd) {
105
78
  }
106
79
 
107
80
  export {
108
- toMove,
109
- shouldFlushAfter,
110
81
  scrubFromCwd
111
82
  };
@@ -0,0 +1,57 @@
1
+ // src/hooks/prim-hook-core.ts
2
+ import { randomUUID } from "crypto";
3
+ import { platform } from "os";
4
+
5
+ // src/protocol/move.ts
6
+ var ENVELOPE_VERSION = 1;
7
+
8
+ // src/hooks/prim-hook-core.ts
9
+ function toMove(parsed, cliVersion, agent = "claude_code") {
10
+ return {
11
+ moveId: randomUUID(),
12
+ capturedAt: Date.now(),
13
+ sessionId: parsed.session_id ?? "",
14
+ eventType: parsed.hook_event_name ?? "unknown",
15
+ payload: parsed,
16
+ env: {
17
+ cwd: parsed.cwd ?? process.cwd(),
18
+ cliVersion,
19
+ osPlatform: platform()
20
+ },
21
+ envelopeVersion: ENVELOPE_VERSION,
22
+ // Stamp the producer only for Codex; Claude Code moves omit it (the
23
+ // backend defaults an absent value to "claude_code"), keeping the
24
+ // Claude wire shape byte-identical.
25
+ ...agent === "codex" ? { producer: "codex" } : {}
26
+ };
27
+ }
28
+ function toCommitMove(commit, cliVersion, cwd) {
29
+ return {
30
+ moveId: `commit:${commit.sha}`,
31
+ capturedAt: Date.now(),
32
+ sessionId: "",
33
+ eventType: "git.commit",
34
+ payload: {
35
+ kind: "git.commit",
36
+ sha: commit.sha,
37
+ parentSha: commit.parentSha,
38
+ branch: commit.branch,
39
+ files: commit.files
40
+ },
41
+ env: {
42
+ cwd,
43
+ cliVersion,
44
+ osPlatform: platform()
45
+ },
46
+ envelopeVersion: ENVELOPE_VERSION
47
+ };
48
+ }
49
+ function shouldFlushAfter(eventType) {
50
+ return eventType === "SessionEnd";
51
+ }
52
+
53
+ export {
54
+ toMove,
55
+ toCommitMove,
56
+ shouldFlushAfter
57
+ };
@@ -0,0 +1,9 @@
1
+ // src/hooks/agent.ts
2
+ function parseAgent(argv) {
3
+ const i = argv.indexOf("--agent");
4
+ return i !== -1 && argv[i + 1] === "codex" ? "codex" : "claude_code";
5
+ }
6
+
7
+ export {
8
+ parseAgent
9
+ };
@@ -1,6 +1,31 @@
1
1
  import {
2
2
  getClient
3
3
  } from "./chunk-6SIEWWUL.js";
4
+ import {
5
+ daemonRequest
6
+ } from "./chunk-UTKQTZHL.js";
7
+
8
+ // src/daemon/proxy.ts
9
+ var DAEMON_HTTP_TIMEOUT_MS = 1e4;
10
+ var DAEMON_PROBE_TIMEOUT_MS = 250;
11
+ async function daemonOrDirect(method, params, direct) {
12
+ const fromDaemon = await daemonRequest(method, params, {
13
+ timeoutMs: DAEMON_PROBE_TIMEOUT_MS
14
+ });
15
+ if (fromDaemon !== null) {
16
+ return fromDaemon;
17
+ }
18
+ return await direct();
19
+ }
20
+ async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
21
+ return await daemonOrDirect(
22
+ method,
23
+ { path },
24
+ async () => await client.get(path, {
25
+ signal: AbortSignal.timeout(timeoutMs)
26
+ })
27
+ );
28
+ }
4
29
 
5
30
  // src/hooks/decisions-check.ts
6
31
  var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
@@ -19,9 +44,12 @@ async function fetchAffecting(client, batch) {
19
44
  params.append("files", file);
20
45
  }
21
46
  try {
22
- return await client.get(`/api/cli/decisions/affecting?${params.toString()}`, {
23
- signal: AbortSignal.timeout(DECISIONS_CHECK_TIMEOUT_MS)
24
- });
47
+ return await daemonOrDirectGet(
48
+ "decisions_affecting",
49
+ `/api/cli/decisions/affecting?${params.toString()}`,
50
+ client,
51
+ DECISIONS_CHECK_TIMEOUT_MS
52
+ );
25
53
  } catch (err) {
26
54
  const detail = err instanceof Error ? err.message : String(err);
27
55
  return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
@@ -116,6 +144,7 @@ function getGitContext() {
116
144
  }
117
145
 
118
146
  export {
147
+ daemonOrDirectGet,
119
148
  checkAffectedDecisions,
120
149
  formatDecisionsWarning,
121
150
  getGitContext
@@ -16,6 +16,7 @@ var PID_PATH = join(CONFIG_DIR, "daemon.pid");
16
16
  var HEARTBEAT_INTERVAL_MS = 3e4;
17
17
  var TOKEN_CHECK_INTERVAL_MS = 6e4;
18
18
  var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
19
+ var HTTP_PROXY_TIMEOUT_MS = 1e4;
19
20
  var PRESENCE_FRESH_WINDOW_MS = 9e4;
20
21
  var SOCKET_DIR_MODE = 448;
21
22
  var PID_FILE_MODE = 384;
@@ -98,6 +99,22 @@ async function handleConflictCheck(params) {
98
99
  }
99
100
  return await client.post("/api/cli/decisions/conflict-check", { file: params.file });
100
101
  }
102
+ function pathParam(params) {
103
+ if (typeof params.path !== "string" || !params.path.startsWith("/api/cli/")) {
104
+ throw new Error("proxy request requires `path: string` under /api/cli/");
105
+ }
106
+ return params.path;
107
+ }
108
+ function assertEndpointPath(path, endpoint) {
109
+ if (path !== endpoint && !path.startsWith(`${endpoint}?`)) {
110
+ throw new Error(`proxy path must be ${endpoint} or ${endpoint}?...`);
111
+ }
112
+ }
113
+ async function proxyGet(params, allowedPrefix) {
114
+ const path = pathParam(params);
115
+ assertEndpointPath(path, allowedPrefix);
116
+ return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
117
+ }
101
118
  function handleStatusSnapshot() {
102
119
  const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
103
120
  const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
@@ -120,6 +137,22 @@ async function dispatchRequest(req) {
120
137
  const result = await handleConflictCheck(req.params ?? {});
121
138
  return { id, ok: true, result };
122
139
  }
140
+ case "decisions_recent": {
141
+ const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/recent");
142
+ return { id, ok: true, result };
143
+ }
144
+ case "decisions_show": {
145
+ const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/show");
146
+ return { id, ok: true, result };
147
+ }
148
+ case "decisions_cascade": {
149
+ const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/cascade");
150
+ return { id, ok: true, result };
151
+ }
152
+ case "decisions_affecting": {
153
+ const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/affecting");
154
+ return { id, ok: true, result };
155
+ }
123
156
  case "session_start": {
124
157
  const sid = req.params?.sessionId;
125
158
  if (typeof sid === "string" && sid.length > 0) {
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ appendMove,
4
+ resolveOrg
5
+ } from "../chunk-JZGWQDM5.js";
6
+ import {
7
+ toCommitMove
8
+ } from "../chunk-7GHOFNJ2.js";
9
+
10
+ // src/hooks/post-commit.ts
11
+ import { execSync, spawn } from "child_process";
12
+ import { readFileSync } from "fs";
13
+ import { dirname, join } from "path";
14
+ import { fileURLToPath } from "url";
15
+ var here = dirname(fileURLToPath(import.meta.url));
16
+ function git(args) {
17
+ try {
18
+ return execSync(`git ${args}`, {
19
+ encoding: "utf-8",
20
+ stdio: ["ignore", "pipe", "ignore"]
21
+ }).trim();
22
+ } catch {
23
+ return;
24
+ }
25
+ }
26
+ function readCommit() {
27
+ const sha = git("rev-parse HEAD");
28
+ if (!sha) {
29
+ return null;
30
+ }
31
+ const branch = git("rev-parse --abbrev-ref HEAD");
32
+ const files = (git("diff-tree --no-commit-id --name-only -r -m --root HEAD") ?? "").split("\n").filter((f) => f.length > 0);
33
+ return {
34
+ sha,
35
+ parentSha: git("rev-parse --verify --quiet HEAD^") || void 0,
36
+ branch: branch && branch !== "HEAD" ? branch : void 0,
37
+ files
38
+ };
39
+ }
40
+ function resolveCliVersion() {
41
+ try {
42
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
43
+ return pkg.version ?? "unknown";
44
+ } catch {
45
+ return "unknown";
46
+ }
47
+ }
48
+ function spawnBackgroundFlush() {
49
+ const entry = join(here, "..", "index.js");
50
+ spawn(process.execPath, [entry, "moves", "flush"], {
51
+ detached: true,
52
+ stdio: "ignore"
53
+ }).unref();
54
+ }
55
+ try {
56
+ const commit = readCommit();
57
+ if (commit) {
58
+ const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
59
+ const move = toCommitMove(commit, resolveCliVersion(), cwd);
60
+ const { orgId } = resolveOrg({ sessionId: "", cwd });
61
+ appendMove(move, orgId);
62
+ spawnBackgroundFlush();
63
+ }
64
+ } catch (err) {
65
+ if (process.env.PRIM_HOOK_DEBUG) {
66
+ const detail = err instanceof Error ? err.message : String(err);
67
+ process.stderr.write(`[prim-post-commit] capture failed: ${detail}
68
+ `);
69
+ }
70
+ }
71
+ process.exit(0);
@@ -7,9 +7,14 @@ import {
7
7
  getClient
8
8
  } from "../chunk-6SIEWWUL.js";
9
9
  import {
10
- scrubFromCwd,
10
+ scrubFromCwd
11
+ } from "../chunk-6LAQVM26.js";
12
+ import {
11
13
  toMove
12
- } from "../chunk-PTLXSXIY.js";
14
+ } from "../chunk-7GHOFNJ2.js";
15
+ import {
16
+ parseAgent
17
+ } from "../chunk-7YRBACIE.js";
13
18
 
14
19
  // src/hooks/post-tool-use.ts
15
20
  import { readFileSync } from "fs";
@@ -36,6 +41,7 @@ function isVerdictFooterContext(value) {
36
41
  var STDIN_TIMEOUT_MS = 1e3;
37
42
  var INGEST_TIMEOUT_MS = 4e3;
38
43
  var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
44
+ var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
39
45
  var here = dirname(fileURLToPath(import.meta.url));
40
46
  function resolveCliVersion() {
41
47
  try {
@@ -100,7 +106,9 @@ async function main() {
100
106
  return;
101
107
  }
102
108
  const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
103
- if (!EDITING_TOOLS.has(toolName)) {
109
+ const agent = parseAgent(process.argv);
110
+ const editingTools = agent === "codex" ? CODEX_EDITING_TOOLS : EDITING_TOOLS;
111
+ if (!editingTools.has(toolName)) {
104
112
  emit();
105
113
  return;
106
114
  }
@@ -109,7 +117,7 @@ async function main() {
109
117
  return;
110
118
  }
111
119
  const cwd = parsed.cwd ?? process.cwd();
112
- const base = toMove(parsed, resolveCliVersion());
120
+ const base = toMove(parsed, resolveCliVersion(), agent);
113
121
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
114
122
  try {
115
123
  const result = await ingestMove(move);
@@ -3,10 +3,11 @@ import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning,
5
5
  getGitContext
6
- } from "../chunk-S47B4VGC.js";
6
+ } from "../chunk-TPQ3X244.js";
7
7
  import {
8
8
  getClient
9
9
  } from "../chunk-6SIEWWUL.js";
10
+ import "../chunk-UTKQTZHL.js";
10
11
 
11
12
  // src/hooks/pre-commit.ts
12
13
  import { execSync } from "child_process";
@@ -2,6 +2,9 @@
2
2
  import {
3
3
  getClient
4
4
  } from "../chunk-6SIEWWUL.js";
5
+ import {
6
+ parseAgent
7
+ } from "../chunk-7YRBACIE.js";
5
8
  import {
6
9
  daemonRequest
7
10
  } from "../chunk-UTKQTZHL.js";
@@ -43,7 +46,7 @@ function unverifiedNote(results) {
43
46
  }
44
47
  return causes.map((c) => `[primitive] ${c}`).join("\n");
45
48
  }
46
- function buildHookOutput(aggregate, results) {
49
+ function buildHookOutput(aggregate, results, agent = "claude_code") {
47
50
  if (aggregate === "deny") {
48
51
  const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
49
52
  return {
@@ -54,6 +57,18 @@ function buildHookOutput(aggregate, results) {
54
57
  }
55
58
  };
56
59
  }
60
+ if (agent === "codex" && aggregate === "ask") {
61
+ const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
62
+ const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
63
+ const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
64
+ const out = {
65
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
66
+ };
67
+ if (merged.length > 0) {
68
+ out.hookSpecificOutput.additionalContext = merged;
69
+ }
70
+ return out;
71
+ }
57
72
  if (aggregate === "ask") {
58
73
  const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
59
74
  const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
@@ -98,7 +113,32 @@ function failOpenOutput() {
98
113
  };
99
114
  }
100
115
  var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
101
- function extractFilePaths(toolName, toolInput) {
116
+ var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
117
+ var LINE_SPLIT_RE = /\r?\n/;
118
+ function parseApplyPatchPaths(command) {
119
+ const paths = /* @__PURE__ */ new Set();
120
+ for (const line of command.split(LINE_SPLIT_RE)) {
121
+ const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path?.trim();
122
+ if (path) {
123
+ paths.add(path);
124
+ }
125
+ }
126
+ return Array.from(paths);
127
+ }
128
+ function extractCodexFilePaths(toolName, toolInput) {
129
+ if (toolName !== "apply_patch") {
130
+ return [];
131
+ }
132
+ if (!toolInput || typeof toolInput !== "object") {
133
+ return [];
134
+ }
135
+ const command = toolInput.command;
136
+ return typeof command === "string" ? parseApplyPatchPaths(command) : [];
137
+ }
138
+ function extractFilePaths(toolName, toolInput, agent = "claude_code") {
139
+ if (agent === "codex") {
140
+ return extractCodexFilePaths(toolName, toolInput);
141
+ }
102
142
  if (!SUPPORTED_TOOLS.has(toolName)) {
103
143
  return [];
104
144
  }
@@ -199,7 +239,10 @@ async function main() {
199
239
  }
200
240
  const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
201
241
  const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
202
- const files = extractFilePaths(toolName, envelope.tool_input).map((f) => toRepoRelative(f, cwd));
242
+ const agent = parseAgent(process.argv);
243
+ const files = extractFilePaths(toolName, envelope.tool_input, agent).map(
244
+ (f) => toRepoRelative(f, cwd)
245
+ );
203
246
  if (files.length === 0) {
204
247
  emit(failOpenOutput());
205
248
  return;
@@ -213,7 +256,7 @@ async function main() {
213
256
  }
214
257
  const rawAggregate = aggregateCheckResults(results);
215
258
  const aggregate = demoteForMode(rawAggregate, mode);
216
- emit(buildHookOutput(aggregate, results));
259
+ emit(buildHookOutput(aggregate, results, agent));
217
260
  }
218
261
  main().catch(() => {
219
262
  emit(failOpenOutput());
@@ -4,10 +4,15 @@ import {
4
4
  resolveOrg
5
5
  } from "../chunk-JZGWQDM5.js";
6
6
  import {
7
- scrubFromCwd,
7
+ scrubFromCwd
8
+ } from "../chunk-6LAQVM26.js";
9
+ import {
8
10
  shouldFlushAfter,
9
11
  toMove
10
- } from "../chunk-PTLXSXIY.js";
12
+ } from "../chunk-7GHOFNJ2.js";
13
+ import {
14
+ parseAgent
15
+ } from "../chunk-7YRBACIE.js";
11
16
 
12
17
  // src/hooks/prim-hook.ts
13
18
  import { spawn } from "child_process";
@@ -34,7 +39,7 @@ try {
34
39
  const raw = readFileSync(0, "utf-8");
35
40
  const parsed = JSON.parse(raw);
36
41
  const cwd = parsed.cwd ?? process.cwd();
37
- const base = toMove(parsed, resolveCliVersion());
42
+ const base = toMove(parsed, resolveCliVersion(), parseAgent(process.argv));
38
43
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
39
44
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
40
45
  appendMove(move, orgId);
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ parseAgent
4
+ } from "../chunk-7YRBACIE.js";
2
5
  import {
3
6
  daemonRequest
4
7
  } from "../chunk-UTKQTZHL.js";
@@ -23,8 +26,19 @@ function readStdin() {
23
26
  });
24
27
  });
25
28
  }
26
- function emit() {
27
- process.stdout.write("{}\n");
29
+ function emit(additionalContext) {
30
+ if (!additionalContext) {
31
+ process.stdout.write("{}\n");
32
+ return;
33
+ }
34
+ const out = {
35
+ hookSpecificOutput: {
36
+ hookEventName: "SessionStart",
37
+ additionalContext
38
+ }
39
+ };
40
+ process.stdout.write(`${JSON.stringify(out)}
41
+ `);
28
42
  }
29
43
  async function main() {
30
44
  let raw;
@@ -54,6 +68,17 @@ async function main() {
54
68
  { sessionId: envelope.session_id },
55
69
  { timeoutMs: DAEMON_TIMEOUT_MS }
56
70
  );
71
+ if (parseAgent(process.argv) === "codex") {
72
+ const snapshot = await daemonRequest(
73
+ "status_snapshot",
74
+ {},
75
+ { timeoutMs: DAEMON_TIMEOUT_MS }
76
+ );
77
+ if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
78
+ emit(`[prim] team: ${snapshot.onlineCount} online`);
79
+ return;
80
+ }
81
+ }
57
82
  emit();
58
83
  }
59
84
  main().catch(() => {
package/dist/index.js CHANGED
@@ -6,9 +6,10 @@ import {
6
6
  } from "./chunk-BEEGFDGU.js";
7
7
  import {
8
8
  checkAffectedDecisions,
9
+ daemonOrDirectGet,
9
10
  formatDecisionsWarning,
10
11
  getGitContext
11
- } from "./chunk-S47B4VGC.js";
12
+ } from "./chunk-TPQ3X244.js";
12
13
  import {
13
14
  HttpError,
14
15
  REFRESH_TOKEN_PATH,
@@ -532,6 +533,154 @@ ${line("project", result.project)}`);
532
533
  });
533
534
  }
534
535
 
536
+ // src/commands/codex-install.ts
537
+ import { homedir as homedir2 } from "os";
538
+ import { join as join2 } from "path";
539
+ var CAPTURE_COMMAND2 = "prim-hook --agent codex";
540
+ var GATE_COMMAND2 = "prim-pre-tool-use --agent codex";
541
+ var POST_TOOL_USE_COMMAND2 = "prim-post-tool-use --agent codex";
542
+ var SESSION_START_COMMAND2 = "prim-session-start --agent codex";
543
+ var JSON_INDENT2 = 2;
544
+ var CODEX_CAPTURE_EVENTS = [
545
+ "SessionStart",
546
+ "UserPromptSubmit",
547
+ "PreToolUse",
548
+ "PostToolUse",
549
+ "Stop",
550
+ "SubagentStop"
551
+ ];
552
+ var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
553
+ CAPTURE_COMMAND2,
554
+ GATE_COMMAND2,
555
+ POST_TOOL_USE_COMMAND2,
556
+ SESSION_START_COMMAND2
557
+ ]);
558
+ var CODEX_REGISTRATIONS = [
559
+ ...CODEX_CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND2 })),
560
+ { event: "PreToolUse", matcher: "apply_patch", command: GATE_COMMAND2 },
561
+ { event: "PostToolUse", matcher: "apply_patch", command: POST_TOOL_USE_COMMAND2 },
562
+ { event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND2 }
563
+ ];
564
+ var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
565
+ var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
566
+ function settingsPathFor2(scope) {
567
+ return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
568
+ }
569
+ function applyInstall2(settings, options = {}) {
570
+ const hooks = { ...settings.hooks ?? {} };
571
+ for (const reg of CODEX_REGISTRATIONS) {
572
+ hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
573
+ }
574
+ return { ...settings, hooks };
575
+ }
576
+ function applyUninstall2(settings) {
577
+ const source = settings.hooks ?? {};
578
+ const hooks = {};
579
+ for (const event of Object.keys(source)) {
580
+ let list = source[event] ?? [];
581
+ for (const command of PRIM_COMMANDS2) {
582
+ list = stripCommand(list, command);
583
+ }
584
+ if (list.length > 0) {
585
+ hooks[event] = list;
586
+ }
587
+ }
588
+ return { ...settings, hooks };
589
+ }
590
+ function captureInstalled2(settings) {
591
+ return CODEX_CAPTURE_EVENTS.some(
592
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
593
+ );
594
+ }
595
+ function isGateInstalled2(settings) {
596
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
597
+ }
598
+ function resultFor(scope, path, after, changed) {
599
+ return {
600
+ scope,
601
+ path,
602
+ gate: isGateInstalled2(after),
603
+ capture: captureInstalled2(after),
604
+ changed
605
+ };
606
+ }
607
+ function performInstall2(scope, force) {
608
+ const path = settingsPathFor2(scope);
609
+ const before = readSettings(path);
610
+ const after = applyInstall2(before, { force });
611
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
612
+ if (changed) {
613
+ atomicWrite(path, after);
614
+ }
615
+ return resultFor(scope, path, after, changed);
616
+ }
617
+ function performUninstall2(scope) {
618
+ const path = settingsPathFor2(scope);
619
+ const before = readSettings(path);
620
+ const after = applyUninstall2(before);
621
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
622
+ if (changed) {
623
+ atomicWrite(path, after);
624
+ }
625
+ return resultFor(scope, path, after, changed);
626
+ }
627
+ function performStatus2() {
628
+ const statusFor = (path) => {
629
+ const settings = readSettings(path);
630
+ return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
631
+ };
632
+ return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(PROJECT_SCOPE_PATH2) };
633
+ }
634
+ function resolveScope2(input) {
635
+ if (input === void 0 || input === "user") {
636
+ return "user";
637
+ }
638
+ if (input === "project") {
639
+ return "project";
640
+ }
641
+ console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
642
+ process.exit(1);
643
+ }
644
+ 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.";
645
+ function registerCodexCommands(program2) {
646
+ const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
647
+ codex.command("install").description("Register the prim hooks in Codex's ~/.codex/hooks.json").option(
648
+ "--scope <scope>",
649
+ "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
650
+ ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
651
+ const scope = resolveScope2(opts.scope);
652
+ const result = performInstall2(scope, opts.force ?? false);
653
+ if (result.changed) {
654
+ console.error(`[prim] Codex integration installed (${scope} scope) at ${result.path}`);
655
+ } else {
656
+ console.error(`[prim] Codex integration already present at ${result.path} (no changes)`);
657
+ }
658
+ console.error(TRUST_NOTICE);
659
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
660
+ });
661
+ codex.command("uninstall").description("Remove all prim hooks from ~/.codex/hooks.json").option(
662
+ "--scope <scope>",
663
+ "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
664
+ ).action((opts) => {
665
+ const scope = resolveScope2(opts.scope);
666
+ const result = performUninstall2(scope);
667
+ if (result.changed) {
668
+ console.error(`[prim] prim hooks removed from ${result.path}`);
669
+ } else {
670
+ console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
671
+ }
672
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
673
+ });
674
+ codex.command("status").description("Report whether each prim surface (gate, capture) is installed per scope").action(() => {
675
+ const result = performStatus2();
676
+ const mark = (b) => b ? "\u2713" : "\u2717";
677
+ const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
678
+ console.error(`${line("user", result.user)}
679
+ ${line("project", result.project)}`);
680
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
681
+ });
682
+ }
683
+
535
684
  // src/commands/context.ts
536
685
  import { readFileSync as readFileSync3 } from "fs";
537
686
  function registerContextCommands(program2) {
@@ -652,11 +801,11 @@ ${contexts.length} context(s)`);
652
801
  // src/commands/daemon.ts
653
802
  import { spawn } from "child_process";
654
803
  import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
655
- import { homedir as homedir2 } from "os";
656
- import { join as join2 } from "path";
804
+ import { homedir as homedir3 } from "os";
805
+ import { join as join3 } from "path";
657
806
  var DAEMON_BIN = "prim-daemon-server";
658
- var PID_PATH = join2(homedir2(), ".config", "prim", "daemon.pid");
659
- var SOCK_PATH = join2(homedir2(), ".config", "prim", "sock");
807
+ var PID_PATH = join3(homedir3(), ".config", "prim", "daemon.pid");
808
+ var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
660
809
  var STOP_TIMEOUT_MS = 5e3;
661
810
  var STOP_POLL_MS = 100;
662
811
  var STATUS_PROBE_TIMEOUT_MS = 500;
@@ -1036,9 +1185,12 @@ async function fetchCascade(idOrShortId, deps = defaultDeps) {
1036
1185
  const params = new URLSearchParams({ id: idOrShortId });
1037
1186
  const client = deps.getClient();
1038
1187
  try {
1039
- return await client.get(`/api/cli/decisions/cascade?${params.toString()}`, {
1040
- signal: AbortSignal.timeout(CASCADE_TIMEOUT_MS)
1041
- });
1188
+ return await daemonOrDirectGet(
1189
+ "decisions_cascade",
1190
+ `/api/cli/decisions/cascade?${params.toString()}`,
1191
+ client,
1192
+ CASCADE_TIMEOUT_MS
1193
+ );
1042
1194
  } catch (err) {
1043
1195
  if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
1044
1196
  throw new CascadeNotFoundError(idOrShortId);
@@ -1063,9 +1215,12 @@ async function fetchRecent(args, deps = defaultDeps2) {
1063
1215
  }
1064
1216
  const client = deps.getClient();
1065
1217
  try {
1066
- const res = await client.get(`/api/cli/decisions/recent?${params.toString()}`, {
1067
- signal: AbortSignal.timeout(RECENT_TIMEOUT_MS)
1068
- });
1218
+ const res = await daemonOrDirectGet(
1219
+ "decisions_recent",
1220
+ `/api/cli/decisions/recent?${params.toString()}`,
1221
+ client,
1222
+ RECENT_TIMEOUT_MS
1223
+ );
1069
1224
  const result = { decisions: res.decisions };
1070
1225
  if (res.unavailable !== void 0) {
1071
1226
  result.unavailable = res.unavailable;
@@ -1092,6 +1247,8 @@ function authorLabel(row) {
1092
1247
  switch (row.producerKind) {
1093
1248
  case "claude_code":
1094
1249
  return "Your Claude Code";
1250
+ case "codex":
1251
+ return "Your Codex";
1095
1252
  case "chat":
1096
1253
  return "Your chat";
1097
1254
  case "spec_edit":
@@ -1232,9 +1389,12 @@ async function fetchShow(idOrShortId, deps = defaultDeps4) {
1232
1389
  const params = new URLSearchParams({ id: idOrShortId });
1233
1390
  const client = deps.getClient();
1234
1391
  try {
1235
- return await client.get(`/api/cli/decisions/show?${params.toString()}`, {
1236
- signal: AbortSignal.timeout(SHOW_TIMEOUT_MS)
1237
- });
1392
+ return await daemonOrDirectGet(
1393
+ "decisions_show",
1394
+ `/api/cli/decisions/show?${params.toString()}`,
1395
+ client,
1396
+ SHOW_TIMEOUT_MS
1397
+ );
1238
1398
  } catch (err) {
1239
1399
  if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
1240
1400
  throw new DecisionNotFoundError(idOrShortId);
@@ -1417,30 +1577,39 @@ import { execSync } from "child_process";
1417
1577
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1418
1578
  import { resolve } from "path";
1419
1579
  import { Option } from "commander";
1420
- var HOOK_SCRIPT = `#!/bin/sh
1421
- # prim pre-commit hook \u2014 auto-syncs affected specs on commit
1422
- # Installed by: prim hooks install
1423
-
1424
- # Find the nearest node_modules/.bin with prim, or use npx
1425
- if command -v prim-pre-commit >/dev/null 2>&1; then
1426
- prim-pre-commit
1427
- elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
1428
- ./node_modules/.bin/prim-pre-commit
1580
+ var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
1581
+ var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
1582
+ var HOOKS = [PRE_COMMIT, POST_COMMIT];
1583
+ function blockMarkers(spec) {
1584
+ return {
1585
+ start: `# >>> prim ${spec.hookName} hook >>>`,
1586
+ end: `# <<< prim ${spec.hookName} hook <<<`
1587
+ };
1588
+ }
1589
+ var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
1590
+ var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
1591
+ function hookShim(binName) {
1592
+ return `if command -v ${binName} >/dev/null 2>&1; then
1593
+ ${binName}
1594
+ elif [ -f "./node_modules/.bin/${binName}" ]; then
1595
+ ./node_modules/.bin/${binName}
1429
1596
  else
1430
- npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
1431
- fi
1597
+ npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
1598
+ fi`;
1599
+ }
1600
+ function dotGitScript(spec) {
1601
+ return `#!/bin/sh
1602
+ # prim ${spec.hookName} hook \u2014 installed by: prim hooks install
1603
+
1604
+ ${hookShim(spec.binName)}
1432
1605
  `;
1433
- var PRIM_BLOCK_START = "# >>> prim pre-commit hook >>>";
1434
- var PRIM_BLOCK_END = "# <<< prim pre-commit hook <<<";
1435
- var PRIM_HUSKY_BLOCK = `${PRIM_BLOCK_START}
1436
- if command -v prim-pre-commit >/dev/null 2>&1; then
1437
- prim-pre-commit
1438
- elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
1439
- ./node_modules/.bin/prim-pre-commit
1440
- else
1441
- npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
1442
- fi
1443
- ${PRIM_BLOCK_END}`;
1606
+ }
1607
+ function huskyBlock(spec) {
1608
+ const { start, end } = blockMarkers(spec);
1609
+ return `${start}
1610
+ ${hookShim(spec.binName)}
1611
+ ${end}`;
1612
+ }
1444
1613
  function getGitRoot() {
1445
1614
  return execSync("git rev-parse --show-toplevel", {
1446
1615
  encoding: "utf-8"
@@ -1464,8 +1633,8 @@ function detectHusky(gitRoot) {
1464
1633
  }
1465
1634
  return false;
1466
1635
  }
1467
- function containsPrimHook(content) {
1468
- return content.includes("prim-pre-commit");
1636
+ function containsPrimHook(content, binName = PRE_COMMIT.binName) {
1637
+ return content.includes(binName);
1469
1638
  }
1470
1639
  async function askConfirmation(question) {
1471
1640
  if (!process.stdin.isTTY) return false;
@@ -1479,52 +1648,63 @@ async function askConfirmation(question) {
1479
1648
  rl.close();
1480
1649
  }
1481
1650
  }
1482
- function installToHusky(gitRoot) {
1483
- const hookPath = resolve(gitRoot, ".husky", "pre-commit");
1651
+ function installToHusky(gitRoot, spec = PRE_COMMIT) {
1652
+ const hookPath = resolve(gitRoot, ".husky", spec.hookName);
1484
1653
  if (existsSync4(hookPath)) {
1485
1654
  const existing = readFileSync5(hookPath, "utf-8");
1486
- if (containsPrimHook(existing)) {
1487
- console.log("Prim pre-commit hook is already installed in .husky/pre-commit.");
1655
+ if (containsPrimHook(existing, spec.binName)) {
1656
+ console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
1488
1657
  return;
1489
1658
  }
1490
1659
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
1491
- writeFileSync3(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
1660
+ writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
1492
1661
  `, {
1493
1662
  mode: 493
1494
1663
  });
1495
- console.log("Appended prim hook block to .husky/pre-commit.");
1664
+ console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
1496
1665
  } else {
1497
1666
  writeFileSync3(hookPath, `#!/bin/sh
1498
1667
 
1499
- ${PRIM_HUSKY_BLOCK}
1668
+ ${huskyBlock(spec)}
1500
1669
  `, {
1501
1670
  mode: 493
1502
1671
  });
1503
- console.log("Created .husky/pre-commit with prim hook block.");
1672
+ console.log(`Created .husky/${spec.hookName} with prim hook block.`);
1504
1673
  }
1505
1674
  }
1506
- function installToDotGit(gitRoot) {
1675
+ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1507
1676
  const hooksDir = resolve(gitRoot, ".git", "hooks");
1508
- const hookPath = resolve(hooksDir, "pre-commit");
1677
+ const hookPath = resolve(hooksDir, spec.hookName);
1509
1678
  if (!existsSync4(hooksDir)) {
1510
1679
  mkdirSync3(hooksDir, { recursive: true });
1511
1680
  }
1512
1681
  if (existsSync4(hookPath)) {
1513
1682
  const existing = readFileSync5(hookPath, "utf-8");
1514
- if (containsPrimHook(existing)) {
1515
- console.log("Prim pre-commit hook is already installed at .git/hooks/pre-commit.");
1683
+ if (containsPrimHook(existing, spec.binName)) {
1684
+ console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
1516
1685
  return;
1517
1686
  }
1518
- console.log(`A pre-commit hook already exists at ${hookPath}.`);
1687
+ console.log(`A ${spec.hookName} hook already exists at ${hookPath}.`);
1519
1688
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
1520
1689
  return;
1521
1690
  }
1522
- writeFileSync3(hookPath, HOOK_SCRIPT, { mode: 493 });
1523
- console.log(`Installed pre-commit hook at ${hookPath}`);
1691
+ writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
1692
+ console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
1693
+ }
1694
+ function installHooks(gitRoot, target) {
1695
+ for (const spec of HOOKS) {
1696
+ if (target === "husky") {
1697
+ installToHusky(gitRoot, spec);
1698
+ } else {
1699
+ installToDotGit(gitRoot, spec);
1700
+ }
1701
+ }
1524
1702
  }
1525
1703
  function registerHooksCommands(program2) {
1526
1704
  const hooks = program2.command("hooks").description("Manage git hooks");
1527
- hooks.command("install").description("Install the prim pre-commit hook (auto-detects Husky; use --target to override)").addOption(
1705
+ hooks.command("install").description(
1706
+ "Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
1707
+ ).addOption(
1528
1708
  new Option("--target <where>", "install destination; bypasses Husky detection").choices([
1529
1709
  "husky",
1530
1710
  "git-hooks"
@@ -1535,10 +1715,10 @@ function registerHooksCommands(program2) {
1535
1715
  globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
1536
1716
  );
1537
1717
  const gitRoot = getGitRoot();
1538
- if (opts.target === "husky") return installToHusky(gitRoot);
1539
- if (opts.target === "git-hooks") return installToDotGit(gitRoot);
1718
+ if (opts.target === "husky") return installHooks(gitRoot, "husky");
1719
+ if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
1540
1720
  if (detectHusky(gitRoot)) {
1541
- if (globals.yes) return installToHusky(gitRoot);
1721
+ if (globals.yes) return installHooks(gitRoot, "husky");
1542
1722
  if (nonInteractive) {
1543
1723
  throw new Error(
1544
1724
  "--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
@@ -1549,30 +1729,36 @@ function registerHooksCommands(program2) {
1549
1729
  "Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
1550
1730
  );
1551
1731
  } else if (await askConfirmation(
1552
- "Husky detected. Install prim hook into .husky/pre-commit instead of .git/hooks/pre-commit?"
1732
+ "Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
1553
1733
  )) {
1554
- return installToHusky(gitRoot);
1734
+ return installHooks(gitRoot, "husky");
1555
1735
  } else {
1556
- console.log("Falling back to .git/hooks/pre-commit install.");
1736
+ console.log("Falling back to .git/hooks install.");
1557
1737
  }
1558
1738
  }
1559
- installToDotGit(gitRoot);
1739
+ installHooks(gitRoot, "git-hooks");
1560
1740
  });
1561
- hooks.command("uninstall").description("Remove the prim pre-commit hook").action(() => {
1741
+ hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
1562
1742
  const gitRoot = getGitRoot();
1563
- const hookPath = resolve(gitRoot, ".git", "hooks", "pre-commit");
1564
- if (!existsSync4(hookPath)) {
1565
- console.log("No pre-commit hook found.");
1566
- return;
1743
+ for (const spec of HOOKS) {
1744
+ const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
1745
+ if (!existsSync4(hookPath)) {
1746
+ console.log(`No ${spec.hookName} hook found.`);
1747
+ continue;
1748
+ }
1749
+ if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
1750
+ unlinkSync2(hookPath);
1751
+ console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
1752
+ } else {
1753
+ console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
1754
+ }
1567
1755
  }
1568
- unlinkSync2(hookPath);
1569
- console.log(`Removed pre-commit hook at ${hookPath}`);
1570
1756
  });
1571
1757
  }
1572
1758
 
1573
1759
  // src/commands/moves.ts
1574
1760
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1575
- import { join as join3 } from "path";
1761
+ import { join as join4 } from "path";
1576
1762
 
1577
1763
  // src/flusher.ts
1578
1764
  import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
@@ -1681,18 +1867,18 @@ function registerMovesCommands(program2) {
1681
1867
  }
1682
1868
  });
1683
1869
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
1684
- const dir = join3(process.cwd(), ".prim");
1870
+ const dir = join4(process.cwd(), ".prim");
1685
1871
  if (!existsSync5(dir)) {
1686
1872
  mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
1687
1873
  }
1688
- const file = join3(process.cwd(), WORKSPACE_FILE);
1874
+ const file = join4(process.cwd(), WORKSPACE_FILE);
1689
1875
  writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
1690
1876
  mode: FILE_MODE2
1691
1877
  });
1692
1878
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
1693
1879
  });
1694
1880
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
1695
- const file = join3(process.cwd(), WORKSPACE_FILE);
1881
+ const file = join4(process.cwd(), WORKSPACE_FILE);
1696
1882
  if (!existsSync5(file)) {
1697
1883
  console.log("[prim] no workspace binding in cwd");
1698
1884
  return;
@@ -1815,7 +2001,7 @@ import {
1815
2001
  unlinkSync as unlinkSync5,
1816
2002
  writeFileSync as writeFileSync5
1817
2003
  } from "fs";
1818
- import { join as join4 } from "path";
2004
+ import { join as join5 } from "path";
1819
2005
  var DIR_MODE2 = 448;
1820
2006
  var FILE_MODE3 = 384;
1821
2007
  function ensureDir() {
@@ -1824,7 +2010,7 @@ function ensureDir() {
1824
2010
  }
1825
2011
  }
1826
2012
  function markerPath(sessionId) {
1827
- return join4(SESSIONS_DIR, `${sessionId}.json`);
2013
+ return join5(SESSIONS_DIR, `${sessionId}.json`);
1828
2014
  }
1829
2015
  function registerSessionCommands(program2) {
1830
2016
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -1852,7 +2038,7 @@ function registerSessionCommands(program2) {
1852
2038
  for (const f of files) {
1853
2039
  const sessionId = f.replace(/\.json$/, "");
1854
2040
  try {
1855
- const m = JSON.parse(readFileSync6(join4(SESSIONS_DIR, f), "utf-8"));
2041
+ const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
1856
2042
  console.log(`${sessionId} org=${m.orgId}`);
1857
2043
  } catch {
1858
2044
  }
@@ -1887,6 +2073,7 @@ var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
1887
2073
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
1888
2074
  var TARGET_CANDIDATES = [
1889
2075
  "CLAUDE.md",
2076
+ "AGENTS.md",
1890
2077
  ".cursor/rules",
1891
2078
  ".windsurfrules",
1892
2079
  ".github/instructions/primitive.md"
@@ -2366,6 +2553,7 @@ registerMovesCommands(program);
2366
2553
  registerSessionCommands(program);
2367
2554
  registerDecisionsCommands(program);
2368
2555
  registerClaudeCommands(program);
2556
+ registerCodexCommands(program);
2369
2557
  registerDaemonCommands(program);
2370
2558
  registerReconcileCommands(program);
2371
2559
  registerStatuslineCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
4
4
  "description": "CLI for managing Primitive specs, contexts, and git hooks",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  "bin": {
31
31
  "prim": "dist/index.js",
32
32
  "prim-pre-commit": "dist/hooks/pre-commit.js",
33
+ "prim-post-commit": "dist/hooks/post-commit.js",
33
34
  "prim-hook": "dist/hooks/prim-hook.js",
34
35
  "prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
35
36
  "prim-post-tool-use": "dist/hooks/post-tool-use.js",
@@ -45,9 +46,9 @@
45
46
  "SKILL.md"
46
47
  ],
47
48
  "scripts": {
48
- "build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
49
- "postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
50
- "dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
49
+ "build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
50
+ "postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/post-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
51
+ "dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
51
52
  "clean": "rm -rf dist coverage",
52
53
  "lint": "biome check src/",
53
54
  "format": "biome check --fix src/",