@primitive.ai/prim 0.1.0-alpha.55 → 0.1.0-alpha.57

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.
@@ -0,0 +1,424 @@
1
+ import {
2
+ DEFAULT_RULES,
3
+ scrub
4
+ } from "./chunk-GUXBUBQ7.js";
5
+ import {
6
+ canonicalGitRoot,
7
+ canonicalRepositoryPath
8
+ } from "./chunk-LDQZASIF.js";
9
+
10
+ // src/hooks/pre-tool-use-scoring.ts
11
+ import { isAbsolute, relative, sep } from "path";
12
+ function anyUnverified(results) {
13
+ return results.some((r) => r.verdict === "unavailable" || r.truncated);
14
+ }
15
+ function unverifiedNote(results) {
16
+ const causes = [];
17
+ const unavailable = results.find((r) => r.verdict === "unavailable");
18
+ if (unavailable) {
19
+ causes.push(
20
+ unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
21
+ );
22
+ }
23
+ if (results.some((r) => r.truncated)) {
24
+ causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
25
+ }
26
+ return causes.map((c) => `[primitive] ${c}`).join("\n");
27
+ }
28
+ function buildHookOutput(aggregate, results, agent = "claude_code") {
29
+ if (aggregate === "deny") {
30
+ 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)";
31
+ return {
32
+ hookSpecificOutput: {
33
+ hookEventName: "PreToolUse",
34
+ permissionDecision: "deny",
35
+ permissionDecisionReason: reason
36
+ }
37
+ };
38
+ }
39
+ if (agent === "codex" && aggregate === "ask") {
40
+ const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
41
+ const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
42
+ const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
43
+ const out = {
44
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
45
+ };
46
+ if (merged.length > 0) {
47
+ out.hookSpecificOutput.additionalContext = merged;
48
+ }
49
+ return out;
50
+ }
51
+ if (aggregate === "ask") {
52
+ 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";
53
+ const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
54
+ const out = {
55
+ hookSpecificOutput: {
56
+ hookEventName: "PreToolUse",
57
+ permissionDecision: "ask",
58
+ permissionDecisionReason: reason
59
+ }
60
+ };
61
+ if (additionalContext.length > 0) {
62
+ out.hookSpecificOutput.additionalContext = additionalContext;
63
+ }
64
+ return out;
65
+ }
66
+ const notes = [
67
+ ...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
68
+ anyUnverified(results) ? unverifiedNote(results) : ""
69
+ ].filter((s) => s.length > 0).join("\n");
70
+ if (notes.length > 0) {
71
+ return {
72
+ hookSpecificOutput: {
73
+ hookEventName: "PreToolUse",
74
+ permissionDecision: "allow",
75
+ additionalContext: notes
76
+ }
77
+ };
78
+ }
79
+ return {
80
+ hookSpecificOutput: {
81
+ hookEventName: "PreToolUse",
82
+ permissionDecision: "allow"
83
+ }
84
+ };
85
+ }
86
+ function failOpenOutput() {
87
+ return {
88
+ hookSpecificOutput: {
89
+ hookEventName: "PreToolUse",
90
+ permissionDecision: "allow"
91
+ }
92
+ };
93
+ }
94
+ function buildHermesOutput(aggregate, results) {
95
+ if (aggregate !== "deny" && aggregate !== "ask") {
96
+ return {};
97
+ }
98
+ const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
99
+ const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
100
+ const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
101
+ return { action: "block", message };
102
+ }
103
+ function failOpenHermes() {
104
+ return {};
105
+ }
106
+ var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
107
+ var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
108
+ var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
109
+ var MOVE_TO_RE = /^\*\*\* Move to: (?<dst>.+)$/;
110
+ var LINE_SPLIT_RE = /\r?\n/;
111
+ function parseApplyPatchTargets(command) {
112
+ const paths = /* @__PURE__ */ new Set();
113
+ let activeUpdate = false;
114
+ let complete = true;
115
+ for (const line of command.split(LINE_SPLIT_RE)) {
116
+ const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path;
117
+ if (path) {
118
+ paths.add(path);
119
+ activeUpdate = line.startsWith("*** Update File:");
120
+ continue;
121
+ }
122
+ const moveTo = MOVE_TO_RE.exec(line)?.groups?.dst;
123
+ if (moveTo) {
124
+ if (activeUpdate) paths.add(moveTo);
125
+ else complete = false;
126
+ activeUpdate = false;
127
+ continue;
128
+ }
129
+ const move = MOVE_FILE_RE.exec(line)?.groups;
130
+ if (move?.src) {
131
+ paths.add(move.src);
132
+ if (move.dst) {
133
+ paths.add(move.dst);
134
+ }
135
+ activeUpdate = false;
136
+ continue;
137
+ }
138
+ if (line === "*** Begin Patch" || line === "*** End Patch") activeUpdate = false;
139
+ else if (line.startsWith("*** ")) complete = false;
140
+ }
141
+ return { paths: [...paths], complete: complete && paths.size > 0 };
142
+ }
143
+ function extractCodexFileTargets(toolName, toolInput) {
144
+ if (toolName !== "apply_patch") {
145
+ return null;
146
+ }
147
+ if (!toolInput || typeof toolInput !== "object") {
148
+ return { paths: [], complete: false };
149
+ }
150
+ const command = toolInput.command;
151
+ return typeof command === "string" ? parseApplyPatchTargets(command) : { paths: [], complete: false };
152
+ }
153
+ var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
154
+ function extractHermesFileTargets(toolName, toolInput) {
155
+ if (!HERMES_EDITING_TOOLS.has(toolName)) {
156
+ return null;
157
+ }
158
+ if (!toolInput || typeof toolInput !== "object") {
159
+ return { paths: [], complete: false };
160
+ }
161
+ const input = toolInput;
162
+ if (toolName === "patch" && input.mode === "patch") {
163
+ return typeof input.patch === "string" ? parseApplyPatchTargets(input.patch) : { paths: [], complete: false };
164
+ }
165
+ const paths = typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
166
+ const mode = input.mode;
167
+ return { paths, complete: paths.length > 0 && (mode === void 0 || mode === "replace") };
168
+ }
169
+ function extractFileTargets(toolName, toolInput, agent = "claude_code") {
170
+ if (agent === "codex") {
171
+ return extractCodexFileTargets(toolName, toolInput);
172
+ }
173
+ if (agent === "hermes") {
174
+ return extractHermesFileTargets(toolName, toolInput);
175
+ }
176
+ if (!SUPPORTED_TOOLS.has(toolName)) {
177
+ return null;
178
+ }
179
+ if (!toolInput || typeof toolInput !== "object") {
180
+ return { paths: [], complete: false };
181
+ }
182
+ const input = toolInput;
183
+ if (toolName === "NotebookEdit") {
184
+ const paths2 = typeof input.notebook_path === "string" && input.notebook_path.length > 0 ? [input.notebook_path] : [];
185
+ return { paths: paths2, complete: paths2.length > 0 };
186
+ }
187
+ const paths = typeof input.file_path === "string" && input.file_path.length > 0 ? [input.file_path] : [];
188
+ return { paths, complete: paths.length > 0 };
189
+ }
190
+ function readHookMode(env) {
191
+ if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
192
+ return "off";
193
+ }
194
+ const mode = env.PRIM_HOOK_MODE;
195
+ if (mode === "off" || mode === "warn") {
196
+ return mode;
197
+ }
198
+ return "block";
199
+ }
200
+ function demoteForMode(verdict, mode) {
201
+ if (mode === "off") {
202
+ return "allow";
203
+ }
204
+ if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
205
+ return "warn";
206
+ }
207
+ return verdict;
208
+ }
209
+
210
+ // src/hooks/preflight-v3.ts
211
+ import { Buffer } from "buffer";
212
+
213
+ // src/hooks/shell-targets.ts
214
+ import { parse } from "unbash";
215
+ var OUTPUT_REDIRECTS = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>"]);
216
+ var READ_ONLY_COMMANDS = new Set(": cat echo false printf pwd test true [".split(" "));
217
+ var SINKS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
218
+ var STATIC_PARTS = /* @__PURE__ */ new Set(["Literal", "SingleQuoted", "AnsiCQuoted"]);
219
+ function staticPart(part) {
220
+ return STATIC_PARTS.has(part.type) || part.type === "DoubleQuoted" && part.parts.every(staticPart);
221
+ }
222
+ function staticWord(word) {
223
+ if (!word || !word.value || word.value.includes("\0") || word.value.includes("\n")) return;
224
+ if (word.parts ? !word.parts.every(staticPart) : word.value.startsWith("~") || /[*?\[$`]/.test(word.text))
225
+ return;
226
+ return word.value;
227
+ }
228
+ function unknown(state) {
229
+ state.complete = false;
230
+ state.mutation = true;
231
+ }
232
+ function recordTarget(word, state) {
233
+ const path = staticWord(word);
234
+ if (path === "-" || path && (SINKS.has(path) || path.startsWith("/dev/fd/"))) return;
235
+ state.mutation = true;
236
+ if (path) state.paths.add(path);
237
+ else state.complete = false;
238
+ }
239
+ function visitRedirect(redirect, state) {
240
+ const target = staticWord(redirect.target);
241
+ if (redirect.operator === ">&" && (target === "-" || /^\d+$/.test(target ?? ""))) return;
242
+ if (OUTPUT_REDIRECTS.has(redirect.operator) || redirect.operator === ">&")
243
+ recordTarget(redirect.target, state);
244
+ else unknown(state);
245
+ }
246
+ function operands(words, short = "", long = /* @__PURE__ */ new Set()) {
247
+ const result = [];
248
+ let optionsEnded = false;
249
+ for (const word of words) {
250
+ const value = staticWord(word);
251
+ if (value === void 0) return;
252
+ if (optionsEnded || value === "-" || !value.startsWith("-")) result.push(value);
253
+ else if (value === "--") optionsEnded = true;
254
+ else if (value.startsWith("--") ? !long.has(value) : [...value.slice(1)].some((c) => !short.includes(c)))
255
+ return;
256
+ }
257
+ return result;
258
+ }
259
+ function visitCommand(command, state) {
260
+ for (const redirect of command.redirects) visitRedirect(redirect, state);
261
+ const name = staticWord(command.name);
262
+ if (!name || name.includes("/") || command.suffix.some((word) => !staticWord(word))) {
263
+ unknown(state);
264
+ } else if (!READ_ONLY_COMMANDS.has(name)) {
265
+ state.mutation = true;
266
+ let targets;
267
+ if (name === "tee") {
268
+ targets = operands(command.suffix, "ai", /* @__PURE__ */ new Set(["--append", "--ignore-interrupts"]));
269
+ } else if (name === "touch") {
270
+ targets = operands(command.suffix);
271
+ } else if (name === "rm") {
272
+ targets = operands(
273
+ command.suffix,
274
+ "fivI",
275
+ /* @__PURE__ */ new Set(["--force", "--interactive", "--interactive=once", "--verbose"])
276
+ );
277
+ } else if (name === "cp" || name === "mv") {
278
+ const pair = operands(command.suffix);
279
+ if (pair?.length === 2) targets = name === "cp" ? [pair[1]] : pair;
280
+ }
281
+ if (!targets?.length) state.complete = false;
282
+ else for (const target of targets) state.paths.add(target);
283
+ }
284
+ visitNested([command.name, command.prefix, command.suffix, command.redirects], state);
285
+ }
286
+ var CONTAINERS = /* @__PURE__ */ new Set(["Script", "Pipeline", "AndOr", "CompoundList"]);
287
+ function visitNested(value, state) {
288
+ if (Array.isArray(value)) {
289
+ for (const child of value) visitNested(child, state);
290
+ } else if (value && typeof value === "object") {
291
+ const child = value;
292
+ if (child.type === "Command") {
293
+ visitCommand(child, state);
294
+ } else if (child.type === "Statement") {
295
+ visitStatement(child, state);
296
+ } else if ("parts" in child) {
297
+ for (const part of value.parts ?? []) visitNested(part, state);
298
+ } else {
299
+ const node = typeof child.type === "string" && "pos" in child && "end" in child;
300
+ if (node && !CONTAINERS.has(child.type)) unknown(state);
301
+ if (child.type === "Function") return;
302
+ for (const nested of Object.values(child)) visitNested(nested, state);
303
+ }
304
+ }
305
+ }
306
+ function visitStatement(statement, state) {
307
+ if (statement.background) unknown(state);
308
+ for (const redirect of statement.redirects) visitRedirect(redirect, state);
309
+ visitNested([statement.redirects, statement.command], state);
310
+ }
311
+ function analyzeShellTargets(source) {
312
+ if (!source.trim())
313
+ return { paths: [], coverage: "complete", mutation: "none" };
314
+ try {
315
+ const script = parse(source);
316
+ const state = { paths: /* @__PURE__ */ new Set(), complete: !script.errors?.length, mutation: false };
317
+ for (const statement of script.commands) visitStatement(statement, state);
318
+ if (script.errors?.length) state.mutation = true;
319
+ return {
320
+ paths: [...state.paths],
321
+ coverage: state.complete ? "complete" : "unverified",
322
+ mutation: state.mutation ? "present" : "none"
323
+ };
324
+ } catch {
325
+ return { paths: [], coverage: "unverified", mutation: "present" };
326
+ }
327
+ }
328
+
329
+ // src/hooks/preflight-v3.ts
330
+ var PREFLIGHT_PROTOCOL_VERSION = 3;
331
+ var MAX_PREFLIGHT_PATHS = 32;
332
+ var MAX_PROPOSAL_BYTES = 6144;
333
+ function resolvePreflightTargets(args) {
334
+ let rawPaths;
335
+ let coverage = "complete";
336
+ let mutation = "present";
337
+ if (args.agent === "claude_code" && args.toolName === "Bash") {
338
+ const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
339
+ if (typeof command !== "string") {
340
+ return { paths: [], coverage: "unverified", mutation: "present" };
341
+ }
342
+ const shell = analyzeShellTargets(command);
343
+ rawPaths = shell.paths;
344
+ coverage = shell.coverage;
345
+ mutation = shell.mutation;
346
+ } else {
347
+ const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
348
+ if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
349
+ rawPaths = extracted.paths;
350
+ if (!extracted.complete) coverage = "unverified";
351
+ }
352
+ const root = canonicalGitRoot(args.cwd);
353
+ const paths = /* @__PURE__ */ new Set();
354
+ for (const rawPath of rawPaths) {
355
+ const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
356
+ if (canonical) paths.add(canonical);
357
+ else coverage = "unverified";
358
+ }
359
+ const bounded = [...paths];
360
+ if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
361
+ return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
362
+ }
363
+ function truncateUtf8(value, maxBytes) {
364
+ const encoded = Buffer.from(value);
365
+ if (encoded.byteLength <= maxBytes) return value;
366
+ return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
367
+ }
368
+ function proposalFor(toolInput) {
369
+ const redacted = scrub(toolInput, DEFAULT_RULES);
370
+ return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
371
+ }
372
+ function parsePreflightResponse(value) {
373
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
374
+ const response = value;
375
+ const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
376
+ if (response.protocolVersion !== PREFLIGHT_PROTOCOL_VERSION || typeof response.verdict !== "string" || !verdicts.has(response.verdict) || typeof response.reasonCode !== "string" || typeof response.message !== "string" || !Array.isArray(response.conflicts) || !Array.isArray(response.bypassed)) {
377
+ return null;
378
+ }
379
+ return response;
380
+ }
381
+ function resultForPreflight(response) {
382
+ const verdict = response.verdict === "block" ? "deny" : response.verdict;
383
+ const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
384
+ const directives = response.conflicts.flatMap((value) => {
385
+ if (!value || typeof value !== "object") return [];
386
+ const { decisionId, shortId } = value;
387
+ const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
388
+ return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
389
+ });
390
+ const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
391
+ return {
392
+ verdict,
393
+ conflicts: response.conflicts,
394
+ reason,
395
+ additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
396
+ truncated: false,
397
+ unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
398
+ };
399
+ }
400
+ function unverifiedResult(message) {
401
+ return {
402
+ verdict: "unavailable",
403
+ conflicts: [],
404
+ reason: "",
405
+ additionalContext: "",
406
+ truncated: false,
407
+ unavailable: message
408
+ };
409
+ }
410
+
411
+ export {
412
+ buildHookOutput,
413
+ failOpenOutput,
414
+ buildHermesOutput,
415
+ failOpenHermes,
416
+ readHookMode,
417
+ demoteForMode,
418
+ PREFLIGHT_PROTOCOL_VERSION,
419
+ resolvePreflightTargets,
420
+ proposalFor,
421
+ parsePreflightResponse,
422
+ resultForPreflight,
423
+ unverifiedResult
424
+ };
@@ -4,20 +4,12 @@ import { platform } from "os";
4
4
 
5
5
  // src/protocol/move.ts
6
6
  var LEGACY_ENVELOPE_VERSION = 1;
7
- var AGENT_ENVELOPE_VERSION = 2;
7
+ var AGENT_ENVELOPE_VERSION = 3;
8
8
  var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
9
- function withAgentProvenance(move, producer, workspaceId) {
10
- return {
11
- ...move,
12
- env: { ...move.env, workspaceId },
13
- envelopeVersion: AGENT_ENVELOPE_VERSION,
14
- producer
15
- };
16
- }
17
9
 
18
10
  // src/hooks/prim-hook-core.ts
19
- function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
20
- const move = {
11
+ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, invocationId) {
12
+ return {
21
13
  moveId: randomUUID(),
22
14
  capturedAt: Date.now(),
23
15
  sessionId: parsed.session_id ?? "",
@@ -26,15 +18,13 @@ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
26
18
  env: {
27
19
  cwd: parsed.cwd ?? process.cwd(),
28
20
  cliVersion,
29
- osPlatform: platform()
21
+ osPlatform: platform(),
22
+ ...workspaceId ? { workspaceId } : {}
30
23
  },
31
- envelopeVersion: ENVELOPE_VERSION,
32
- // Stamp the producer for non-Claude agents (codex, hermes); Claude Code
33
- // moves omit it (the backend defaults an absent value to "claude_code"),
34
- // keeping the Claude wire shape byte-identical.
35
- ...agent === "claude_code" ? {} : { producer: agent }
24
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
25
+ producer: agent,
26
+ ...invocationId ? { invocationId } : {}
36
27
  };
37
- return workspaceId ? withAgentProvenance(move, agent, workspaceId) : move;
38
28
  }
39
29
  function toCommitMove(commit, cliVersion, cwd) {
40
30
  return {
@@ -10,8 +10,6 @@ import { fileURLToPath } from "url";
10
10
  var PKG_NAME = "@primitive.ai/prim";
11
11
  var ROOT_WALK_LIMIT = 6;
12
12
  var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
13
- var BIN_CACHE_DIR_SH = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
14
- var BIN_CACHE_TTL_MIN_DEFAULT = 1440;
15
13
  var resolvedRoot;
16
14
  function locateRoot() {
17
15
  if (resolvedRoot !== void 0) {
@@ -24,7 +22,7 @@ function locateRoot() {
24
22
  try {
25
23
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
26
24
  if (manifest.name === PKG_NAME && manifest.bin) {
27
- resolvedRoot = { dir, bin: manifest.bin };
25
+ resolvedRoot = { dir, version: manifest.version, bin: manifest.bin };
28
26
  return resolvedRoot;
29
27
  }
30
28
  } catch {
@@ -47,18 +45,23 @@ function binFile(bin) {
47
45
  }
48
46
  return isAbsolute(rel) ? rel : join(root.dir, rel);
49
47
  }
50
- function hookShimCommand(bin, args = "", opts = {}) {
51
- const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
52
- const ladder = `if command -v ${bin} >/dev/null 2>&1; then ${invoke(bin)}; elif [ -f "./node_modules/.bin/${bin}" ]; then ${invoke(`./node_modules/.bin/${bin}`)}; else ${invoke(`${NPX_FALLBACK} ${bin}`)}; fi`;
53
- if (opts.cacheRead === false) {
54
- return ladder;
55
- }
56
- const execArgs = args ? ` ${args}` : "";
57
- const cacheBranch = `d="${BIN_CACHE_DIR_SH}"; if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$d/${bin}" ] && [ -f "$d/node" ] && [ -n "$(find "$d/${bin}" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${BIN_CACHE_TTL_MIN_DEFAULT}}" 2>/dev/null)" ]; then n=$(cat "$d/node"); p=$(cat "$d/${bin}"); if [ -x "$n" ] && [ -f "$p" ]; then export PRIM_BIN_CACHE_HIT=1; exec "$n" "$p"${execArgs}; fi; fi; `;
58
- return cacheBranch + ladder;
48
+ function packageVersion() {
49
+ return locateRoot()?.version ?? null;
50
+ }
51
+ function shellQuote(value) {
52
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
53
+ }
54
+ function pinnedHookCommand(bin, args = "") {
55
+ const version = packageVersion();
56
+ if (!version) throw new Error("cannot determine Primitive package version");
57
+ const suffix = args ? ` ${args}` : "";
58
+ const fallback = `npx --yes -p ${PKG_NAME}@${version} ${bin}${suffix}`;
59
+ const file = binFile(bin);
60
+ if (!file) return fallback;
61
+ return `if [ -x ${shellQuote(process.execPath)} ] && [ -f ${shellQuote(file)} ]; then ${shellQuote(process.execPath)} ${shellQuote(file)}${suffix}; else ${fallback}; fi`;
59
62
  }
60
63
  function detachedHookShimCommand(bin, args = "") {
61
- return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${hookShimCommand(bin, args, { cacheRead: false })}; }; } </dev/null >/dev/null 2>&1 &`;
64
+ return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${pinnedHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
62
65
  }
63
66
  function commandMatchesBin(command, bin) {
64
67
  if (!command) {
@@ -68,7 +71,8 @@ function commandMatchesBin(command, bin) {
68
71
  if (c === bin || c.startsWith(`${bin} `)) {
69
72
  return true;
70
73
  }
71
- return c.includes(`command -v ${bin} `);
74
+ const exactBin = new RegExp(`(?:^|\\s)${bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|;|$)`);
75
+ return c.includes(`command -v ${bin} `) || c.includes(`-p ${PKG_NAME}@`) && exactBin.test(c);
72
76
  }
73
77
 
74
78
  // src/lib/bin-cache.ts
@@ -106,7 +110,8 @@ function warmBinCache() {
106
110
 
107
111
  export {
108
112
  binFile,
109
- hookShimCommand,
113
+ packageVersion,
114
+ pinnedHookCommand,
110
115
  detachedHookShimCommand,
111
116
  commandMatchesBin,
112
117
  warmBinCache
@@ -1488,6 +1488,13 @@ async function runIngestionLoop() {
1488
1488
  }
1489
1489
  async function handleConflictCheck(params) {
1490
1490
  assertCallerEnvMatches(params.callerEnv, getSiteUrl());
1491
+ if (params.protocolVersion === 3) {
1492
+ const { callerEnv: _callerEnv, ...body } = params;
1493
+ return await client.post("/api/cli/decisions/conflict-check", body, {
1494
+ signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS),
1495
+ quietRefresh: true
1496
+ });
1497
+ }
1491
1498
  if (typeof params.file !== "string") {
1492
1499
  throw new Error("conflict_check requires `file: string`");
1493
1500
  }
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  toCommitMove
4
- } from "../chunk-F5QCFBJ6.js";
4
+ } from "../chunk-TNNHR3EZ.js";
5
5
  import {
6
6
  appendMove,
7
7
  resolveOrg
8
8
  } from "../chunk-BOTKPLTI.js";
9
9
  import {
10
10
  isRepoActiveForCapture
11
- } from "../chunk-R5ZJRSLV.js";
11
+ } from "../chunk-LDQZASIF.js";
12
12
  import "../chunk-DWDEQRWN.js";
13
13
 
14
14
  // src/hooks/post-commit.ts
@@ -5,24 +5,28 @@ import {
5
5
  requireDurableIngestAcknowledgement
6
6
  } from "../chunk-QY75BQMF.js";
7
7
  import {
8
- scrubFromCwd
9
- } from "../chunk-6LAQVM26.js";
8
+ resolvePreflightTargets
9
+ } from "../chunk-SWWBK4MP.js";
10
10
  import {
11
11
  toMove
12
- } from "../chunk-F5QCFBJ6.js";
12
+ } from "../chunk-TNNHR3EZ.js";
13
13
  import {
14
14
  appendMove,
15
15
  resolveOrg
16
16
  } from "../chunk-BOTKPLTI.js";
17
+ import {
18
+ scrubFromCwd
19
+ } from "../chunk-GUXBUBQ7.js";
17
20
  import {
18
21
  getOrCreateWorkspaceId
19
22
  } from "../chunk-IMAIBPUC.js";
20
23
  import {
21
24
  warmBinCache
22
- } from "../chunk-HSZPTVKU.js";
25
+ } from "../chunk-YIMUIPMD.js";
23
26
  import {
24
- isRepoActiveForCapture
25
- } from "../chunk-R5ZJRSLV.js";
27
+ isRepoActive,
28
+ repoSyncId
29
+ } from "../chunk-LDQZASIF.js";
26
30
  import {
27
31
  getClient
28
32
  } from "../chunk-DWDEQRWN.js";
