@primitive.ai/prim 0.1.0-alpha.61 → 0.1.0-alpha.62

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,117 @@
1
+ import {
2
+ isCanonicalWorkspaceId
3
+ } from "./chunk-UVIWZWM6.js";
4
+ import {
5
+ isValidRepoSyncId
6
+ } from "./chunk-XTNMSS4C.js";
7
+
8
+ // src/hooks/prim-hook-core.ts
9
+ import { createHash, randomUUID } from "crypto";
10
+ import { platform } from "os";
11
+
12
+ // src/protocol/move.ts
13
+ var LEGACY_ENVELOPE_VERSION = 1;
14
+ var AGENT_ENVELOPE_VERSION = 3;
15
+ var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
16
+
17
+ // src/hooks/prim-hook-core.ts
18
+ function postToolInvocationId(parsed, agent) {
19
+ if (parsed.hook_event_name !== "PostToolUse") return void 0;
20
+ const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
21
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
22
+ }
23
+ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
24
+ const sessionId = parsed.session_id ?? "";
25
+ const eventType = parsed.hook_event_name ?? "unknown";
26
+ const moveId = eventType === "PostToolUse" && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
27
+ return {
28
+ moveId,
29
+ capturedAt: Date.now(),
30
+ sessionId,
31
+ eventType,
32
+ payload: parsed,
33
+ env: {
34
+ cwd: parsed.cwd ?? process.cwd(),
35
+ cliVersion,
36
+ osPlatform: platform(),
37
+ ...repository ? {
38
+ repoRoot: repository.repoRoot,
39
+ repoKey: repository.repoKey,
40
+ gitRoot: repository.repoRoot,
41
+ repoFullName: repository.repoFullName,
42
+ repoSyncId: repository.repoSyncId
43
+ } : {},
44
+ ...workspaceId ? { workspaceId } : {}
45
+ },
46
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
47
+ producer: agent,
48
+ ...invocationId ? { invocationId } : {}
49
+ };
50
+ }
51
+ var MAX_CLASSIFIER_ID_CHARS = 256;
52
+ var INVALID_CLASSIFIER_ID_RE = /[\u0000-\u001f\u007f\uD800-\uDFFF]/u;
53
+ function validSessionHint(value) {
54
+ return typeof value === "string" && value.trim().length > 0 && value === value.trim() && value.length <= MAX_CLASSIFIER_ID_CHARS && !INVALID_CLASSIFIER_ID_RE.test(value);
55
+ }
56
+ function commitAttributionFromEnvironment(env, workspaceId) {
57
+ if (env.HERMES_INTERACTIVE) return void 0;
58
+ if (!workspaceId || !isCanonicalWorkspaceId(workspaceId)) return void 0;
59
+ const candidates = [
60
+ validSessionHint(env.CLAUDE_CODE_SESSION_ID) ? { sessionId: env.CLAUDE_CODE_SESSION_ID, producer: "claude_code" } : void 0,
61
+ validSessionHint(env.CODEX_THREAD_ID) ? { sessionId: env.CODEX_THREAD_ID, producer: "codex" } : void 0
62
+ ].filter((candidate) => candidate !== void 0);
63
+ return candidates.length === 1 ? candidates[0] : void 0;
64
+ }
65
+ function toCommitMove(commit, cliVersion, cwd, context = {}) {
66
+ const { repository, repoFullName, repoSyncId, workspaceId, attribution } = context;
67
+ const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
68
+ const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
69
+ const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
70
+ const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
71
+ const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
72
+ JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
73
+ ).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
74
+ const base = {
75
+ moveId,
76
+ capturedAt: Date.now(),
77
+ sessionId: validatedAttribution?.sessionId ?? "",
78
+ eventType: "git.commit",
79
+ payload: {
80
+ kind: "git.commit",
81
+ sha: commit.sha,
82
+ parentSha: commit.parentSha,
83
+ branch: commit.branch,
84
+ changedFiles: commit.files,
85
+ changedFilesComplete: commit.filesComplete
86
+ },
87
+ env: {
88
+ cwd,
89
+ cliVersion,
90
+ osPlatform: platform(),
91
+ ...repository ? {
92
+ repoRoot: repository.repoRoot,
93
+ repoKey: repository.repoKey,
94
+ gitRoot: repository.repoRoot
95
+ } : {},
96
+ ...repoFullName ? { repoFullName } : {},
97
+ ...validatedRepoSyncId ? { repoSyncId: validatedRepoSyncId } : {},
98
+ ...validatedWorkspaceId ? { workspaceId: validatedWorkspaceId } : {}
99
+ }
100
+ };
101
+ return validatedAttribution ? {
102
+ ...base,
103
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
104
+ producer: validatedAttribution.producer
105
+ } : { ...base, envelopeVersion: ENVELOPE_VERSION };
106
+ }
107
+ function shouldFlushAfter(eventType) {
108
+ return eventType === "SessionEnd";
109
+ }
110
+
111
+ export {
112
+ postToolInvocationId,
113
+ toMove,
114
+ commitAttributionFromEnvironment,
115
+ toCommitMove,
116
+ shouldFlushAfter
117
+ };
@@ -205,6 +205,7 @@ function errorCode(error) {
205
205
  }
