@primitive.ai/prim 0.1.0-alpha.65 → 0.1.0-alpha.66

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,194 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ canonicalRewritePairs,
4
+ toRewriteMove
5
+ } from "../chunk-CPSE3Y3T.js";
6
+ import {
7
+ appendMove,
8
+ resolveOrg
9
+ } from "../chunk-HIMWM526.js";
10
+ import {
11
+ getOrCreateWorkspaceId
12
+ } from "../chunk-UVIWZWM6.js";
13
+ import {
14
+ githubRepositoryFullName,
15
+ isRepoActiveForCapture,
16
+ repoSyncId,
17
+ resolveRepositoryContext
18
+ } from "../chunk-TDRXHBEX.js";
19
+ import "../chunk-CPX3BUOC.js";
20
+
21
+ // src/hooks/post-rewrite.ts
22
+ import { execFileSync, spawn } from "child_process";
23
+ import {
24
+ constants,
25
+ closeSync,
26
+ fstatSync,
27
+ lstatSync,
28
+ openSync,
29
+ readFileSync,
30
+ unlinkSync
31
+ } from "fs";
32
+ import { basename, dirname, isAbsolute, join } from "path";
33
+ import { fileURLToPath } from "url";
34
+ var here = dirname(fileURLToPath(import.meta.url));
35
+ var GIT_TIMEOUT_MS = 1e3;
36
+ var MAX_PAIRS_FILE_BYTES = 1048576;
37
+ var MAX_PAIRS_FILE_AGE_MS = 10 * 60 * 1e3;
38
+ var MAX_PAIRS_FILE_FUTURE_SKEW_MS = 5e3;
39
+ var PAIRS_FILE_RE = /^prim-post-rewrite-pairs\.[A-Za-z0-9]+$/u;
40
+ var FULL_SHA_SOURCE = "(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})";
41
+ var REWRITE_PAIR_RE = new RegExp(
42
+ `^(${FULL_SHA_SOURCE}) (${FULL_SHA_SOURCE})(?: ([^\\s]+))?$`,
43
+ "u"
44
+ );
45
+ function parseRewritePairs(value) {
46
+ if (value.length === 0) return [];
47
+ const body = value.endsWith("\n") ? value.slice(0, -1) : value;
48
+ if (body.length === 0) return [];
49
+ const parsed = [];
50
+ for (const line of body.split("\n")) {
51
+ const match = REWRITE_PAIR_RE.exec(line);
52
+ if (!match?.[1] || !match[2]) return;
53
+ parsed.push({ oldSha: match[1], newSha: match[2] });
54
+ }
55
+ return canonicalRewritePairs(parsed) ?? void 0;
56
+ }
57
+ function privatePairsFileStat(stat, now) {
58
+ const capturedAt = Math.trunc(stat.mtimeMs);
59
+ const uid = process.getuid?.();
60
+ return stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_PAIRS_FILE_BYTES && stat.nlink === 1 && (stat.mode & 511) === 384 && (uid === void 0 || stat.uid === uid) && Number.isSafeInteger(capturedAt) && capturedAt > 0 && capturedAt >= now - MAX_PAIRS_FILE_AGE_MS && capturedAt <= now + MAX_PAIRS_FILE_FUTURE_SKEW_MS;
61
+ }
62
+ function rewritePairsFromLauncher(env = process.env, now = Date.now()) {
63
+ const path = env.PRIM_REWRITE_PAIRS_FILE;
64
+ if (!path || !isAbsolute(path) || !PAIRS_FILE_RE.test(basename(path))) return;
65
+ let fd;
66
+ try {
67
+ const pathStat = lstatSync(path);
68
+ if (!privatePairsFileStat(pathStat, now)) return;
69
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
70
+ const openStat = fstatSync(fd);
71
+ if (!privatePairsFileStat(openStat, now) || openStat.dev !== pathStat.dev || openStat.ino !== pathStat.ino) {
72
+ return;
73
+ }
74
+ const bytes = readFileSync(fd);
75
+ if (bytes.length !== openStat.size) return;
76
+ let text;
77
+ try {
78
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
79
+ } catch {
80
+ return;
81
+ }
82
+ const pairs = parseRewritePairs(text);
83
+ if (!pairs) return;
84
+ return {
85
+ path,
86
+ dev: openStat.dev,
87
+ ino: openStat.ino,
88
+ capturedAt: Math.trunc(openStat.mtimeMs),
89
+ pairs
90
+ };
91
+ } catch {
92
+ return;
93
+ } finally {
94
+ if (fd !== void 0) {
95
+ try {
96
+ closeSync(fd);
97
+ } catch {
98
+ }
99
+ }
100
+ }
101
+ }
102
+ function unlinkSnapshot(snapshot) {
103
+ try {
104
+ const current = lstatSync(snapshot.path);
105
+ if (current.isFile() && !current.isSymbolicLink() && current.dev === snapshot.dev && current.ino === snapshot.ino) {
106
+ unlinkSync(snapshot.path);
107
+ }
108
+ } catch {
109
+ }
110
+ }
111
+ function gitText(args) {
112
+ try {
113
+ return execFileSync("git", args, {
114
+ encoding: "utf-8",
115
+ stdio: ["ignore", "pipe", "ignore"],
116
+ timeout: GIT_TIMEOUT_MS
117
+ }).trim();
118
+ } catch {
119
+ return;
120
+ }
121
+ }
122
+ function validSource(value) {
123
+ return value === "amend" || value === "rebase";
124
+ }
125
+ function validBranch(value) {
126
+ return value && value === value.trim() && ![...value].some((char) => char < " " || char === "\x7F") ? value : void 0;
127
+ }
128
+ function resolveCliVersion() {
129
+ try {
130
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
131
+ return pkg.version ?? "unknown";
132
+ } catch {
133
+ return "unknown";
134
+ }
135
+ }
136
+ function spawnBackgroundFlush() {
137
+ const entry = join(here, "..", "index.js");
138
+ spawn(process.execPath, [entry, "moves", "flush"], {
139
+ detached: true,
140
+ stdio: "ignore"
141
+ }).unref();
142
+ }
143
+ function runPostRewrite() {
144
+ const snapshot = rewritePairsFromLauncher();
145
+ if (!snapshot) return;
146
+ try {
147
+ const source = process.env.PRIM_REWRITE_SOURCE;
148
+ if (!validSource(source) || snapshot.pairs.length === 0) return;
149
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
150
+ if (!isRepoActiveForCapture(cwd)) return;
151
+ const repository = resolveRepositoryContext(cwd);
152
+ const identity = getOrCreateWorkspaceId(cwd);
153
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
154
+ const moves = toRewriteMove(
155
+ {
156
+ source,
157
+ pairs: snapshot.pairs,
158
+ branch: validBranch(process.env.PRIM_REWRITE_BRANCH)
159
+ },
160
+ resolveCliVersion(),
161
+ cwd,
162
+ {
163
+ repository,
164
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
165
+ repoSyncId: repoSyncId(cwd),
166
+ workspaceId,
167
+ capturedAt: snapshot.capturedAt
168
+ }
169
+ );
170
+ if (moves.length === 0) return;
171
+ const { orgId } = resolveOrg({ sessionId: "", cwd });
172
+ for (const move of moves) appendMove(move, orgId);
173
+ spawnBackgroundFlush();
174
+ } finally {
175
+ unlinkSnapshot(snapshot);
176
+ }
177
+ }
178
+ if (!process.env.VITEST) {
179
+ try {
180
+ runPostRewrite();
181
+ } catch (error) {
182
+ if (process.env.PRIM_HOOK_DEBUG) {
183
+ const detail = error instanceof Error ? error.message : String(error);
184
+ process.stderr.write(`[prim-post-rewrite] capture failed: ${detail}
185
+ `);
186
+ }
187
+ }
188
+ process.exit(0);
189
+ }
190
+ export {
191
+ parseRewritePairs,
192
+ rewritePairsFromLauncher,
193
+ runPostRewrite
194
+ };
@@ -12,20 +12,20 @@ import {
12
12
  postToolInvocationId,
13
13
  toMove,
14
14
  toolOutcomeFor
15
- } from "../chunk-5CKL5EYQ.js";
15
+ } from "../chunk-CPSE3Y3T.js";
16
16
  import {
17
17
  appendMove,
18
18
  resolveOrg
19
19
  } from "../chunk-HIMWM526.js";
