knodin 0.6.0 → 0.7.4
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 +26 -3
- package/dist/bin/cli.js +168 -12
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +3 -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 +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +417 -70
- package/dist/src/engine/scip-import.js +408 -0
- 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/init.js +319 -33
- package/dist/src/lifecycle-health.js +42 -4
- package/dist/src/output-telemetry.js +4 -0
- 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/repository-management.js +142 -0
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +22 -2
- package/dist/src/structural-fast-path.js +338 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/tools/knodin-tools.js +105 -14
- package/dist/src/update-ceremony.js +158 -0
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +31 -15
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +5 -2
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +30 -0
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/TELEMETRY.md +4 -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/docs/releases/0.7.4.md +17 -0
- package/package.json +34 -2
package/README.md
CHANGED
|
@@ -147,9 +147,10 @@ knodin repair
|
|
|
147
147
|
|---|---|
|
|
148
148
|
| `knodin context` | Compact repository orientation and next-operation hint |
|
|
149
149
|
| `knodin explain <symbol>` | Source, identity, callers, callees, and blast radius |
|
|
150
|
-
| `knodin review` |
|
|
150
|
+
| `knodin review` | Itemized graph impact, test gaps, centrality, and bounded Git-history evidence |
|
|
151
151
|
| `knodin query …` | Impact, paths, callers, callees, tests, flows, and structured graph queries |
|
|
152
|
-
| `knodin search <query>` |
|
|
152
|
+
| `knodin search <query>` | Explainable structural-first search with a bounded local embedding fallback |
|
|
153
|
+
| `knodin index --scip <file>` | Opt in to a bounded local SCIP snapshot while preserving native and LSIF facts |
|
|
153
154
|
| `knodin map` | Subsystems, communities, hubs, bridges, and boundaries |
|
|
154
155
|
| `knodin pack …` | Deterministic source context under hard budgets |
|
|
155
156
|
| `knodin compress …` | Bounded diagnostic output with recoverable local detail |
|
|
@@ -158,6 +159,7 @@ knodin repair
|
|
|
158
159
|
| `knodin visualize` | Self-contained local graph visualization |
|
|
159
160
|
| `knodin status --deep` | Index freshness, health, hooks, and integration status |
|
|
160
161
|
| `knodin repair` | Repair or rebuild unhealthy local graph state |
|
|
162
|
+
| `knodin diagnostics …` | Retain local failure evidence and create a redacted support bundle |
|
|
161
163
|
|
|
162
164
|
Run `knodin --help` or read the [CLI reference](docs/CLI.md) for complete syntax.
|
|
163
165
|
|
|
@@ -186,7 +188,8 @@ repository, and lifecycle capabilities. See the [MCP guide](docs/MCP.md).
|
|
|
186
188
|
Each checkout stores its graph and lifecycle state in `.knodin/`. Shared model
|
|
187
189
|
files live in the user cache rather than being duplicated per repository.
|
|
188
190
|
Source and graph data stay local. Telemetry is metadata-only, disabled by
|
|
189
|
-
default, and never sent by knodin.
|
|
191
|
+
default, and never sent by knodin. Troubleshooting diagnostics are also
|
|
192
|
+
explicitly enabled, local-only, bounded, and never uploaded automatically.
|
|
190
193
|
|
|
191
194
|
The optional `prs` command invokes the user's authenticated `gh` CLI. Network
|
|
192
195
|
access may also occur when installing packages, downloading the configured
|
|
@@ -206,9 +209,29 @@ productivity percentage or superiority over every competing product.
|
|
|
206
209
|
- [Competitive roadmap and limitations](roadmap/competitive-roadmap.md)
|
|
207
210
|
- [Systems and relationships](docs/SYSTEMS-AND-RELATIONSHIPS.md)
|
|
208
211
|
- [Command-output compression](docs/COMMAND-OUTPUT-COMPRESSION.md)
|
|
212
|
+
- [Local troubleshooting diagnostics](docs/DIAGNOSTICS.md)
|
|
209
213
|
|
|
210
214
|
## Develop
|
|
211
215
|
|
|
216
|
+
### Internal GHES mirror
|
|
217
|
+
|
|
218
|
+
GitHub SaaS `knodin/knodin` is the authoritative review and release repository.
|
|
219
|
+
The internal `github.docusignhq.com/Enterprise-Apps/knodin` repository is a
|
|
220
|
+
read-only mirror for colleagues who need GHES access; work must not be opened
|
|
221
|
+
or reviewed there.
|
|
222
|
+
|
|
223
|
+
After a pull fast-forwards local `main`, Lefthook automatically fast-forwards
|
|
224
|
+
the mirror when this maintainer-local remote exists:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
git remote add ghes git@github.docusignhq.com:Enterprise-Apps/knodin.git
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The hook silently no-ops off `main`, on CI, or without that remote. It never
|
|
231
|
+
forces and never breaks the surrounding merge when GHES is unavailable. Run
|
|
232
|
+
`npm run mirror:sync` for manual recovery. A divergence is reported for
|
|
233
|
+
investigation instead of being overwritten.
|
|
234
|
+
|
|
212
235
|
```bash
|
|
213
236
|
npm ci
|
|
214
237
|
npm run build
|
package/dist/bin/cli.js
CHANGED
|
@@ -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`;
|
|
@@ -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
|
}
|
|
@@ -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`.",
|
|
@@ -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`);
|
|
@@ -81,7 +81,9 @@ export function configurePersonalAgents(options) {
|
|
|
81
81
|
(options.remove ? removed : configured).push(agent);
|
|
82
82
|
}
|
|
83
83
|
catch (error) {
|
|
84
|
-
//
|
|
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.
|
|
85
87
|
if (options.remove)
|
|
86
88
|
removed.push(agent);
|
|
87
89
|
else
|
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
|
+
}
|