knodin 0.8.6 → 0.9.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 (67) hide show
  1. package/README.md +6 -0
  2. package/dist/bin/cli.js +459 -95
  3. package/dist/bin/launcher.js +0 -0
  4. package/dist/fixtures/update-verification/config/application.xml +4 -0
  5. package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
  6. package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
  7. package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
  8. package/dist/fixtures/update-verification/fixture.json +147 -0
  9. package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
  10. package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
  11. package/dist/fixtures/update-verification/package.json +5 -0
  12. package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
  13. package/dist/fixtures/update-verification/sql/functions.sql +16 -0
  14. package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
  15. package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
  16. package/dist/fixtures/update-verification/src/javascript.js +7 -0
  17. package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
  18. package/dist/fixtures/update-verification/src/typescript.ts +7 -0
  19. package/dist/fixtures/update-verification/terraform/main.tf +7 -0
  20. package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
  21. package/dist/src/agent-integration.js +26 -3
  22. package/dist/src/backup-retention.js +811 -0
  23. package/dist/src/cli-model.js +66 -9
  24. package/dist/src/docs-sections.js +1 -0
  25. package/dist/src/doctor.js +142 -12
  26. package/dist/src/engine/embeddings.js +8 -1
  27. package/dist/src/engine/index.js +355 -292
  28. package/dist/src/init.js +223 -164
  29. package/dist/src/manager-update.js +455 -0
  30. package/dist/src/release-preflight.js +106 -95
  31. package/dist/src/repair-lease.js +77 -8
  32. package/dist/src/repository-management.js +219 -3
  33. package/dist/src/response-budget.js +5 -1
  34. package/dist/src/server.js +18 -5
  35. package/dist/src/shared-index/compatibility.js +4 -2
  36. package/dist/src/skill-management.js +149 -0
  37. package/dist/src/system-config.js +176 -16
  38. package/dist/src/system-management.js +508 -0
  39. package/dist/src/tools/knodin-tools.js +174 -22
  40. package/dist/src/update-ceremony.js +20 -24
  41. package/dist/src/update-coordination.js +158 -0
  42. package/dist/src/update-executor.js +336 -0
  43. package/dist/src/update-verifier.js +358 -0
  44. package/docs/BACKUP-RETENTION.md +54 -0
  45. package/docs/CLI.md +13 -0
  46. package/docs/DOCTOR-AND-UPDATES.md +44 -12
  47. package/docs/SIGNED-UPDATES.md +29 -22
  48. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +27 -6
  49. package/docs/prompts/declare-multi-repository-system.md +18 -0
  50. package/docs/releases/0.8.7.md +27 -0
  51. package/docs/releases/0.9.0.md +43 -0
  52. package/package.json +11 -2
  53. package/roadmap/competitive-roadmap.md +8 -0
  54. package/skills/knodin-debug/SKILL.md +12 -0
  55. package/skills/knodin-debug/agents/openai.yaml +4 -0
  56. package/skills/knodin-explore/SKILL.md +11 -0
  57. package/skills/knodin-explore/agents/openai.yaml +4 -0
  58. package/skills/knodin-impact/SKILL.md +11 -0
  59. package/skills/knodin-impact/agents/openai.yaml +4 -0
  60. package/skills/knodin-lifecycle/SKILL.md +12 -0
  61. package/skills/knodin-lifecycle/agents/openai.yaml +4 -0
  62. package/skills/knodin-refactor/SKILL.md +12 -0
  63. package/skills/knodin-refactor/agents/openai.yaml +4 -0
  64. package/skills/knodin-review/SKILL.md +11 -0
  65. package/skills/knodin-review/agents/openai.yaml +4 -0
  66. package/skills/knodin-systems/SKILL.md +14 -0
  67. package/skills/knodin-systems/agents/openai.yaml +4 -0
package/dist/bin/cli.js CHANGED
@@ -14,12 +14,12 @@
14
14
  import { spawn, spawnSync } from "node:child_process";
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
- import readline from "node:readline/promises";
18
17
  import { fileURLToPath } from "node:url";
19
18
  import { canUseSessionContextCache, parseClaudeHookPayload, readSessionContextCache, recordClaudeLifecycleEvent, renderSessionContext, SESSION_CONTEXT_BUDGET_LINE, sessionStartHookOutput, writeSessionContextCache, } from "../src/agent-events.js";
20
19
  import { inspectClaudeAgentHooks, installClaudeAgentHooks, uninstallClaudeAgentHooks, } from "../src/agent-hooks.js";
21
20
  import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
22
21
  import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
22
+ import { formatBackupHuman, installBackupRetention, listBackups, maybeRunOpportunisticRetention, pruneBackups, removeBackupRetention, retentionDoctor, retentionStatus, runInstalledRetention, } from "../src/backup-retention.js";
23
23
  import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
24
24
  import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
25
25
  import { buildKnodinContext } from "../src/context.js";
@@ -33,20 +33,26 @@ import { diagnoseFailure, } from "../src/failure-diagnosis.js";
33
33
  import { gitExecutable } from "../src/git-executable.js";
34
34
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
35
35
  import { createIndexActivityReporter } from "../src/index-activity.js";
