knodin 0.5.0 → 0.6.0

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 (41) hide show
  1. package/README.md +166 -534
  2. package/dist/bin/cli.js +5 -5
  3. package/dist/src/agent-integration.js +1 -15
  4. package/dist/src/artifact-refresh.js +1 -1
  5. package/dist/src/competitive-sandbox.js +4 -4
  6. package/dist/src/context-export.js +1 -1
  7. package/dist/src/docs-sections.js +5 -5
  8. package/dist/src/engine/ann-hnsw.js +5 -5
  9. package/dist/src/engine/embeddings.js +4 -4
  10. package/dist/src/engine/index.js +33 -33
  11. package/dist/src/engine/prune.js +2 -2
  12. package/dist/src/engine/symbol-delete.js +1 -1
  13. package/dist/src/index-activity.js +1 -1
  14. package/dist/src/init-progress-worker.js +1 -1
  15. package/dist/src/init.js +42 -59
  16. package/dist/src/lifecycle-health.js +10 -11
  17. package/dist/src/lsp-readonly.js +1 -1
  18. package/dist/src/output-compression.js +1 -1
  19. package/dist/src/output-telemetry.js +4 -4
  20. package/dist/src/pr-triage.js +2 -2
  21. package/dist/src/repair-progress-worker.js +1 -1
  22. package/dist/src/repository-init-process.js +1 -1
  23. package/dist/src/repository-management.js +3 -3
  24. package/dist/src/server.js +1 -1
  25. package/dist/src/system-config.js +7 -7
  26. package/dist/src/tools/knodin-tools.js +2 -2
  27. package/dist/src/update-policy.js +4 -4
  28. package/dist/src/wait-for-fresh.js +2 -2
  29. package/dist/src/worktree-lifecycle.js +2 -2
  30. package/docs/COMMAND-OUTPUT-COMPRESSION.md +1 -1
  31. package/docs/DOCTOR-AND-UPDATES.md +2 -2
  32. package/docs/INSTALLATION.md +5 -5
  33. package/docs/REPOSITORIES-AND-WORKTREES.md +1 -1
  34. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
  35. package/docs/TELEMETRY.md +2 -2
  36. package/docs/assets/knodin-favicon.svg +4 -0
  37. package/docs/releases/0.5.1.md +17 -0
  38. package/docs/releases/0.6.0.md +18 -0
  39. package/package.json +8 -6
  40. package/schemas/release-attestation-v1.schema.json +1 -1
  41. package/dist/src/tools/reckon-tools.js +0 -5
package/dist/src/init.js CHANGED
@@ -5,7 +5,6 @@ import { configureProjectAgents, } from "./agent-integration.js";
5
5
  import { isIndexableSourcePath } from "./engine/source-policy.js";
6
6
  import { registerInitializedWorktree } from "./worktree-lifecycle.js";
7
7
  const MANAGED_MARKER = "KNODIN MANAGED HOOK";
8
- const LEGACY_MANAGED_MARKER = "RECKON-GRAPH MANAGED HOOK";
9
8
  const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
10
9
  const HOOK_LOG_MAX_BYTES = 1_048_576;
11
10
  const HOOK_FAILURE_FILE = "last-refresh-failure";
@@ -78,14 +77,14 @@ export async function refreshFromGitEvent(repo, event, index) {
78
77
  changed = diffPaths(resolvedRepo, event.before, event.after);
79
78
  }
