knodin 0.5.1 → 0.7.3

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.
Files changed (70) hide show
  1. package/README.md +190 -535
  2. package/dist/bin/cli.js +173 -17
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +4 -16
  5. package/dist/src/artifact-refresh.js +1 -1
  6. package/dist/src/cli-args.js +8 -1
  7. package/dist/src/cli-model.js +35 -1
  8. package/dist/src/codeflow-replay.js +80 -0
  9. package/dist/src/competitive-cold-mcp.js +40 -0
  10. package/dist/src/competitive-manifest.js +106 -25
  11. package/dist/src/competitive-runner.js +37 -3
  12. package/dist/src/competitive-sandbox.js +4 -4
  13. package/dist/src/context-export.js +1 -1
  14. package/dist/src/diagnostics.js +449 -0
  15. package/dist/src/docs-sections.js +5 -5
  16. package/dist/src/engine/ann-hnsw.js +5 -5
  17. package/dist/src/engine/embeddings.js +4 -4
  18. package/dist/src/engine/git-history.js +289 -0
  19. package/dist/src/engine/index.js +450 -103
  20. package/dist/src/engine/prune.js +2 -2
  21. package/dist/src/engine/scip-import.js +408 -0
  22. package/dist/src/engine/symbol-delete.js +1 -1
  23. package/dist/src/execution-profile.js +203 -0
  24. package/dist/src/failure-diagnosis.js +69 -10
  25. package/dist/src/hook-manager-integration.js +156 -0
  26. package/dist/src/index-activity.js +1 -1
  27. package/dist/src/init-progress-worker.js +1 -1
  28. package/dist/src/init.js +361 -92
  29. package/dist/src/lifecycle-health.js +51 -14
  30. package/dist/src/lsp-readonly.js +1 -1
  31. package/dist/src/output-compression.js +1 -1
  32. package/dist/src/output-telemetry.js +8 -4
  33. package/dist/src/pr-triage.js +2 -2
  34. package/dist/src/progressive-evidence.js +473 -0
  35. package/dist/src/pure-compression-cli.js +101 -0
  36. package/dist/src/release-preflight.js +510 -0
  37. package/dist/src/repair-progress-worker.js +1 -1
  38. package/dist/src/repository-init-process.js +1 -1
  39. package/dist/src/repository-management.js +145 -3
  40. package/dist/src/response-budget.js +11 -1
  41. package/dist/src/server.js +23 -3
  42. package/dist/src/structural-fast-path.js +303 -0
  43. package/dist/src/structural-snapshot.js +33 -0
  44. package/dist/src/system-config.js +7 -7
  45. package/dist/src/tools/knodin-tools.js +106 -15
  46. package/dist/src/update-ceremony.js +158 -0
  47. package/dist/src/update-policy.js +4 -4
  48. package/dist/src/wait-for-fresh.js +2 -2
  49. package/dist/src/worktree-lifecycle.js +2 -2
  50. package/docs/CLI.md +22 -0
  51. package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
  52. package/docs/CONTAINED-EXECUTION.md +77 -0
  53. package/docs/DIAGNOSTICS.md +45 -0
  54. package/docs/DOCTOR-AND-UPDATES.md +7 -4
  55. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  56. package/docs/INSTALLATION.md +1 -1
  57. package/docs/MCP.md +15 -0
  58. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  59. package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
  60. package/docs/SCIP-IMPORT.md +57 -0
  61. package/docs/SIGNED-UPDATES.md +5 -0
  62. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
  63. package/docs/TELEMETRY.md +6 -2
  64. package/docs/releases/0.6.0.md +18 -0
  65. package/docs/releases/0.7.0.md +24 -0
  66. package/docs/releases/0.7.1.md +21 -0
  67. package/docs/releases/0.7.2.md +21 -0
  68. package/docs/releases/0.7.3.md +23 -0
  69. package/package.json +34 -3
  70. package/dist/src/tools/reckon-tools.js +0 -5
package/dist/src/init.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import child_process from "node:child_process";
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { configureProjectAgents, } from "./agent-integration.js";
5
6
  import { isIndexableSourcePath } from "./engine/source-policy.js";
7
+ import { inspectLefthookIntegration, installHookManagerIntegration, isActiveLefthookHook, } from "./hook-manager-integration.js";
6
8
  import { registerInitializedWorktree } from "./worktree-lifecycle.js";
7
9
  const MANAGED_MARKER = "KNODIN MANAGED HOOK";
8
- const LEGACY_MANAGED_MARKER = "RECKON-GRAPH MANAGED HOOK";
9
10
  const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
10
11
  const HOOK_LOG_MAX_BYTES = 1_048_576;
11
12
  const HOOK_FAILURE_FILE = "last-refresh-failure";
@@ -40,6 +41,203 @@ function runGit(repo, args, encoding = "utf-8") {
40
41
  stdio: ["ignore", "pipe", "ignore"],
41
42
  });
42
43
  }