206
206
 
207
207
  export {
208
+ isCanonicalWorkspaceId,
208
209
  inspectWorkspaceId,
209
210
  getOrCreateWorkspaceId
210
211
  };
@@ -0,0 +1,283 @@
1
+ // src/lib/git.ts
2
+ import { execFileSync } from "child_process";
3
+ import { createHash } from "crypto";
4
+ import { lstatSync, realpathSync } from "fs";
5
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
6
+ var GIT_TIMEOUT_MS = 1e3;
7
+ function gitToplevel(cwd) {
8
+ try {
9
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
10
+ cwd,
11
+ encoding: "utf-8",
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ timeout: GIT_TIMEOUT_MS
14
+ }).trim();
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function gitValue(cwd, args) {
20
+ try {
21
+ const value = execFileSync("git", args, {
22
+ cwd,
23
+ encoding: "utf-8",
24
+ stdio: ["ignore", "pipe", "ignore"],
25
+ timeout: GIT_TIMEOUT_MS
26
+ }).trim();
27
+ return value || void 0;
28
+ } catch {
29
+ return void 0;
30
+ }
31
+ }
32
+ function githubRepositoryFullName(cwd) {
33
+ const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
34
+ if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
35
+ const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
36
+ remote
37
+ );
38
+ const path = match?.[1] ?? match?.[2];
39
+ if (!path) return null;
40
+ const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
41
+ if (parts.length !== 2 || parts.some((part) => !part || part !== part.trim())) return null;
42
+ return `${parts[0]}/${parts[1]}`;
43
+ }
44
+ function canonicalGitRoot(cwd) {
45
+ try {
46
+ const top = gitToplevel(cwd);
47
+ return top ? realpathSync.native(top) : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function normalizeOriginRemote(remote) {
53
+ const value = remote.trim();
54
+ if (!value) return void 0;
55
+ const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
56
+ if (scp?.host && scp.path) {
57
+ const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
58
+ return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
59
+ }
60
+ try {
61
+ const url = new URL(value);
62
+ url.username = "";
63
+ url.password = "";
64
+ url.search = "";
65
+ url.hash = "";
66
+ const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
67
+ if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
68
+ return void 0;
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ function opaqueRepoKey(source, value) {
74
+ return `repo_v1_${createHash("sha256").update(`${source}\0${value}`).digest("hex")}`;
75
+ }
76
+ function resolveRepositoryContext(cwd = process.cwd()) {
77
+ const top = gitToplevel(cwd);
78
+ if (!top) return null;
79
+ let repoRoot;
80
+ try {
81
+ repoRoot = realpathSync.native(top);
82
+ } catch {
83
+ repoRoot = resolve(top);
84
+ }
85
+ const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
86
+ const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
87
+ const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
88
+ if (normalizedOrigin) {
89
+ return {
90
+ repoRoot,
91
+ repoKey: opaqueRepoKey("origin", normalizedOrigin),
92
+ identitySource: "origin",
93
+ repoFullName
94
+ };
95
+ }
96
+ const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
97
+ if (roots && roots.length > 0) {
98
+ return {
99
+ repoRoot,
100
+ repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
101
+ identitySource: "root_commit",
102
+ repoFullName
103
+ };
104
+ }
105
+ return { repoRoot, repoFullName };
106
+ }
107
+ function nearestExistingAncestor(path) {
108
+ const suffix = [];
109
+ let cursor = path;
110
+ while (true) {
111
+ try {
112
+ lstatSync(cursor);
113
+ break;
114
+ } catch (error) {
115
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
116
+ if (code !== "ENOENT" && code !== "ENOTDIR") return null;
117
+ const parent = dirname(cursor);
118
+ if (parent === cursor) return null;
119
+ suffix.unshift(cursor.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)));
120
+ cursor = parent;
121
+ }
122
+ }
123
+ try {
124
+ return { real: realpathSync.native(cursor), suffix };
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ function canonicalRepositoryPathScoped(filePath, context, baseDir) {
130
+ if (!filePath || filePath !== filePath.trim() || filePath.includes("\0")) {
131
+ return { ok: false, reason: "invalid_path" };
132
+ }
133
+ const absolute = resolve(baseDir, filePath);
134
+ const resolved = nearestExistingAncestor(absolute);
135
+ if (!resolved) return { ok: false, reason: "invalid_path" };
136
+ const target = resolve(resolved.real, ...resolved.suffix);
137
+ const rel = relative(context.repoRoot, target);
138
+ if (!rel || rel === ".") return { ok: false, reason: "invalid_path" };
139
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
140
+ return { ok: false, reason: "outside_repository" };
141
+ }
142
+ return { ok: true, file: sep === "/" ? rel : rel.split(sep).join("/") };
143
+ }
144
+ function canonicalRepositoryPathLegacy(filePath, cwd, repositoryRoot = canonicalGitRoot(cwd)) {
145
+ if (!repositoryRoot || !filePath || [...filePath].some((char) => char < " " || char === "\x7F" || "~*?[]{}$`\\".includes(char)))
146
+ return null;
147
+ const absolute = resolve(cwd, filePath);
148
+ let target;
149
+ try {
150
+ const stat = lstatSync(absolute);
151
+ if (!stat.isFile() || stat.isSymbolicLink()) return null;
152
+ target = realpathSync.native(absolute);
153
+ } catch (error) {
154
+ if (!(error instanceof Error) || error.code !== "ENOENT") {
155
+ return null;
156
+ }
157
+ try {
158
+ const parent = realpathSync.native(dirname(absolute));
159
+ if (!lstatSync(parent).isDirectory()) return null;
160
+ target = resolve(parent, basename(absolute));
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ const rel = relative(repositoryRoot, target);
166
+ if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
167
+ return null;
168
+ }
169
+ return sep === "/" ? rel : rel.split(sep).join("/");
170
+ }
171
+ function canonicalRepositoryPath(filePath, contextOrCwd, baseDirOrRoot) {
172
+ if (typeof contextOrCwd === "string") {
173
+ return canonicalRepositoryPathLegacy(filePath, contextOrCwd, baseDirOrRoot);
174
+ }
175
+ return canonicalRepositoryPathScoped(
176
+ filePath,
177
+ contextOrCwd,
178
+ baseDirOrRoot ?? contextOrCwd.repoRoot
179
+ );
180
+ }
181
+
182
+ // src/lib/activation.ts
183
+ import { execFileSync as execFileSync2 } from "child_process";
184
+ import { existsSync, readFileSync } from "fs";
185
+ import { homedir } from "os";
186
+ import { join } from "path";
187
+ var PRIM_ACTIVE_KEY = "prim.active";
188
+ var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
189
+ var GIT_TIMEOUT_MS2 = 1e3;
190
+ var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
191
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
192
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
193
+ function repoActiveFlag(cwd) {
194
+ try {
195
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
196
+ cwd,
197
+ encoding: "utf-8",
198
+ stdio: ["ignore", "pipe", "ignore"],
199
+ timeout: GIT_TIMEOUT_MS2
200
+ }).trim();
201
+ if (value === "true") return "true";
202
+ if (value === "false") return "false";
203
+ return void 0;
204
+ } catch {
205
+ return void 0;
206
+ }
207
+ }
208
+ function isRepoActive(cwd) {
209
+ return repoActiveFlag(cwd) === "true";
210
+ }
211
+ function setRepoActive(cwd, active) {
212
+ execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
213
+ cwd,
214
+ stdio: ["ignore", "ignore", "pipe"],
215
+ timeout: GIT_TIMEOUT_MS2
216
+ });
217
+ }
218
+ function isValidRepoSyncId(value) {
219
+ return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
220
+ }
221
+ function repoSyncId(cwd) {
222
+ try {
223
+ const value = execFileSync2("git", ["config", "--local", "--get", PRIM_REPO_SYNC_ID_KEY], {
224
+ cwd,
225
+ encoding: "utf-8",
226
+ stdio: ["ignore", "pipe", "ignore"],
227
+ timeout: GIT_TIMEOUT_MS2
228
+ }).trim();
229
+ return isValidRepoSyncId(value) ? value : void 0;
230
+ } catch {
231
+ return void 0;
232
+ }
233
+ }
234
+ function setRepoSyncId(cwd, value) {
235
+ if (!isValidRepoSyncId(value)) {
236
+ throw new Error("invalid repository binding returned by server");
237
+ }
238
+ execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_ID_KEY, value], {
239
+ cwd,
240
+ stdio: ["ignore", "ignore", "pipe"],
241
+ timeout: GIT_TIMEOUT_MS2
242
+ });
243
+ }
244
+ function hasProjectPrimInstall(cwd) {
245
+ const root = gitToplevel(cwd);
246
+ if (root === null || root === homedir()) return false;
247
+ for (const rel of PROJECT_INSTALL_FILES) {
248
+ const path = join(root, rel);
249
+ if (existsSync(path)) {
250
+ try {
251
+ const content = readFileSync(path, "utf-8");
252
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
253
+ } catch {
254
+ }
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ function isRepoActiveForCapture(cwd) {
260
+ const flag = repoActiveFlag(cwd);
261
+ if (flag === "true") return true;
262
+ if (flag === "false") return false;
263
+ return hasProjectPrimInstall(cwd);
264
+ }
265
+ function decisionIngestionStatus(cwd) {
266
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
267
+ }
268
+
269
+ export {
270
+ gitToplevel,
271
+ githubRepositoryFullName,
272
+ canonicalGitRoot,
273
+ resolveRepositoryContext,
274
+ canonicalRepositoryPath,
275
+ repoActiveFlag,
276
+ isRepoActive,
277
+ setRepoActive,
278
+ isValidRepoSyncId,
279
+ repoSyncId,
280
+ setRepoSyncId,
281
+ isRepoActiveForCapture,
282
+ decisionIngestionStatus
283
+ };
@@ -1,44 +1,100 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ commitAttributionFromEnvironment,
3
4
  toCommitMove
4
- } from "../chunk-YDBZTFW5.js";
5
+ } from "../chunk-QGVHLP5J.js";
5
6
  import {
6
7
  appendMove,
7
8
  resolveOrg
8
9
  } from "../chunk-S4LFRVGD.js";
9
10
  import {
10
- isRepoActiveForCapture
11
- } from "../chunk-LDQZASIF.js";
11
+ getOrCreateWorkspaceId
12
+ } from "../chunk-UVIWZWM6.js";
13
+ import {
14
+ githubRepositoryFullName,
15
+ isRepoActiveForCapture,
16
+ repoSyncId,
17
+ resolveRepositoryContext
18
+ } from "../chunk-XTNMSS4C.js";
12
19
  import "../chunk-I3HIPAIR.js";