80
79
  else {
81
- const hooksRoot = path.join(resolvedRepo, ".reckon", "hooks");
80
+ const hooksRoot = path.join(resolvedRepo, ".knodin", "hooks");
82
81
  const inputPath = path.resolve(event.inputPath);
83
82
  const relativeInput = path.relative(hooksRoot, inputPath);
84
83
  if (relativeInput === "" ||
85
84
  relativeInput === ".." ||
86
85
  relativeInput.startsWith(`..${path.sep}`) ||
87
86
  path.isAbsolute(relativeInput)) {
88
- throw new Error("knodin hook refresh: rewrite input must be inside .reckon/hooks");
87
+ throw new Error("knodin hook refresh: rewrite input must be inside .knodin/hooks");
89
88
  }
90
89
  const [realHooksRoot, realInputPath, inputStat] = await Promise.all([
91
90
  fs.promises.realpath(hooksRoot),
@@ -99,7 +98,7 @@ export async function refreshFromGitEvent(repo, event, index) {
99
98
  realRelativeInput === ".." ||
100
99
  realRelativeInput.startsWith(`..${path.sep}`) ||
101
100
  path.isAbsolute(realRelativeInput)) {
102
- throw new Error("knodin hook refresh: rewrite input must be a regular file inside .reckon/hooks");
101
+ throw new Error("knodin hook refresh: rewrite input must be a regular file inside .knodin/hooks");
103
102
  }
104
103
  rewriteInputToRemove = inputPath;
105
104
  const pairs = (await fs.promises.readFile(inputPath, "utf-8"))
@@ -130,11 +129,11 @@ set -u
130
129
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
131
130
  [ -n "$REPO_ROOT" ] || exit 0
132
131
  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}"
132
+ mkdir -p "$REPO_ROOT/.knodin"
133
+ LOCK_DIR="$REPO_ROOT/.knodin/hooks/refresh.lock"
134
+ EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
135
+ LAST_EVENT_PATH="$REPO_ROOT/.knodin/hooks/last-refresh-event"
136
+ FAILURE_PATH="$REPO_ROOT/.knodin/hooks/${HOOK_FAILURE_FILE}"
138
137
  FAILURE_TMP="$FAILURE_PATH.$$"
139
138
  mkdir -p "$EVENT_DIR"
140
139
  if [ "$#" -gt 0 ]; then
@@ -165,7 +164,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
165
164
  fi
166
165
  printf '%s\\n' "$$" > "$LOCK_DIR/pid"
167
166
  trap 'rm -rf "$LOCK_DIR"; rm -f "$FAILURE_TMP"' EXIT HUP INT TERM
168
- LOG_PATH="$REPO_ROOT/.reckon/indexer.log"
167
+ LOG_PATH="$REPO_ROOT/.knodin/indexer.log"
169
168
  if [ -f "$LOG_PATH" ]; then
170
169
  LOG_BYTES="$(wc -c < "$LOG_PATH" 2>/dev/null | tr -d '[:space:]')"
171
170
  case "$LOG_BYTES" in ''|*[!0-9]*) LOG_BYTES=0 ;; esac
@@ -197,7 +196,7 @@ while :; do
197
196
  {
198
197
  printf 'knodin background refresh failed with exit code %s.\\n' "$STATUS"
199
198
  printf 'Pending event retained at %s.\\n' "$EVENT"
200
- printf 'See .reckon/indexer.log for details.\\n'
199
+ printf 'See .knodin/indexer.log for details.\\n'
201
200
  } > "$FAILURE_TMP"
202
201
  mv -f "$FAILURE_TMP" "$FAILURE_PATH"
203
202
  exit "$STATUS"
@@ -206,7 +205,7 @@ done
206
205
  `;
207
206
  }
208
207
  function queuedLifecycleEvents(repo) {
209
- const eventDirectory = path.join(repo, ".reckon", "hooks", "events");
208
+ const eventDirectory = path.join(repo, ".knodin", "hooks", "events");
210
209
  try {
211
210
  return fs.readdirSync(eventDirectory).filter((entry) => entry.endsWith(".event"));
212
211
  }
@@ -216,7 +215,7 @@ function queuedLifecycleEvents(repo) {
216
215
  }
217
216
  function refreshProcessorIsRunning(repo) {
218
217
  try {
219
- const pid = Number(fs.readFileSync(path.join(repo, ".reckon", "hooks", "refresh.lock", "pid"), "utf-8"));
218
+ const pid = Number(fs.readFileSync(path.join(repo, ".knodin", "hooks", "refresh.lock", "pid"), "utf-8"));
220
219
  if (!Number.isInteger(pid) || pid <= 0)
221
220
  return false;
222
221
  process.kill(pid, 0);
@@ -238,7 +237,7 @@ function drainQueuedLifecycleEvents(repo, backgroundPath) {
238
237
  });
239
238
  if (result.status !== 0) {
240
239
  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`);
240
+ throw new Error(`knodin init: queued lifecycle refresh could not be drained (${detail}); run ${backgroundPath} after resolving .knodin/hooks/last-refresh-failure`);
242
241
  }
243
242
  const remaining = queuedLifecycleEvents(repo);
244
243
  if (remaining.length === 0)
@@ -279,7 +278,7 @@ AFTER="$(git rev-parse HEAD 2>/dev/null || true)"
279
278
  ? `if [ ! -x "$BACKGROUND" ]; then
280
279
  ${rewriteWithoutIndexer}
281
280
  fi
282
- REWRITE_INPUT="$(mktemp "$REPO_ROOT/.reckon/hooks/rewrite-input.XXXXXX")" || exit 0
281
+ REWRITE_INPUT="$(mktemp "$REPO_ROOT/.knodin/hooks/rewrite-input.XXXXXX")" || exit 0
283
282
  cat > "$REWRITE_INPUT"
284
283
  `
285
284
  : "";
@@ -289,7 +288,7 @@ cat > "$REWRITE_INPUT"
289
288
  # ${MANAGED_MARKER}: ${hookName}
290
289
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
291
290
  [ -n "$REPO_ROOT" ] || exit 0
292
- BACKGROUND="$REPO_ROOT/.reckon/hooks/background-index.sh"
291
+ BACKGROUND="$REPO_ROOT/.knodin/hooks/background-index.sh"
293
292
  ORIGINAL_STATUS=0