44
+ const MUTATION_AUDIT_MAX_ENTRIES = 2_048;
45
+ const MUTATION_AUDIT_MAX_FILE_BYTES = 8 * 1_024 * 1_024;
46
+ function boundedDirectoryEntries(directory) {
47
+ try {
48
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
49
+ if (entries.length > MUTATION_AUDIT_MAX_ENTRIES)
50
+ throw new Error(`${directory}: configure mutation audit exceeds ${MUTATION_AUDIT_MAX_ENTRIES} managed entries`);
51
+ return entries;
52
+ }
53
+ catch (error) {
54
+ if (error.code === "ENOENT")
55
+ return [];
56
+ throw error;
57
+ }
58
+ }
59
+ function configureMutationAuditTargets(repo) {
60
+ const commonRaw = runGit(repo, [
61
+ "rev-parse",
62
+ "--path-format=absolute",
63
+ "--git-common-dir",
64
+ ]).trim();
65
+ const common = path.isAbsolute(commonRaw) ? commonRaw : path.resolve(repo, commonRaw);
66
+ const excludeRaw = runGit(repo, [
67
+ "rev-parse",
68
+ "--path-format=absolute",
69
+ "--git-path",
70
+ "info/exclude",
71
+ ]).trim();
72
+ const exclude = path.isAbsolute(excludeRaw) ? excludeRaw : path.resolve(repo, excludeRaw);
73
+ const effectiveHooks = effectiveHooksDirectory(repo);
74
+ const targets = new Map();
75
+ const add = (target) => {
76
+ const key = path.resolve(target.absolute);
77
+ if (!targets.has(key))
78
+ targets.set(key, { ...target, absolute: key });
79
+ };
80
+ for (const relative of [
81
+ "AGENTS.md",
82
+ "GEMINI.md",
83
+ ".mcp.json",
84
+ ".codex",
85
+ ".codex/config.toml",
86
+ ".gemini",
87
+ ".gemini/settings.json",
88
+ ".agents",
89
+ ".agents/mcp_config.json",
90
+ ".gitignore",
91
+ ".claude",
92
+ ".claude/hooks",
93
+ ".claude/hooks/nudge.sh",
94
+ ".claude/settings.example.json",
95
+ ".knodin",
96
+ ".knodin/integration.json",
97
+ ".knodin/indexer.log",
98
+ ".knodin/indexer.log.1",
99
+ "lefthook-local.yml",
100
+ "lefthook-local.yaml",
101
+ ".lefthook-local.yml",
102
+ ".lefthook-local.yaml",
103
+ ".config/lefthook-local.yml",
104
+ ".config/lefthook-local.yaml",
105
+ ])
106
+ add({ absolute: path.join(repo, relative), label: relative });
107
+ for (const relative of [
108
+ ".knodin/hooks",
109
+ ".knodin/hooks/background-index.sh",
110
+ ".knodin/hooks/manager-dispatch.sh",
111
+ ".knodin/hooks/events",
112
+ ".knodin/hooks/last-refresh-event",
113
+ ".knodin/hooks/last-refresh-failure",
114
+ ".knodin/hooks/refresh.lock",
115
+ ".knodin/hooks/refresh.lock/pid",
116
+ ])
117
+ add({ absolute: path.join(repo, relative), label: relative });
118
+ for (const entry of boundedDirectoryEntries(path.join(repo, ".knodin", "hooks", "events")))
119
+ add({
120
+ absolute: path.join(repo, ".knodin", "hooks", "events", entry.name),
121
+ label: `.knodin/hooks/events/${entry.name}`,
122
+ });
123
+ add({ absolute: path.join(common, "config"), label: ".git/config" });
124
+ add({ absolute: exclude, label: ".git/info/exclude" });
125
+ add({ absolute: path.join(common, "knodin"), label: ".git/knodin" });
126
+ add({
127
+ absolute: path.join(common, "knodin", "worktrees.json"),
128
+ label: ".git/knodin/worktrees.json",
129
+ });
130
+ const defaultHooks = path.join(common, "hooks");
131
+ const managedHooks = path.join(common, "knodin-hooks");
132
+ add({ absolute: defaultHooks, label: ".git/hooks" });
133
+ add({ absolute: managedHooks, label: ".git/knodin-hooks" });
134
+ const effectiveLabel = path.resolve(effectiveHooks) === path.resolve(defaultHooks)
135
+ ? ".git/hooks"
136
+ : path.resolve(effectiveHooks) === path.resolve(managedHooks)
137
+ ? ".git/knodin-hooks"
138
+ : "effective-git-hooks";
139
+ add({ absolute: effectiveHooks, label: effectiveLabel });
140
+ for (const hookRoot of [
141
+ { absolute: defaultHooks, label: ".git/hooks" },
142
+ { absolute: managedHooks, label: ".git/knodin-hooks" },
143
+ { absolute: effectiveHooks, label: effectiveLabel },
144
+ ]) {
145
+ for (const entry of boundedDirectoryEntries(hookRoot.absolute))
146
+ add({
147
+ absolute: path.join(hookRoot.absolute, entry.name),
148
+ label: `${hookRoot.label}/${entry.name}`,
149
+ });
150
+ }
151
+ for (const hookName of HOOK_NAMES) {
152
+ add({ absolute: path.join(effectiveHooks, hookName), label: `${effectiveLabel}/${hookName}` });
153
+ add({
154
+ absolute: path.join(effectiveHooks, `${hookName}.knodin-original`),
155
+ label: `${effectiveLabel}/${hookName}.knodin-original`,
156
+ });
157
+ }
158
+ for (const hookRoot of [
159
+ { absolute: defaultHooks, label: ".git/hooks" },
160
+ { absolute: managedHooks, label: ".git/knodin-hooks" },
161
+ ]) {
162
+ for (const hookName of HOOK_NAMES) {
163
+ add({
164
+ absolute: path.join(hookRoot.absolute, hookName),
165
+ label: `${hookRoot.label}/${hookName}`,
166
+ });
167
+ add({
168
+ absolute: path.join(hookRoot.absolute, `${hookName}.knodin-original`),
169
+ label: `${hookRoot.label}/${hookName}.knodin-original`,
170
+ });
171
+ }
172
+ }
173
+ const configured = configuredHooksPath(repo);
174
+ const relativeConfigured = configured ? repoRelativeHooksPath(repo, configured) : null;
175
+ if (relativeConfigured && hasTrackedHooks(repo, relativeConfigured)) {
176
+ const configuredRoot = path.join(repo, relativeConfigured);
177
+ for (const entry of boundedDirectoryEntries(configuredRoot)) {
178
+ if (!entry.isFile() || HOOK_NAMES.includes(entry.name))
179
+ continue;
180
+ const source = path.join(configuredRoot, entry.name);
181
+ if ((fs.statSync(source).mode & 0o111) === 0)
182
+ continue;
183
+ add({
184
+ absolute: path.join(managedHooks, entry.name),
185
+ label: `.git/knodin-hooks/${entry.name}`,
186
+ });
187
+ }
188
+ }
189
+ if (targets.size > MUTATION_AUDIT_MAX_ENTRIES)
190
+ throw new Error(`configure mutation audit exceeds ${MUTATION_AUDIT_MAX_ENTRIES} managed paths`);
191
+ return [...targets.values()];
192
+ }
193
+ function fileSignature(filePath, allowOversized = false) {
194
+ try {
195
+ const stat = fs.lstatSync(filePath);
196
+ if (stat.isDirectory())
197
+ return `directory:${stat.mode & 0o777}`;
198
+ if (stat.isSymbolicLink()) {
199
+ const link = fs.readlinkSync(filePath);
200
+ const resolved = fs.realpathSync(filePath);
201
+ return `symlink:${stat.mode & 0o777}:${link}:${fileSignature(resolved, allowOversized)}`;
202
+ }
203
+ if (!stat.isFile())
204
+ throw new Error(`${filePath}: unsupported filesystem entry`);
205
+ if (stat.size > MUTATION_AUDIT_MAX_FILE_BYTES && !allowOversized)
206
+ throw new Error(`${filePath}: configure mutation audit refuses files larger than ${MUTATION_AUDIT_MAX_FILE_BYTES} bytes`);
207
+ if (stat.size > MUTATION_AUDIT_MAX_FILE_BYTES)
208
+ return `oversized-file:${stat.mode & 0o777}:${stat.size}`;
209
+ const digest = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
210
+ return `file:${stat.mode & 0o777}:${stat.size}:${digest}`;
211
+ }
212
+ catch (error) {
213
+ if (error.code === "ENOENT")
214
+ return null;
215
+ throw error;
216
+ }
217
+ }
218
+ function mutationSnapshot(targets, allowOversized = false) {
219
+ const snapshot = new Map();
220
+ for (const target of targets) {
221
+ const rootSignature = fileSignature(target.absolute, allowOversized);
222
+ if (rootSignature !== null)
223
+ snapshot.set(target.label, rootSignature);
224
+ }
225
+ return snapshot;
226
+ }
227
+ function filesystemChanges(before, after) {
228
+ return [...new Set([...before.keys(), ...after.keys()])].sort().flatMap((changedPath) => {
229
+ const prior = before.get(changedPath);
230
+ const current = after.get(changedPath);
231
+ if (prior === current)
232
+ return [];
233
+ return [
234
+ {
235
+ path: changedPath,
236
+ action: prior === undefined ? "created" : current === undefined ? "removed" : "modified",
237
+ },
238
+ ];
239
+ });
240
+ }
43
241
  function shellQuote(value) {
44
242
  return `'${value.replaceAll("'", `'\\''`)}'`;
45
243
  }
@@ -78,14 +276,14 @@ export async function refreshFromGitEvent(repo, event, index) {
78
276
  changed = diffPaths(resolvedRepo, event.before, event.after);
79
277
  }
80
278
  else {
81
- const hooksRoot = path.join(resolvedRepo, ".reckon", "hooks");
279
+ const hooksRoot = path.join(resolvedRepo, ".knodin", "hooks");
82
280
  const inputPath = path.resolve(event.inputPath);
83
281
  const relativeInput = path.relative(hooksRoot, inputPath);
84
282
  if (relativeInput === "" ||
85
283
  relativeInput === ".." ||
86
284
  relativeInput.startsWith(`..${path.sep}`) ||
87
285
  path.isAbsolute(relativeInput)) {
88
- throw new Error("knodin hook refresh: rewrite input must be inside .reckon/hooks");
286
+ throw new Error("knodin hook refresh: rewrite input must be inside .knodin/hooks");
89
287
  }
90
288
  const [realHooksRoot, realInputPath, inputStat] = await Promise.all([
91
289
  fs.promises.realpath(hooksRoot),
@@ -99,7 +297,7 @@ export async function refreshFromGitEvent(repo, event, index) {
99
297
  realRelativeInput === ".." ||
100
298
  realRelativeInput.startsWith(`..${path.sep}`) ||
101
299
  path.isAbsolute(realRelativeInput)) {
102
- throw new Error("knodin hook refresh: rewrite input must be a regular file inside .reckon/hooks");
300
+ throw new Error("knodin hook refresh: rewrite input must be a regular file inside .knodin/hooks");
103
301
  }
104
302
  rewriteInputToRemove = inputPath;
105
303
  const pairs = (await fs.promises.readFile(inputPath, "utf-8"))
@@ -130,11 +328,11 @@ set -u
130
328
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
131
329
  [ -n "$REPO_ROOT" ] || exit 0
132
330
  cd "$REPO_ROOT" || exit 0
133
- mkdir -p "$REPO_ROOT/.reckon"
134
- LOCK_DIR="$REPO_ROOT/.reckon/hooks/refresh.lock"
135
- EVENT_DIR="$REPO_ROOT/.reckon/hooks/events"
136
- LAST_EVENT_PATH="$REPO_ROOT/.reckon/hooks/last-refresh-event"
137
- FAILURE_PATH="$REPO_ROOT/.reckon/hooks/${HOOK_FAILURE_FILE}"
331
+ mkdir -p "$REPO_ROOT/.knodin"
332
+ LOCK_DIR="$REPO_ROOT/.knodin/hooks/refresh.lock"
333
+ EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
334
+ LAST_EVENT_PATH="$REPO_ROOT/.knodin/hooks/last-refresh-event"
335
+ FAILURE_PATH="$REPO_ROOT/.knodin/hooks/${HOOK_FAILURE_FILE}"
138
336
  FAILURE_TMP="$FAILURE_PATH.$$"
139
337
  mkdir -p "$EVENT_DIR"
140
338
  if [ "$#" -gt 0 ]; then
@@ -165,7 +363,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
165
363
  fi
166
364
  printf '%s\\n' "$$" > "$LOCK_DIR/pid"
167
365
  trap 'rm -rf "$LOCK_DIR"; rm -f "$FAILURE_TMP"' EXIT HUP INT TERM
168
- LOG_PATH="$REPO_ROOT/.reckon/indexer.log"
366
+ LOG_PATH="$REPO_ROOT/.knodin/indexer.log"
169
367
  if [ -f "$LOG_PATH" ]; then
170
368
  LOG_BYTES="$(wc -c < "$LOG_PATH" 2>/dev/null | tr -d '[:space:]')"
171
369
  case "$LOG_BYTES" in ''|*[!0-9]*) LOG_BYTES=0 ;; esac
@@ -197,7 +395,7 @@ while :; do
197
395
  {
198
396
  printf 'knodin background refresh failed with exit code %s.\\n' "$STATUS"
199
397
  printf 'Pending event retained at %s.\\n' "$EVENT"
200
- printf 'See .reckon/indexer.log for details.\\n'
398
+ printf 'See .knodin/indexer.log for details.\\n'
201
399
  } > "$FAILURE_TMP"
202
400
  mv -f "$FAILURE_TMP" "$FAILURE_PATH"
203
401
  exit "$STATUS"
@@ -206,7 +404,7 @@ done
206
404
  `;
207
405
  }