20
- import {
21
- scrubFromCwd
22
- } from "../chunk-G5D3FF3D.js";
23
20
  import {
24
21
  getOrCreateWorkspaceId
25
22
  } from "../chunk-UVIWZWM6.js";
23
+ import {
24
+ scrubFromCwd
25
+ } from "../chunk-G5D3FF3D.js";
26
26
  import {
27
27
  warmBinCache
28
- } from "../chunk-LNU44OJV.js";
28
+ } from "../chunk-CZ24W5QU.js";
29
29
  import {
30
30
  isRepoActiveForCapture,
31
31
  repoSyncId,
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  packageVersion,
18
18
  warmBinCache
19
- } from "../chunk-LNU44OJV.js";
19
+ } from "../chunk-CZ24W5QU.js";
20
20
  import {
21
21
  canonicalGitRoot,
22
22
  canonicalRepositoryPath,
@@ -1,4 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ buildHookOutput,
4
+ handoffHookOutput
5
+ } from "../chunk-4GLVJLEQ.js";
6
+ import {
7
+ FEEDBACK_DEADLINE_MS,
8
+ acknowledgeDecisionFeedback,
9
+ leaseDecisionFeedback,
10
+ renderFeedback
11
+ } from "../chunk-KHZNYEUV.js";
2
12
  import {
3
13
  enrichHookPayloadWithFileRefs,
4
14
  preserveHookFileMetadata
@@ -7,30 +17,20 @@ import {
7
17
  postToolInvocationId,
8
18
  shouldFlushAfter,
9
19
  toMove
10
- } from "../chunk-5CKL5EYQ.js";
20
+ } from "../chunk-CPSE3Y3T.js";
11
21
  import {
12
22
  appendMove,
13
23
  resolveOrg
14
24
  } from "../chunk-HIMWM526.js";