294
293
  trigger_knodin() {
295
294
  [ -x "$BACKGROUND" ] || return 0
@@ -304,7 +303,7 @@ trigger_knodin() {
304
303
  EVENT_BEFORE="$(sed -n '1{s/ .*//;p;}' "$2")"
305
304
  EVENT_AFTER="$(sed -n '1{s/^[^ ]* //;s/ .*//;p;}' "$2")"
306
305
  fi
307
- EVENT_DIR="$REPO_ROOT/.reckon/hooks/events"
306
+ EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
308
307
  mkdir -p "$EVENT_DIR" || return 0
309
308
  EVENT_TMP="$EVENT_DIR/$(date +%s).$$.$PPID.tmp"
310
309
  EVENT_PATH="$EVENT_TMP.event"
@@ -319,7 +318,7 @@ trigger_knodin() {
319
318
  "$EVENT_SEQUENCE" "$EVENT_WORKTREE" "$EVENT_AT" "$EVENT_BEFORE" "$EVENT_AFTER"
320
319
  } > "$EVENT_TMP" || return 0
321
320
  mv "$EVENT_TMP" "$EVENT_PATH" || return 0
322
- if [ "\${RECKON_HOOK_FOREGROUND:-0}" = "1" ]; then
321
+ if [ "\${KNODIN_HOOK_FOREGROUND:-0}" = "1" ]; then
323
322
  "$BACKGROUND"
324
323
  else
325
324
  nohup "$BACKGROUND" >/dev/null 2>&1 &
@@ -346,13 +345,12 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
346
345
  let originalPath = preservedOriginal ?? null;
347
346
  if (preservedOriginal === undefined && fs.existsSync(hookPath)) {
348
347
  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`)) {
348
+ if (content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`)) {
351
349
  const match = content.match(/^# knodin original: (.+)$/m);
352
350
  originalPath = match?.[1] ?? null;
353
351
  }
354
352
  else {
355
- const backupPath = `${hookPath}.reckon-original`;
353
+ const backupPath = `${hookPath}.knodin-original`;
356
354
  const mode = (await fs.promises.stat(hookPath)).mode;
357
355
  await fs.promises.copyFile(hookPath, backupPath);
358
356
  await fs.promises.rm(hookPath, { force: true });
@@ -365,9 +363,7 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
365
363
  if (!fs.existsSync(candidate))
366
364
  break;
367
365
  const candidateContent = await fs.promises.readFile(candidate, "utf-8");
368
- if (candidateContent === canonical ||
369
- candidateContent.includes(MANAGED_MARKER) ||
370
- candidateContent.includes(LEGACY_MANAGED_MARKER))
366
+ if (candidateContent === canonical || candidateContent.includes(MANAGED_MARKER))
371
367
  await fs.promises.rm(candidate, { force: true });
372
368
  }
373
369
  // Match Git's semantics: preserve inactive hook files as backups, but
@@ -388,7 +384,6 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
388
384
  await fs.promises.chmod(hookPath, 0o755);
389
385
  }
390
386
  const ORIGINAL_HOOKS_CONFIG = "knodin.originalHooksPath";
391
- const LEGACY_ORIGINAL_HOOKS_CONFIG = "reckon.originalHooksPath";
392
387
  function effectiveHooksDirectory(repo) {
393
388
  let raw;
394
389
  try {
@@ -405,15 +400,10 @@ function configuredHooksPath(repo) {
405
400
  }
406
401
  catch {
407
402
  try {
408
- return (runGit(repo, ["config", "--local", "--get", LEGACY_ORIGINAL_HOOKS_CONFIG]).trim() || null);
403
+ return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
409
404
  }
410
405
  catch {
411
- try {
412
- return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
413
- }
414
- catch {
415
- return null;
416
- }
406
+ return null;
417
407
  }
418
408
  }
419
409
  }
@@ -494,7 +484,7 @@ export const SETTINGS_TEMPLATE = `{
494
484
  }
495
485
  }
496
486
  `;
497
- const RECKON_AGENT_INSTRUCTIONS = `<!-- knodin:start -->
487
+ const KNODIN_AGENT_INSTRUCTIONS = `<!-- knodin:start -->
498
488
  ## knodin — Local Code Intelligence
499
489
 
500
490
  This repository is indexed locally by knodin. Start with \`knodin status\`; if it reports \`repair-needed\`, run \`knodin repair\` before relying on graph evidence.
@@ -540,24 +530,19 @@ async function installManagedAgentInstructions(repo, fileName = "AGENTS.md") {
540
530
  }
541
531
  const canonicalStart = "<!-- knodin:start -->";
542
532
  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);
533
+ const start = existing.indexOf(canonicalStart);
534
+ const end = existing.indexOf(canonicalEnd);
550
535
  const next = start >= 0 && end >= start
551
- ? `${existing.slice(0, start)}${RECKON_AGENT_INSTRUCTIONS}${existing.slice(end + endMarker.length).replace(/^\r?\n/, "")}`
536
+ ? `${existing.slice(0, start)}${KNODIN_AGENT_INSTRUCTIONS}${existing.slice(end + canonicalEnd.length).replace(/^\r?\n/, "")}`
552
537
  : existing.length > 0
553
- ? `${existing.replace(/\s*$/, "")}\n\n${RECKON_AGENT_INSTRUCTIONS}`
554
- : RECKON_AGENT_INSTRUCTIONS;
538
+ ? `${existing.trimEnd()}\n\n${KNODIN_AGENT_INSTRUCTIONS}`
539
+ : KNODIN_AGENT_INSTRUCTIONS;
555
540
  if (next !== existing)
556
541
  await fs.promises.writeFile(instructionsPath, next, "utf-8");
557
542
  }
558
543
  async function hasManagedAgentInstructions(repo, fileName = "AGENTS.md") {
559
544
  try {
560
- return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(RECKON_AGENT_INSTRUCTIONS);
545
+ return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(KNODIN_AGENT_INSTRUCTIONS);
561
546
  }
562
547
  catch {
563
548
  return false;
@@ -589,9 +574,7 @@ async function excludeUntrackedPaths(repo, candidates) {
589
574
  }
590
575
  const startMarker = "# knodin:start";
591
576
  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");
577
+ const outside = existing.replace(/(?:^|\n)# knodin:start\n[\s\S]*?\n# knodin:end\n?/, "\n");
595
578
  const outsideLines = new Set(outside.split(/\r?\n/));
596
579
  const owned = safe.filter((candidate) => !outsideLines.has(candidate));
597
580
  const prefix = outside && !outside.endsWith("\n") ? `${outside}\n` : outside;
@@ -666,7 +649,7 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
666
649
  try {
667
650
  const current = await fs.promises.readFile(gitIgnorePath, "utf-8");
668
651
  const next = current
669
- .replace(/(?:^|\n)# knodin:start\n\.reckon\/\n# knodin:end\n?/g, "\n")
652
+ .replace(/(?:^|\n)# knodin:start\n\.knodin\/\n# knodin:end\n?/g, "\n")
670
653
  .trim();
671
654
  if (isTracked(repo, ".gitignore")) {
672
655
  const head = runGit(repo, ["show", "HEAD:.gitignore"]);
@@ -702,7 +685,7 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
702
685
  }
703
686
  export function readRepositoryIntegrationConfig(repo) {
704
687
  try {
705
- const value = JSON.parse(fs.readFileSync(path.join(repo, ".reckon", "integration.json"), "utf-8"));
688
+ const value = JSON.parse(fs.readFileSync(path.join(repo, ".knodin", "integration.json"), "utf-8"));
706
689
  if ((value.scope === "personal" || value.scope === "team" || value.scope === "cli-only") &&
707
690
  Array.isArray(value.agents))
708
691
  return { scope: value.scope, agents: value.agents };
@@ -745,10 +728,10 @@ export function inspectRepositoryIntegrationStatus(repo) {
745
728
  };
746
729
  }
747
730
  async function writeRepositoryIntegrationConfig(repo, config) {
748
- await fs.promises.writeFile(path.join(repo, ".reckon", "integration.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
731
+ await fs.promises.writeFile(path.join(repo, ".knodin", "integration.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
749
732
  }
750
733
  async function ensureTeamGitIgnore(repo) {
751
- if (isTracked(repo, ".reckon"))
734
+ if (isTracked(repo, ".knodin"))
752
735
  return;
753
736
  await assertSafeRepositoryFile(repo, ".gitignore");
754
737
  const ignorePath = path.join(repo, ".gitignore");
@@ -760,10 +743,10 @@ async function ensureTeamGitIgnore(repo) {
760
743
  if (error.code !== "ENOENT")
761
744
  throw error;
762
745
  }
763
- if (existing.split(/\r?\n/).includes(".reckon/"))
746
+ if (existing.split(/\r?\n/).includes(".knodin/"))
764
747
  return;
765
748
  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");
749
+ await fs.promises.writeFile(ignorePath, `${prefix}# knodin:start\n.knodin/\n# knodin:end\n`, "utf-8");
767
750
  }
768
751
  export function detectTrackedTeamIntegration(repo) {
769
752
  const checks = [
@@ -802,7 +785,7 @@ export async function initializeRepository(repo, options) {
802
785
  detectTrackedTeamIntegration(resolvedRepo)) {
803
786
  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
787
  }
805
- const knodinHooksDir = path.join(resolvedRepo, ".reckon", "hooks");
788
+ const knodinHooksDir = path.join(resolvedRepo, ".knodin", "hooks");
806
789
  const hookInstallation = await prepareHookInstallation(resolvedRepo);
807
790
  const hooksDir = hookInstallation.hooksDir;
808
791
  await Promise.all([
@@ -908,15 +891,15 @@ export async function initializeRepository(repo, options) {
908
891
  const excluded = scope === "team"
909
892
  ? []
910
893
  : await excludeUntrackedPaths(resolvedRepo, [
911
- ...(isTracked(resolvedRepo, ".reckon") ? [] : [".reckon/"]),
894
+ ...(isTracked(resolvedRepo, ".knodin") ? [] : [".knodin/"]),
912
895
  ...agentIntegration.projectFiles,
913
896
  ]);
914
897
  registerInitializedWorktree(resolvedRepo);
915
898
  return {
916
899
  scope,
917
- knodin: ".reckon",
918
- database: ".reckon/db.sqlite",
919
- backgroundIndexer: ".reckon/hooks/background-index.sh",
900
+ knodin: ".knodin",
901
+ database: ".knodin/db.sqlite",
902
+ backgroundIndexer: ".knodin/hooks/background-index.sh",
920
903
  hooksDirectory: path.relative(resolvedRepo, hooksDir) || ".",
921
904
  gitHooks: [...HOOK_NAMES],
922
905
  claudeSettings: null,
@@ -979,7 +962,7 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
979
962
  ((await hasManagedAgentInstructions(resolvedRepo, "AGENTS.md")) &&
980
963
  (await hasManagedAgentInstructions(resolvedRepo, "GEMINI.md")));
981
964
  const integrationCurrent = readRepositoryIntegrationConfig(resolvedRepo)?.scope === scope;
982
- return ((await matchesManagedFile(path.join(resolvedRepo, ".reckon", "hooks", "background-index.sh"), backgroundScript(command), true)) &&
965
+ return ((await matchesManagedFile(path.join(resolvedRepo, ".knodin", "hooks", "background-index.sh"), backgroundScript(command), true)) &&
983
966
  sharedFilesCurrent &&
984
967
  integrationCurrent);
985
968
  }
@@ -1,7 +1,7 @@
1
1
  import childProcess from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- const MANAGED_MARKERS = ["KNODIN MANAGED HOOK", "RECKON-GRAPH MANAGED HOOK"];
4
+ const MANAGED_MARKER = "KNODIN MANAGED HOOK";
5
5
  const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
6
6
  const HOOK_FAILURE_FILE = "last-refresh-failure";
7
7
  function readEvent(target) {
@@ -35,11 +35,11 @@ function readEvent(target) {
35
35
  }
36
36
  }
37
37
  function readLastEvent(repo) {
38
- return readEvent(path.join(repo, ".reckon", "hooks", "last-refresh-event"));
38
+ return readEvent(path.join(repo, ".knodin", "hooks", "last-refresh-event"));
39
39
  }
40
40
  function readPendingEvent(repo) {
41
41
  try {
42
- const directory = path.join(repo, ".reckon", "hooks", "events");
42
+ const directory = path.join(repo, ".knodin", "hooks", "events");
43
43
  const next = fs
44
44
  .readdirSync(directory)
45
45
  .filter((entry) => entry.endsWith(".event"))
@@ -51,7 +51,7 @@ function readPendingEvent(repo) {
51
51
  }
52
52
  }
53
53
  function processorState(repo) {
54
- const pidPath = path.join(repo, ".reckon", "hooks", "refresh.lock", "pid");
54
+ const pidPath = path.join(repo, ".knodin", "hooks", "refresh.lock", "pid");
55
55
  if (!fs.existsSync(path.dirname(pidPath)))
56
56
  return { state: "idle", pid: null };
57
57
  let pid = null;
@@ -78,7 +78,7 @@ function processorState(repo) {
78
78
  function queuedEventCount(repo) {
79
79
  try {
80
80
  return fs
81
- .readdirSync(path.join(repo, ".reckon", "hooks", "events"))
81
+ .readdirSync(path.join(repo, ".knodin", "hooks", "events"))
82
82
  .filter((entry) => entry.endsWith(".event")).length;
83
83
  }
84
84
  catch {
@@ -97,8 +97,7 @@ function inspectManagedHooks(hooksDirectory) {
97
97
  const hookPath = path.join(hooksDirectory, hookName);
98
98
  const content = fs.readFileSync(hookPath, "utf-8");
99
99
  const mode = fs.statSync(hookPath).mode;
100
- const active = (mode & 0o111) !== 0 &&
101
- MANAGED_MARKERS.some((marker) => content.startsWith(`#!/bin/sh\n# ${marker}: ${hookName}\n`));
100
+ const active = (mode & 0o111) !== 0 && content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`);
102
101
  (active ? activeHooks : missingHooks).push(hookName);
103
102
  }
104
103
  catch {
@@ -108,7 +107,7 @@ function inspectManagedHooks(hooksDirectory) {
108
107
  return { activeHooks, missingHooks };
109
108
  }
110
109
  function readRefreshFailure(repo) {
111
- const failurePath = path.join(repo, ".reckon", "hooks", HOOK_FAILURE_FILE);
110
+ const failurePath = path.join(repo, ".knodin", "hooks", HOOK_FAILURE_FILE);
112
111
  if (!fs.existsSync(failurePath))
113
112
  return null;
114
113
  try {
@@ -122,7 +121,7 @@ function readRefreshFailure(repo) {
122
121
  /** Read-only proof that committed Git activity can still reach knodin's indexer. */
123
122
  export function inspectLifecycleHealth(repoPath) {
124
123
  const repo = path.resolve(repoPath);
125
- if (!fs.existsSync(path.join(repo, ".reckon", "db.sqlite"))) {
124
+ if (!fs.existsSync(path.join(repo, ".knodin", "db.sqlite"))) {
126
125
  return {
127
126
  status: "unconfigured",
128
127
  refreshCapable: false,
@@ -157,14 +156,14 @@ export function inspectLifecycleHealth(repoPath) {
157
156
  };
158
157
  }
159
158
  const { activeHooks, missingHooks } = inspectManagedHooks(hooksDirectory);
160
- const background = path.join(repo, ".reckon", "hooks", "background-index.sh");
159
+ const background = path.join(repo, ".knodin", "hooks", "background-index.sh");
161
160
  const backgroundReady = fs.existsSync(background) && (fs.statSync(background).mode & 0o111) !== 0;
162
161
  const issues = missingHooks.map((hook) => `${hook} no longer routes through knodin's active Git hook path`);
163
162
  if (!backgroundReady)
164
163
  issues.push("background indexer is missing or not executable");
165
164
  const lastError = readRefreshFailure(repo);
166
165
  if (lastError)
167
- issues.push(`${lastError} See .reckon/indexer.log for details`);
166
+ issues.push(`${lastError} See .knodin/indexer.log for details`);
168
167
  const refreshCapable = missingHooks.length === 0 && backgroundReady && issues.length === 0;
169
168
  return {
170
169
  status: refreshCapable ? "healthy" : "degraded",
@@ -59,7 +59,7 @@ function filesForService(repoPath, ts) {
59
59
  const files = [];
60
60
  const visit = (directory) => {
61
61
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
62
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".reckon")
62
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".knodin")
63
63
  continue;
64
64
  const candidate = path.join(directory, entry.name);
65
65
  if (entry.isDirectory())
@@ -335,7 +335,7 @@ function percentReduction(input, output) {
335
335
  }
336
336
  function artifactDirectory(repo) {
337
337
  const resolvedRepo = fs.realpathSync(repo);
338
- const knodinDirectory = path.join(resolvedRepo, ".reckon");
338
+ const knodinDirectory = path.join(resolvedRepo, ".knodin");
339
339
  const outputDirectory = path.join(knodinDirectory, "output");
340
340
  for (const candidate of [knodinDirectory, outputDirectory]) {
341
341
  if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink())
@@ -5,7 +5,7 @@ import { encode } from "gpt-tokenizer/encoding/o200k_base";
5
5
  export const TELEMETRY_TOKENIZER = "gpt-tokenizer@3.4.0:o200k_base";
6
6
  export const countOutputTokens = (value) => encode(value).length;
7
7
  export const DEFAULT_TELEMETRY_RETENTION_DAYS = 30;
8
- const DEFAULT_INPUT = ".reckon-telemetry.jsonl";
8
+ const DEFAULT_INPUT = ".knodin-telemetry.jsonl";
9
9
  function lineCount(value) {
10
10
  return value.length === 0 ? 0 : value.split(/\r?\n/).length;
11
11
  }
@@ -104,7 +104,7 @@ export function measureOutput(options) {
104
104
  const availability = options.output.availability;
105
105
  const freshness = options.output.freshness;
106
106
  const fidelity = options.output.fidelity;
107
- const database = path.join(fs.realpathSync(options.repo), ".reckon", "db.sqlite");
107
+ const database = path.join(fs.realpathSync(options.repo), ".knodin", "db.sqlite");
108
108
  const latencyMs = Math.round((performance.now() - options.startedAt) * 100) / 100;
109
109
  const outcome = (status !== undefined &&
110
110
  ["error", "failed", "unavailable", "blocked", "timeout"].includes(status)) ||
@@ -180,7 +180,7 @@ function telemetrySummary(records) {
180
180
  indexingMs: records.reduce((sum, record) => sum + (record.indexingMs ?? 0), 0),
181
181
  };
182
182
  }
183
- export function writeTelemetryReport(repoPath, records, outputPath = ".reckon/telemetry-report.html") {
183
+ export function writeTelemetryReport(repoPath, records, outputPath = ".knodin/telemetry-report.html") {
184
184
  const { repo, target: output } = privatePath(repoPath, outputPath, "report");
185
185
  const groups = new Map();
186
186
  for (const record of records) {
@@ -326,7 +326,7 @@ export function telemetryStatus(repoPath, inputPath = DEFAULT_INPUT, retentionDa
326
326
  tokenizer: TELEMETRY_TOKENIZER,
327
327
  };
328
328
  }
329
- export function exportTelemetry(repoPath, records, outputPath = ".reckon/telemetry-export.json") {
329
+ export function exportTelemetry(repoPath, records, outputPath = ".knodin/telemetry-export.json") {
330
330
  const { repo, target } = privatePath(repoPath, outputPath, "export");
331
331
  const bundle = {
332
332
  schemaVersion: 1,
@@ -12,10 +12,10 @@
12
12
  * than `ensureGhAvailable()` (throwing, still used by the CLI's hard-fail).
13
13
  */
14
14
  import child_process from "node:child_process";
15
- /** Resolve the `gh` binary to invoke. Tests override via `RECKON_GH_BIN` to
15
+ /** Resolve the `gh` binary to invoke. Tests override via `KNODIN_GH_BIN` to
16
16
  * point at a stub script fixture — no network in tests. */
17
17
  function ghBin() {
18
- return process.env.RECKON_GH_BIN || "gh";
18
+ return process.env.KNODIN_GH_BIN || "gh";
19
19
  }
20
20
  const STALE_DAYS = 30;
21
21
  const PR_LIST_FIELDS = "number,title,updatedAt,reviewDecision,statusCheckRollup,files,headRefName";
@@ -3,7 +3,7 @@ import { clearLine as clearTerminalLine, moveCursor as moveTerminalCursor, curso
3
3
  import ora from "ora";
4
4
  import { normalizeTerminalDimension } from "./init-progress.js";
5
5
  import { formatRepairProgress } from "./repair-progress.js";
6
- const terminalEnabled = process.stderr.isTTY === true || process.env.RECKON_FORCE_PROGRESS_TTY === "1";
6
+ const terminalEnabled = process.stderr.isTTY === true || process.env.KNODIN_FORCE_PROGRESS_TTY === "1";
7
7
  const progressStream = {
8
8
  isTTY: terminalEnabled,
9
9
  columns: normalizeTerminalDimension(process.stderr.columns, 80),
@@ -68,7 +68,7 @@ export async function runRepositoryInitializationProcess(options) {
68
68
  detached: process.platform !== "win32",
69
69
  env: {
70
70
  ...process.env,
71
- RECKON_INTERNAL_INIT_MEMORY_LIMIT_BYTES: String(memoryLimitBytes),
71
+ KNODIN_INTERNAL_INIT_MEMORY_LIMIT_BYTES: String(memoryLimitBytes),
72
72
  },
73
73
  stdio: ["ignore", "ignore", "pipe", "ipc"],
74
74
  });
@@ -10,7 +10,7 @@ const MAX_DEPTH = 32;
10
10
  const MAX_GIT_ERROR_BYTES = 16 * 1024;
11
11
  const MAX_TRACKED_PATH_BYTES = 1024 * 1024;
12
12
  const MAX_DOCUMENT_CANDIDATES = 20;
13
- const SKIPPED_DIRECTORIES = new Set([".git", ".reckon", "node_modules"]);
13
+ const SKIPPED_DIRECTORIES = new Set([".git", ".knodin", "node_modules"]);
14
14
  function gitTopLevel(directory) {
15
15
  return child_process
16
16
  .execFileSync(gitExecutable(), ["rev-parse", "--show-toplevel"], {
@@ -650,7 +650,7 @@ function inventoryFreshness(lastIndexedHead, head) {
650
650
  export async function inventoryRepository(record, options) {
651
651
  const id = repositoryIdentity(record.path);
652
652
  const mainWorktreeId = repositoryIdentity(record.mainWorktree);
653
- const databaseExists = fs.existsSync(path.join(record.path, ".reckon", "db.sqlite"));
653
+ const databaseExists = fs.existsSync(path.join(record.path, ".knodin", "db.sqlite"));
654
654
  const head = gitValue(record.path, ["rev-parse", "HEAD"]);
655
655
  let tracked;
656
656
  try {
@@ -946,7 +946,7 @@ async function initializeOneRepository(repository, options) {
946
946
  if (!options.dryRun && options.isolatedInitialize) {
947
947
  return await options.isolatedInitialize(repository);
948
948
  }
949
- const databaseExists = fs.existsSync(path.join(repository, ".reckon", "db.sqlite"));
949
+ const databaseExists = fs.existsSync(path.join(repository, ".knodin", "db.sqlite"));
950
950
  const integration = readRepositoryIntegrationConfig(repository);
951
951
  const scope = integration?.scope ?? "personal";
952
952
  const agents = [...new Set([...(options.agents ?? []), ...(integration?.agents ?? [])])];
@@ -23,7 +23,7 @@ export function createServer() {
23
23
  }));
24
24
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
25
25
  const { name, arguments: args } = request.params;
26
- if (name !== "knodin" && name !== "reckon") {
26
+ if (name !== "knodin") {
27
27
  throw new Error(`unknown tool: ${name}`);
28
28
  }
29
29
  const result = await handleKnodinTool(args);
@@ -78,7 +78,7 @@ function configuredRepository(value, index) {
78
78
  id: stringValue(record.id, `repositories[${index}].id`),
79
79
  role,
80
80
  indexMode: explicitIndex ?? (derived ? "provenance-only" : "full"),
81
- policySource: explicitIndex ? "reckon.yaml" : "role-default",
81
+ policySource: explicitIndex ? "knodin.yaml" : "role-default",
82
82
  };
83
83
  }
84
84
  function configuredSystem(value, systemIndex) {
@@ -97,7 +97,7 @@ function configuredSystem(value, systemIndex) {
97
97
  };
98
98
  }
99
99
  function applyLegacyFederation(repo, repositories, relationships) {
100
- const legacyPath = path.join(repo, ".reckon", "federation.json");
100
+ const legacyPath = path.join(repo, ".knodin", "federation.json");
101
101
  let legacy;
102
102
  try {
103
103
  legacy = asRecord(JSON.parse(fs.readFileSync(legacyPath, "utf-8")));
@@ -146,7 +146,7 @@ function applyLegacyFederation(repo, repositories, relationships) {
146
146
  to: id,
147
147
  targetIdentity: id,
148
148
  evidence: {
149
- location: ".reckon/federation.json",
149
+ location: ".knodin/federation.json",
150
150
  adapter: "legacy-federation",
151
151
  kind: "compatibility",
152
152
  confidence: 1,
@@ -165,7 +165,7 @@ function applyPersonalMappings(repositories, xdgConfigHome) {
165
165
  if (typeof checkout !== "string")
166
166
  continue;
167
167
  if (!path.isAbsolute(checkout)) {
168
- throw new Error(`personal repository path for ${repository.id} must be absolute; team config keeps paths out of reckon.yaml`);
168
+ throw new Error(`personal repository path for ${repository.id} must be absolute; team config keeps paths out of knodin.yaml`);
169
169
  }
170
170
  repository.path = path.normalize(checkout);
171
171
  repository.pathSource = "personal-config";
@@ -303,7 +303,7 @@ function declaredRelationship(value, index) {
303
303
  to,
304
304
  targetIdentity: to,
305
305
  evidence: {
306
- location: typeof source.location === "string" && source.location ? source.location : "reckon.yaml",
306
+ location: typeof source.location === "string" && source.location ? source.location : "knodin.yaml",
307
307
  adapter: "knodin-yaml",
308
308
  kind: "declared",
309
309
  confidence,
@@ -314,9 +314,9 @@ function declaredRelationship(value, index) {
314
314
  }
315
315
  export function loadSystemConfiguration(repoPath, options = {}) {
316
316
  const repo = path.resolve(repoPath);
317
- const team = asRecord(optionalYaml(path.join(repo, "reckon.yaml")));
317
+ const team = asRecord(optionalYaml(path.join(repo, "knodin.yaml")));
318
318
  if (team.schemaVersion !== undefined && team.schemaVersion !== 1) {
319
- throw new Error("reckon.yaml: schemaVersion must be 1");
319
+ throw new Error("knodin.yaml: schemaVersion must be 1");
320
320
  }
321
321
  const repositories = (Array.isArray(team.repositories) ? team.repositories : []).map((value, index) => configuredRepository(value, index));
322
322
  const systems = (Array.isArray(team.systems) ? team.systems : []).map((value, index) => configuredSystem(value, index));
@@ -163,7 +163,7 @@ function buildDocumentedKnodinTools() {
163
163
  "system",
164
164
  "telemetry",
165
165
  ],
166
- description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.reckon/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
166
+ description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
167
167
  },
168
168
  symbol: {
169
169
  type: "string",
@@ -610,7 +610,7 @@ function buildDocumentedKnodinTools() {
610
610
  },
611
611
  persistTelemetry: {
612
612
  type: "boolean",
613
- description: "Opt in to local metadata-only JSONL persistence at .reckon-telemetry.jsonl; never records source.",
613
+ description: "Opt in to local metadata-only JSONL persistence at .knodin-telemetry.jsonl; never records source.",
614
614
  },
615
615
  },
616
616
  required: ["operation"],
@@ -100,8 +100,8 @@ export function claimScheduledUpdateCheck(options) {
100
100
  if (!config ||
101
101
  !config.enabled ||
102
102
  config.offline ||
103
- env.RECKON_UPDATE_CHECK === "0" ||
104
- env.RECKON_OFFLINE === "1")
103
+ env.KNODIN_UPDATE_CHECK === "0" ||
104
+ env.KNODIN_OFFLINE === "1")
105
105
  return false;
106
106
  const now = options.now?.() ?? new Date();
107
107
  const checkedAt = readTrustedUpdateState(options.stateHome)?.checkedAt;
@@ -474,9 +474,9 @@ async function checkedUpdate(options) {
474
474
  if (!config)
475
475
  return baseResult(options, "trust-unconfigured");
476
476
  const env = options.env ?? process.env;
477
- if (!config.enabled || env.RECKON_UPDATE_CHECK === "0")
477
+ if (!config.enabled || env.KNODIN_UPDATE_CHECK === "0")
478
478
  return baseResult(options, "disabled");
479
- if (config.offline || env.RECKON_OFFLINE === "1")
479
+ if (config.offline || env.KNODIN_OFFLINE === "1")
480
480
  return baseResult(options, "offline");
481
481
  const now = options.now?.() ?? new Date();
482
482
  const previous = readTrustedUpdateState(options.stateHome);