knodin 0.8.7 → 0.10.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 (57) hide show
  1. package/dist/bin/cli.js +341 -91
  2. package/dist/fixtures/update-verification/config/application.xml +4 -0
  3. package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
  4. package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
  5. package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
  6. package/dist/fixtures/update-verification/fixture.json +147 -0
  7. package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
  8. package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
  9. package/dist/fixtures/update-verification/package.json +5 -0
  10. package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
  11. package/dist/fixtures/update-verification/sql/functions.sql +16 -0
  12. package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
  13. package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
  14. package/dist/fixtures/update-verification/src/javascript.js +7 -0
  15. package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
  16. package/dist/fixtures/update-verification/src/typescript.ts +7 -0
  17. package/dist/fixtures/update-verification/terraform/main.tf +7 -0
  18. package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
  19. package/dist/src/agent-integration.js +26 -3
  20. package/dist/src/backup-retention.js +811 -0
  21. package/dist/src/cli-model.js +49 -8
  22. package/dist/src/docs-sections.js +1 -0
  23. package/dist/src/doctor.js +142 -12
  24. package/dist/src/engine/embeddings.js +8 -1
  25. package/dist/src/engine/index.js +1272 -645
  26. package/dist/src/engine/parse-pool-resources.js +96 -0
  27. package/dist/src/engine/parse-pool.js +352 -0
  28. package/dist/src/engine/parse-protocol.js +1 -0
  29. package/dist/src/engine/parse-worker.js +51 -0
  30. package/dist/src/engine/reflink-copy.js +23 -0
  31. package/dist/src/engine/seal-command.js +116 -0
  32. package/dist/src/engine/seal.js +270 -0
  33. package/dist/src/engine/sealed-open.js +116 -0
  34. package/dist/src/engine/sealed-query.js +49 -0
  35. package/dist/src/init.js +251 -183
  36. package/dist/src/manager-update.js +455 -0
  37. package/dist/src/release-preflight.js +106 -95
  38. package/dist/src/repair-lease.js +77 -8
  39. package/dist/src/response-budget.js +5 -1
  40. package/dist/src/server.js +18 -5
  41. package/dist/src/shared-index/compatibility.js +4 -2
  42. package/dist/src/shared-index/selection.js +1 -1
  43. package/dist/src/tools/knodin-tools.js +85 -13
  44. package/dist/src/update-ceremony.js +20 -24
  45. package/dist/src/update-coordination.js +158 -0
  46. package/dist/src/update-executor.js +355 -0
  47. package/dist/src/update-verifier.js +358 -0
  48. package/dist/src/worktree-seed.js +172 -0
  49. package/docs/BACKUP-RETENTION.md +54 -0
  50. package/docs/CLI.md +6 -0
  51. package/docs/DOCTOR-AND-UPDATES.md +44 -12
  52. package/docs/SHARED-INDEX-CONTRACT.md +9 -0
  53. package/docs/SIGNED-UPDATES.md +29 -22
  54. package/docs/releases/0.10.0.md +127 -0
  55. package/docs/releases/0.9.0.md +43 -0
  56. package/package.json +8 -1
  57. package/roadmap/competitive-roadmap.md +8 -0
package/dist/src/init.js CHANGED
@@ -7,6 +7,7 @@ import { compareBytes } from "./compare.js";
7
7
  import { isIndexableSourcePath } from "./engine/source-policy.js";
8
8
  import { lookupMirror } from "./engine/state-paths.js";
9
9
  import { inspectLefthookIntegration, installHookManagerIntegration, isActiveLefthookHook, } from "./hook-manager-integration.js";
10
+ import { acquireRepairLease } from "./repair-lease.js";
10
11
  import { installKnodinSkills, removeKnodinSkills } from "./skill-management.js";
11
12
  import { registerInitializedWorktree } from "./worktree-lifecycle.js";
12
13
  const MANAGED_MARKER = "KNODIN MANAGED HOOK";
