knodin 0.10.8 → 0.12.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.
package/dist/bin/cli.js CHANGED
@@ -35,7 +35,7 @@ import { diagnoseFailure, } from "../src/failure-diagnosis.js";
35
35
  import { gitExecutable } from "../src/git-executable.js";
36
36
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
37
37
  import { createIndexActivityReporter } from "../src/index-activity.js";
38
- import { InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
38
+ import { InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, repairLifecycleRouting, } from "../src/init.js";
39
39
  import { createInitProgressRenderer } from "../src/init-progress.js";
40
40
  import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
41
41
  import { acknowledgeUpdateFailure, queryOwnerAvailability, readManagerUpdateState, readUpdateJournal, resolveManagerOwnership, setManualPin, unpinUpdate, updateAttention, writeManagerUpdateState, } from "../src/manager-update.js";
@@ -60,7 +60,7 @@ import { KNODIN_VERSION } from "../src/version.js";
60
60
  import { writeVisualization, } from "../src/visualization.js";
61
61
  import { waitForFresh } from "../src/wait-for-fresh.js";
62
62
  import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../src/worktree-lifecycle.js";
63
- import { indexOrSeed } from "../src/worktree-seed.js";
63
+ import { indexOrSeed, seedWorktreeIndex } from "../src/worktree-seed.js";
64
64
  function explicitScope(args) {
65
65
  const index = args.indexOf("--scope");
66
66
  if (index >= 0) {
@@ -138,7 +138,7 @@ function formatRepairHuman(result) {
138
138
  }
139
139
  if (result.verified) {
140
140
  if (result.lifecycle?.status === "degraded")
141
- 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`;
141
+ return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols), but lifecycle routing is degraded. Run \`knodin repair --lifecycle\`, then \`knodin status\`.\n`;
142
142
  return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols).\n`;
143
143
  }
144
144
  const outstanding = result.outstandingIssues?.total ??
@@ -198,7 +198,7 @@ function formatLifecycleLine(lifecycle, isMirror) {
198
198
  if (lifecycle.status === "healthy")
199
199
  return "Hooks: installed and executable.\n";
200
200
  const issue = lifecycle.issues[0] ?? "refresh capability is not verified";
201
- return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin init\`.\n`;
201
+ return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin repair --lifecycle\`.\n`;
202
202
  }
203
203
  /** Renders the top extensions of a tally as `.md 180, .yml 74, +3 more`. */
204
204
  function formatTally(counts, limit = 4) {
@@ -1522,6 +1522,7 @@ async function main() {
1522
1522
  let result;
1523
1523
  let repairOutput;
1524
1524
  let repairWasPlan = false;
1525
+ let repairWasLifecycle = false;
1525
1526
  let repairExitCode = 0;
1526
1527
  let statusWasWatched = false;
1527
1528
  let agentEventOutput = false;
@@ -2381,6 +2382,27 @@ async function main() {
2381
2382
  repairWasPlan = true;
2382
2383
  break;
2383
2384
  }
2385
+ if (options.lifecycle) {
2386
+ result = await repairLifecycleRouting(repo, { command: runtimeCommand });
2387
+ repairOutput = options.output;
2388
+ repairWasLifecycle = true;
2389
+ break;
2390
+ }
2391
+ // Cold start only. With no database at all there is nothing to rebuild
2392
+ // *from*, so repair would index the whole corpus while a sibling
2393
+ // worktree already holds a reusable baseline — reflink-copying it and
2394
+ // reconciling the differing paths is seconds against minutes
2395
+ // (KNODIN-14). Repair's contract is untouched: it still rebuilds from
2396
+ // this working tree, it just starts from a baseline instead of zero.
2397
+ //
2398
+ // A database that exists but is damaged is deliberately excluded. That
2399
+ // is the case repair exists for, and seeding it would replace a
2400
+ // verifiable local rebuild with another checkout's state.
2401
+ if (!fs.existsSync(resolveDbPath(repo))) {
2402
+ const seed = await seedWorktreeIndex(repo, engine, KNODIN_SCHEMA_VERSION);
2403
+ if (seed.seeded)
2404
+ process.stderr.write("[repair:seed] Seeded baseline from an indexed sibling worktree\n");
2405
+ }
2384
2406
  const progressMode = resolveRepairProgressMode(options, process.env, process.stderr.isTTY);
2385
2407
  const renderer = progressMode === "tty"
2386
2408
  ? createProgressWorkerRenderer("repair-progress-worker", {
@@ -2787,6 +2809,12 @@ async function main() {
2787
2809
  }
2788
2810
  result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness);
2789
2811
  }
2812
+ // A target that does not exist is operator error, not a negative
2813
+ // answer, so it exits non-zero like an unavailable graph rather than
2814
+ // like real dead code (KNODIN-28). `resolved` with zero rows keeps
2815
+ // exit 0: the symbol exists and genuinely has no callers.
2816
+ if (result?.targetResolution === "not-found")
2817
+ process.exitCode = 1;
2790
2818
  break;
2791
2819
  }
2792
2820
  case "rename": {
@@ -2999,6 +3027,15 @@ async function main() {
2999
3027
  process.stdout.write(formatConfigureStatusHuman(boundedResult));
3000
3028
  return;
3001
3029
  }
3030
+ if (cmd === "repair" && repairOutput === "human" && repairWasLifecycle) {
3031
+ // A lifecycle repair carries no graph coverage, so it cannot go through
3032
+ // the graph-repair renderer, and saying nothing about what it left alone
3033
+ // would omit the only reason to reach for it (KNODIN-31).
3034
+ const lifecycle = boundedResult;
3035
+ process.stdout.write(`Lifecycle routing rewritten: ${lifecycle.gitHooks.length} managed Git hook(s) and the background indexer, in ${lifecycle.hooksDirectory}. Agent configuration untouched. Run \`knodin status\` to confirm.\n`);
3036
+ process.exitCode = finalExitCode;
3037
+ return;
3038
+ }
3002
3039
  if (cmd === "repair" && repairOutput === "human" && !repairWasPlan) {
3003
3040
  process.stdout.write(formatRepairHuman(boundedResult));
3004
3041
  process.exitCode = finalExitCode;
@@ -309,6 +309,7 @@ function createCliProgram(capture = () => { }) {
309
309
  .addOption(option("--timeout <seconds>", "deadline", "number"));
310
310
  leaf(program, "repair", "audit and reconcile graph state", capture)
311
311
  .option("--plan", "report the repair plan without mutation")
312
+ .option("--lifecycle", "rewrite lifecycle hooks only, leaving agent configuration untouched")
312
313
  .option("--jsonl", "stream JSONL progress")
313
314
  .option("--progress <mode>", "tty, jsonl, plain, or none")
314
315
  .option("--progress-interval <duration>", "progress interval such as 750ms, 30s, or 2m");
@@ -12,12 +12,17 @@ export function compactIdentity(identity) {
12
12
  export function expandCompactIdentity(identity) {
13
13
  return identity?.startsWith("~") ? `${IDENTITY_PREFIX}${identity.slice(1)}*` : identity;
14
14
  }
15
+ // Missing evidence is "unknown", never "fresh" — the same default the engine's
16
+ // own `stalenessFor()` uses. Search stamps staleness per row and `SearchPage`
17
+ // carries no page-level field, so a zero-row page has no row to read it from:
18
+ // defaulting to "fresh" rendered an empty result on a stale or unprobed graph as
19
+ // `fresh 0/0`, an authoritative-looking negative (KNODIN-22).
15
20
  function freshness(value) {
16
21
  if (value === "reconciled")
17
22
  return "reconciled";
18
- if (value === "unknown")
19
- return "unknown";
20
- return "fresh";
23
+ if (value === "fresh")
24
+ return "fresh";
25
+ return "unknown";
21
26
  }
22
27
  function terseSignature(row) {
23
28
  const signature = row.signature?.replace(/\s+/g, " ").trim() ?? "";
@@ -25,8 +25,21 @@ export function credentialPatterns() {
25
25
  // consumed BEFORE the value, because `authorization: Bearer <token>`
26
26
  // otherwise matches only the word "Bearer" — redacting the label and
27
27
  // preserving the credential, exactly inverted (KNODIN-10).
28
+ //
29
+ // The keyword is surrounded by `[\w-]*` rather than anchored with `\b`,
30
+ // because `\b` does not exist between `_` and a letter: a leading `\b`
31
+ // could never match inside `AWS_SECRET_ACCESS_KEY`, and a trailing one
32
+ // stopped the name short of the `=`. That excluded the dominant spelling
33
+ // of every real secret — `AWS_SECRET_ACCESS_KEY=`, `DB_PASSWORD=`,
34
+ // `NPM_TOKEN=`, any `*_SECRET=` — while `password=` alone was redacted
35
+ // (KNODIN-19).
36
+ //
37
+ // The trailing half is `[_-]`-led rather than a bare `[\w-]*` so the
38
+ // keyword has to be a whole segment of the name. knodin echoes source
39
+ // into diagnostics and compressed output, and an unanchored tail
40
+ // redacted the value of a plain `const tokenCount = 5`.
28
41
  label: "credential",
29
- pattern: /\b(authorization|password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(?:(?:bearer|basic|digest|token)\s+)?[^\s,;]+/gi,
42
+ pattern: /(?<![\w-])([\w-]*(?:authorization|password|passwd|secret|token|api[_-]?key)(?:[_-][\w-]*)?)(\s*[:=]\s*)(?:(?:bearer|basic|digest|token)\s+)?[^\s,;]+/gi,
30
43
  preservedGroups: 2,
31
44
  },
32
45
  {