13
20
 
14
21
  // src/hooks/post-commit.ts
15
- import { execSync, spawn } from "child_process";
22
+ import { execFileSync, spawn } from "child_process";
16
23
  import { readFileSync } from "fs";
17
24
  import { dirname, join } from "path";
18
25
  import { fileURLToPath } from "url";
19
26
  var here = dirname(fileURLToPath(import.meta.url));
20
- function git(args) {
27
+ var GIT_TIMEOUT_MS = 1e3;
28
+ var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
29
+ function gitText(args) {
21
30
  try {
22
- return execSync(`git ${args}`, {
31
+ return execFileSync("git", args, {
23
32
  encoding: "utf-8",
24
- stdio: ["ignore", "pipe", "ignore"]
33
+ stdio: ["ignore", "pipe", "ignore"],
34
+ timeout: GIT_TIMEOUT_MS
25
35
  }).trim();
26
36
  } catch {
27
37
  return;
28
38
  }
29
39
  }
40
+ function gitBytes(args) {
41
+ try {
42
+ return execFileSync("git", args, {
43
+ encoding: "buffer",
44
+ stdio: ["ignore", "pipe", "ignore"],
45
+ timeout: GIT_TIMEOUT_MS
46
+ });
47
+ } catch {
48
+ return;
49
+ }
50
+ }
51
+ function parseNulPaths(value) {
52
+ if (value.length === 0) return { files: [], complete: true };
53
+ const chunks = [];
54
+ let start = 0;
55
+ for (let i = 0; i < value.length; i += 1) {
56
+ if (value[i] !== 0) continue;
57
+ chunks.push(value.subarray(start, i));
58
+ start = i + 1;
59
+ }
60
+ if (start !== value.length || chunks.some((chunk) => chunk.length === 0)) {
61
+ return { files: [], complete: false };
62
+ }
63
+ const files = [];
64
+ for (const chunk of chunks) {
65
+ const file = chunk.toString("utf8");
66
+ if (!Buffer.from(file, "utf8").equals(chunk)) {
67
+ return { files: [], complete: false };
68
+ }
69
+ files.push(file);
70
+ }
71
+ return { files: [...new Set(files)], complete: true };
72
+ }
30
73
  function readCommit() {
31
- const sha = git("rev-parse HEAD");
32
- if (!sha) {
74
+ const snapshotProvided = process.env.PRIM_COMMIT_SHA !== void 0;
75
+ const sha = snapshotProvided ? process.env.PRIM_COMMIT_SHA : gitText(["rev-parse", "HEAD"]);
76
+ if (!sha || !FULL_SHA_RE.test(sha)) {
33
77
  return null;
34
78
  }
35
- const branch = git("rev-parse --abbrev-ref HEAD");
36
- const files = (git("diff-tree --no-commit-id --name-only -r -m --root HEAD") ?? "").split("\n").filter((f) => f.length > 0);
79
+ const rawBranch = process.env.PRIM_COMMIT_BRANCH !== void 0 ? process.env.PRIM_COMMIT_BRANCH : gitText(["rev-parse", "--abbrev-ref", "HEAD"]);
80
+ const branch = rawBranch && rawBranch === rawBranch.trim() && ![...rawBranch].some((char) => char < " " || char === "\x7F") ? rawBranch : void 0;
81
+ const rawFiles = gitBytes([
82
+ "diff-tree",
83
+ "--no-commit-id",
84
+ "--name-only",
85
+ "-z",
86
+ "-r",
87
+ "-m",
88
+ "--root",
89
+ sha
90
+ ]);
91
+ const { files, complete } = rawFiles === void 0 ? { files: [], complete: false } : parseNulPaths(rawFiles);
37
92
  return {
38
93
  sha,
39
- parentSha: git("rev-parse --verify --quiet HEAD^") || void 0,
94
+ parentSha: gitText(["rev-parse", "--verify", "--quiet", `${sha}^`]) || void 0,
40
95
  branch: branch && branch !== "HEAD" ? branch : void 0,
41
- files
96
+ files,
97
+ filesComplete: complete
42
98
  };
43
99
  }
44
100
  function resolveCliVersion() {
@@ -57,14 +113,24 @@ function spawnBackgroundFlush() {
57
113
  }).unref();
58
114
  }
59
115
  function runPostCommit() {
60
- const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
116
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
61
117
  if (!isRepoActiveForCapture(cwd)) {
62
118
  return;
63
119
  }
64
120
  const commit = readCommit();
65
121
  if (commit) {
66
- const move = toCommitMove(commit, resolveCliVersion(), cwd);
67
- const { orgId } = resolveOrg({ sessionId: "", cwd });
122
+ const repository = resolveRepositoryContext(cwd);
123
+ const identity = getOrCreateWorkspaceId(cwd);
124
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
125
+ const attribution = commitAttributionFromEnvironment(process.env, workspaceId);
126
+ const move = toCommitMove(commit, resolveCliVersion(), cwd, {
127
+ repository,
128
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
129
+ repoSyncId: repoSyncId(cwd),
130
+ workspaceId,
131
+ attribution
132
+ });
133
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
68
134
  appendMove(move, orgId);
69
135
  spawnBackgroundFlush();
70
136
  }
@@ -5,29 +5,31 @@ import {
5
5
  requireDurableIngestAcknowledgement
6
6
  } from "../chunk-JQFN6LBO.js";
7
7
  import {
8
- resolvePreflightTargets
9
- } from "../chunk-B4SHJYIN.js";
8
+ enrichHookPayloadWithFileRefs,
9
+ preserveHookFileMetadata
10
+ } from "../chunk-DA4BMHEB.js";
10
11
  import {
11
12
  postToolInvocationId,
12
13
  toMove
13
- } from "../chunk-YDBZTFW5.js";
14
+ } from "../chunk-QGVHLP5J.js";
14
15
  import {
15
16
  appendMove,
16
17
  resolveOrg
17
18
  } from "../chunk-S4LFRVGD.js";
18
19
  import {
19
20
  scrubFromCwd
20
- } from "../chunk-GUXBUBQ7.js";
21
+ } from "../chunk-GF5QII7T.js";
21
22
  import {
22
23
  getOrCreateWorkspaceId
23
- } from "../chunk-IMAIBPUC.js";
24
+ } from "../chunk-UVIWZWM6.js";
24
25
  import {
25
26
  warmBinCache
26
- } from "../chunk-YIMUIPMD.js";
27
+ } from "../chunk-KBFFVYBC.js";
27
28
  import {
28
- isRepoActive,
29
- repoSyncId
30
- } from "../chunk-LDQZASIF.js";
29
+ isRepoActiveForCapture,
30
+ repoSyncId,
31
+ resolveRepositoryContext
32
+ } from "../chunk-XTNMSS4C.js";
31
33
  import {
32
34
  getClient
33
35
  } from "../chunk-I3HIPAIR.js";