@@ -42,6 +43,13 @@ function runGit(repo, args, encoding = "utf-8") {
42
43
  cwd: repo,
43
44
  encoding,
44
45
  stdio: ["ignore", "pipe", "ignore"],
46
+ // A commit/checkout/merge touching many files (a large monorepo, an
47
+ // initial import, a mass rename) can produce a path list well past
48
+ // Node's 1 MiB execFileSync default, which throws rather than
49
+ // truncates. Match the 64 MiB bound already used for git output
50
+ // elsewhere in this codebase (see gitTrackedCandidates,
51
+ // execGitAbortable in src/engine/index.ts).
52
+ maxBuffer: 64 * 1024 * 1024,
45
53
  });
46
54
  }
47
55
  const MUTATION_AUDIT_MAX_ENTRIES = 2_048;
@@ -153,7 +161,10 @@ function configureMutationAuditTargets(repo) {
153
161
  });
154
162
  }
155
163
  for (const hookName of HOOK_NAMES) {
156
- add({ absolute: path.join(effectiveHooks, hookName), label: `${effectiveLabel}/${hookName}` });
164
+ add({
165
+ absolute: path.join(effectiveHooks, hookName),
166
+ label: `${effectiveLabel}/${hookName}`,
167
+ });
157
168
  add({
158
169
  absolute: path.join(effectiveHooks, `${hookName}.knodin-original`),
159
170
  label: `${effectiveLabel}/${hookName}.knodin-original`,
@@ -260,6 +271,24 @@ function diffPaths(repo, before, after) {
260
271
  }
261
272
  return nulPaths(repo, ["diff", "--name-only", before, after, "--"]);
262
273
  }
274
+ /**
275
+ * Runs a git-diff-shaped path computation and degrades to "no known changes"
276
+ * on failure instead of throwing. A lifecycle hook process crashing on an
277
+ * oversized or otherwise-failing git subprocess call has historically taken
278
+ * down the whole background refresh (exit code 1, nothing indexed at all);
279
+ * the per-query freshness guard and the next reconcile pass already detect
280
+ * and catch up any drift this misses, so failing soft here is strictly safer
281
+ * than failing hard.
282
+ */
283
+ function gitPathsOrEmpty(compute, context) {
284
+ try {
285
+ return compute();
286
+ }
287
+ catch (error) {
288
+ console.warn(`knodin: lifecycle hook refresh could not compute changed paths for ${context} (${error instanceof Error ? error.message : String(error)}); the next reconcile will catch up any drift.`);
289
+ return [];
290
+ }
291
+ }
263
292
  /**
264
293
  * Resolve changed paths from a Git lifecycle event and incrementally reconcile
265
294
  * exactly the candidates accepted by the engine's full-repository policy.
@@ -270,17 +299,17 @@ export async function refreshFromGitEvent(repo, event, index) {
270
299
  let rewriteInputToRemove;
271
300
  try {
272
301
  if (event.kind === "commit") {
273
- changed = nulPaths(resolvedRepo, [
302
+ changed = gitPathsOrEmpty(() => nulPaths(resolvedRepo, [
274
303
  "diff-tree",
275
304
  "--root",
276
305
  "--no-commit-id",
277
306
  "--name-only",
278
307
  "-r",
279
308
  "HEAD",
280
- ]);
309
+ ]), "a commit event");
281
310
  }
282
311
  else if (event.kind === "checkout" || event.kind === "merge") {
283
- changed = diffPaths(resolvedRepo, event.before, event.after);
312
+ changed = gitPathsOrEmpty(() => diffPaths(resolvedRepo, event.before, event.after), `a ${event.kind} event`);
284
313
  }
285
314
  else {
286
315
  const hooksRoot = path.join(resolvedRepo, ".knodin", "hooks");
@@ -312,7 +341,7 @@ export async function refreshFromGitEvent(repo, event, index) {
312
341
  .map((line) => line.trim().split(/\s+/))
313
342
  .filter((pair) => pair.length >= 2);
314
343
  for (const [before, after] of pairs) {
315
- changed.push(...diffPaths(resolvedRepo, before, after));
344
+ changed.push(...gitPathsOrEmpty(() => diffPaths(resolvedRepo, before, after), "a rewrite pair"));
316
345
  }
317
346
  }
318
347
  const eligible = [...new Set(changed.map((file) => file.replaceAll("\\", "/")))]
@@ -385,11 +414,11 @@ while :; do
385
414
  ARG1="$(sed -n '2p' "$EVENT")"
386
415
  ARG2="$(sed -n '3p' "$EVENT")"
387
416
  if [ "$KIND" = "commit" ]; then
388
- ${invocation} --repo "$REPO_ROOT" hook-refresh commit --json >> "$LOG_PATH" 2>&1
417
+ ${invocation} --repo "$REPO_ROOT" hook-refresh commit --json >> "$LOG_PATH" 2>> "$LOG_PATH.stderr"
389
418
  elif [ "$KIND" = "checkout" ] || [ "$KIND" = "merge" ]; then
390
- ${invocation} --repo "$REPO_ROOT" hook-refresh "$KIND" "$ARG1" "$ARG2" --json >> "$LOG_PATH" 2>&1
419
+ ${invocation} --repo "$REPO_ROOT" hook-refresh "$KIND" "$ARG1" "$ARG2" --json >> "$LOG_PATH" 2>> "$LOG_PATH.stderr"
391
420
  elif [ "$KIND" = "rewrite" ]; then
392
- ${invocation} --repo "$REPO_ROOT" hook-refresh rewrite "$ARG1" --json >> "$LOG_PATH" 2>&1
421
+ ${invocation} --repo "$REPO_ROOT" hook-refresh rewrite "$ARG1" --json >> "$LOG_PATH" 2>> "$LOG_PATH.stderr"
393
422
  else
394
423
  printf 'knodin background refresh: invalid queued event %s\n' "$KIND" >> "$LOG_PATH"
395
424
  false
@@ -701,6 +730,7 @@ This repository is indexed locally by knodin. Start with \`knodin status\`; if i
701
730
  - Use gateway operations \`explain\`, \`review\`, and \`query\` for source-evidenced context. CLI fallbacks are \`knodin explain\`, \`knodin review --scope compare\`, and \`knodin rename\`.
702
731
  - Direct reads are appropriate for files just authored in the current session; do not force a graph round trip when the agent already has fresh source context.
703
732
  - Run \`knodin index <changed-files>\` while working when you need immediately fresh graph evidence. Managed Git hooks installed by \`knodin init\` refresh the graph after commits, checkouts, merges, and rewrites.
733
+ - When indexing is active, run \`knodin wait --fresh\` and recheck status before falling back to direct traversal.
704
734
  - Keep output truthful: local graph evidence can be stale or ambiguous; say so and repair/reindex instead of claiming certainty.
705
735
 
706
736
  <!-- knodin:end -->
@@ -960,9 +990,17 @@ export function inspectRepositoryIntegrationStatus(repo) {
960
990
  const receipt = readRepositoryIntegrationConfig(repo);
961
991
  const candidates = [
962
992
  { agent: "claude", path: ".mcp.json", marker: '"knodin"' },
963
- { agent: "codex", path: ".codex/config.toml", marker: 'mcp_servers."knodin"' },
993
+ {
994
+ agent: "codex",
995
+ path: ".codex/config.toml",
996
+ marker: 'mcp_servers."knodin"',
997
+ },
964
998
  { agent: "gemini", path: ".gemini/settings.json", marker: '"knodin"' },
965
- { agent: "antigravity", path: ".agents/mcp_config.json", marker: '"knodin"' },
999
+ {
1000
+ agent: "antigravity",
1001
+ path: ".agents/mcp_config.json",
1002
+ marker: '"knodin"',
1003
+ },
966
1004
  {
967
1005
  agent: "codex",
968
1006
  path: "AGENTS.md",
@@ -1047,6 +1085,7 @@ export function detectTrackedTeamIntegration(repo) {
1047
1085
  export async function initializeRepository(repo, options) {
1048
1086
  const resolvedRepo = path.resolve(repo);
1049
1087
  const scope = options.scope ?? "personal";
1088
+ const configureIntegration = options.configureIntegration ?? true;
1050
1089
  const priorIntegration = readRepositoryIntegrationConfig(resolvedRepo);
1051
1090
  if (runGit(resolvedRepo, ["rev-parse", "--is-inside-work-tree"]).trim() !== "true") {
1052
1091
  throw new Error(`knodin init: not a Git worktree: ${resolvedRepo}`);
@@ -1065,188 +1104,217 @@ export async function initializeRepository(repo, options) {
1065
1104
  if (!path.isAbsolute(options.command[0])) {
1066
1105
  throw new Error("knodin init: packaged runtime command must be an absolute path");
1067
1106
  }
1068
- const mutationTargets = options.auditConfigurationChanges
1069
- ? configureMutationAuditTargets(resolvedRepo)
1070
- : [];
1071
- const beforeMutation = mutationSnapshot(mutationTargets);
1072
- if (scope !== "team" &&
1073
- !options.allowTrackedTransition &&
1074
- detectTrackedTeamIntegration(resolvedRepo)) {
1075
- 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");
1076
- }
1077
- const knodinHooksDir = path.join(resolvedRepo, ".knodin", "hooks");
1078
- const hookInstallation = await prepareHookInstallation(resolvedRepo);
1079
- const hooksDir = hookInstallation.hooksDir;
1080
- await Promise.all([
1081
- fs.promises.mkdir(knodinHooksDir, { recursive: true }),
1082
- fs.promises.mkdir(hooksDir, { recursive: true }),
1083
- ]);
1084
- const backgroundPath = path.join(knodinHooksDir, "background-index.sh");
1085
- await fs.promises.writeFile(backgroundPath, backgroundScript(options.command), {
1086
- encoding: "utf-8",
1087
- mode: 0o755,
1088
- });
1089
- await fs.promises.chmod(backgroundPath, 0o755);
1090
- const hookManagerIntegration = installHookManagerIntegration(resolvedRepo);
1091
- for (const hookName of HOOK_NAMES) {
1092
- let preservedOriginal;
1093
- if (hookInstallation.trackedHooksDir) {
1094
- const relativeHook = `${hookInstallation.trackedHooksDir}/${hookName}`;
1095
- const sourceHook = path.join(resolvedRepo, relativeHook);
1096
- try {
1097
- const stat = await fs.promises.stat(sourceHook);
1098
- preservedOriginal =
1099
- stat.isFile() && (stat.mode & 0o111) !== 0 ? `repo:${relativeHook}` : null;
1100
- }
1101
- catch {
1102
- preservedOriginal = null;
1103
- }
1107
+ const lifecycleLease = acquireRepairLease(resolvedRepo, options.configureIntegration ? "configure" : "init");
1108
+ try {
1109
+ const mutationTargets = options.auditConfigurationChanges
1110
+ ? configureMutationAuditTargets(resolvedRepo)
1111
+ : [];
1112
+ const beforeMutation = mutationSnapshot(mutationTargets);
1113
+ if (configureIntegration &&
1114
+ scope !== "team" &&
1115
+ !options.allowTrackedTransition &&
1116
+ detectTrackedTeamIntegration(resolvedRepo)) {
1117
+ 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");
1104
1118
  }
1105
- await installManagedHook(hooksDir, hookName, preservedOriginal);
1106
- }
1107
- for (const entry of await fs.promises.readdir(hooksDir, {
1108
- withFileTypes: true,
1109
- })) {
1110
- if (entry.isFile())
1111
- await preventLefthookAutoInstall(path.join(hooksDir, entry.name));
1112
- }
1113
- const indexResult = await options.index(resolvedRepo, {
1114
- onProgress: options.onProgress,
1115
- });
1116
- if (isIndexResult(indexResult) && indexResult.verification.status !== "healthy") {
1117
- throw new InitializationHealthError(indexResult);
1118
- }
1119
- const lifecycleRefresh = drainQueuedLifecycleEvents(resolvedRepo, backgroundPath);
1120
- await fs.promises.rm(path.join(knodinHooksDir, HOOK_FAILURE_FILE), { force: true });
1121
- let agentInstructionChanges = [];
1122
- if (scope === "team") {
1123
- const installed = await Promise.all([
1124
- installManagedAgentInstructions(resolvedRepo, "AGENTS.md"),
1125
- installManagedAgentInstructions(resolvedRepo, "GEMINI.md"),
1119
+ const knodinHooksDir = path.join(resolvedRepo, ".knodin", "hooks");
1120
+ const hookInstallation = await prepareHookInstallation(resolvedRepo);
1121
+ const hooksDir = hookInstallation.hooksDir;
1122
+ await Promise.all([
1123
+ fs.promises.mkdir(knodinHooksDir, { recursive: true }),
1124
+ fs.promises.mkdir(hooksDir, { recursive: true }),
1126
1125
  ]);
1127
- agentInstructionChanges = installed.filter((change) => change !== null);
1128
- }
1129
- else {
1130
- agentInstructionChanges = await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
1131
- }
1132
- const requestedAgents = options.agents ?? [];
1133
- const projectPathByAgent = {
1134
- codex: ".codex/config.toml",
1135
- gemini: ".gemini/settings.json",
1136
- copilot: ".vscode/mcp.json",
1137
- antigravity: ".agents/mcp_config.json",
1138
- };
1139
- const blockedAgents = [];
1140
- const externalConfigurationOutcomes = [];
1141
- if (scope === "personal" &&
1142
- options.allowTrackedTransition &&
1143
- priorIntegration?.scope === "team") {
1144
- const cleanup = configureProjectAgents({
1145
- repo: resolvedRepo,
1146
- scope: "cli-only",
1147
- agents: ["claude", "codex", "gemini", "copilot", "antigravity"],
1148
- run: options.runAgentCommand,
1126
+ const backgroundPath = path.join(knodinHooksDir, "background-index.sh");
1127
+ await fs.promises.writeFile(backgroundPath, backgroundScript(options.command), {
1128
+ encoding: "utf-8",
1129
+ mode: 0o755,
1149
1130
  });
1150
- if (cleanup.failed.length > 0)
1151
- throw new Error(`knodin configure: could not remove tracked team integration: ${cleanup.failed[0]?.message}`);
1152
- if (cleanup.removed.includes("claude"))
1153
- externalConfigurationOutcomes.push({
1154
- system: "claude-local-mcp",
1155
- state: "removal-attempted",
1131
+ await fs.promises.chmod(backgroundPath, 0o755);
1132
+ const hookManagerIntegration = installHookManagerIntegration(resolvedRepo);
1133
+ for (const hookName of HOOK_NAMES) {
1134
+ let preservedOriginal;
1135
+ if (hookInstallation.trackedHooksDir) {
1136
+ const relativeHook = `${hookInstallation.trackedHooksDir}/${hookName}`;
1137
+ const sourceHook = path.join(resolvedRepo, relativeHook);
1138
+ try {
1139
+ const stat = await fs.promises.stat(sourceHook);
1140
+ preservedOriginal =
1141
+ stat.isFile() && (stat.mode & 0o111) !== 0 ? `repo:${relativeHook}` : null;
1142
+ }
1143
+ catch {
1144
+ preservedOriginal = null;
1145
+ }
1146
+ }
1147
+ await installManagedHook(hooksDir, hookName, preservedOriginal);
1148
+ }
1149
+ for (const entry of await fs.promises.readdir(hooksDir, {
1150
+ withFileTypes: true,
1151
+ })) {
1152
+ if (entry.isFile())
1153
+ await preventLefthookAutoInstall(path.join(hooksDir, entry.name));
1154
+ }
1155
+ let agentInstructionChanges = [];
1156
+ let agentIntegration = {
1157
+ configured: [],
1158
+ removed: [],
1159
+ failed: [],
1160
+ projectFiles: [],
1161
+ };
1162
+ const externalConfigurationOutcomes = [];
1163
+ let excluded = [];
1164
+ if (configureIntegration && scope === "team") {
1165
+ const installed = await Promise.all([
1166
+ installManagedAgentInstructions(resolvedRepo, "AGENTS.md"),
1167
+ installManagedAgentInstructions(resolvedRepo, "GEMINI.md"),
1168
+ ]);
1169
+ agentInstructionChanges = installed.filter((change) => change !== null);
1170
+ }
1171
+ else if (configureIntegration) {
1172
+ agentInstructionChanges = await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
1173
+ }
1174
+ const requestedAgents = options.agents ?? [];
1175
+ const projectPathByAgent = {
1176
+ codex: ".codex/config.toml",
1177
+ gemini: ".gemini/settings.json",
1178
+ copilot: ".vscode/mcp.json",
1179
+ antigravity: ".agents/mcp_config.json",
1180
+ };
1181
+ const blockedAgents = [];
1182
+ if (configureIntegration) {
1183
+ if (scope === "personal" &&
1184
+ options.allowTrackedTransition &&
1185
+ priorIntegration?.scope === "team") {
1186
+ const cleanup = configureProjectAgents({
1187
+ repo: resolvedRepo,
1188
+ scope: "cli-only",
1189
+ agents: ["claude", "codex", "gemini", "copilot", "antigravity"],
1190
+ run: options.runAgentCommand,
1191
+ });
1192
+ if (cleanup.failed.length > 0)
1193
+ throw new Error(`knodin configure: could not remove tracked team integration: ${cleanup.failed[0]?.message}`);
1194
+ if (cleanup.removed.includes("claude"))
1195
+ externalConfigurationOutcomes.push({
1196
+ system: "claude-local-mcp",
1197
+ state: "removal-attempted",
1198
+ });
1199
+ }
1200
+ const eligibleAgents = scope === "team"
1201
+ ? requestedAgents
1202
+ : requestedAgents.filter((agent) => {
1203
+ const projectPath = agent === "claude" ? ".mcp.json" : projectPathByAgent[agent];
1204
+ if (!projectPath ||
1205
+ !isTracked(resolvedRepo, projectPath) ||
1206
+ (scope === "cli-only" && options.allowTrackedTransition))
1207
+ return true;
1208
+ if (options.allowTrackedTransition && priorIntegration?.scope === "team")
1209
+ return false;
1210
+ blockedAgents.push({
1211
+ agent,
1212
+ message: `${projectPath} is tracked; personal mode left it unchanged and did not exclude it`,
1213
+ });
1214
+ return false;
1215
+ });
1216
+ agentIntegration = configureProjectAgents({
1217
+ repo: resolvedRepo,
1218
+ scope,
1219
+ agents: eligibleAgents,
1220
+ run: options.runAgentCommand,
1156
1221
  });
1157
- }
1158
- const eligibleAgents = scope === "team"
1159
- ? requestedAgents
1160
- : requestedAgents.filter((agent) => {
1161
- const projectPath = agent === "claude" ? ".mcp.json" : projectPathByAgent[agent];
1162
- if (!projectPath ||
1163
- !isTracked(resolvedRepo, projectPath) ||
1164
- (scope === "cli-only" && options.allowTrackedTransition))
1165
- return true;
1166
- if (options.allowTrackedTransition && priorIntegration?.scope === "team")
1167
- return false;
1168
- blockedAgents.push({
1169
- agent,
1170
- message: `${projectPath} is tracked; personal mode left it unchanged and did not exclude it`,
1222
+ const skillClients = (scope === "team"
1223
+ ? ["codex", "claude", "gemini"]
1224
+ : agentIntegration.configured.filter((agent) => ["codex", "claude", "gemini"].includes(agent)));
1225
+ const skillIntegration = scope === "cli-only"
1226
+ ? removeKnodinSkills({
1227
+ repo: resolvedRepo,
1228
+ scope: "team",
1229
+ clients: ["codex", "claude", "gemini"],
1230
+ })
1231
+ : installKnodinSkills({
1232
+ repo: resolvedRepo,
1233
+ scope,
1234
+ clients: skillClients,
1235
+ });
1236
+ for (const issue of skillIntegration.issues)
1237
+ agentIntegration.failed.push({
1238
+ agent: skillClients.find((client) => issue.path.includes(`.${client}`)) ?? "codex",
1239
+ message: `${issue.path}: ${issue.message}`,
1240
+ });
1241
+ if (scope === "personal" && agentIntegration.configured.includes("claude"))
1242
+ externalConfigurationOutcomes.push({
1243
+ system: "claude-local-mcp",
1244
+ state: "configured-or-present",
1245
+ });
1246
+ if (scope === "cli-only" &&
1247
+ agentIntegration.removed.includes("claude") &&
1248
+ !externalConfigurationOutcomes.some(({ system, state }) => system === "claude-local-mcp" && state === "removal-attempted"))
1249
+ externalConfigurationOutcomes.push({
1250
+ system: "claude-local-mcp",
1251
+ state: "removal-attempted",
1252
+ });
1253
+ agentIntegration.failed.push(...blockedAgents);
1254
+ await writeRepositoryIntegrationConfig(resolvedRepo, {
1255
+ scope,
1256
+ agents: agentIntegration.configured,
1171
1257
  });
1172
- return false;
1173
- });
1174
- const agentIntegration = configureProjectAgents({
1175
- repo: resolvedRepo,
1176
- scope,
1177
- agents: eligibleAgents,
1178
- run: options.runAgentCommand,
1179
- });
1180
- const skillClients = (scope === "team"
1181
- ? ["codex", "claude", "gemini"]
1182
- : agentIntegration.configured.filter((agent) => ["codex", "claude", "gemini"].includes(agent)));
1183
- const skillIntegration = scope === "cli-only"
1184
- ? removeKnodinSkills({
1185
- repo: resolvedRepo,
1186
- scope: "team",
1187
- clients: ["codex", "claude", "gemini"],
1188
- })
1189
- : installKnodinSkills({ repo: resolvedRepo, scope, clients: skillClients });
1190
- for (const issue of skillIntegration.issues)
1191
- agentIntegration.failed.push({
1192
- agent: skillClients.find((client) => issue.path.includes(`.${client}`)) ?? "codex",
1193
- message: `${issue.path}: ${issue.message}`,
1194
- });
1195
- if (scope === "personal" && agentIntegration.configured.includes("claude"))
1196
- externalConfigurationOutcomes.push({
1197
- system: "claude-local-mcp",
1198
- state: "configured-or-present",
1258
+ if (scope === "team") {
1259
+ await ensureTeamGitIgnore(resolvedRepo);
1260
+ await removeManagedExcludes(resolvedRepo);
1261
+ }
1262
+ else if (scope === "cli-only") {
1263
+ await removeManagedExcludes(resolvedRepo);
1264
+ }
1265
+ excluded =
1266
+ scope === "team"
1267
+ ? []
1268
+ : await excludeUntrackedPaths(resolvedRepo, [
1269
+ ...(isTracked(resolvedRepo, ".knodin") ? [] : [".knodin/"]),
1270
+ ...agentIntegration.projectFiles,
1271
+ ]);
1272
+ }
1273
+ if (!configureIntegration && !isTracked(resolvedRepo, ".knodin")) {
1274
+ excluded = await excludeUntrackedPaths(resolvedRepo, [".knodin/"]);
1275
+ }
1276
+ if (hookManagerIntegration) {
1277
+ const managerExcludes = await excludeUntrackedPaths(resolvedRepo, [
1278
+ hookManagerIntegration.localConfig,
1279
+ ]);
1280
+ excluded = [...new Set([...excluded, ...managerExcludes])].sort(compareBytes);
1281
+ }
1282
+ registerInitializedWorktree(resolvedRepo);
1283
+ // Configuration is graph-visible, so explicit configuration must complete
1284
+ // before the final index and health verification. Plain init reaches this
1285
+ // point without touching agent instructions, MCP files, skills, or receipts.
1286
+ const indexResult = await options.index(resolvedRepo, {
1287
+ onProgress: options.onProgress,
1199
1288
  });
1200
- if (scope === "cli-only" &&
1201
- agentIntegration.removed.includes("claude") &&
1202
- !externalConfigurationOutcomes.some(({ system, state }) => system === "claude-local-mcp" && state === "removal-attempted"))
1203
- externalConfigurationOutcomes.push({
1204
- system: "claude-local-mcp",
1205
- state: "removal-attempted",
1289
+ if (isIndexResult(indexResult) && indexResult.verification.status !== "healthy") {
1290
+ throw new InitializationHealthError(indexResult);
1291
+ }
1292
+ const lifecycleRefresh = drainQueuedLifecycleEvents(resolvedRepo, backgroundPath);
1293
+ await fs.promises.rm(path.join(knodinHooksDir, HOOK_FAILURE_FILE), {
1294
+ force: true,
1206
1295
  });
1207
- agentIntegration.failed.push(...blockedAgents);
1208
- await writeRepositoryIntegrationConfig(resolvedRepo, {
1209
- scope,
1210
- agents: agentIntegration.configured,
1211
- });
1212
- if (scope === "team") {
1213
- await ensureTeamGitIgnore(resolvedRepo);
1214
- await removeManagedExcludes(resolvedRepo);
1296
+ const detectedFilesystemChanges = filesystemChanges(beforeMutation, mutationSnapshot(mutationTargets, true));
1297
+ return {
1298
+ scope,
1299
+ knodin: ".knodin",
1300
+ database: ".knodin/db.sqlite",
1301
+ backgroundIndexer: ".knodin/hooks/background-index.sh",
1302
+ hooksDirectory: path.relative(resolvedRepo, hooksDir) || ".",
1303
+ gitHooks: [...HOOK_NAMES],
1304
+ claudeSettings: null,
1305
+ agentInstructions: scope === "team" ? "AGENTS.md" : null,
1306
+ agentIntegration,
1307
+ agentInstructionChanges,
1308
+ filesystemChanges: detectedFilesystemChanges,
1309
+ externalConfigurationOutcomes,
1310
+ excluded,
1311
+ lifecycleRefresh,
1312
+ hookManagerIntegration,
1313
+ };
1215
1314
  }
1216
- else if (scope === "cli-only") {
1217
- await removeManagedExcludes(resolvedRepo);
1218
- }
1219
- let excluded = scope === "team"
1220
- ? []
1221
- : await excludeUntrackedPaths(resolvedRepo, [
1222
- ...(isTracked(resolvedRepo, ".knodin") ? [] : [".knodin/"]),
1223
- ...agentIntegration.projectFiles,
1224
- ]);
1225
- if (hookManagerIntegration) {
1226
- const managerExcludes = await excludeUntrackedPaths(resolvedRepo, [
1227
- hookManagerIntegration.localConfig,
1228
- ]);
1229
- excluded = [...new Set([...excluded, ...managerExcludes])].sort(compareBytes);
1315
+ finally {
1316
+ lifecycleLease.release();
1230
1317
  }
1231
- registerInitializedWorktree(resolvedRepo);
1232
- const detectedFilesystemChanges = filesystemChanges(beforeMutation, mutationSnapshot(mutationTargets, true));
1233
- return {
1234
- scope,
1235
- knodin: ".knodin",
1236
- database: ".knodin/db.sqlite",
1237
- backgroundIndexer: ".knodin/hooks/background-index.sh",
1238
- hooksDirectory: path.relative(resolvedRepo, hooksDir) || ".",
1239
- gitHooks: [...HOOK_NAMES],
1240
- claudeSettings: null,
1241
- agentInstructions: scope === "team" ? "AGENTS.md" : null,
1242
- agentIntegration,
1243
- agentInstructionChanges,
1244
- filesystemChanges: detectedFilesystemChanges,
1245
- externalConfigurationOutcomes,
1246
- excluded,
1247
- lifecycleRefresh,
1248
- hookManagerIntegration,
1249
- };
1250
1318
  }
1251
1319
  async function matchesManagedFile(filePath, expected, executable) {
1252
1320
  try {