@@ -66,7 +70,7 @@ function isVerdictFooterContext(value) {
66
70
 
67
71
  // src/hooks/post-tool-use.ts
68
72
  var STDIN_TIMEOUT_MS = 1e3;
69
- var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
73
+ var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
70
74
  var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
71
75
  var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
72
76
  function editingToolsFor(agent) {
@@ -146,13 +150,26 @@ async function main() {
146
150
  return;
147
151
  }
148
152
  const cwd = parsed.cwd ?? process.cwd();
149
- if (!isRepoActiveForCapture(cwd)) {
153
+ if (!isRepoActive(cwd) || !repoSyncId(cwd)) {
150
154
  emit();
151
155
  return;
152
156
  }
157
+ if (agent === "claude_code") {
158
+ const targets = resolvePreflightTargets({
159
+ toolName,
160
+ toolInput: envelope.tool_input,
161
+ agent,
162
+ cwd
163
+ });
164
+ if (targets.mutation === "none") {
165
+ emit();
166
+ return;
167
+ }
168
+ }
153
169
  const identity = getOrCreateWorkspaceId(cwd);
154
170
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
155
- const base = toMove(parsed, resolveCliVersion(), agent, workspaceId);
171
+ const invocationId = agent === "claude_code" && typeof envelope.tool_use_id === "string" ? envelope.tool_use_id : void 0;
172
+ const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
156
173
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
157
174
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
158
175
  try {