36
- import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
36
+ import { InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
37
37
  import { createInitProgressRenderer } from "../src/init-progress.js";
38
38
  import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
39
+ import { acknowledgeUpdateFailure, queryOwnerAvailability, readManagerUpdateState, readUpdateJournal, resolveManagerOwnership, setManualPin, unpinUpdate, updateAttention, writeManagerUpdateState, } from "../src/manager-update.js";
39
40
  import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
40
41
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
41
42
  import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
42
43
  import { auditPullRequests } from "../src/pr-triage.js";
43
44
  import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
45
+ import { acquireRepairLease } from "../src/repair-lease.js";
44
46
  import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
45
47
  import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
46
48
  import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
47
49
  import { applyResponseBudget } from "../src/response-budget.js";
48
50
  import { appendSessionEvent, clearSessionTelemetry, disableSessionTelemetry, enableSessionTelemetry, readSessionEvents, sessionTelemetryStatus, } from "../src/session-telemetry.js";
51
+ import { inspectKnodinSkills, installKnodinSkills, KNODIN_SKILLS, removeKnodinSkills, } from "../src/skill-management.js";
49
52
  import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
53
+ import { applySystemPlan, planSystemImport, planSystemLink, planSystemRelate, planSystemUnlink, planSystemUnrelate, } from "../src/system-management.js";
54
+ import { coordinationStatus } from "../src/update-coordination.js";
55
+ import { certifyAutomaticUpdates, executeManagerUpdate, recoverInterruptedUpdate, } from "../src/update-executor.js";
50
56
  import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
51
57
  import { KNODIN_VERSION } from "../src/version.js";
52
58
  import { writeVisualization, } from "../src/visualization.js";
@@ -63,44 +69,6 @@ function explicitScope(args) {
63
69
  const equals = args.find((argument) => argument.startsWith("--scope="));
64
70
  return equals ? parseInitScope(equals.slice("--scope=".length)) : null;
65
71
  }
66
- async function chooseInitScope(args, repo) {
67
- const selected = explicitScope(args);
68
- if (selected)
69
- return selected;
70
- if (detectTrackedTeamIntegration(repo)) {
71
- process.stderr.write("[init:scope] Detected tracked knodin team integration; preserving team scope\n");
72
- return "team";
73
- }
74
- if (!process.stdin.isTTY || !process.stderr.isTTY)
75
- return "personal";
76
- const prompt = readline.createInterface({
77
- input: process.stdin,
78
- output: process.stderr,
79
- });
80
- try {
81
- const answer = await prompt.question([
82
- "How should knodin integrate with coding agents?",
83
- " 1. Personal (recommended) — all detected agents; Git stays clean",
84
- " 2. Team — create commit-ready shared configuration",
85
- " 3. CLI-only — agents will not discover or invoke knodin automatically",
86
- "Select [1]: ",
87
- ].join("\n"));
88
- if (!answer.trim() || answer.trim() === "1")
89
- return "personal";
90
- if (answer.trim() === "2")
91
- return "team";
92
- if (answer.trim() === "3") {
93
- const confirmation = await prompt.question("CLI-only requires manual knodin commands. Continue? [y/N] ");
94
- if (!/^y(?:es)?$/i.test(confirmation.trim()))
95
- throw new Error("knodin init: CLI-only selection cancelled");
96
- return "cli-only";
97
- }
98
- return parseInitScope(answer.trim());
99
- }
100
- finally {
101
- prompt.close();
102
- }
103
- }
104
72
  function integrationAgents(repo) {
105
73
  const previous = readRepositoryIntegrationConfig(repo)?.agents ?? [];
106
74
  const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
@@ -149,7 +117,7 @@ function formatConfigureHuman(result) {
149
117
  const externalOutcomeLines = result.paths.externalConfigurationOutcomes
150
118
  .map(({ system, state }) => `\nExternal configuration outcome: ${system} ${state} (external mutation not locally observable)`)
151
119
  .join("");
152
- return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}${filesystemMutationLines}${externalOutcomeLines}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
120
+ return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}${filesystemMutationLines}${externalOutcomeLines}\nGraph initialization: refreshed and verified\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
153
121
  }