208
406
  function queuedLifecycleEvents(repo) {
209
- const eventDirectory = path.join(repo, ".reckon", "hooks", "events");
407
+ const eventDirectory = path.join(repo, ".knodin", "hooks", "events");
210
408
  try {
211
409
  return fs.readdirSync(eventDirectory).filter((entry) => entry.endsWith(".event"));
212
410
  }
@@ -216,7 +414,7 @@ function queuedLifecycleEvents(repo) {
216
414
  }
217
415
  function refreshProcessorIsRunning(repo) {
218
416
  try {
219
- const pid = Number(fs.readFileSync(path.join(repo, ".reckon", "hooks", "refresh.lock", "pid"), "utf-8"));
417
+ const pid = Number(fs.readFileSync(path.join(repo, ".knodin", "hooks", "refresh.lock", "pid"), "utf-8"));
220
418
  if (!Number.isInteger(pid) || pid <= 0)
221
419
  return false;
222
420
  process.kill(pid, 0);
@@ -238,7 +436,7 @@ function drainQueuedLifecycleEvents(repo, backgroundPath) {
238
436
  });
239
437
  if (result.status !== 0) {
240
438
  const detail = result.error?.message || result.stderr.trim() || `exit ${result.status ?? "unknown"}`;
241
- throw new Error(`knodin init: queued lifecycle refresh could not be drained (${detail}); run ${backgroundPath} after resolving .reckon/hooks/last-refresh-failure`);
439
+ throw new Error(`knodin init: queued lifecycle refresh could not be drained (${detail}); run ${backgroundPath} after resolving .knodin/hooks/last-refresh-failure`);
242
440
  }
243
441
  const remaining = queuedLifecycleEvents(repo);
244
442
  if (remaining.length === 0)
@@ -258,7 +456,7 @@ function managedHookScript(hookName, originalPath) {
258
456
  : shellQuote(originalPath)
259
457
  : "";
260
458
  const runOriginal = originalPath
261
- ? `${original} "$@"${hookName === "post-rewrite" ? ' < "$REWRITE_INPUT"' : ""} || ORIGINAL_STATUS=$?`
459
+ ? `KNODIN_WRAPPER_ACTIVE=1 ${original} "$@"${hookName === "post-rewrite" ? ' < "$REWRITE_INPUT"' : ""} || ORIGINAL_STATUS=$?`
262
460
  : ":";
263
461
  let trigger = "";
264
462
  if (hookName === "post-commit")
@@ -272,14 +470,14 @@ AFTER="$(git rev-parse HEAD 2>/dev/null || true)"
272
470
  [ -n "$BEFORE" ] && [ -n "$AFTER" ] && trigger_knodin merge "$BEFORE" "$AFTER"`;
273
471
  }
274
472
  const rewriteWithoutIndexer = originalPath
275
- ? `${original} "$@"
473
+ ? `KNODIN_WRAPPER_ACTIVE=1 ${original} "$@"
276
474
  exit $?`
277
475
  : "exit 0";
278
476
  const rewriteSetup = hookName === "post-rewrite"
279
477
  ? `if [ ! -x "$BACKGROUND" ]; then
280
478
  ${rewriteWithoutIndexer}
281
479
  fi
282
- REWRITE_INPUT="$(mktemp "$REPO_ROOT/.reckon/hooks/rewrite-input.XXXXXX")" || exit 0
480
+ REWRITE_INPUT="$(mktemp "$REPO_ROOT/.knodin/hooks/rewrite-input.XXXXXX")" || exit 0
283
481
  cat > "$REWRITE_INPUT"
284
482
  `
285
483
  : "";
@@ -289,7 +487,7 @@ cat > "$REWRITE_INPUT"
289
487
  # ${MANAGED_MARKER}: ${hookName}
290
488
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
291
489
  [ -n "$REPO_ROOT" ] || exit 0
292
- BACKGROUND="$REPO_ROOT/.reckon/hooks/background-index.sh"
490
+ BACKGROUND="$REPO_ROOT/.knodin/hooks/background-index.sh"
293
491
  ORIGINAL_STATUS=0
294
492
  trigger_knodin() {
295
493
  [ -x "$BACKGROUND" ] || return 0
@@ -304,7 +502,7 @@ trigger_knodin() {
304
502
  EVENT_BEFORE="$(sed -n '1{s/ .*//;p;}' "$2")"
305
503
  EVENT_AFTER="$(sed -n '1{s/^[^ ]* //;s/ .*//;p;}' "$2")"
306
504
  fi
307
- EVENT_DIR="$REPO_ROOT/.reckon/hooks/events"
505
+ EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
308
506
  mkdir -p "$EVENT_DIR" || return 0
309
507
  EVENT_TMP="$EVENT_DIR/$(date +%s).$$.$PPID.tmp"
310
508
  EVENT_PATH="$EVENT_TMP.event"
@@ -319,7 +517,7 @@ trigger_knodin() {
319
517
  "$EVENT_SEQUENCE" "$EVENT_WORKTREE" "$EVENT_AT" "$EVENT_BEFORE" "$EVENT_AFTER"
320
518
  } > "$EVENT_TMP" || return 0
321
519
  mv "$EVENT_TMP" "$EVENT_PATH" || return 0
322
- if [ "\${RECKON_HOOK_FOREGROUND:-0}" = "1" ]; then
520
+ if [ "\${KNODIN_HOOK_FOREGROUND:-0}" = "1" ]; then
323
521
  "$BACKGROUND"
324
522
  else
325
523
  nohup "$BACKGROUND" >/dev/null 2>&1 &
@@ -346,13 +544,12 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
346
544
  let originalPath = preservedOriginal ?? null;
347
545
  if (preservedOriginal === undefined && fs.existsSync(hookPath)) {
348
546
  const content = await fs.promises.readFile(hookPath, "utf-8");
349
- if (content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`) ||
350
- content.startsWith(`#!/bin/sh\n# ${LEGACY_MANAGED_MARKER}: ${hookName}\n`)) {
547
+ if (content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`)) {
351
548
  const match = content.match(/^# knodin original: (.+)$/m);
352
549
  originalPath = match?.[1] ?? null;
353
550
  }
354
551
  else {
355
- const backupPath = `${hookPath}.reckon-original`;
552
+ const backupPath = `${hookPath}.knodin-original`;
356
553
  const mode = (await fs.promises.stat(hookPath)).mode;
357
554
  await fs.promises.copyFile(hookPath, backupPath);
358
555
  await fs.promises.rm(hookPath, { force: true });
@@ -365,9 +562,7 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
365
562
  if (!fs.existsSync(candidate))
366
563
  break;
367
564
  const candidateContent = await fs.promises.readFile(candidate, "utf-8");
368
- if (candidateContent === canonical ||
369
- candidateContent.includes(MANAGED_MARKER) ||
370
- candidateContent.includes(LEGACY_MANAGED_MARKER))
565
+ if (candidateContent === canonical || candidateContent.includes(MANAGED_MARKER))
371
566
  await fs.promises.rm(candidate, { force: true });
372
567
  }
373
568
  // Match Git's semantics: preserve inactive hook files as backups, but
@@ -388,7 +583,6 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
388
583
  await fs.promises.chmod(hookPath, 0o755);
389
584
  }
390
585
  const ORIGINAL_HOOKS_CONFIG = "knodin.originalHooksPath";
391
- const LEGACY_ORIGINAL_HOOKS_CONFIG = "reckon.originalHooksPath";
392
586
  function effectiveHooksDirectory(repo) {
393
587
  let raw;
394
588
  try {
@@ -405,15 +599,10 @@ function configuredHooksPath(repo) {
405
599
  }
406
600
  catch {
407
601
  try {
408
- return (runGit(repo, ["config", "--local", "--get", LEGACY_ORIGINAL_HOOKS_CONFIG]).trim() || null);
602
+ return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
409
603
  }
410
604
  catch {
411
- try {
412
- return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
413
- }
414
- catch {
415
- return null;
416
- }
605
+ return null;
417
606
  }
418
607
  }
419
608
  }
@@ -494,10 +683,10 @@ export const SETTINGS_TEMPLATE = `{
494
683
  }
495
684
  }
496
685
  `;
497
- const RECKON_AGENT_INSTRUCTIONS = `<!-- knodin:start -->
686
+ const KNODIN_AGENT_INSTRUCTIONS = `<!-- knodin:start -->
498
687
  ## knodin — Local Code Intelligence
499
688
 
500
- This repository is indexed locally by knodin. Start with \`knodin status\`; if it reports \`repair-needed\`, run \`knodin repair\` before relying on graph evidence.
689
+ This repository is indexed locally by knodin. Start with \`knodin status\`; if it reports \`repair-needed\`, follow its repair steps before relying on graph evidence. Use \`knodin repair\` for graph damage and \`knodin init\` for displaced lifecycle hooks.
501
690
 
502
691
  - When the single connected knodin MCP tool is available, use it directly. For cold or unfamiliar work, call its \`context\` operation before broad Read/Grep/Glob traversal. Use shell commands only as a fallback when the gateway is unavailable.
503
692
  - Before a meaningful edit, call the gateway's \`query\` operation with the \`impact\` pattern (CLI fallback: \`knodin query impact <symbol> --direction upstream\`) and report material risk to the user.
@@ -531,38 +720,50 @@ async function installManagedAgentInstructions(repo, fileName = "AGENTS.md") {
531
720
  await assertSafeRepositoryFile(repo, fileName);
532
721
  const instructionsPath = path.join(repo, fileName);
533
722
  let existing = "";
723
+ let existed = true;
534
724
  try {
535
725
  existing = await fs.promises.readFile(instructionsPath, "utf-8");
536
726
  }
537
727
  catch (error) {
538
728
  if (error.code !== "ENOENT")
539
729
  throw error;
730
+ existed = false;
540
731
  }
541
732
  const canonicalStart = "<!-- knodin:start -->";
542
733
  const canonicalEnd = "<!-- knodin:end -->";
543
- const legacyStart = "<!-- reckon-graph:start -->";
544
- const legacyEnd = "<!-- reckon-graph:end -->";
545
- const usesCanonical = existing.includes(canonicalStart);
546
- const startMarker = usesCanonical ? canonicalStart : legacyStart;
547
- const endMarker = usesCanonical ? canonicalEnd : legacyEnd;
548
- const start = existing.indexOf(startMarker);
549
- const end = existing.indexOf(endMarker);
734
+ const start = existing.indexOf(canonicalStart);
735
+ const end = existing.indexOf(canonicalEnd);
550
736
  const next = start >= 0 && end >= start
551
- ? `${existing.slice(0, start)}${RECKON_AGENT_INSTRUCTIONS}${existing.slice(end + endMarker.length).replace(/^\r?\n/, "")}`
737
+ ? `${existing.slice(0, start)}${KNODIN_AGENT_INSTRUCTIONS}${existing.slice(end + canonicalEnd.length).replace(/^\r?\n/, "")}`
552
738
  : existing.length > 0
553
- ? `${existing.replace(/\s*$/, "")}\n\n${RECKON_AGENT_INSTRUCTIONS}`
554
- : RECKON_AGENT_INSTRUCTIONS;
555
- if (next !== existing)
556
- await fs.promises.writeFile(instructionsPath, next, "utf-8");
739
+ ? `${existing.trimEnd()}\n\n${KNODIN_AGENT_INSTRUCTIONS}`
740
+ : KNODIN_AGENT_INSTRUCTIONS;
741
+ if (next === existing)
742
+ return null;
743
+ await fs.promises.writeFile(instructionsPath, next, "utf-8");
744
+ return { path: fileName, action: existed ? "modified" : "created" };
557
745
  }
558
746
  async function hasManagedAgentInstructions(repo, fileName = "AGENTS.md") {
559
747
  try {
560
- return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(RECKON_AGENT_INSTRUCTIONS);
748
+ return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(KNODIN_AGENT_INSTRUCTIONS);
561
749
  }
562
750
  catch {
563
751
  return false;
564
752
  }
565
753
  }
754
+ function removeManagedAgentBlock(content) {
755
+ const start = content.indexOf("<!-- knodin:start -->");
756
+ const end = content.indexOf("<!-- knodin:end -->", start);
757
+ if (start < 0 || end < start)
758
+ return null;
759
+ const prefix = content.slice(0, start);
760
+ const suffix = content.slice(end + "<!-- knodin:end -->".length);
761
+ // The canonical marker-only file owns its final line terminator as part of
762
+ // the generated block. Any other prefix/suffix bytes remain untouched.
763
+ if (prefix.length === 0 && /^\r?\n$/.test(suffix))
764
+ return "";
765
+ return `${prefix}${suffix}`;
766
+ }
566
767
  function isTracked(repo, relativePath) {
567
768
  try {
568
769
  return nulPaths(repo, ["ls-files", "--", relativePath]).length > 0;
@@ -589,9 +790,7 @@ async function excludeUntrackedPaths(repo, candidates) {
589
790
  }
590
791
  const startMarker = "# knodin:start";
591
792
  const endMarker = "# knodin:end";
592
- const outside = existing
593
- .replace(/(?:^|\n)# knodin:start\n[\s\S]*?\n# knodin:end\n?/, "\n")
594
- .replace(/(?:^|\n)# reckon-graph:start\n[\s\S]*?\n# reckon-graph:end\n?/, "\n");
793
+ const outside = existing.replace(/(?:^|\n)# knodin:start\n[\s\S]*?\n# knodin:end\n?/, "\n");
595
794
  const outsideLines = new Set(outside.split(/\r?\n/));
596
795
  const owned = safe.filter((candidate) => !outsideLines.has(candidate));
597
796
  const prefix = outside && !outside.endsWith("\n") ? `${outside}\n` : outside;
@@ -618,38 +817,35 @@ async function removeManagedExcludes(repo) {
618
817
  await fs.promises.writeFile(excludePath, next, "utf-8");
619
818
  }
620
819
  async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = false) {
820
+ const changes = [];
621
821
  for (const fileName of ["AGENTS.md", "GEMINI.md"]) {
622
822
  const instructionsPath = path.join(repo, fileName);
623
823
  const tracked = isTracked(repo, fileName);
624
824
  try {
825
+ await assertSafeRepositoryFile(repo, fileName);
625
826
  const current = await fs.promises.readFile(instructionsPath, "utf-8");
827
+ const start = current.indexOf("<!-- knodin:start -->");
828
+ const end = current.indexOf("<!-- knodin:end -->", start);
829
+ const without = removeManagedAgentBlock(current);
626
830
  if (!tracked) {
627
- const start = current.indexOf("<!-- knodin:start -->");
628
- const end = current.indexOf("<!-- knodin:end -->");
629
- if (start >= 0 && end >= start) {
630
- const without = `${current.slice(0, start)}${current.slice(end + "<!-- knodin:end -->".length)}`
631
- .replace(/\n{3,}/g, "\n\n")
632
- .trim();
633
- if (without)
634
- await fs.promises.writeFile(instructionsPath, `${without}\n`, "utf-8");
635
- else
831
+ if (without !== null) {
832
+ if (without.length > 0) {
833
+ await fs.promises.writeFile(instructionsPath, without, "utf-8");
834
+ changes.push({ path: fileName, action: "modified" });
835
+ }
836
+ else {
636
837
  await fs.promises.rm(instructionsPath, { force: true });
838
+ changes.push({ path: fileName, action: "removed" });
839
+ }
637
840
  }
638
841
  continue;
639
842
  }
640
- const head = runGit(repo, ["show", `HEAD:${fileName}`]);
641
- const start = current.indexOf("<!-- knodin:start -->");
642
- const end = current.indexOf("<!-- knodin:end -->");
643
- if (allowTrackedTransition && start >= 0 && end >= start) {
644
- const without = `${current.slice(0, start)}${current.slice(end + "<!-- knodin:end -->".length)}`
645
- .replace(/\n{3,}/g, "\n\n")
646
- .trimEnd();
647
- if (without)
648
- await fs.promises.writeFile(instructionsPath, `${without}\n`, "utf-8");
649
- else
650
- await fs.promises.rm(instructionsPath, { force: true });
843
+ if (allowTrackedTransition && without !== null) {
844
+ await fs.promises.writeFile(instructionsPath, without, "utf-8");
845
+ changes.push({ path: fileName, action: "modified" });
651
846
  continue;
652
847
  }
848
+ const head = runGit(repo, ["show", `HEAD:${fileName}`]);
653
849
  if (start >= 0 && end >= start && !head.includes("<!-- knodin:start -->")) {
654
850
  const without = `${current.slice(0, start)}${current.slice(end + "<!-- knodin:end -->".length)}`
655
851
  .replace(/\n{3,}/g, "\n\n")
@@ -666,7 +862,7 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
666
862
  try {
667
863
  const current = await fs.promises.readFile(gitIgnorePath, "utf-8");
668
864
  const next = current
669
- .replace(/(?:^|\n)# knodin:start\n\.reckon\/\n# knodin:end\n?/g, "\n")
865
+ .replace(/(?:^|\n)# knodin:start\n\.knodin\/\n# knodin:end\n?/g, "\n")
670
866
  .trim();
671
867
  if (isTracked(repo, ".gitignore")) {
672
868
  const head = runGit(repo, ["show", "HEAD:.gitignore"]);
@@ -699,10 +895,11 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
699
895
  // Missing or user-modified files are intentionally preserved.
700
896
  }
701
897
  }
898
+ return changes;
702
899
  }
703
900
  export function readRepositoryIntegrationConfig(repo) {
704
901
  try {
705
- const value = JSON.parse(fs.readFileSync(path.join(repo, ".reckon", "integration.json"), "utf-8"));
902
+ const value = JSON.parse(fs.readFileSync(path.join(repo, ".knodin", "integration.json"), "utf-8"));
706
903
  if ((value.scope === "personal" || value.scope === "team" || value.scope === "cli-only") &&
707
904
  Array.isArray(value.agents))
708
905
  return { scope: value.scope, agents: value.agents };
@@ -712,6 +909,22 @@ export function readRepositoryIntegrationConfig(repo) {
712
909
  }
713
910
  return null;
714
911
  }
912
+ function hasSafeManagedAgentBlock(repo, relativePath) {
913
+ try {
914
+ let current = repo;
915
+ for (const part of relativePath.split(path.sep)) {
916
+ current = path.join(current, part);
917
+ if (fs.lstatSync(current).isSymbolicLink())
918
+ return false;
919
+ }
920
+ const content = fs.readFileSync(path.join(repo, relativePath), "utf-8");
921
+ const start = content.indexOf("<!-- knodin:start -->");
922
+ return start >= 0 && content.indexOf("<!-- knodin:end -->", start) >= start;
923
+ }
924
+ catch {
925
+ return false;
926
+ }
927
+ }
715
928
  /** Detect repository-owned client configuration independently of knodin's local receipt. */
716
929
  export function inspectRepositoryIntegrationStatus(repo) {
717
930
  const receipt = readRepositoryIntegrationConfig(repo);
@@ -720,13 +933,27 @@ export function inspectRepositoryIntegrationStatus(repo) {
720
933
  { agent: "codex", path: ".codex/config.toml", marker: 'mcp_servers."knodin"' },
721
934
  { agent: "gemini", path: ".gemini/settings.json", marker: '"knodin"' },
722
935
  { agent: "antigravity", path: ".agents/mcp_config.json", marker: '"knodin"' },
936
+ {
937
+ agent: "codex",
938
+ path: "AGENTS.md",
939
+ marker: "<!-- knodin:start -->",
940
+ managedGuidance: true,
941
+ },
942
+ {
943
+ agent: "gemini",
944
+ path: "GEMINI.md",
945
+ marker: "<!-- knodin:start -->",
946
+ managedGuidance: true,
947
+ },
723
948
  ];
724
- const configurations = candidates.map(({ agent, path: relative, marker }) => {
949
+ const configurations = candidates.map(({ agent, path: relative, marker, managedGuidance }) => {
725
950
  try {
726
951
  return {
727
952
  agent,
728
953
  path: relative,
729
- present: fs.readFileSync(path.join(repo, relative), "utf-8").includes(marker),
954
+ present: managedGuidance
955
+ ? hasSafeManagedAgentBlock(repo, relative)
956
+ : fs.readFileSync(path.join(repo, relative), "utf-8").includes(marker),
730
957
  };
731
958
  }
732
959
  catch {
@@ -745,10 +972,10 @@ export function inspectRepositoryIntegrationStatus(repo) {
745
972
  };
746
973
  }
747
974
  async function writeRepositoryIntegrationConfig(repo, config) {
748
- await fs.promises.writeFile(path.join(repo, ".reckon", "integration.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
975
+ await fs.promises.writeFile(path.join(repo, ".knodin", "integration.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
749
976
  }
750
977
  async function ensureTeamGitIgnore(repo) {
751
- if (isTracked(repo, ".reckon"))
978
+ if (isTracked(repo, ".knodin"))
752
979
  return;
753
980
  await assertSafeRepositoryFile(repo, ".gitignore");
754
981
  const ignorePath = path.join(repo, ".gitignore");
@@ -760,10 +987,10 @@ async function ensureTeamGitIgnore(repo) {
760
987
  if (error.code !== "ENOENT")
761
988
  throw error;
762
989
  }
763
- if (existing.split(/\r?\n/).includes(".reckon/"))
990
+ if (existing.split(/\r?\n/).includes(".knodin/"))
764
991
  return;
765
992
  const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing;
766
- await fs.promises.writeFile(ignorePath, `${prefix}# knodin:start\n.reckon/\n# knodin:end\n`, "utf-8");
993
+ await fs.promises.writeFile(ignorePath, `${prefix}# knodin:start\n.knodin/\n# knodin:end\n`, "utf-8");
767
994
  }
768
995
  export function detectTrackedTeamIntegration(repo) {
769
996
  const checks = [
@@ -797,12 +1024,16 @@ export async function initializeRepository(repo, options) {
797
1024
  if (!path.isAbsolute(options.command[0])) {
798
1025
  throw new Error("knodin init: packaged runtime command must be an absolute path");
799
1026
  }
1027
+ const mutationTargets = options.auditConfigurationChanges
1028
+ ? configureMutationAuditTargets(resolvedRepo)
1029
+ : [];
1030
+ const beforeMutation = mutationSnapshot(mutationTargets);
800
1031
  if (scope !== "team" &&
801
1032
  !options.allowTrackedTransition &&
802
1033
  detectTrackedTeamIntegration(resolvedRepo)) {
803
1034
  throw new Error("knodin init: tracked team integration is active; use `knodin configure --scope personal` or `--scope cli-only` for an explicit commit-ready transition");
804
1035
  }
805
- const knodinHooksDir = path.join(resolvedRepo, ".reckon", "hooks");
1036
+ const knodinHooksDir = path.join(resolvedRepo, ".knodin", "hooks");
806
1037
  const hookInstallation = await prepareHookInstallation(resolvedRepo);
807
1038
  const hooksDir = hookInstallation.hooksDir;
808
1039
  await Promise.all([
@@ -815,6 +1046,7 @@ export async function initializeRepository(repo, options) {
815
1046
  mode: 0o755,
816
1047
  });
817
1048
  await fs.promises.chmod(backgroundPath, 0o755);
1049
+ const hookManagerIntegration = installHookManagerIntegration(resolvedRepo);
818
1050
  for (const hookName of HOOK_NAMES) {
819
1051
  let preservedOriginal;
820
1052
  if (hookInstallation.trackedHooksDir) {
@@ -845,12 +1077,16 @@ export async function initializeRepository(repo, options) {
845
1077
  }
846
1078
  const lifecycleRefresh = drainQueuedLifecycleEvents(resolvedRepo, backgroundPath);
847
1079
  await fs.promises.rm(path.join(knodinHooksDir, HOOK_FAILURE_FILE), { force: true });
1080
+ let agentInstructionChanges = [];
848
1081
  if (scope === "team") {
849
- await installManagedAgentInstructions(resolvedRepo, "AGENTS.md");
850
- await installManagedAgentInstructions(resolvedRepo, "GEMINI.md");
1082
+ const installed = await Promise.all([
1083
+ installManagedAgentInstructions(resolvedRepo, "AGENTS.md"),
1084
+ installManagedAgentInstructions(resolvedRepo, "GEMINI.md"),
1085
+ ]);
1086
+ agentInstructionChanges = installed.filter((change) => change !== null);
851
1087
  }
852
1088
  else {
853
- await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
1089
+ agentInstructionChanges = await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
854
1090
  }
855
1091
  const requestedAgents = options.agents ?? [];
856
1092
  const projectPathByAgent = {
@@ -859,6 +1095,7 @@ export async function initializeRepository(repo, options) {
859
1095
  antigravity: ".agents/mcp_config.json",
860
1096
  };
861
1097
  const blockedAgents = [];
1098
+ const externalConfigurationOutcomes = [];
862
1099
  if (scope === "personal" &&
863
1100
  options.allowTrackedTransition &&
864
1101
  priorIntegration?.scope === "team") {
@@ -870,6 +1107,11 @@ export async function initializeRepository(repo, options) {
870
1107
  });
871
1108
  if (cleanup.failed.length > 0)
872
1109
  throw new Error(`knodin configure: could not remove tracked team integration: ${cleanup.failed[0]?.message}`);
1110
+ if (cleanup.removed.includes("claude"))
1111
+ externalConfigurationOutcomes.push({
1112
+ system: "claude-local-mcp",
1113
+ state: "removal-attempted",
1114
+ });
873
1115
  }
874
1116
  const eligibleAgents = scope === "team"
875
1117
  ? requestedAgents
@@ -893,6 +1135,18 @@ export async function initializeRepository(repo, options) {
893
1135
  agents: eligibleAgents,
894
1136
  run: options.runAgentCommand,
895
1137
  });
1138
+ if (scope === "personal" && agentIntegration.configured.includes("claude"))
1139
+ externalConfigurationOutcomes.push({
1140
+ system: "claude-local-mcp",
1141
+ state: "configured-or-present",
1142
+ });
1143
+ if (scope === "cli-only" &&
1144
+ agentIntegration.removed.includes("claude") &&
1145
+ !externalConfigurationOutcomes.some(({ system, state }) => system === "claude-local-mcp" && state === "removal-attempted"))
1146
+ externalConfigurationOutcomes.push({
1147
+ system: "claude-local-mcp",
1148
+ state: "removal-attempted",
1149
+ });
896
1150
  agentIntegration.failed.push(...blockedAgents);
897
1151
  await writeRepositoryIntegrationConfig(resolvedRepo, {
898
1152
  scope,
@@ -905,25 +1159,36 @@ export async function initializeRepository(repo, options) {
905
1159
  else if (scope === "cli-only") {
906
1160
  await removeManagedExcludes(resolvedRepo);
907
1161
  }
908
- const excluded = scope === "team"
1162
+ let excluded = scope === "team"
909
1163
  ? []
910
1164
  : await excludeUntrackedPaths(resolvedRepo, [
911
- ...(isTracked(resolvedRepo, ".reckon") ? [] : [".reckon/"]),
1165
+ ...(isTracked(resolvedRepo, ".knodin") ? [] : [".knodin/"]),
912
1166
  ...agentIntegration.projectFiles,
913
1167
  ]);
1168
+ if (hookManagerIntegration) {
1169
+ const managerExcludes = await excludeUntrackedPaths(resolvedRepo, [
1170
+ hookManagerIntegration.localConfig,
1171
+ ]);
1172
+ excluded = [...new Set([...excluded, ...managerExcludes])].sort();
1173
+ }
914
1174
  registerInitializedWorktree(resolvedRepo);
1175
+ const detectedFilesystemChanges = filesystemChanges(beforeMutation, mutationSnapshot(mutationTargets, true));
915
1176
  return {
916
1177
  scope,
917
- knodin: ".reckon",
918
- database: ".reckon/db.sqlite",
919
- backgroundIndexer: ".reckon/hooks/background-index.sh",
1178
+ knodin: ".knodin",
1179
+ database: ".knodin/db.sqlite",
1180
+ backgroundIndexer: ".knodin/hooks/background-index.sh",
920
1181
  hooksDirectory: path.relative(resolvedRepo, hooksDir) || ".",
921
1182
  gitHooks: [...HOOK_NAMES],
922
1183
  claudeSettings: null,
923
1184
  agentInstructions: scope === "team" ? "AGENTS.md" : null,
924
1185
  agentIntegration,
1186
+ agentInstructionChanges,
1187
+ filesystemChanges: detectedFilesystemChanges,
1188
+ externalConfigurationOutcomes,
925
1189
  excluded,
926
1190
  lifecycleRefresh,
1191
+ hookManagerIntegration,
927
1192
  };
928
1193
  }
929
1194
  async function matchesManagedFile(filePath, expected, executable) {
@@ -956,6 +1221,7 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
956
1221
  catch {
957
1222
  return false;
958
1223
  }
1224
+ const lefthook = inspectLefthookIntegration(resolvedRepo);
959
1225
  const managedHooks = await Promise.all(HOOK_NAMES.map(async (hookName) => {
960
1226
  const hookPath = path.join(hooksDir, hookName);
961
1227
  try {
@@ -967,7 +1233,9 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
967
1233
  let expected = managedHookScript(hookName, originalPath);
968
1234
  if (originalPath)
969
1235
  expected = expected.replace("\nREPO_ROOT=", `\n# knodin original: ${originalPath}\nREPO_ROOT=`);
970
- return stat.isFile() && (stat.mode & 0o111) !== 0 && content === expected;
1236
+ return (stat.isFile() &&
1237
+ (stat.mode & 0o111) !== 0 &&
1238
+ (content === expected || (lefthook.configured && isActiveLefthookHook(content))));
971
1239
  }
972
1240
  catch {
973
1241
  return false;
@@ -979,7 +1247,8 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
979
1247
  ((await hasManagedAgentInstructions(resolvedRepo, "AGENTS.md")) &&
980
1248
  (await hasManagedAgentInstructions(resolvedRepo, "GEMINI.md")));
981
1249
  const integrationCurrent = readRepositoryIntegrationConfig(resolvedRepo)?.scope === scope;
982
- return ((await matchesManagedFile(path.join(resolvedRepo, ".reckon", "hooks", "background-index.sh"), backgroundScript(command), true)) &&
1250
+ return ((await matchesManagedFile(path.join(resolvedRepo, ".knodin", "hooks", "background-index.sh"), backgroundScript(command), true)) &&
983
1251
  sharedFilesCurrent &&
984
- integrationCurrent);
1252
+ integrationCurrent &&
1253
+ (lefthook.localConfig === null || lefthook.configured));
985
1254
  }