15
- import {
16
- scrubFromCwd
17
- } from "../chunk-G5D3FF3D.js";
18
- import {
19
- buildHookOutput,
20
- handoffHookOutput
21
- } from "../chunk-4GLVJLEQ.js";
22
- import {
23
- FEEDBACK_DEADLINE_MS,
24
- acknowledgeDecisionFeedback,
25
- leaseDecisionFeedback,
26
- renderFeedback
27
- } from "../chunk-KHZNYEUV.js";
28
25
  import {
29
26
  getOrCreateWorkspaceId
30
27
  } from "../chunk-UVIWZWM6.js";
28
+ import {
29
+ scrubFromCwd
30
+ } from "../chunk-G5D3FF3D.js";
31
31
  import {
32
32
  warmBinCache
33
- } from "../chunk-LNU44OJV.js";
33
+ } from "../chunk-CZ24W5QU.js";
34
34
  import {
35
35
  isRepoActiveForCapture,
36
36
  repoSyncId,
@@ -2,9 +2,10 @@
2
2
  import {
3
3
  bindRepository,
4
4
  ensureEffectivePostCommitHook,
5
+ ensureEffectivePostRewriteHook,
5
6
  hasUsableCodexGuidance,
6
7
  refreshClaudePlugins
7
- } from "../chunk-CPHJUQNE.js";
8
+ } from "../chunk-DR66XM5L.js";
8
9
  import {
9
10
  buildHookOutput,
10
11
  handoffHookOutput
@@ -21,7 +22,7 @@ import {
21
22
  import {
22
23
  binFile,
23
24
  warmBinCache
24
- } from "../chunk-LNU44OJV.js";
25
+ } from "../chunk-CZ24W5QU.js";
25
26
  import {
26
27
  gitToplevel,
27
28
  isRepoActiveForCapture,
@@ -100,6 +101,10 @@ async function activeProjectRoot(cwd) {
100
101
  ensureEffectivePostCommitHook(root);
101
102
  } catch {
102
103
  }
104
+ try {
105
+ ensureEffectivePostRewriteHook(root);
106
+ } catch {
107
+ }
103
108
  }
104
109
  try {
105
110
  await bindRepository(root, {
package/dist/index.js CHANGED
@@ -1,4 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ bindRepository,
4
+ compareSemver,
5
+ ensureEffectivePostCommitHook,
6
+ ensureEffectivePostRewriteHook,
7
+ ensurePostCommitHookAtPath,
8
+ ensurePostRewriteHookAtPath,
9
+ inspectEffectivePostCommitHook,
10
+ inspectEffectivePostRewriteHook,
11
+ postCommitHookBlock,
12
+ postRewriteHookBlock,
13
+ printJson,
14
+ registerSkillCommands,
15
+ resolveRepositoryBinding,
16
+ uninstallPostCommitHookAtPath,
17
+ uninstallPostRewriteHookAtPath,
18
+ uninstallProjectPostCommitHook,
19
+ uninstallProjectPostRewriteHook
20
+ } from "./chunk-DR66XM5L.js";
2
21
  import {
3
22
  bold,
4
23
  boundedHealthError,
@@ -14,6 +33,9 @@ import {
14
33
  daemonOrDirectGet,
15
34
  formatDecisionsWarning
16
35
  } from "./chunk-WS3MCNFD.js";
36
+ import {
37
+ fetchFeedbackCapability
38
+ } from "./chunk-KHZNYEUV.js";
17
39
  import {
18
40
  JOURNAL_DIR,
19
41
  SESSIONS_DIR,
@@ -23,22 +45,6 @@ import {
23
45
  pendingJournalStats,
24
46
  readMovesFromPath
25
47
  } from "./chunk-HIMWM526.js";
26
- import {
27
- bindRepository,
28
- compareSemver,
29
- ensureEffectivePostCommitHook,
30
- ensurePostCommitHookAtPath,
31
- inspectEffectivePostCommitHook,
32
- postCommitHookBlock,
33
- printJson,
34
- registerSkillCommands,
35
- resolveRepositoryBinding,
36
- uninstallPostCommitHookAtPath,
37
- uninstallProjectPostCommitHook
38
- } from "./chunk-CPHJUQNE.js";
39
- import {
40
- fetchFeedbackCapability
41
- } from "./chunk-KHZNYEUV.js";
42
48
  import {
43
49
  inspectWorkspaceId
44
50
  } from "./chunk-UVIWZWM6.js";
@@ -49,7 +55,7 @@ import {
49
55
  packageVersion,
50
56
  pinnedHookCommand,
51
57
  warmBinCache
52
- } from "./chunk-LNU44OJV.js";
58
+ } from "./chunk-CZ24W5QU.js";
53
59
  import {
54
60
  canonicalGitRoot,
55
61
  canonicalRepositoryPath,
@@ -101,8 +107,16 @@ async function applyActivation(active) {
101
107
  try {
102
108
  let binding;
103
109
  let postCommitHook;
110
+ let postRewriteHook;
104
111
  if (active) {
105
112
  postCommitHook = ensureEffectivePostCommitHook(root).path;
113
+ try {
114
+ postRewriteHook = ensureEffectivePostRewriteHook(root).path;
115
+ } catch (error) {
116
+ const detail = error instanceof Error ? error.message : String(error);
117
+ process.stderr.write(`[prim] post-rewrite hook coverage is degraded: ${detail}
118
+ `);
119
+ }
106
120
  phase = "repository binding";
107
121
  binding = await bindRepository(root);
108
122
  phase = "local activation";
@@ -132,7 +146,8 @@ async function applyActivation(active) {
132
146
  repositoryFullName: binding.repositoryFullName,
133
147
  ...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
134
148
  } : {},
135
- ...postCommitHook ? { postCommitHook } : {}
149
+ ...postCommitHook ? { postCommitHook } : {},
150
+ ...postRewriteHook ? { postRewriteHook } : {}
136
151
  });
137
152
  } catch (err) {
138
153
  const detail = err instanceof Error ? err.message : String(err);
@@ -4004,27 +4019,27 @@ async function checkRepositoryBinding() {
4004
4019
  };
4005
4020
  }
4006
4021
  }
4007
- function classifyPostCommitHook(inspection) {
4022
+ function classifyManagedHook(hookName, inspection) {
4008
4023
  if (inspection.covered) {
4009
4024
  return {
4010
- name: "post-commit",
4025
+ name: hookName,
4011
4026
  status: "ok",
4012
4027
  detail: `effective and executable \xB7 ${inspection.kind} \xB7 ${inspection.hookPath}`
4013
4028
  };
4014
4029
  }
4015
4030
  return {
4016
- name: "post-commit",
4031
+ name: hookName,
4017
4032
  status: "fail",
4018
4033
  detail: `${inspection.reason ?? "uncovered"} \xB7 ${inspection.hookPath}`
4019
4034
  };
4020
4035
  }
4021
- function checkPostCommitHook() {
4036
+ function checkManagedHook(hookName, inspect) {
4022
4037
  try {
4023
- return classifyPostCommitHook(inspectEffectivePostCommitHook());
4038
+ return classifyManagedHook(hookName, inspect());
4024
4039
  } catch (error) {
4025
4040
  const detail = error instanceof Error ? error.message : String(error);
4026
4041
  return {
4027
- name: "post-commit",
4042
+ name: hookName,
4028
4043
  status: /not a git repository/iu.test(detail) ? "warn" : "fail",
4029
4044
  detail: detail.slice(0, 120)
4030
4045
  };
@@ -4150,7 +4165,8 @@ async function collectChecks() {
4150
4165
  checkFeedbackHooks(),
4151
4166
  checkWorkspaceIdentity(),
4152
4167
  await checkRepositoryBinding(),
4153
- checkPostCommitHook(),
4168
+ checkManagedHook("post-commit", inspectEffectivePostCommitHook),
4169
+ checkManagedHook("post-rewrite", inspectEffectivePostRewriteHook),
4154
4170
  ...backend,
4155
4171
  await checkFeedbackCapability()
4156
4172
  ];
@@ -4534,7 +4550,8 @@ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
4534
4550
  import { Option as Option3 } from "commander";
4535
4551
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
4536
4552
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
4537
- var HOOKS = [PRE_COMMIT, POST_COMMIT];
4553
+ var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
4554
+ var HOOKS = [PRE_COMMIT, POST_COMMIT, POST_REWRITE];
4538
4555
  var GIT_TIMEOUT_MS = 1e3;
4539
4556
  function blockMarkers(spec) {
4540
4557
  return {
@@ -4563,6 +4580,7 @@ ${hookShim(spec.binName)}
4563
4580
  }
4564
4581
  function huskyBlock(spec) {
4565
4582
  if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
4583
+ if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
4566
4584
  const { start, end } = blockMarkers(spec);
4567
4585
  return `${start}
4568
4586
  ${hookShim(spec.binName)}
@@ -4570,6 +4588,7 @@ ${end}`;
4570
4588
  }
4571
4589
  function gatedBlock(spec) {
4572
4590
  if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
4591
+ if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
4573
4592
  const { start, end } = blockMarkers(spec);
4574
4593
  return `${start}
4575
4594
  ${gatedShim(spec.binName)}
@@ -4707,7 +4726,6 @@ var PASSTHROUGH_HOOKS = [
4707
4726
  "post-checkout",
4708
4727
  "post-merge",
4709
4728
  "pre-push",
4710
- "post-rewrite",
4711
4729
  "pre-auto-gc",
4712
4730
  "push-to-checkout",
4713
4731
  "sendemail-validate",
@@ -4717,11 +4735,13 @@ var PASSTHROUGH_HOOKS = [
4717
4735
  // with a query protocol, not a lifecycle event, so a bare exec stub is wrong.)
4718
4736
  ];
4719
4737
  function globalHookScript(spec) {
4720
- const chainExit = spec.hookName === "pre-commit" ? "|| exit $?" : "|| true";
4721
- const invocation = spec.hookName === POST_COMMIT.hookName ? postCommitHookBlock() : gatedShim(spec.binName);
4722
- const beforeComments = spec.hookName === POST_COMMIT.hookName ? `${invocation}
4738
+ const chainExit = spec.hookName === PRE_COMMIT.hookName ? "|| exit $?" : "|| true";
4739
+ const managedBlock = spec.hookName === POST_COMMIT.hookName ? postCommitHookBlock() : spec.hookName === POST_REWRITE.hookName ? postRewriteHookBlock() : void 0;
4740
+ const invocation = managedBlock ?? gatedShim(spec.binName);
4741
+ const managedRepoGuard = managedBlock ? ` && ! grep -Fq '${blockMarkers(spec).start}' "$repo_hook" 2>/dev/null` : "";
4742
+ const beforeComments = managedBlock ? `${invocation}
4723
4743
  ` : "";
4724
- const afterComments = spec.hookName === POST_COMMIT.hookName ? "" : `${invocation}
4744
+ const afterComments = managedBlock ? "" : `${invocation}
4725
4745
  `;
4726
4746
  return `#!/bin/sh
4727
4747
  ${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
@@ -4731,7 +4751,7 @@ ${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 man
4731
4751
  # hook regardless, so inactive repos are unaffected.
4732
4752
  ${afterComments}common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
4733
4753
  repo_hook="$common_dir/hooks/${spec.hookName}"
4734
- if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null; then
4754
+ if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
4735
4755
  "$repo_hook" "$@" ${chainExit}
4736
4756
  fi
4737
4757
  exit 0
@@ -4757,6 +4777,8 @@ function writeOwnHooks() {
4757
4777
  const path = resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName);
4758
4778
  if (spec.hookName === POST_COMMIT.hookName) {
4759
4779
  ensurePostCommitHookAtPath(path, globalHookScript(spec));
4780
+ } else if (spec.hookName === POST_REWRITE.hookName) {
4781
+ writeFileSync4(path, globalHookScript(spec), { mode: 493 });
4760
4782
  } else {
4761
4783
  writeFileSync4(path, globalHookScript(spec), { mode: 493 });
4762
4784
  }
@@ -4770,6 +4792,10 @@ function appendPrimBlock(hookPath, spec) {
4770
4792
  ensurePostCommitHookAtPath(hookPath);
4771
4793
  return;
4772
4794
  }
4795
+ if (spec.hookName === POST_REWRITE.hookName) {
4796
+ ensurePostRewriteHookAtPath(hookPath);
4797
+ return;
4798
+ }
4773
4799
  mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
4774
4800
  }
4775
4801
  function stripPrimBlock(hookPath, spec) {
@@ -4777,6 +4803,10 @@ function stripPrimBlock(hookPath, spec) {
4777
4803
  uninstallPostCommitHookAtPath(hookPath);
4778
4804
  return;
4779
4805
  }
4806
+ if (spec.hookName === POST_REWRITE.hookName) {
4807
+ uninstallPostRewriteHookAtPath(hookPath);
4808
+ return;
4809
+ }
4780
4810
  if (!existsSync7(hookPath)) return;
4781
4811
  const existing = readFileSync6(hookPath, "utf-8");
4782
4812
  const primCreated = existing.includes(PRIM_CREATED_MARK);
@@ -4856,22 +4886,29 @@ function uninstallGlobalHooks() {
4856
4886
  console.log("No prim global git hooks found.");
4857
4887
  }
4858
4888
  function installHooks(gitRoot, target) {
4859
- for (const spec of HOOKS.filter((candidate) => candidate !== POST_COMMIT)) {
4860
- if (target === "husky") {
4861
- installToHusky(gitRoot, spec);
4862
- } else {
4863
- installToDotGit(gitRoot, spec);
4864
- }
4889
+ if (target === "husky") {
4890
+ installToHusky(gitRoot, PRE_COMMIT);
4891
+ } else {
4892
+ installToDotGit(gitRoot, PRE_COMMIT);
4865
4893
  }
4866
4894
  const postCommit = ensureEffectivePostCommitHook(gitRoot);
4867
4895
  console.log(
4868
4896
  `${postCommit.changed ? "Installed" : "Refreshed"} effective post-commit hook at ${postCommit.path}.`
4869
4897
  );
4898
+ try {
4899
+ const postRewrite = ensureEffectivePostRewriteHook(gitRoot);
4900
+ console.log(
4901
+ `${postRewrite.changed ? "Installed" : "Refreshed"} effective post-rewrite hook at ${postRewrite.path}.`
4902
+ );
4903
+ } catch (error) {
4904
+ const detail = error instanceof Error ? error.message : String(error);
4905
+ console.error(`[prim] post-rewrite hook coverage is degraded: ${detail}`);
4906
+ }
4870
4907
  }
4871
4908
  function registerHooksCommands(program2) {
4872
4909
  const hooks = program2.command("hooks").description("Manage git hooks");
4873
4910
  hooks.command("install").description(
4874
- "Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
4911
+ "Install the prim git hooks \u2014 pre-commit + post-commit + post-rewrite (auto-detects Husky; use --target to override)"
4875
4912
  ).addOption(
4876
4913
  new Option3("--target <where>", "install destination; bypasses Husky detection").choices([
4877
4914
  "husky",
@@ -4929,27 +4966,30 @@ function registerHooksCommands(program2) {
4929
4966
  }
4930
4967
  const gitRoot = getGitRoot();
4931
4968
  const hooksDir = projectHooksDir(gitRoot);
4932
- for (const spec of HOOKS.filter((candidate) => candidate !== POST_COMMIT)) {
4933
- const hookPath = resolve2(hooksDir, spec.hookName);
4934
- if (!existsSync7(hookPath)) {
4935
- console.log(`No ${spec.hookName} hook found.`);
4936
- continue;
4937
- }
4938
- if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
4939
- unlinkSync2(hookPath);
4940
- console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
4969
+ const preCommitPath = resolve2(hooksDir, PRE_COMMIT.hookName);
4970
+ if (!existsSync7(preCommitPath)) {
4971
+ console.log(`No ${PRE_COMMIT.hookName} hook found.`);
4972
+ } else if (containsPrimHook(readFileSync6(preCommitPath, "utf-8"), PRE_COMMIT.binName)) {
4973
+ unlinkSync2(preCommitPath);
4974
+ console.log(`Removed ${PRE_COMMIT.hookName} hook at ${preCommitPath}`);
4975
+ } else {
4976
+ console.log(
4977
+ `Left ${PRE_COMMIT.hookName} hook at ${preCommitPath} untouched (not a prim hook).`
4978
+ );
4979
+ }
4980
+ for (const [hookName, uninstall] of [
4981
+ [POST_COMMIT.hookName, uninstallProjectPostCommitHook],
4982
+ [POST_REWRITE.hookName, uninstallProjectPostRewriteHook]
4983
+ ]) {
4984
+ const result = uninstall(gitRoot);
4985
+ if (!result.changed) {
4986
+ console.log(`No Prim ${hookName} block found at ${result.path}.`);
4987
+ } else if (result.removedFile) {
4988
+ console.log(`Removed Prim-created ${hookName} hook at ${result.path}.`);
4941
4989
  } else {
4942
- console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
4990
+ console.log(`Removed the Prim ${hookName} block from ${result.path}.`);
4943
4991
  }
4944
4992
  }
4945
- const postCommit = uninstallProjectPostCommitHook(gitRoot);
4946
- if (!postCommit.changed) {
4947
- console.log(`No Prim post-commit block found at ${postCommit.path}.`);
4948
- } else if (postCommit.removedFile) {
4949
- console.log(`Removed Prim-created post-commit hook at ${postCommit.path}.`);
4950
- } else {
4951
- console.log(`Removed the Prim post-commit block from ${postCommit.path}.`);
4952
- }
4953
4993
  });
4954
4994
  }
4955
4995
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.65",
3
+ "version": "0.1.0-alpha.66",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  "prim": "dist/index.js",
32
32
  "prim-pre-commit": "dist/hooks/pre-commit.js",
33
33
  "prim-post-commit": "dist/hooks/post-commit.js",
34
+ "prim-post-rewrite": "dist/hooks/post-rewrite.js",
34
35
  "prim-hook": "dist/hooks/prim-hook.js",
35
36
  "prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
36
37
  "prim-post-tool-use": "dist/hooks/post-tool-use.js",
@@ -47,9 +48,9 @@
47
48
  "SKILL.md"
48
49
  ],
49
50
  "scripts": {
50
- "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 --format esm --clean && tsup src/statusline-main.ts src/daemon/server.ts --format esm --splitting false",
51
- "postbuild": "chmod +x ./dist/index.js ./dist/statusline-main.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",
52
- "dev": "tsup src/index.ts src/statusline-main.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 --splitting false",
51
+ "build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/post-rewrite.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 --format esm --clean && tsup src/statusline-main.ts src/daemon/server.ts --format esm --splitting false",
52
+ "postbuild": "chmod +x ./dist/index.js ./dist/statusline-main.js ./dist/hooks/pre-commit.js ./dist/hooks/post-commit.js ./dist/hooks/post-rewrite.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",
53
+ "dev": "tsup src/index.ts src/statusline-main.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/post-rewrite.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 --splitting false",
53
54
  "clean": "rm -rf dist coverage",
54
55
  "lint": "biome check src/",
55
56
  "format": "biome check --fix src/",