154
122
  function formatRepairHuman(result) {
155
123
  const coverage = result.after.coverage;
@@ -643,7 +611,6 @@ async function main() {
643
611
  process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
644
612
  const engine = createEngine({ watcher: "disabled" });
645
613
  try {
646
- const systemConfig = loadSystemConfiguration(repository);
647
614
  const summary = await initializeRepositories([repository], {
648
615
  // A portfolio worker owns exactly one already-discovered worktree.
649
616
  // Do not recursively initialize nested repositories inside the same
@@ -654,7 +621,7 @@ async function main() {
654
621
  index: (target) => engine.index(target),
655
622
  status: (target) => engine.status(target),
656
623
  agents: detectSupportedAgents(),
657
- indexMode: (target) => indexModeForPath(systemConfig, target),
624
+ indexMode: (target) => indexModeForPath(loadSystemConfiguration(target), target),
658
625
  });
659
626
  const result = summary.results.find(({ repository: target }) => target === repository);
660
627
  if (!result)
@@ -704,6 +671,26 @@ async function main() {
704
671
  // `--json` is a shared output flag. Repair owns its richer --json/--jsonl
705
672
  // parser; all other commands receive their original arguments minus it.
706
673
  const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
674
+ let recoveryJournal = readUpdateJournal();
675
+ const recoverySafeStates = new Set(["planned", "candidate-verified", "rollback-verified"]);
676
+ if (recoveryJournal && !recoverySafeStates.has(recoveryJournal.state) && cmd !== "doctor") {
677
+ const recovery = await recoverInterruptedUpdate({ runtimeCommand, env: process.env });
678
+ recoveryJournal = readUpdateJournal();
679
+ if (recovery?.status === "terminal-unhealthy" && cmd !== "update") {
680
+ throw new Error(`knodin update recovery is terminal-unhealthy: ${recovery.reason ?? "candidate and rollback verification failed"}`);
681
+ }
682
+ }
683
+ if (recoveryJournal &&
684
+ !recoverySafeStates.has(recoveryJournal.state) &&
685
+ cmd !== "update" &&
686
+ cmd !== "doctor") {
687
+ throw new Error(`knodin is in unverified update state ${recoveryJournal.state}; run \`knodin update status\` for recovery guidance`);
688
+ }
689
+ if (!jsonOutput && cmd !== "update") {
690
+ const attention = updateAttention(readManagerUpdateState());
691
+ if (attention)
692
+ process.stderr.write(`${attention}\n`);
693
+ }
707
694
  const selector = {
708
695
  identity: selectorValue("--identity"),
709
696
  file: selectorValue("--file"),
@@ -805,7 +792,6 @@ async function main() {
805
792
  linkedWorktrees: plan.linkedWorktrees,
806
793
  });
807
794
  const engine = createEngine({ watcher: "disabled" });
808
- const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
809
795
  const repositories = [];
810
796
  const discoveryRecords = plan.signals
811
797
  ? discovery.repositories.slice(0, repositorySignalInspectionLimit(plan))
@@ -813,7 +799,7 @@ async function main() {
813
799
  for (const record of discoveryRecords) {
814
800
  const inventory = await inventoryRepository(record, {
815
801
  status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
816
- systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
802
+ systemMemberships: (target) => systemMembershipsForPath(loadSystemConfiguration(target), target),
817
803
  });
818
804
  repositories.push(withRepositorySignals(inventory, plan.signals, plan.signals ? await detectRepositorySignals(record.path) : {}));
819
805
  }
@@ -826,7 +812,11 @@ async function main() {
826
812
  repositories,
827
813
  };
828
814
  const boundedOutput = plan.signals
829
- ? applyResponseBudget(output, "repositories:discover", { bytes: plan.byteBudget, tokens: plan.tokenBudget, items: plan.itemBudget }, { bytes: 65_536, tokens: 16_384, items: 100 })
815
+ ? applyResponseBudget(output, "repositories:discover", {
816
+ bytes: plan.byteBudget,
817
+ tokens: plan.tokenBudget,
818
+ items: plan.itemBudget,
819
+ }, { bytes: 65_536, tokens: 16_384, items: 100 })
830
820
  : output;
831
821
  process.stdout.write(`${JSON.stringify(boundedOutput, null, plan.json ? 0 : 2)}\n`);
832
822
  process.exitCode = discovery.issues.length > 0 ? 1 : 0;
@@ -903,14 +893,18 @@ async function main() {
903
893
  }));
904
894
  }
905
895
  await engine.close();
906
- const output = { schemaVersion: 1, command: "init", ...summary, repositories };
896
+ const output = {
897
+ schemaVersion: 1,
898
+ command: "init",
899
+ ...summary,
900
+ repositories,
901
+ };
907
902
  process.stdout.write(plan.json ? `${JSON.stringify(output)}\n` : formatRepositoryHuman(summary));
908
903
  process.exitCode = summary.exitCode;
909
904
  return;
910
905
  }
911
906
  if (plan.command === "search") {
912
907
  const engine = createEngine({ watcher: "disabled" });
913
- const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
914
908
  const output = await searchRepositories(plan.roots, plan.query ?? "", {
915
909
  depth: plan.depth,
916
910
  linkedWorktrees: plan.linkedWorktrees,
@@ -927,7 +921,7 @@ async function main() {
927
921
  includeSource: true,
928
922
  federate: false,
929
923
  }),
930
- systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
924
+ systemMemberships: (target) => systemMembershipsForPath(loadSystemConfiguration(target), target),
931
925
  });
932
926
  await engine.close();
933
927
  process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
@@ -1011,13 +1005,15 @@ async function main() {
1011
1005
  if (cmd === "update") {
1012
1006
  if (repoFlag !== undefined)
1013
1007
  throw new Error("knodin update does not accept --repo");
1014
- const [action, ...unsupported] = rest;
1015
- if (!action || !["status", "check", "explain", "apply", "rollback"].includes(action)) {
1016
- throw new Error("knodin update requires status, check, explain, apply, or rollback");
1017
- }
1018
- if (unsupported.length > 0)
1019
- throw new Error(`knodin update ${action}: unknown argument ${unsupported[0]}`);
1008
+ const [action, ...actionArguments] = rest;
1009
+ if (!action)
1010
+ throw new Error("knodin update requires an action");
1020
1011
  const method = detectUpdateInstallMethod(runtimeCommand);
1012
+ const owner = resolveManagerOwnership({
1013
+ runtimeCommand,
1014
+ executablePath: runtimeCommand.at(-1),
1015
+ env: process.env,
1016
+ });
1021
1017
  const runManager = async (argv) => {
1022
1018
  const [executable, ...arguments_] = argv;
1023
1019
  if (!executable)
@@ -1053,8 +1049,16 @@ async function main() {
1053
1049
  healthCheck,
1054
1050
  };
1055
1051
  let output;
1056
- if (action === "status")
1057
- output = trustedUpdateStatus(options);
1052
+ if (action === "status") {
1053
+ output = {
1054
+ ...trustedUpdateStatus(options),
1055
+ owner,
1056
+ managerState: readManagerUpdateState(),
1057
+ journal: readUpdateJournal(),
1058
+ coordination: coordinationStatus(),
1059
+ attention: updateAttention(readManagerUpdateState()),
1060
+ };
1061
+ }
1058
1062
  else if (action === "check") {
1059
1063
  try {
1060
1064
  output = await checkTrustedUpdate(options);
@@ -1068,11 +1072,178 @@ async function main() {
1068
1072
  output = explainTrustedUpdate(options);
1069
1073
  else if (action === "apply")
1070
1074
  output = await applyTrustedUpdate(options);
1071
- else
1075
+ else if (action === "rollback")
1072
1076
  output = await rollbackTrustedUpdate(options);
1077
+ else if (action === "available") {
1078
+ output = await queryOwnerAvailability({ installedVersion: KNODIN_VERSION, owner });
1079
+ const availability = output;
1080
+ const state = readManagerUpdateState();
1081
+ if (availability.latestVersion &&
1082
+ state.pin?.kind === "automatic-rollback" &&
1083
+ state.pin.acknowledgedFailedVersion === state.pin.failedVersion &&
1084
+ availability.latestVersion !== state.pin.failedVersion) {
1085
+ writeManagerUpdateState({
1086
+ ...state,
1087
+ lastAvailableVersion: availability.latestVersion,
1088
+ pin: { ...state.pin, newerAvailableVersion: availability.latestVersion },
1089
+ });
1090
+ }
1091
+ }
1092
+ else if (action === "upgrade") {
1093
+ const apply = actionArguments.includes("--apply");
1094
+ const explicitVersion = actionArguments.find((argument) => argument !== "--apply");
1095
+ let targetVersion = explicitVersion;
1096
+ if (!targetVersion) {
1097
+ const availability = await queryOwnerAvailability({
1098
+ installedVersion: KNODIN_VERSION,
1099
+ owner,
1100
+ });
1101
+ if (!availability.latestVersion)
1102
+ output = availability;
1103
+ else
1104
+ targetVersion = availability.latestVersion;
1105
+ }
1106
+ if (targetVersion)
1107
+ output = await executeManagerUpdate({
1108
+ currentVersion: KNODIN_VERSION,
1109
+ targetVersion,
1110
+ runtimeCommand,
1111
+ env: process.env,
1112
+ apply,
1113
+ });
1114
+ }
1115
+ else if (action === "pin") {
1116
+ const apply = actionArguments.includes("--apply");
1117
+ const version = actionArguments.find((argument) => argument !== "--apply");
1118
+ if (!version)
1119
+ throw new Error("knodin update pin requires a version");
1120
+ output = apply
1121
+ ? await executeManagerUpdate({
1122
+ currentVersion: KNODIN_VERSION,
1123
+ targetVersion: version,
1124
+ runtimeCommand,
1125
+ env: process.env,
1126
+ apply: true,
1127
+ pinKind: "manual",
1128
+ })
1129
+ : setManualPin(version);
1130
+ }
1131
+ else if (action === "unpin")
1132
+ output = unpinUpdate();
1133
+ else if (action === "acknowledge")
1134
+ output = acknowledgeUpdateFailure();
1135
+ else if (action === "auto") {
1136
+ const subaction = actionArguments[0];
1137
+ if (!subaction || !["status", "enable", "disable", "run"].includes(subaction))
1138
+ throw new Error("knodin update auto requires status, enable, disable, or run");
1139
+ const state = readManagerUpdateState();
1140
+ if (subaction === "status")
1141
+ output = state;
1142
+ else if (subaction === "disable") {
1143
+ output = { ...state, autoEnabled: false };
1144
+ writeManagerUpdateState(output);
1145
+ }
1146
+ else if (subaction === "enable") {
1147
+ if (state.platformAutoEligible) {
1148
+ output = { ...state, autoEnabled: true };
1149
+ writeManagerUpdateState(output);
1150
+ }
1151
+ else
1152
+ output = await certifyAutomaticUpdates({
1153
+ currentVersion: KNODIN_VERSION,
1154
+ runtimeCommand,
1155
+ env: process.env,
1156
+ });
1157
+ }
1158
+ else if (!state.autoEnabled || !state.platformAutoEligible) {
1159
+ output = { ...state, status: "disabled" };
1160
+ }
1161
+ else {
1162
+ const availability = await queryOwnerAvailability({
1163
+ installedVersion: KNODIN_VERSION,
1164
+ owner,
1165
+ });
1166
+ output =
1167
+ availability.status === "available" &&
1168
+ availability.autoEligible &&
1169
+ availability.latestVersion
1170
+ ? await executeManagerUpdate({
1171
+ currentVersion: KNODIN_VERSION,
1172
+ targetVersion: availability.latestVersion,
1173
+ runtimeCommand,
1174
+ env: process.env,
1175
+ apply: true,
1176
+ })
1177
+ : availability;
1178
+ }
1179
+ }
1180
+ else
1181
+ throw new Error(`knodin update: unknown action ${action}`);
1073
1182
  process.stdout.write(jsonOutput ? `${JSON.stringify(output)}\n` : formatGenericHuman("update", output));
1074
1183
  return;
1075
1184
  }
1185
+ if (cmd === "backups") {
1186
+ const action = invocation.commandPath[1];
1187
+ const retentionAction = invocation.commandPath[2];
1188
+ const roots = invocation.positionals;
1189
+ const defaultRoots = () => {
1190
+ if (roots.length > 0)
1191
+ return roots;
1192
+ const target = resolveRepo(repoFlag, process.cwd());
1193
+ if (!target.ok)
1194
+ throw new Error(target.error);
1195
+ return [target.repo];
1196
+ };
1197
+ const depth = invocation.options.depth;
1198
+ const retentionDays = invocation.options.retentionDays;
1199
+ const keepNewest = invocation.options.keepNewest;
1200
+ let output;
1201
+ if (action === "list")
1202
+ output = listBackups(defaultRoots(), { depth });
1203
+ else if (action === "prune") {
1204
+ output = pruneBackups(defaultRoots(), {
1205
+ depth,
1206
+ retentionDays,
1207
+ keepNewest,
1208
+ apply: invocation.options.apply === true,
1209
+ });
1210
+ }
1211
+ else if (action === "retention" && retentionAction === "install") {
1212
+ output = installBackupRetention(roots, {
1213
+ depth,
1214
+ retentionDays,
1215
+ keepNewest,
1216
+ schedule: invocation.options.schedule,
1217
+ dryRun: invocation.options.dryRun === true,
1218
+ launcher: runtimeCommand,
1219
+ });
1220
+ }
1221
+ else if (action === "retention" && retentionAction === "status") {
1222
+ output = retentionStatus();
1223
+ }
1224
+ else if (action === "retention" && retentionAction === "doctor") {
1225
+ output = retentionDoctor();
1226
+ }
1227
+ else if (action === "retention" && retentionAction === "remove") {
1228
+ output = removeBackupRetention();
1229
+ }
1230
+ else if (action === "retention" && retentionAction === "run") {
1231
+ output = runInstalledRetention();
1232
+ }
1233
+ else {
1234
+ throw new Error("knodin backups requires list, prune, or retention install|status|doctor|remove");
1235
+ }
1236
+ if (invocation.options.jsonl === true) {
1237
+ const records = output.backups ??
1238
+ output.candidates ?? [output];
1239
+ for (const record of records)
1240
+ process.stdout.write(`${JSON.stringify(record)}\n`);
1241
+ }
1242
+ else {
1243
+ process.stdout.write(jsonOutput ? `${JSON.stringify(output)}\n` : formatBackupHuman("backups", output));
1244
+ }
1245
+ return;
1246
+ }
1076
1247
  if (process.stdout.isTTY &&
1077
1248
  claimScheduledUpdateCheck({
1078
1249
  currentVersion: KNODIN_VERSION,
@@ -1124,6 +1295,14 @@ async function main() {
1124
1295
  process.exit(1);
1125
1296
  }
1126
1297
  const repo = resolved.repo;
1298
+ if (cmd !== "doctor") {
1299
+ try {
1300
+ maybeRunOpportunisticRetention(repo);
1301
+ }
1302
+ catch {
1303
+ // Best-effort cleanup must never make an ordinary command fail.
1304
+ }
1305
+ }
1127
1306
  if (cmd === "agent-event" && !fs.existsSync(resolveDbPath(repo)))
1128
1307
  return;
1129
1308
  if (cmd === "doctor") {
@@ -1247,7 +1426,10 @@ async function main() {
1247
1426
  const input = positionals[0] === "create" ? (positionals[1] ?? "-") : (positionals[0] ?? "-");
1248
1427
  compressionResult =
1249
1428
  input === "-"
1250
- ? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
1429
+ ? compressOutput(repo, {
1430
+ ...request,
1431
+ text: await readBoundedStdin(maxInputBytes),
1432
+ })
1251
1433
  : compressOutputFile(repo, input, request);
1252
1434
  }
1253
1435
  if (action === "diagnose" &&
@@ -1326,7 +1508,10 @@ async function main() {
1326
1508
  if (existing.state !== "configured")
1327
1509
  throw new Error("knodin shared configure --disable requires a valid existing configuration");
1328
1510
  writeSharedIndexConfig(repo, { ...existing.config, enabled: false });
1329
- result = { state: "configured", config: { ...existing.config, enabled: false } };
1511
+ result = {
1512
+ state: "configured",
1513
+ config: { ...existing.config, enabled: false },
1514
+ };
1330
1515
  break;
1331
1516
  }
1332
1517
  const required = (name) => {
@@ -1361,7 +1546,11 @@ async function main() {
1361
1546
  },
1362
1547
  };
1363
1548
  const target = writeSharedIndexConfig(repo, config);
1364
- result = { state: "configured", path: target, config: loadSharedIndexConfig(repo) };
1549
+ result = {
1550
+ state: "configured",
1551
+ path: target,
1552
+ config: loadSharedIndexConfig(repo),
1553
+ };
1365
1554
  break;
1366
1555
  }
1367
1556
  if (action === "publisher-metadata") {
@@ -1395,7 +1584,10 @@ async function main() {
1395
1584
  if (!bundlePath)
1396
1585
  throw new Error("knodin shared publisher-finalize requires <bundle>");
1397
1586
  const bundle = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), bundlePath), "utf8"));
1398
- const pointer = finalizeSharedPublisherBundle({ config: configuration.config, bundle });
1587
+ const pointer = finalizeSharedPublisherBundle({
1588
+ config: configuration.config,
1589
+ bundle,
1590
+ });
1399
1591
  result = { bundle, pointer: JSON.parse(pointer.toString("utf8")) };
1400
1592
  break;
1401
1593
  }
@@ -1422,12 +1614,19 @@ async function main() {
1422
1614
  let credentialProbe = "not-requested";
1423
1615
  if (invocation.options.probe === true) {
1424
1616
  if (configuration.state !== "configured" || !configuration.config.enabled) {
1425
- credentialProbe = { state: "unavailable", category: "configuration" };
1617
+ credentialProbe = {
1618
+ state: "unavailable",
1619
+ category: "configuration",
1620
+ };
1426
1621
  }
1427
1622
  else {
1428
1623
  const store = createSharedObjectStore(configuration.config);
1429
1624
  try {
1430
- const current = spawnSync(gitExecutable(), ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim();
1625
+ const current = spawnSync(gitExecutable(), ["symbolic-ref", "--quiet", "--short", "HEAD"], {
1626
+ cwd: repo,
1627
+ encoding: "utf8",
1628
+ stdio: ["ignore", "pipe", "ignore"],
1629
+ }).stdout.trim();
1431
1630
  const branch = configuredBranchCandidates(configuration.config, current || undefined)[0];
1432
1631
  if (!store || !branch)
1433
1632
  throw new SharedIndexError("configuration", "shared-index probe has no configured local branch candidate");
@@ -1628,13 +1827,123 @@ async function main() {
1628
1827
  break;
1629
1828
  }
1630
1829
  case "system": {
1631
- const allowPartial = rest.includes("--allow-partial");
1632
- const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial");
1830
+ const requestedAction = invocation.commandPath[1];
1831
+ if (requestedAction === "link") {
1832
+ const [systemId, ...repositories] = invocation.positionals;
1833
+ if (!systemId)
1834
+ throw new Error("knodin system link requires <system-id>");
1835
+ const overrides = Object.fromEntries((Array.isArray(invocation.options.repositoryId) ? invocation.options.repositoryId : [])
1836
+ .map(String)
1837
+ .map((value) => {
1838
+ const at = value.lastIndexOf("=");
1839
+ if (at < 1)
1840
+ throw new Error("--repository-id must be <path>=<id>");
1841
+ return [value.slice(0, at), value.slice(at + 1)];
1842
+ }));
1843
+ const componentOverrides = Object.fromEntries((Array.isArray(invocation.options.component) ? invocation.options.component : [])
1844
+ .map(String)
1845
+ .map((value) => {
1846
+ const at = value.indexOf("=");
1847
+ if (at < 1)
1848
+ throw new Error("--component must be <repo-id>=<component-id>[:type]");
1849
+ const repositoryId = value.slice(0, at);
1850
+ const [id, componentType] = value.slice(at + 1).split(":");
1851
+ if (!id)
1852
+ throw new Error("--component requires a component id");
1853
+ if (componentType &&
1854
+ !["component", "api", "resource", "artifact"].includes(componentType))
1855
+ throw new Error(`--component: unsupported type ${componentType}`);
1856
+ return [
1857
+ repositoryId,
1858
+ {
1859
+ id,
1860
+ type: componentType,
1861
+ },
1862
+ ];
1863
+ }));
1864
+ if (invocation.options.initMissing === true && invocation.options.dryRun !== true) {
1865
+ for (const checkout of repositories.map((value) => path.resolve(repo, value))) {
1866
+ if (fs.existsSync(path.join(checkout, ".knodin")))
1867
+ continue;
1868
+ await initializeRepository(checkout, {
1869
+ command: runtimeCommand,
1870
+ index: (target, indexOptions) => engine.index(target, undefined, false, indexOptions),
1871
+ scope: "personal",
1872
+ agents: [],
1873
+ });
1874
+ }
1875
+ }
1876
+ const plan = planSystemLink({
1877
+ systemId,
1878
+ repositories,
1879
+ scope: invocation.options.scope === "team" ? "team" : "personal",
1880
+ distributed: invocation.options.distributed === true,
1881
+ repositoryIds: overrides,
1882
+ components: componentOverrides,
1883
+ });
1884
+ result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
1885
+ break;
1886
+ }
1887
+ if (requestedAction === "relate") {
1888
+ const [systemId] = invocation.positionals;
1889
+ if (!systemId)
1890
+ throw new Error("knodin system relate requires <system-id>");
1891
+ const plan = planSystemRelate({
1892
+ systemId,
1893
+ type: String(invocation.options.type),
1894
+ from: String(invocation.options.from),
1895
+ to: String(invocation.options.to),
1896
+ evidence: String(invocation.options.evidence),
1897
+ repoPath: repo,
1898
+ });
1899
+ result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
1900
+ break;
1901
+ }
1902
+ if (requestedAction === "unlink") {
1903
+ const [systemId, ...repositories] = invocation.positionals;
1904
+ if (!systemId)
1905
+ throw new Error("knodin system unlink requires <system-id>");
1906
+ const plan = planSystemUnlink({
1907
+ systemId,
1908
+ repositories,
1909
+ repoPath: repo,
1910
+ });
1911
+ result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
1912
+ break;
1913
+ }
1914
+ if (requestedAction === "unrelate") {
1915
+ const [systemId] = invocation.positionals;
1916
+ if (!systemId)
1917
+ throw new Error("knodin system unrelate requires <system-id>");
1918
+ const plan = planSystemUnrelate({
1919
+ systemId,
1920
+ type: String(invocation.options.type),
1921
+ from: String(invocation.options.from),
1922
+ to: String(invocation.options.to),
1923
+ repoPath: repo,
1924
+ });
1925
+ result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
1926
+ break;
1927
+ }
1928
+ if (requestedAction === "import") {
1929
+ const [manifestPath] = invocation.positionals;
1930
+ if (!manifestPath)
1931
+ throw new Error("knodin system import requires <path>");
1932
+ const plan = planSystemImport({
1933
+ manifestPath,
1934
+ scope: invocation.options.scope === "team" ? "team" : "personal",
1935
+ repoPath: repo,
1936
+ });
1937
+ result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
1938
+ break;
1939
+ }
1940
+ const requireComplete = rest.includes("--require-complete");
1941
+ const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial" && argument !== "--require-complete");
1633
1942
  if (!action || !["list", "show", "validate", "query"].includes(action)) {
1634
1943
  throw new Error("knodin system requires list, show, validate, or query");
1635
1944
  }
1636
- if (allowPartial && action !== "query")
1637
- throw new Error(`knodin system ${action}: --allow-partial applies only to query`);
1945
+ if (requireComplete && action !== "query")
1946
+ throw new Error(`knodin system ${action}: --require-complete applies only to query`);
1638
1947
  if (unsupported.length > 0)
1639
1948
  throw new Error(`knodin system ${action}: unknown argument ${unsupported[0]}`);
1640
1949
  const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
@@ -1673,39 +1982,83 @@ async function main() {
1673
1982
  }
1674
1983
  else {
1675
1984
  const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
1676
- result = queryConfiguredSystem(config, systemId, allowPartial, validation);
1985
+ result = queryConfiguredSystem(config, systemId, requireComplete, validation);
1677
1986
  if (result.status === "unavailable")
1678
1987
  process.exitCode = 1;
1679
1988
  }
1680
1989
  break;
1681
1990
  }
1682
- case "hook-refresh": {
1683
- const [kind, first, second] = rest;
1684
- let event;
1685
- if (kind === "commit")
1686
- event = { kind };
1687
- else if (kind === "checkout" && first && second) {
1688
- event = { kind, before: first, after: second };
1689
- }
1690
- else if (kind === "merge" && first && second) {
1691
- event = { kind, before: first, after: second };
1991
+ case "skills": {
1992
+ const action = invocation.commandPath[1];
1993
+ const requested = Array.isArray(invocation.options.client)
1994
+ ? invocation.options.client.map(String)
1995
+ : [];
1996
+ const supported = new Set(["codex", "claude", "gemini"]);
1997
+ for (const client of requested)
1998
+ if (!supported.has(client))
1999
+ throw new Error(`knodin skills: unsupported client ${client}`);
2000
+ const clients = (requested.length ? requested : ["codex", "claude", "gemini"]);
2001
+ const scope = typeof invocation.options.scope === "string"
2002
+ ? parseInitScope(invocation.options.scope)
2003
+ : "personal";
2004
+ if (action === "list") {
2005
+ result = { skills: KNODIN_SKILLS, clients, scope };
2006
+ break;
1692
2007
  }
1693
- else if (kind === "rewrite" && first) {
1694
- event = { kind, inputPath: first };
2008
+ if (action === "install")
2009
+ result = installKnodinSkills({
2010
+ repo,
2011
+ scope,
2012
+ clients,
2013
+ dryRun: invocation.options.dryRun === true,
2014
+ });
2015
+ else if (action === "remove")
2016
+ result = removeKnodinSkills({
2017
+ repo,
2018
+ scope,
2019
+ clients,
2020
+ dryRun: invocation.options.dryRun === true,
2021
+ });
2022
+ else if (action === "doctor")
2023
+ result = inspectKnodinSkills({ repo, scope, clients });
2024
+ else
2025
+ throw new Error("knodin skills requires list, install, remove, or doctor");
2026
+ break;
2027
+ }
2028
+ case "hook-refresh": {
2029
+ const lifecycleLease = acquireRepairLease(repo, "lifecycle-hook");
2030
+ try {
2031
+ const [kind, first, second] = rest;
2032
+ let event;
2033
+ if (kind === "commit")
2034
+ event = { kind };
2035
+ else if (kind === "checkout" && first && second) {
2036
+ event = { kind, before: first, after: second };
2037
+ }
2038
+ else if (kind === "merge" && first && second) {
2039
+ event = { kind, before: first, after: second };
2040
+ }
2041
+ else if (kind === "rewrite" && first) {
2042
+ event = { kind, inputPath: first };
2043
+ }
2044
+ else {
2045
+ throw new Error("knodin hook-refresh: invalid lifecycle event");
2046
+ }
2047
+ const shared = await opportunisticSharedRestore();
2048
+ const indexed = shared?.restored
2049
+ ? []
2050
+ : await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
2051
+ result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
1695
2052
  }
1696
- else {
1697
- throw new Error("knodin hook-refresh: invalid lifecycle event");
2053
+ finally {
2054
+ lifecycleLease.release();
1698
2055
  }
1699
- const shared = await opportunisticSharedRestore();
1700
- const indexed = shared?.restored
1701
- ? []
1702
- : await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1703
- result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
1704
2056
  break;
1705
2057
  }
1706
2058
  case "init": {
1707
- const scope = await chooseInitScope(rawRest, repo);
1708
- const agents = scope === "team" ? [] : integrationAgents(repo);
2059
+ const requestedScope = explicitScope(rawRest);
2060
+ const scope = requestedScope ?? readRepositoryIntegrationConfig(repo)?.scope ?? "cli-only";
2061
+ const agents = requestedScope && scope !== "team" ? integrationAgents(repo) : [];
1709
2062
  const renderer = createInitRenderer();
1710
2063
  const activity = createIndexActivityReporter(repo);
1711
2064
  activity.start();
@@ -1723,6 +2076,7 @@ async function main() {
1723
2076
  index: (target, options) => engine.index(target, undefined, false, options),
1724
2077
  scope,
1725
2078
  agents,
2079
+ configureIntegration: requestedScope !== null,
1726
2080
  onProgress: (event) => {
1727
2081
  activity.update(event);
1728
2082
  renderer.onProgress(event);
@@ -1767,16 +2121,17 @@ async function main() {
1767
2121
  const agents = scope === "team" ? [] : integrationAgents(repo);
1768
2122
  const paths = await initializeRepository(repo, {
1769
2123
  command: runtimeCommand,
1770
- index: async () => undefined,
2124
+ index: (target, options) => engine.index(target, undefined, false, options),
1771
2125
  scope,
1772
2126
  agents,
1773
2127
  allowTrackedTransition: true,
1774
2128
  auditConfigurationChanges: true,
2129
+ configureIntegration: true,
1775
2130
  });
1776
2131
  result = {
1777
2132
  status: "success",
1778
2133
  message: `knodin agent integration changed to ${scope}.`,
1779
- graphInitialization: "unchanged",
2134
+ graphInitialization: "refreshed",
1780
2135
  nextAction: "run `knodin status` and reload the configured client",
1781
2136
  paths,
1782
2137
  };
@@ -1926,7 +2281,9 @@ async function main() {
1926
2281
  }
1927
2282
  const progressMode = resolveRepairProgressMode(options, process.env, process.stderr.isTTY);
1928
2283
  const renderer = progressMode === "tty"
1929
- ? createProgressWorkerRenderer("repair-progress-worker", { type: "start" })
2284
+ ? createProgressWorkerRenderer("repair-progress-worker", {
2285
+ type: "start",
2286
+ })
1930
2287
  : (() => {
1931
2288
  const direct = createRepairProgressRenderer({
1932
2289
  mode: progressMode,
@@ -2367,7 +2724,10 @@ async function main() {
2367
2724
  else if (action === "export")
2368
2725
  result = exportTelemetry(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"), readSessionEvents(repo, retentionDays));
2369
2726
  else if (action === "clear")
2370
- result = { ...clearTelemetry(repo, input), session: clearSessionTelemetry(repo) };
2727
+ result = {
2728
+ ...clearTelemetry(repo, input),
2729
+ session: clearSessionTelemetry(repo),
2730
+ };
2371
2731
  else
2372
2732
  throw new Error("knodin telemetry requires enable, disable, status, report, export, or clear");
2373
2733
  break;
@@ -2439,7 +2799,11 @@ async function main() {
2439
2799
  result =
2440
2800
  action === "preview"
2441
2801
  ? persistDiagnosticsPreview(repo, options)
2442
- : collectDiagnostics(repo, { ...options, outputPath: outputOption, previewId });
2802
+ : collectDiagnostics(repo, {
2803
+ ...options,
2804
+ outputPath: outputOption,
2805
+ previewId,
2806
+ });
2443
2807
  }
2444
2808
  else {
2445
2809
  throw new Error("knodin diagnostics requires enable, status, preview, archive, inspect, clear, or disable");