@@ -151,27 +153,41 @@ async function main() {
151
153
  return;
152
154
  }
153
155
  const cwd = parsed.cwd ?? process.cwd();
154
- if (!isRepoActive(cwd) || !repoSyncId(cwd)) {
156
+ if (!isRepoActiveForCapture(cwd)) {
155
157
  emit();
156
158
  return;
157
159
  }
158
- if (agent === "claude_code") {
159
- const targets = resolvePreflightTargets({
160
- toolName,
161
- toolInput: envelope.tool_input,
162
- agent,
163
- cwd
164
- });
165
- if (targets.mutation === "none") {
166
- emit();
167
- return;
168
- }
160
+ const resolvedRepository = resolveRepositoryContext(cwd);
161
+ if (!resolvedRepository) {
162
+ emit();
163
+ return;
164
+ }
165
+ const repository = { ...resolvedRepository, repoSyncId: repoSyncId(cwd) };
166
+ const enrichment = enrichHookPayloadWithFileRefs({
167
+ parsed,
168
+ agent,
169
+ cwd,
170
+ repository
171
+ });
172
+ const { resolution } = enrichment;
173
+ if (resolution.shellMutation === "none") {
174
+ emit();
175
+ return;
176
+ }
177
+ if (resolution.shellMutation === void 0 && resolution.fileRefs.length === 0) {
178
+ emit();
179
+ return;
169
180
  }
181
+ const enriched = enrichment.parsed;
170
182
  const identity = getOrCreateWorkspaceId(cwd);
171
183
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
172
- const invocationId = postToolInvocationId(parsed, agent);
173
- const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
174
- const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
184
+ const invocationId = postToolInvocationId(enriched, agent);
185
+ const base = toMove(enriched, resolveCliVersion(), agent, workspaceId, repository, invocationId);
186
+ const scrubbed = scrubFromCwd(enriched, cwd);
187
+ const move = {
188
+ ...base,
189
+ payload: preserveHookFileMetadata(scrubbed, resolution)
190
+ };
175
191
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
176
192
  try {
177
193
  const result = await deliverPostToolMove(move, orgId);