knodin 0.5.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -535
- package/dist/bin/cli.js +173 -17
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +4 -16
- package/dist/src/artifact-refresh.js +1 -1
- package/dist/src/cli-args.js +8 -1
- package/dist/src/cli-model.js +35 -1
- package/dist/src/codeflow-replay.js +80 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-manifest.js +106 -25
- package/dist/src/competitive-runner.js +37 -3
- package/dist/src/competitive-sandbox.js +4 -4
- package/dist/src/context-export.js +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/docs-sections.js +5 -5
- package/dist/src/engine/ann-hnsw.js +5 -5
- package/dist/src/engine/embeddings.js +4 -4
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +450 -103
- package/dist/src/engine/prune.js +2 -2
- package/dist/src/engine/scip-import.js +408 -0
- package/dist/src/engine/symbol-delete.js +1 -1
- package/dist/src/execution-profile.js +203 -0
- package/dist/src/failure-diagnosis.js +69 -10
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/index-activity.js +1 -1
- package/dist/src/init-progress-worker.js +1 -1
- package/dist/src/init.js +361 -92
- package/dist/src/lifecycle-health.js +51 -14
- package/dist/src/lsp-readonly.js +1 -1
- package/dist/src/output-compression.js +1 -1
- package/dist/src/output-telemetry.js +8 -4
- package/dist/src/pr-triage.js +2 -2
- package/dist/src/progressive-evidence.js +473 -0
- package/dist/src/pure-compression-cli.js +101 -0
- package/dist/src/release-preflight.js +510 -0
- package/dist/src/repair-progress-worker.js +1 -1
- package/dist/src/repository-init-process.js +1 -1
- package/dist/src/repository-management.js +145 -3
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +23 -3
- package/dist/src/structural-fast-path.js +303 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/system-config.js +7 -7
- package/dist/src/tools/knodin-tools.js +106 -15
- package/dist/src/update-ceremony.js +158 -0
- package/dist/src/update-policy.js +4 -4
- package/dist/src/wait-for-fresh.js +2 -2
- package/dist/src/worktree-lifecycle.js +2 -2
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +7 -4
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
- package/docs/TELEMETRY.md +6 -2
- package/docs/releases/0.6.0.md +18 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/package.json +34 -3
- package/dist/src/tools/reckon-tools.js +0 -5
package/dist/bin/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* knodin explain <symbol> [minimal] edit-ready source + direct call paths + blast radius
|
|
6
6
|
* knodin review [base] risk-scored context with explicit git diff scopes
|
|
7
7
|
* knodin map subsystems + confidence-tagged edges
|
|
8
|
-
* knodin wiki [--force] write .
|
|
8
|
+
* knodin wiki [--force] write .knodin/wiki/ (index.md + per-community pages)
|
|
9
9
|
* knodin serve run the MCP server on stdio
|
|
10
10
|
*
|
|
11
11
|
* The same engine backs both the CLI and the MCP server, so any assistant and
|
|
@@ -22,6 +22,7 @@ import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, pla
|
|
|
22
22
|
import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
|
|
23
23
|
import { buildKnodinContext } from "../src/context.js";
|
|
24
24
|
import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/context-export.js";
|
|
25
|
+
import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, recordDiagnosticFailure, } from "../src/diagnostics.js";
|
|
25
26
|
import { getDocSection, listDocTopics } from "../src/docs-sections.js";
|
|
26
27
|
import { diagnoseInstallation } from "../src/doctor.js";
|
|
27
28
|
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
|
|
@@ -31,13 +32,14 @@ import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-
|
|
|
31
32
|
import { createIndexActivityReporter } from "../src/index-activity.js";
|
|
32
33
|
import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
|
|
33
34
|
import { createInitProgressRenderer } from "../src/init-progress.js";
|
|
34
|
-
import { attachLifecycleHealth } from "../src/lifecycle-health.js";
|
|
35
|
+
import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
|
|
35
36
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
|
|
36
37
|
import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
|
|
37
38
|
import { auditPullRequests } from "../src/pr-triage.js";
|
|
39
|
+
import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
|
|
38
40
|
import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
|
|
39
41
|
import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
|
|
40
|
-
import { discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, searchRepositories, } from "../src/repository-management.js";
|
|
42
|
+
import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
|
|
41
43
|
import { applyResponseBudget } from "../src/response-budget.js";
|
|
42
44
|
import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
|
|
43
45
|
import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
|
|
@@ -96,7 +98,8 @@ async function chooseInitScope(args, repo) {
|
|
|
96
98
|
}
|
|
97
99
|
function integrationAgents(repo) {
|
|
98
100
|
const previous = readRepositoryIntegrationConfig(repo)?.agents ?? [];
|
|
99
|
-
|
|
101
|
+
const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
|
|
102
|
+
return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
|
|
100
103
|
}
|
|
101
104
|
function formatInitHuman(result) {
|
|
102
105
|
const agents = result.paths.scope === "cli-only"
|
|
@@ -117,6 +120,8 @@ function formatConfigureStatusHuman(result) {
|
|
|
117
120
|
return "Agent integration: unconfigured.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal`.\n";
|
|
118
121
|
if (result.scope === "cli-only")
|
|
119
122
|
return "Agent integration: CLI-only.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal` or `--scope team` to enable them.\n";
|
|
123
|
+
if (result.scope === "repository-detected")
|
|
124
|
+
return `Agent integration: repository-detected${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\nLocal scope receipt: missing; managed repository integration is present.\n`;
|
|
120
125
|
return `Agent integration: ${result.scope}${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\n`;
|
|
121
126
|
}
|
|
122
127
|
function formatConfigureHuman(result) {
|
|
@@ -133,7 +138,13 @@ function formatConfigureHuman(result) {
|
|
|
133
138
|
const refresh = result.paths.lifecycleRefresh.state === "fresh"
|
|
134
139
|
? "fresh"
|
|
135
140
|
: `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s))`;
|
|
136
|
-
|
|
141
|
+
const filesystemMutationLines = result.paths.filesystemChanges
|
|
142
|
+
.map(({ path: changedPath, action }) => `\nFilesystem ${action}: ${changedPath}`)
|
|
143
|
+
.join("");
|
|
144
|
+
const externalOutcomeLines = result.paths.externalConfigurationOutcomes
|
|
145
|
+
.map(({ system, state }) => `\nExternal configuration outcome: ${system} ${state} (external mutation not locally observable)`)
|
|
146
|
+
.join("");
|
|
147
|
+
return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}${filesystemMutationLines}${externalOutcomeLines}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
|
|
137
148
|
}
|
|
138
149
|
function formatRepairHuman(result) {
|
|
139
150
|
const coverage = result.after.coverage;
|
|
@@ -141,6 +152,8 @@ function formatRepairHuman(result) {
|
|
|
141
152
|
return `Repair paused: ${result.remaining ?? 0} file(s) remaining. Run \`knodin repair\` again to finish.\n`;
|
|
142
153
|
}
|
|
143
154
|
if (result.verified) {
|
|
155
|
+
if (result.lifecycle?.status === "degraded")
|
|
156
|
+
return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols), but lifecycle routing is degraded. Run \`knodin init\`, then \`knodin status\`.\n`;
|
|
144
157
|
return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols).\n`;
|
|
145
158
|
}
|
|
146
159
|
return `Repair finished with remaining issues. Run \`knodin status --deep\` for details.\n`;
|
|
@@ -313,7 +326,7 @@ function createProgressWorkerRenderer(workerName, startMessage) {
|
|
|
313
326
|
: [workerPath];
|
|
314
327
|
const worker = spawn(process.execPath, workerArgs, {
|
|
315
328
|
stdio: ["pipe", "ignore", "inherit"],
|
|
316
|
-
env: { ...process.env,
|
|
329
|
+
env: { ...process.env, KNODIN_FORCE_PROGRESS_TTY: "1" },
|
|
317
330
|
});
|
|
318
331
|
let failed = false;
|
|
319
332
|
const closed = new Promise((resolve) => {
|
|
@@ -547,11 +560,15 @@ async function main() {
|
|
|
547
560
|
const engine = createEngine();
|
|
548
561
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
549
562
|
const repositories = [];
|
|
550
|
-
|
|
551
|
-
repositories.
|
|
563
|
+
const discoveryRecords = plan.signals
|
|
564
|
+
? discovery.repositories.slice(0, repositorySignalInspectionLimit(plan))
|
|
565
|
+
: discovery.repositories;
|
|
566
|
+
for (const record of discoveryRecords) {
|
|
567
|
+
const inventory = await inventoryRepository(record, {
|
|
552
568
|
status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
|
|
553
569
|
systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
|
|
554
|
-
})
|
|
570
|
+
});
|
|
571
|
+
repositories.push(withRepositorySignals(inventory, plan.signals, plan.signals ? await detectRepositorySignals(record.path) : {}));
|
|
555
572
|
}
|
|
556
573
|
await engine.close();
|
|
557
574
|
const output = {
|
|
@@ -561,7 +578,10 @@ async function main() {
|
|
|
561
578
|
...discovery,
|
|
562
579
|
repositories,
|
|
563
580
|
};
|
|
564
|
-
|
|
581
|
+
const boundedOutput = plan.signals
|
|
582
|
+
? applyResponseBudget(output, "repositories:discover", { bytes: plan.byteBudget, tokens: plan.tokenBudget, items: plan.itemBudget }, { bytes: 65_536, tokens: 16_384, items: 100 })
|
|
583
|
+
: output;
|
|
584
|
+
process.stdout.write(`${JSON.stringify(boundedOutput, null, plan.json ? 0 : 2)}\n`);
|
|
565
585
|
process.exitCode = discovery.issues.length > 0 ? 1 : 0;
|
|
566
586
|
return;
|
|
567
587
|
}
|
|
@@ -788,7 +808,7 @@ async function main() {
|
|
|
788
808
|
output = await checkTrustedUpdate(options);
|
|
789
809
|
}
|
|
790
810
|
finally {
|
|
791
|
-
if (process.env.
|
|
811
|
+
if (process.env.KNODIN_UPDATE_BACKGROUND === "1")
|
|
792
812
|
releaseScheduledUpdateCheck();
|
|
793
813
|
}
|
|
794
814
|
}
|
|
@@ -812,7 +832,7 @@ async function main() {
|
|
|
812
832
|
const background = spawn(executable, [...arguments_, "update", "check", "--json"], {
|
|
813
833
|
detached: true,
|
|
814
834
|
stdio: "ignore",
|
|
815
|
-
env: { ...process.env,
|
|
835
|
+
env: { ...process.env, KNODIN_UPDATE_BACKGROUND: "1" },
|
|
816
836
|
});
|
|
817
837
|
background.unref();
|
|
818
838
|
}
|
|
@@ -856,6 +876,7 @@ async function main() {
|
|
|
856
876
|
"--start",
|
|
857
877
|
"--end",
|
|
858
878
|
"--limit",
|
|
879
|
+
"--offset",
|
|
859
880
|
]);
|
|
860
881
|
const positionals = rest.filter((argument, index) => !argument.startsWith("--") && !valueFlags.has(rest[index - 1]));
|
|
861
882
|
const integer = (flag, fallback, minimum, maximum) => {
|
|
@@ -881,6 +902,7 @@ async function main() {
|
|
|
881
902
|
compressionResult = await diagnoseFailure(diagnosisEngine, repo, {
|
|
882
903
|
artifactId: artifactId ?? "",
|
|
883
904
|
maxDiagnostics: integer("--limit", 10, 1, 50),
|
|
905
|
+
diagnosticOffset: integer("--offset", 0, 0, 1_000_000),
|
|
884
906
|
contextLines: integer("--context", 2, 0, 10),
|
|
885
907
|
contextByteBudget: integer("--max-output-bytes", 16_384, 256, 128 * 1024),
|
|
886
908
|
});
|
|
@@ -945,6 +967,12 @@ async function main() {
|
|
|
945
967
|
? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
|
|
946
968
|
: compressOutputFile(repo, input, request);
|
|
947
969
|
}
|
|
970
|
+
if (action === "diagnose" &&
|
|
971
|
+
(responseBudget.bytes !== undefined ||
|
|
972
|
+
responseBudget.tokens !== undefined ||
|
|
973
|
+
responseBudget.items !== undefined)) {
|
|
974
|
+
compressionResult = applyResponseBudget(compressionResult, "compress:diagnose", responseBudget, { bytes: 65_536, tokens: 16_384, items: 50 });
|
|
975
|
+
}
|
|
948
976
|
if (jsonOutput)
|
|
949
977
|
process.stdout.write(`${JSON.stringify(compressionResult)}\n`);
|
|
950
978
|
else if (action === "diagnose" && !diagnosisUnavailable)
|
|
@@ -1120,7 +1148,7 @@ async function main() {
|
|
|
1120
1148
|
}
|
|
1121
1149
|
case "configure": {
|
|
1122
1150
|
if (rawRest.includes("--status")) {
|
|
1123
|
-
result =
|
|
1151
|
+
result = inspectRepositoryIntegrationStatus(repo) ?? {
|
|
1124
1152
|
scope: "unconfigured",
|
|
1125
1153
|
agents: [],
|
|
1126
1154
|
warning: "AI agents are not configured by knodin. Run `knodin configure --scope personal`.",
|
|
@@ -1130,7 +1158,7 @@ async function main() {
|
|
|
1130
1158
|
const scope = explicitScope(rawRest);
|
|
1131
1159
|
if (!scope)
|
|
1132
1160
|
throw new Error("knodin configure: missing --scope");
|
|
1133
|
-
if (!fs.existsSync(path.join(repo, ".
|
|
1161
|
+
if (!fs.existsSync(path.join(repo, ".knodin", "db.sqlite"))) {
|
|
1134
1162
|
throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
|
|
1135
1163
|
}
|
|
1136
1164
|
const agents = scope === "team" ? [] : integrationAgents(repo);
|
|
@@ -1140,6 +1168,7 @@ async function main() {
|
|
|
1140
1168
|
scope,
|
|
1141
1169
|
agents,
|
|
1142
1170
|
allowTrackedTransition: true,
|
|
1171
|
+
auditConfigurationChanges: true,
|
|
1143
1172
|
});
|
|
1144
1173
|
result = {
|
|
1145
1174
|
status: "success",
|
|
@@ -1162,6 +1191,7 @@ async function main() {
|
|
|
1162
1191
|
process.exit(1);
|
|
1163
1192
|
}
|
|
1164
1193
|
const clean = rest.includes("--clean") || rest.includes("--force");
|
|
1194
|
+
const scipPath = selectorValue("--scip");
|
|
1165
1195
|
const renderer = createInitRenderer("index");
|
|
1166
1196
|
const activity = createIndexActivityReporter(plan.repo);
|
|
1167
1197
|
activity.start();
|
|
@@ -1169,6 +1199,7 @@ async function main() {
|
|
|
1169
1199
|
let indexResult;
|
|
1170
1200
|
try {
|
|
1171
1201
|
indexResult = await engine.index(plan.repo, plan.files, clean, {
|
|
1202
|
+
scip: scipPath ? { path: scipPath } : undefined,
|
|
1172
1203
|
onProgress: (event) => {
|
|
1173
1204
|
activity.update(event);
|
|
1174
1205
|
renderer.onProgress(event);
|
|
@@ -1271,10 +1302,10 @@ async function main() {
|
|
|
1271
1302
|
process.once("SIGINT", abortRepair);
|
|
1272
1303
|
renderer.start();
|
|
1273
1304
|
try {
|
|
1274
|
-
result = await engine.repair(repo, {
|
|
1305
|
+
result = attachRepairLifecycle(repo, await engine.repair(repo, {
|
|
1275
1306
|
signal: controller.signal,
|
|
1276
1307
|
onProgress: (event) => renderer.onProgress(event),
|
|
1277
|
-
});
|
|
1308
|
+
}));
|
|
1278
1309
|
}
|
|
1279
1310
|
finally {
|
|
1280
1311
|
process.removeListener("SIGINT", abortRepair);
|
|
@@ -1436,6 +1467,28 @@ async function main() {
|
|
|
1436
1467
|
}
|
|
1437
1468
|
break;
|
|
1438
1469
|
}
|
|
1470
|
+
case "evidence": {
|
|
1471
|
+
const level = rest[0];
|
|
1472
|
+
const file = rest[1];
|
|
1473
|
+
if (!file || !["locate", "outline", "evidence", "expand"].includes(level))
|
|
1474
|
+
throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");
|
|
1475
|
+
result = deliverProgressiveEvidence({
|
|
1476
|
+
repo,
|
|
1477
|
+
file,
|
|
1478
|
+
level,
|
|
1479
|
+
continuation: selectorValue("--continuation"),
|
|
1480
|
+
baselineHash: selectorValue("--baseline-hash"),
|
|
1481
|
+
baselineBytes: selectorValue("--baseline-bytes")
|
|
1482
|
+
? Number(selectorValue("--baseline-bytes"))
|
|
1483
|
+
: undefined,
|
|
1484
|
+
startLine: selectorValue("--start") ? Number(selectorValue("--start")) : undefined,
|
|
1485
|
+
endLine: selectorValue("--end") ? Number(selectorValue("--end")) : undefined,
|
|
1486
|
+
byteLimit: responseBudget.bytes,
|
|
1487
|
+
tokenLimit: responseBudget.tokens,
|
|
1488
|
+
itemLimit: responseBudget.items,
|
|
1489
|
+
});
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1439
1492
|
case "query": {
|
|
1440
1493
|
const pattern = rest[0];
|
|
1441
1494
|
const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
|
|
@@ -1634,6 +1687,61 @@ async function main() {
|
|
|
1634
1687
|
throw new Error("knodin telemetry requires status, report, export, or clear");
|
|
1635
1688
|
break;
|
|
1636
1689
|
}
|
|
1690
|
+
case "diagnostics": {
|
|
1691
|
+
const [action, bundlePath] = invocation.positionals;
|
|
1692
|
+
const rawRetention = selectorValue("--retention-days");
|
|
1693
|
+
const sinceOption = selectorValue("--since");
|
|
1694
|
+
const outputOption = selectorValue("--output");
|
|
1695
|
+
const retentionDays = rawRetention === undefined ? 14 : Number(rawRetention);
|
|
1696
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365)
|
|
1697
|
+
throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");
|
|
1698
|
+
if (bundlePath && action !== "inspect")
|
|
1699
|
+
throw new Error(`knodin diagnostics ${action}: unexpected bundle argument`);
|
|
1700
|
+
if (rawRetention !== undefined && action !== "enable")
|
|
1701
|
+
throw new Error(`knodin diagnostics ${action}: --retention-days applies only to enable`);
|
|
1702
|
+
if (sinceOption !== undefined && action !== "collect")
|
|
1703
|
+
throw new Error(`knodin diagnostics ${action}: --since applies only to collect`);
|
|
1704
|
+
if (outputOption !== undefined && action !== "collect")
|
|
1705
|
+
throw new Error(`knodin diagnostics ${action}: --output applies only to collect`);
|
|
1706
|
+
if (action === "enable")
|
|
1707
|
+
result = enableDiagnostics(repo, retentionDays);
|
|
1708
|
+
else if (action === "status")
|
|
1709
|
+
result = diagnosticsStatus(repo);
|
|
1710
|
+
else if (action === "disable")
|
|
1711
|
+
result = disableDiagnostics(repo);
|
|
1712
|
+
else if (action === "clear")
|
|
1713
|
+
result = clearDiagnostics(repo);
|
|
1714
|
+
else if (action === "inspect") {
|
|
1715
|
+
if (!bundlePath)
|
|
1716
|
+
throw new Error("knodin diagnostics inspect requires <bundle>");
|
|
1717
|
+
result = inspectDiagnosticsBundle(repo, bundlePath);
|
|
1718
|
+
}
|
|
1719
|
+
else if (action === "collect") {
|
|
1720
|
+
const since = sinceOption ?? "24h";
|
|
1721
|
+
const match = /^(\d+)(h|d)$/.exec(since);
|
|
1722
|
+
if (!match)
|
|
1723
|
+
throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");
|
|
1724
|
+
const sinceHours = Number(match[1]) * (match[2] === "d" ? 24 : 1);
|
|
1725
|
+
const graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
|
|
1726
|
+
const doctor = await diagnoseInstallation(repo, {
|
|
1727
|
+
currentVersion: KNODIN_VERSION,
|
|
1728
|
+
runtimeCommand: [...runtimeCommand, "serve"],
|
|
1729
|
+
graph,
|
|
1730
|
+
});
|
|
1731
|
+
result = collectDiagnostics(repo, {
|
|
1732
|
+
sinceHours,
|
|
1733
|
+
outputPath: outputOption,
|
|
1734
|
+
doctor,
|
|
1735
|
+
graph,
|
|
1736
|
+
telemetry: readTelemetryRecords(repo, undefined, Math.max(1, Math.ceil(sinceHours / 24))),
|
|
1737
|
+
knodinVersion: KNODIN_VERSION,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
throw new Error("knodin diagnostics requires enable, status, collect, inspect, clear, or disable");
|
|
1742
|
+
}
|
|
1743
|
+
break;
|
|
1744
|
+
}
|
|
1637
1745
|
case "context": {
|
|
1638
1746
|
// knodin context "<task>" [base]
|
|
1639
1747
|
const task = rest[0];
|
|
@@ -1699,6 +1807,54 @@ async function main() {
|
|
|
1699
1807
|
process.exitCode = finalExitCode;
|
|
1700
1808
|
}
|
|
1701
1809
|
main().catch((err) => {
|
|
1702
|
-
|
|
1810
|
+
const argv = process.argv.slice(2);
|
|
1811
|
+
const repoIndex = argv.indexOf("--repo");
|
|
1812
|
+
const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
|
|
1813
|
+
const candidate = repoIndex >= 0 && argv[repoIndex + 1]
|
|
1814
|
+
? argv[repoIndex + 1]
|
|
1815
|
+
: equalsRepo
|
|
1816
|
+
? equalsRepo.slice("--repo=".length)
|
|
1817
|
+
: process.cwd();
|
|
1818
|
+
const command = argv.find((argument, index) => {
|
|
1819
|
+
if (argument.startsWith("-"))
|
|
1820
|
+
return false;
|
|
1821
|
+
return !(index > 0 && argv[index - 1] === "--repo") && argument !== candidate;
|
|
1822
|
+
});
|
|
1823
|
+
const knownCommands = new Set([
|
|
1824
|
+
"init",
|
|
1825
|
+
"configure",
|
|
1826
|
+
"index",
|
|
1827
|
+
"doctor",
|
|
1828
|
+
"status",
|
|
1829
|
+
"wait",
|
|
1830
|
+
"repair",
|
|
1831
|
+
"serve",
|
|
1832
|
+
"context",
|
|
1833
|
+
"explain",
|
|
1834
|
+
"review",
|
|
1835
|
+
"map",
|
|
1836
|
+
"search",
|
|
1837
|
+
"query",
|
|
1838
|
+
"rename",
|
|
1839
|
+
"wiki",
|
|
1840
|
+
"visualize",
|
|
1841
|
+
"pack",
|
|
1842
|
+
"compress",
|
|
1843
|
+
"prs",
|
|
1844
|
+
"worktrees",
|
|
1845
|
+
"telemetry",
|
|
1846
|
+
"diagnostics",
|
|
1847
|
+
"system",
|
|
1848
|
+
"repos",
|
|
1849
|
+
"update",
|
|
1850
|
+
]);
|
|
1851
|
+
const diagnostic = recordDiagnosticFailure(candidate, {
|
|
1852
|
+
surface: "cli",
|
|
1853
|
+
operation: command && knownCommands.has(command) ? command : "unknown",
|
|
1854
|
+
phase: "dispatch",
|
|
1855
|
+
error: err,
|
|
1856
|
+
});
|
|
1857
|
+
const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
|
|
1858
|
+
console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
|
|
1703
1859
|
process.exit(1);
|
|
1704
1860
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { tryStructuralFastPath } from "../src/structural-fast-path.js";
|
|
3
|
+
const argv = process.argv.slice(2);
|
|
4
|
+
const fast = tryStructuralFastPath(argv);
|
|
5
|
+
if (!fast) {
|
|
6
|
+
const { tryPureCompressionFastPath } = await import("../src/pure-compression-cli.js");
|
|
7
|
+
if (!(await tryPureCompressionFastPath(argv)))
|
|
8
|
+
await import("./cli.js");
|
|
9
|
+
}
|
|
10
|
+
if (process.env.KNODIN_BENCH_PROCESS_UPTIME === "1")
|
|
11
|
+
process.stderr.write(`KNODIN_PROCESS_UPTIME_MS=${(process.uptime() * 1000).toFixed(3)}\n`);
|
|
@@ -7,11 +7,8 @@ export function parseInitScope(value) {
|
|
|
7
7
|
throw new Error("knodin: --scope must be personal, team, or cli-only");
|
|
8
8
|
}
|
|
9
9
|
const MCP_NAME = "knodin";
|
|
10
|
-
const LEGACY_MCP_NAME = "reckon-graph";
|
|
11
10
|
const TOML_START = "# knodin:start";
|
|
12
11
|
const TOML_END = "# knodin:end";
|
|
13
|
-
const LEGACY_TOML_START = "# reckon-graph:start";
|
|
14
|
-
const LEGACY_TOML_END = "# reckon-graph:end";
|
|
15
12
|
const TOML_BLOCK = `${TOML_START}
|
|
16
13
|
[mcp_servers."${MCP_NAME}"]
|
|
17
14
|
command = "knodin"
|
|
@@ -70,16 +67,6 @@ export function configurePersonalAgents(options) {
|
|
|
70
67
|
for (const agent of options.detected) {
|
|
71
68
|
if (agent !== "claude")
|
|
72
69
|
continue;
|
|
73
|
-
const removeLegacy = {
|
|
74
|
-
executable: "claude",
|
|
75
|
-
args: ["mcp", "remove", LEGACY_MCP_NAME, "--scope", "local"],
|
|
76
|
-
};
|
|
77
|
-
try {
|
|
78
|
-
run(removeLegacy);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// A missing legacy registration is already the desired state.
|
|
82
|
-
}
|
|
83
70
|
const command = options.remove
|
|
84
71
|
? {
|
|
85
72
|
executable: "claude",
|
|
@@ -94,7 +81,9 @@ export function configurePersonalAgents(options) {
|
|
|
94
81
|
(options.remove ? removed : configured).push(agent);
|
|
95
82
|
}
|
|
96
83
|
catch (error) {
|
|
97
|
-
//
|
|
84
|
+
// Claude reports a missing local registration as a remove failure. Keep
|
|
85
|
+
// this backward-compatible result conservative at the presentation layer:
|
|
86
|
+
// it means removal was attempted, not that absence was verified.
|
|
98
87
|
if (options.remove)
|
|
99
88
|
removed.push(agent);
|
|
100
89
|
else
|
|
@@ -143,7 +132,6 @@ function updateJsonMcp(filePath, remove) {
|
|
|
143
132
|
const servers = typeof existing === "object" && existing !== null && !Array.isArray(existing)
|
|
144
133
|
? { ...existing }
|
|
145
134
|
: {};
|
|
146
|
-
delete servers[LEGACY_MCP_NAME];
|
|
147
135
|
if (remove)
|
|
148
136
|
delete servers[MCP_NAME];
|
|
149
137
|
else
|
|
@@ -176,7 +164,7 @@ function updateTomlMcp(filePath, remove) {
|
|
|
176
164
|
? `${content.slice(0, start)}${content.slice(end + endMarker.length)}`.trim()
|
|
177
165
|
: content.trim();
|
|
178
166
|
}
|
|
179
|
-
const without = removeManagedBlock(
|
|
167
|
+
const without = removeManagedBlock(existing, TOML_START, TOML_END);
|
|
180
168
|
const next = remove ? without : [without, TOML_BLOCK].filter(Boolean).join("\n\n");
|
|
181
169
|
if (!next) {
|
|
182
170
|
fs.rmSync(filePath, { force: true });
|
|
@@ -75,7 +75,7 @@ export function refreshExternalGraphArtifacts(repoPath, event, runtime = default
|
|
|
75
75
|
}
|
|
76
76
|
/** Persist an audit record locally; no source content or tool output is retained. */
|
|
77
77
|
export function writeArtifactRefreshRecord(repoPath, result) {
|
|
78
|
-
const knodinDir = path.join(path.resolve(repoPath), ".
|
|
78
|
+
const knodinDir = path.join(path.resolve(repoPath), ".knodin");
|
|
79
79
|
fs.mkdirSync(knodinDir, { recursive: true });
|
|
80
80
|
fs.appendFileSync(path.join(knodinDir, "artifact-refresh.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), ...result })}\n`, "utf8");
|
|
81
81
|
}
|
package/dist/src/cli-args.js
CHANGED
|
@@ -240,7 +240,14 @@ export function parseReviewArgs(args) {
|
|
|
240
240
|
if (rawScope && !scopes.includes(rawScope)) {
|
|
241
241
|
throw new Error(`invalid review scope: ${rawScope}`);
|
|
242
242
|
}
|
|
243
|
-
const optionValues = new Set([
|
|
243
|
+
const optionValues = new Set([
|
|
244
|
+
...GLOBAL_VALUE_FLAGS,
|
|
245
|
+
"--scope",
|
|
246
|
+
"--from",
|
|
247
|
+
"--to",
|
|
248
|
+
"--files",
|
|
249
|
+
"--scip",
|
|
250
|
+
]);
|
|
244
251
|
const positionals = args.filter((arg, index) => {
|
|
245
252
|
if (arg.startsWith("--"))
|
|
246
253
|
return false;
|
package/dist/src/cli-model.js
CHANGED
|
@@ -66,6 +66,14 @@ function addRepositoryCommands(program, capture) {
|
|
|
66
66
|
.option("--manifest <path>", "write or resume a portfolio manifest")
|
|
67
67
|
.option("--dry-run", "report actions without changing repositories");
|
|
68
68
|
}
|
|
69
|
+
if (action === "discover") {
|
|
70
|
+
command
|
|
71
|
+
.option("--signals", "include bounded repository applicability signals")
|
|
72
|
+
.addOption(option("--items <count>", "bound returned repositories and signal arrays", "integer"))
|
|
73
|
+
.addOption(option("--bytes <count>", "bound serialized response bytes", "integer"))
|
|
74
|
+
.addOption(option("--tokens <count>", "bound estimated response tokens", "integer"))
|
|
75
|
+
.addHelpText("after", "\n--signals returns hookManager, markerFiles, sanitized remotes, ciProviders, agentConfigs, and aidevTrackReferenced when known; every returned repository has signals, including {}. Inspection uses fixed documented marker/hook allowlists, reads only bounded hook configuration, and performs no network, credential, or marker-content reads. Linked worktrees inspect their own checkout when --linked-worktrees include is selected. See docs/REPOSITORIES-AND-WORKTREES.md for omissions and limits.\n");
|
|
76
|
+
}
|
|
69
77
|
}
|
|
70
78
|
leaf(repos, "search <query>", "search selected repositories sequentially", capture)
|
|
71
79
|
.addOption(option("--root <path>", "portfolio root", "collect"))
|
|
@@ -129,6 +137,12 @@ function addGraphCommands(program, capture) {
|
|
|
129
137
|
.option("--no-verify", "skip post-apply typecheck");
|
|
130
138
|
}
|
|
131
139
|
function addArtifactCommands(program, capture) {
|
|
140
|
+
leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
|
|
141
|
+
.option("--continuation <handle>", "resume an exact prior response")
|
|
142
|
+
.option("--baseline-hash <sha256>", "complete baseline SHA-256")
|
|
143
|
+
.addOption(option("--baseline-bytes <count>", "complete baseline UTF-8 bytes", "integer"))
|
|
144
|
+
.addOption(option("--start <line>", "first source line", "integer"))
|
|
145
|
+
.addOption(option("--end <line>", "last source line", "integer"));
|
|
132
146
|
const pack = program.command("pack").description("export bounded portable context");
|
|
133
147
|
pack
|
|
134
148
|
.argument("[input]")
|
|
@@ -173,6 +187,7 @@ function addArtifactCommands(program, capture) {
|
|
|
173
187
|
.addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
|
|
174
188
|
.addOption(option("--context <count>", "diagnostic context lines", "integer"))
|
|
175
189
|
.addOption(option("--limit <count>", "diagnostic result limit", "integer"))
|
|
190
|
+
.addOption(option("--offset <count>", "resume retained diagnostics at offset", "integer"))
|
|
176
191
|
.option("--raw", "return unredacted retained bytes");
|
|
177
192
|
}
|
|
178
193
|
leaf(compress, "delete <artifact>", "delete a retained output artifact", capture);
|
|
@@ -196,7 +211,8 @@ function createCliProgram(capture = () => { }) {
|
|
|
196
211
|
.option("--status", "inspect configuration without changing it");
|
|
197
212
|
leaf(program, "index [files...]", "index a repository or selected files", capture)
|
|
198
213
|
.option("--clean", "rebuild selected index state")
|
|
199
|
-
.option("--force", "force clean indexing")
|
|
214
|
+
.option("--force", "force clean indexing")
|
|
215
|
+
.option("--scip <file>", "opt in to a bounded local SCIP protobuf import");
|
|
200
216
|
leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
|
|
201
217
|
leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
|
|
202
218
|
.option("--deep", "run a full graph audit")
|
|
@@ -250,6 +266,24 @@ function createCliProgram(capture = () => { }) {
|
|
|
250
266
|
.option("--output <path>", "dashboard or evidence-bundle output path")
|
|
251
267
|
.addOption(option("--retention-days <count>", "retention window in days", "integer"))
|
|
252
268
|
.action((...values) => capture(values.at(-1)));
|
|
269
|
+
const diagnostics = program
|
|
270
|
+
.command("diagnostics")
|
|
271
|
+
.description("manage local troubleshooting diagnostics and support bundles")
|
|
272
|
+
.allowExcessArguments(false)
|
|
273
|
+
.addArgument(new Argument("<action>").choices([
|
|
274
|
+
"enable",
|
|
275
|
+
"status",
|
|
276
|
+
"collect",
|
|
277
|
+
"inspect",
|
|
278
|
+
"clear",
|
|
279
|
+
"disable",
|
|
280
|
+
]))
|
|
281
|
+
.addArgument(new Argument("[bundle]"));
|
|
282
|
+
diagnostics
|
|
283
|
+
.addOption(option("--retention-days <count>", "local event retention in days", "integer"))
|
|
284
|
+
.option("--since <duration>", "collection window, such as 24h or 7d")
|
|
285
|
+
.option("--output <path>", "repository-contained .json.gz bundle path")
|
|
286
|
+
.action((...values) => capture(values.at(-1)));
|
|
253
287
|
return program;
|
|
254
288
|
}
|
|
255
289
|
function flattenPositionals(values) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const EXPECTED_RELATIONSHIPS = [
|
|
2
|
+
"packages/api/route.ts->packages/auth/service.ts",
|
|
3
|
+
"packages/api/route.ts->packages/audit/store.ts",
|
|
4
|
+
"packages/auth/service.ts->packages/data/users.ts",
|
|
5
|
+
];
|
|
6
|
+
export function normalizeCodeFlowRelationships(connections) {
|
|
7
|
+
return connections.map((value) => {
|
|
8
|
+
const edge = value;
|
|
9
|
+
return {
|
|
10
|
+
// CodeFlow exports callee/definition as source and caller as target.
|
|
11
|
+
from: String(edge.target ?? ""),
|
|
12
|
+
to: String(edge.source ?? ""),
|
|
13
|
+
kind: typeof edge.kind === "string" ? edge.kind : null,
|
|
14
|
+
extractor: typeof edge.extractor === "string" ? edge.extractor : null,
|
|
15
|
+
confidence: edge.confidence === "exact" || edge.confidence === "heuristic" ? edge.confidence : null,
|
|
16
|
+
sourceFile: typeof edge.sourceFile === "string" ? edge.sourceFile : null,
|
|
17
|
+
sourceLine: typeof edge.sourceLine === "number" ? edge.sourceLine : null,
|
|
18
|
+
evidence: typeof edge.evidence === "string" ? edge.evidence : null,
|
|
19
|
+
raw: value,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function scoreRelationships(relationships) {
|
|
24
|
+
const actual = new Set(relationships.map((edge) => `${edge.from}->${edge.to}`));
|
|
25
|
+
const expected = new Set(EXPECTED_RELATIONSHIPS);
|
|
26
|
+
const truePositives = [...expected].filter((edge) => actual.has(edge));
|
|
27
|
+
const falseNegatives = [...expected].filter((edge) => !actual.has(edge));
|
|
28
|
+
const falsePositives = [...actual].filter((edge) => !expected.has(edge));
|
|
29
|
+
const malformed = relationships.filter((edge) => !edge.from || !edge.to);
|
|
30
|
+
return {
|
|
31
|
+
truePositives,
|
|
32
|
+
falsePositives,
|
|
33
|
+
falseNegatives,
|
|
34
|
+
precision: actual.size === 0 ? 0 : truePositives.length / actual.size,
|
|
35
|
+
recall: truePositives.length / expected.size,
|
|
36
|
+
unsupportedEdgeRate: falseNegatives.length / expected.size,
|
|
37
|
+
malformedRelationships: malformed.length,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function blastRadiusCompleteness(relationships) {
|
|
41
|
+
const origin = "packages/data/users.ts";
|
|
42
|
+
const expected = new Set(["packages/auth/service.ts", "packages/api/route.ts"]);
|
|
43
|
+
const adjacency = new Map();
|
|
44
|
+
for (const edge of relationships) {
|
|
45
|
+
if (!adjacency.has(edge.to))
|
|
46
|
+
adjacency.set(edge.to, new Set());
|
|
47
|
+
adjacency.get(edge.to)?.add(edge.from);
|
|
48
|
+
}
|
|
49
|
+
const found = new Set();
|
|
50
|
+
const queue = [origin];
|
|
51
|
+
while (queue.length) {
|
|
52
|
+
const current = queue.shift();
|
|
53
|
+
for (const dependent of adjacency.get(current) ?? []) {
|
|
54
|
+
if (dependent === origin || found.has(dependent))
|
|
55
|
+
continue;
|
|
56
|
+
found.add(dependent);
|
|
57
|
+
queue.push(dependent);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
origin,
|
|
62
|
+
expected: [...expected],
|
|
63
|
+
found: [...found].sort(),
|
|
64
|
+
missing: [...expected].filter((file) => !found.has(file)),
|
|
65
|
+
completeness: [...expected].filter((file) => found.has(file)).length / expected.size,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function provenanceCoverage(relationships) {
|
|
69
|
+
const count = relationships.length;
|
|
70
|
+
const rate = (predicate) => count === 0 ? 0 : relationships.filter(predicate).length / count;
|
|
71
|
+
return {
|
|
72
|
+
relationships: count,
|
|
73
|
+
relationKind: rate((edge) => edge.kind !== null),
|
|
74
|
+
extractorIdentity: rate((edge) => edge.extractor !== null),
|
|
75
|
+
exactOrHeuristicConfidence: rate((edge) => edge.confidence !== null),
|
|
76
|
+
sourceFile: rate((edge) => edge.sourceFile !== null),
|
|
77
|
+
sourceLine: rate((edge) => edge.sourceLine !== null),
|
|
78
|
+
boundedSourceEvidence: rate((edge) => edge.evidence !== null),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { sampleOperation } from "./competitive-measurement.js";
|
|
5
|
+
function textOf(value) {
|
|
6
|
+
return (value.content ?? [])
|
|
7
|
+
.filter((item) => item.type === "text")
|
|
8
|
+
.map((item) => item.text ?? "")
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|
|
11
|
+
/** Measure five cold calls, rebuilding the MCP process before every recorded call. */
|
|
12
|
+
export async function measureColdMcp(command, args, cwd, side, clientName) {
|
|
13
|
+
let last;
|
|
14
|
+
let error;
|
|
15
|
+
const { measurement } = await sampleOperation(async () => {
|
|
16
|
+
const transport = new StdioClientTransport({ command, args, cwd, stderr: "pipe" });
|
|
17
|
+
const client = new Client({ name: clientName, version: "1" }, { capabilities: {} });
|
|
18
|
+
try {
|
|
19
|
+
await client.connect(transport);
|
|
20
|
+
last = await client.callTool({ name: side.tool, arguments: side.args });
|
|
21
|
+
error = undefined;
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await transport.close().catch(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
return last;
|
|
30
|
+
}, { mode: "cold" });
|
|
31
|
+
const response = last ? textOf(last) : "";
|
|
32
|
+
return {
|
|
33
|
+
ok: !error && !last?.isError,
|
|
34
|
+
error,
|
|
35
|
+
...measurement,
|
|
36
|
+
responseBytes: Buffer.byteLength(response),
|
|
37
|
+
responseSha256: createHash("sha256").update(response).digest("hex"),
|
|
38
|
+
response,
|
|
39
|
+
};
|
|
40
|
+
}
|