knodin 0.7.6 → 0.8.2

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 (130) hide show
  1. package/README.md +19 -7
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +2164 -108
  4. package/dist/bin/launcher.js +25 -3
  5. package/dist/src/agent-integration.js +304 -0
  6. package/dist/src/artifact-refresh.js +82 -0
  7. package/dist/src/cli-args.js +292 -0
  8. package/dist/src/cli-model.js +384 -0
  9. package/dist/src/codeflow-replay.js +81 -0
  10. package/dist/src/compact-structural.js +96 -0
  11. package/dist/src/compare.js +39 -0
  12. package/dist/src/competitive-cold-mcp.js +40 -0
  13. package/dist/src/competitive-constraints.js +21 -0
  14. package/dist/src/competitive-manifest.js +411 -0
  15. package/dist/src/competitive-measurement.js +183 -0
  16. package/dist/src/competitive-runner.js +487 -0
  17. package/dist/src/competitive-sandbox.js +108 -0
  18. package/dist/src/context-export.js +423 -0
  19. package/dist/src/context.js +102 -0
  20. package/dist/src/deterministic-random.js +34 -0
  21. package/dist/src/diagnostics-write-helper.js +473 -0
  22. package/dist/src/diagnostics.js +1476 -0
  23. package/dist/src/docs-sections.js +141 -0
  24. package/dist/src/doctor.js +382 -0
  25. package/dist/src/engine/ann-hnsw.js +261 -0
  26. package/dist/src/engine/embeddings.js +193 -0
  27. package/dist/src/engine/file-walker.js +49 -0
  28. package/dist/src/engine/git-history.js +289 -0
  29. package/dist/src/engine/index.js +14238 -0
  30. package/dist/src/engine/perf.js +115 -0
  31. package/dist/src/engine/prune.js +112 -0
  32. package/dist/src/engine/sarif-import.js +341 -0
  33. package/dist/src/engine/scip-import.js +423 -0
  34. package/dist/src/engine/source-policy.js +85 -0
  35. package/dist/src/engine/sqlite.js +71 -0
  36. package/dist/src/engine/state-paths.js +175 -0
  37. package/dist/src/engine/symbol-delete.js +58 -0
  38. package/dist/src/execution-profile.js +208 -0
  39. package/dist/src/failure-diagnosis.js +655 -0
  40. package/dist/src/fleet.js +7 -0
  41. package/dist/src/git-executable.js +31 -0
  42. package/dist/src/graph-layout.js +173 -0
  43. package/dist/src/graph-query-health.js +115 -0
  44. package/dist/src/hook-manager-integration.js +156 -0
  45. package/dist/src/index-activity.js +126 -0
  46. package/dist/src/init-progress-worker.js +106 -2
  47. package/dist/src/init-progress.js +155 -0
  48. package/dist/src/init.js +1295 -0
  49. package/dist/src/lifecycle-health.js +282 -0
  50. package/dist/src/lsp-readonly.js +217 -0
  51. package/dist/src/mcp-graph-worker.js +69 -0
  52. package/dist/src/mcp-reliability.js +154 -0
  53. package/dist/src/mcp-worker-supervisor.js +350 -0
  54. package/dist/src/mirror.js +290 -0
  55. package/dist/src/node-runtime.js +157 -0
  56. package/dist/src/output-compression.js +630 -0
  57. package/dist/src/output-telemetry.js +368 -0
  58. package/dist/src/pr-triage.js +638 -0
  59. package/dist/src/progressive-evidence.js +477 -0
  60. package/dist/src/pure-compression-cli.js +102 -0
  61. package/dist/src/relationship-adapters.js +377 -0
  62. package/dist/src/release-attestation.js +533 -0
  63. package/dist/src/release-preflight.js +513 -0
  64. package/dist/src/repair-lease.js +85 -0
  65. package/dist/src/repair-progress-worker.js +120 -2
  66. package/dist/src/repair-progress.js +262 -0
  67. package/dist/src/repository-init-process.js +177 -0
  68. package/dist/src/repository-management.js +1261 -0
  69. package/dist/src/response-budget.js +196 -0
  70. package/dist/src/server.js +217 -0
  71. package/dist/src/structural-fast-path.js +344 -0
  72. package/dist/src/structural-snapshot.js +37 -0
  73. package/dist/src/system-config.js +638 -0
  74. package/dist/src/terminal-help.js +83 -0
  75. package/dist/src/tools/knodin-tools.js +1640 -0
  76. package/dist/src/update-ceremony.js +162 -0
  77. package/dist/src/update-policy.js +944 -0
  78. package/dist/src/update-trust.js +504 -0
  79. package/dist/src/version.js +13 -0
  80. package/dist/src/visualization.js +515 -0
  81. package/dist/src/wait-for-fresh.js +98 -0
  82. package/dist/src/worktree-lifecycle.js +234 -0
  83. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  84. package/docs/CLI.md +20 -1
  85. package/docs/COMPARISON.md +403 -0
  86. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  87. package/docs/CONTAINED-EXECUTION.md +77 -0
  88. package/docs/DIAGNOSTICS.md +80 -0
  89. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  90. package/docs/HANDOFF.md +180 -0
  91. package/docs/INSTALLATION.md +21 -18
  92. package/docs/MCP.md +59 -8
  93. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  94. package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
  95. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  96. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  97. package/docs/SCIP-IMPORT.md +62 -0
  98. package/docs/SIGNED-UPDATES.md +151 -0
  99. package/docs/TELEMETRY.md +46 -0
  100. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  101. package/docs/assets/knodin-favicon.svg +4 -0
  102. package/docs/releases/0.3.0.md +46 -0
  103. package/docs/releases/0.4.0.md +68 -0
  104. package/docs/releases/0.4.1.md +28 -0
  105. package/docs/releases/0.4.2.md +27 -0
  106. package/docs/releases/0.4.3.md +23 -0
  107. package/docs/releases/0.5.0.md +29 -0
  108. package/docs/releases/0.5.1.md +17 -0
  109. package/docs/releases/0.6.0.md +18 -0
  110. package/docs/releases/0.7.0.md +24 -0
  111. package/docs/releases/0.7.1.md +21 -0
  112. package/docs/releases/0.7.2.md +21 -0
  113. package/docs/releases/0.7.3.md +23 -0
  114. package/docs/releases/0.7.4.md +17 -0
  115. package/docs/releases/0.7.5.md +20 -0
  116. package/docs/releases/0.8.0.md +74 -0
  117. package/docs/releases/0.8.2.md +34 -0
  118. package/package.json +127 -4
  119. package/roadmap/competitive-roadmap.md +3801 -0
  120. package/schemas/release-attestation-v1.schema.json +210 -0
  121. package/schemas/support-bundle-v2.schema.json +212 -0
  122. package/dist/chunks/chunk-DMQAGX77.js +0 -654
  123. package/dist/chunks/chunk-F4Z3Z766.js +0 -4
  124. package/dist/chunks/chunk-SIJAQVSX.js +0 -3
  125. package/dist/chunks/chunk-X6M4HUUE.js +0 -2
  126. package/dist/chunks/chunk-YPRMY2LP.js +0 -8
  127. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
  128. package/dist/chunks/server-7EDF4CBY.js +0 -14
  129. package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
  130. package/docs/releases/0.7.6.md +0 -25
package/dist/bin/cli.js CHANGED
@@ -1,109 +1,2165 @@
1
1
  #!/usr/bin/env node
2
- import{c as vt}from"../chunks/chunk-SIJAQVSX.js";import{$ as $t,A as at,Aa as Se,B as ct,Ba as Bt,C as ke,Ca as Ht,D as be,E as dt,F as lt,G as ut,H as ae,I as xe,J as F,K as pt,L as B,M as mt,N as ft,O as V,P as ve,Q as ht,R as gt,S as wt,T as yt,U as kt,V as bt,W as ce,X as xt,Y as $e,a as se,aa as de,b as Q,c as ye,ca as Et,d as Be,da as Rt,e as He,ea as St,f as De,g as ze,h as Ge,ha as Ct,i as Ue,ia as Ot,ja as Ee,k as Ve,ka as le,l as Ke,la as Pt,m as Je,ma as Lt,n as Ye,na as qt,o as Qe,oa as ue,p as Xe,pa as Mt,q as Ze,qa as Ft,r as et,ra as Z,s as tt,sa as jt,t as nt,ta as ee,u as ot,ua as pe,v as it,va as H,w as st,wa as Re,x as A,xa as Wt,y as rt,ya as X,z as re,za as te}from"../chunks/chunk-DMQAGX77.js";import{a as _t,b as It,c as Nt,e as Tt,f as At}from"../chunks/chunk-F4Z3Z766.js";import"../chunks/chunk-X6M4HUUE.js";import{a as Me,b as Fe,c as je,d as We}from"../chunks/chunk-YPRMY2LP.js";import{spawn as en,spawnSync as Ie}from"node:child_process";import Te from"node:fs";import W from"node:path";import $n from"node:readline/promises";import{fileURLToPath as En}from"node:url";import{spawnSync as Dt}from"node:child_process";import Ce from"node:fs";import ne from"node:path";var zt=3e4;function nn(){return{commandExists:e=>{let o=Dt(e,["--help"],{stdio:"ignore",timeout:5e3});return o.status!==null||o.error?.code!=="ENOENT"},run:(e,o)=>{let n=performance.now(),r=Dt(e[0],e.slice(1),{stdio:"ignore",timeout:zt,cwd:o});return{exitCode:r.status,elapsedMs:Math.round(performance.now()-n),timedOut:r.signal==="SIGTERM"||r.signal==="SIGKILL"}}}}function Gt(e,o,n=nn()){let r=ne.resolve(e),c=ne.join(r,".gitnexus","run.cjs"),i=[Ce.existsSync(c)?{artifact:"gitnexus",command:["node",c,"analyze"],requiredCommand:"node"}:{artifact:"gitnexus",command:["gitnexus","analyze",r],requiredCommand:"gitnexus"},{artifact:"graphify",command:["graphify","update",r],requiredCommand:"graphify"}].map(({artifact:d,command:O,requiredCommand:C})=>{if(!n.commandExists(C))return{artifact:d,state:"skipped",reason:`${d} is not installed`};let T=n.run(O,r);return T.exitCode===0?{artifact:d,state:"success",command:O,elapsedMs:T.elapsedMs}:{artifact:d,state:"failed",command:O,elapsedMs:T.elapsedMs,reason:T.timedOut?`timed out after ${zt}ms`:`exited ${T.exitCode??"unknown"}`}}),g=i.filter(d=>d.state==="success").length,b=i.filter(d=>d.state==="failed").length,$=b>0&&g===0?"failed":b>0||g>0?"partial":"skipped";return{event:o,status:$,artifacts:i}}function Ut(e,o){let n=ne.join(ne.resolve(e),".knodin");Ce.mkdirSync(n,{recursive:!0}),Ce.appendFileSync(ne.join(n,"artifact-refresh.jsonl"),`${JSON.stringify({at:new Date().toISOString(),...o})}
3
- `,"utf8")}import{Argument as he,Command as rn,CommanderError as Kt,Option as an}from"commander";function fe(e){return e.length-e.trimStart().length}function on(e){let o=[],n=null;for(let r of e.replaceAll(" "," ").split(`
4
- `)){if(r.trim()===""){n&&o.push(n),n=null,o.push(null);continue}if(n&&fe(r)>=4){n.continuations.push(r);continue}n&&o.push(n),n={first:r.trimEnd(),continuations:[]}}return n&&o.push(n),o}function me(e,o,n,r){let c=e.trim().split(/\s+/),l=[],i=o;for(let g of c){let b=i.trim().length>0?" ":"";i.length+b.length+g.length>r&&i.trim().length>0?(l.push(i.trimEnd()),i=`${n}${g}`):i+=`${b}${g}`}return i.trim().length>0&&l.push(i.trimEnd()),l}function sn(e,o){let n=" ".repeat(fe(e.first)),r=e.first.trim();if(e.continuations.length===0&&e.first.length<=o)return[e.first];let c=e.continuations.length>0?Math.min(...e.continuations.map(fe)):fe(e.first),l=[r,...e.continuations.map(g=>g.trim())].join(" ");if(e.continuations.length>0&&n.length+r.length>o*.55)return me(l,n,`${n} `,o);let i=/\s{2,}/.exec(r);if(i?.index!==void 0){let g=r.slice(0,i.index).trimEnd(),b=l.slice(i.index+i[0].length).trim(),$=Math.max(n.length+g.length+2,c);return me(b,`${n}${g}${" ".repeat(Math.max(2,$-n.length-g.length))}`," ".repeat($),o)}if(e.continuations.length>0){let g=n.length+r.length,b=Math.max(g+2,c),$=e.continuations.map(d=>d.trim()).join(" ");return me($,`${n}${r}${" ".repeat(Math.max(2,b-g))}`," ".repeat(b),o)}return me(r,n,n,o)}function Vt(e,o){let n=o===void 0||!Number.isFinite(o)?100:Math.max(60,Math.floor(o)-1),r=on(e).flatMap(c=>c===null?[""]:sn(c,n));for(;r.at(-1)==="";)r.pop();return`${r.join(`
5
- `)}
6
- `}function cn(e,o){let n=Number(e);if(!Number.isInteger(n))throw new Kt(1,"knodin.invalidNumber",`${o.split(" ")[0]} must be an integer`);return n}function dn(e,o){let n=Number(e);if(!Number.isFinite(n))throw new Kt(1,"knodin.invalidNumber",`${o.split(" ")[0]} must be a number`);return n}function ln(e,o){return[...o,e]}function k(e,o,n){let r=new an(e,o);return n==="integer"?r.argParser(c=>cn(c,e)):n==="number"?r.argParser(c=>dn(c,e)):n==="collect"&&r.argParser(ln).default([]),r}function S(e,o,n,r){let c=e.command(o).description(n).allowExcessArguments(!1);return c.action((...l)=>r(l.at(-1))),c}function un(e){e.option("-v, --version","print the installed knodin version").addOption(k("--repo <path>","target a repository instead of the current directory")).option("--json","emit stable JSON").addOption(k("--identity <id>","select a stable symbol identity")).addOption(k("--file <path>","select a repo-relative definition file")).addOption(k("--kind <kind>","select a symbol kind")).addOption(k("--to-identity <id>","select a destination identity")).addOption(k("--to-file <path>","select a destination file")).addOption(k("--to-kind <kind>","select a destination kind")).addOption(k("--bytes <count>","bound serialized response bytes","integer")).addOption(k("--tokens <count>","bound serialized response tokens","integer")).addOption(k("--items <count>","bound serialized response items","integer")).option("--impact-mode <mode>","symbol or file impact").option("--direction <direction>","upstream, downstream, or both").addOption(k("--depth <count>","bounded traversal depth","integer")).option("--relations <kinds>","comma-separated relationship kinds").addOption(k("--min-confidence <value>","minimum edge confidence","number")).option("--limit <count>","result limit").option("--exclude-tests","exclude tests from impact").option("--data-flow","include bounded data-flow evidence")}function pn(e,o){let n=e.command("repos").description("manage a portfolio of repositories");for(let c of["discover","init","status","doctor"]){let l=S(n,`${c} <roots...>`,`${c} repositories beneath one or more roots`,o).addOption(k("--depth <count>","maximum discovery depth","integer")).option("--linked-worktrees <mode>","skip or include linked worktrees");c==="init"&&l.addOption(k("--include <selector>","include a repository id or path","collect")).addOption(k("--exclude <selector>","exclude a repository id or path","collect")).option("--manifest <path>","write or resume a portfolio manifest").option("--dry-run","report actions without changing repositories"),c==="discover"&&l.option("--signals","include bounded repository applicability signals").addOption(k("--items <count>","bound returned repositories and signal arrays","integer")).addOption(k("--bytes <count>","bound serialized response bytes","integer")).addOption(k("--tokens <count>","bound estimated response tokens","integer")).addHelpText("after",`
7
- --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.
8
- `)}S(n,"search <query>","search selected repositories sequentially",o).addOption(k("--root <path>","portfolio root","collect")).addOption(k("--include <selector>","include a repository id or path","collect")).addOption(k("--exclude <selector>","exclude a repository id or path","collect")).option("--cursor <cursor>","resume from an opaque cursor").option("--allow-partial","return healthy repository results when another degrades");let r=e.command("fleet").description("deprecated repository-fleet compatibility");S(r,"init <roots...>","initialize a repository fleet",o).addOption(k("--depth <count>","maximum discovery depth","integer")).option("--worktrees <mode>","skip or include linked worktrees").option("--dry-run","report actions without mutation")}function mn(e,o){S(e,"context <task> [base]","build compact task orientation",o),S(e,"explain <symbol> [detail]","explain one ambiguity-safe symbol",o),S(e,"review [base] [detail]","review an explicit Git diff scope",o).option("--scope <scope>","unstaged, staged, all, or compare").option("--from <ref>","comparison start revision").option("--to <ref>","comparison end revision").option("--files <paths>","comma-separated repo-relative paths"),S(e,"map","show architecture communities and edges",o).option("--standard","include full map detail").addOption(k("--top <count>","maximum ranked communities","integer")).option("--sort <mode>","relevance, name, size, degree, or complexity").option("--relations <kinds>","comma-separated relationship kinds"),S(e,"wiki","write local architecture wiki pages",o).option("--force","rewrite unchanged pages"),S(e,"visualize <entry>","write a local architecture/call-flow HTML artifact",o).requiredOption("--output <path>","repo-relative HTML output path").addOption(k("--depth <count>","call-flow depth","integer")).addOption(k("--max-bytes <count>","hard artifact budget","integer")),S(e,"search <query> [limit]","hybrid symbol search",o).option("--languages <values>","comma-separated languages").option("--extensions <values>","comma-separated extensions").option("--kinds <values>","comma-separated symbol kinds").option("--path <prefix>","repo-relative path prefix").option("--tests-only","search test symbols only").option("--production-only","search production symbols only").option("--no-source","omit source snippets").addOption(k("--offset <count>","pagination offset","integer")).addOption(k("--limit <count>","result limit","integer")),S(e,"query <pattern> [targets...]","run one structured graph query",o).option("--impact-mode <mode>","symbol or file impact").option("--direction <direction>","upstream, downstream, or both").addOption(k("--depth <count>","bounded traversal depth","integer")).option("--relations <kinds>","comma-separated relationship kinds").addOption(k("--min-confidence <value>","minimum edge confidence","number")).option("--exclude-tests","exclude tests from impact").option("--data-flow","include bounded data-flow evidence").option("--limit <count>","result limit").addOption(k("--min-lines <count>","minimum line count","integer")).addOption(k("--min-complexity <count>","minimum complexity","integer")).option("--kinds <values>","comma-separated symbol kinds").option("--path <prefix>","repo-relative path prefix").option("--variable <name>","flow-analysis variable").option("--facets <values>","comma-separated architecture facets").addOption(k("--top <count>","maximum ranked results","integer")).option("--sort <mode>","result ordering"),S(e,"rename <old> <new>","preview or apply an ambiguity-safe rename",o).option("--apply","apply the verified edit").option("--no-verify","skip post-apply typecheck")}function fn(e,o){S(e,"evidence <level> <file>","deliver progressive source evidence",o).option("--continuation <handle>","resume an exact prior response").option("--baseline-hash <sha256>","complete baseline SHA-256").addOption(k("--baseline-bytes <count>","complete baseline UTF-8 bytes","integer")).addOption(k("--start <line>","first source line","integer")).addOption(k("--end <line>","last source line","integer"));let n=e.command("pack").description("export bounded portable context");n.argument("[input]").option("--format <format>","markdown, json, or xml").option("--include <globs>","comma-separated include globs").option("--exclude <globs>","comma-separated exclude globs").option("--policy <assignments>","comma-separated glob policies").option("--already-present <paths>","comma-separated paths already in context").option("--chat-files <paths>","comma-separated chat paths").option("--line-numbers","include source line numbers").option("--tree","include repository tree").option("--output <path>","write a repo-relative retained artifact").option("--diff-scope <scope>","unstaged, staged, all, or compare").option("--from <ref>","comparison start revision").option("--to <ref>","comparison end revision").addOption(k("--log <count>","include recent commits","integer")).action((...c)=>o(c.at(-1))),S(n,"read <artifact>","read a retained context artifact",o).addOption(k("--start <line>","first line","integer")).addOption(k("--end <line>","last line","integer")),S(n,"grep <artifact> <regex>","search an artifact with a linear-time regex",o).option("--flags <flags>","regular-expression flags").addOption(k("--limit <count>","match limit","integer"));let r=e.command("compress").description("compress already-produced output");r.argument("[input]").option("--strategy <strategy>","smart, head-tail, or errors-only").option("--adapter <adapter>","structured output adapter").addOption(k("--lines <count>","hard line budget","integer")).addOption(k("--max-output-bytes <count>","hard output byte budget","integer")).addOption(k("--context <count>","signal context lines","integer")).addOption(k("--exit-code <code>","source process exit code","integer")).option("--signal <name>","source process termination signal").addOption(k("--max-input-bytes <count>","hard input byte limit","integer")).option("--no-retain","do not retain raw drill-down data").option("--no-redact","disable secret redaction").action((...c)=>o(c.at(-1)));for(let c of["read","diagnose"])S(r,`${c} <artifact>`,`${c} a retained output artifact`,o).addOption(k("--start <line>","first line","integer")).addOption(k("--end <line>","last line","integer")).addOption(k("--max-output-bytes <count>","hard output byte budget","integer")).addOption(k("--context <count>","diagnostic context lines","integer")).addOption(k("--limit <count>","diagnostic result limit","integer")).addOption(k("--offset <count>","resume retained diagnostics at offset","integer")).option("--raw","return unredacted retained bytes");S(r,"delete <artifact>","delete a retained output artifact",o)}function Oe(e=()=>{}){let o=new rn("knodin").description("knodin \u2014 source-evidenced local code intelligence with known bounds").showHelpAfterError().showSuggestionAfterError().passThroughOptions(!1).allowExcessArguments(!1).exitOverride();o.configureOutput({writeErr:()=>{}}),un(o),o.addHelpText("after","\nMCP callers use the `prs` operation and corresponding operations on the single `knodin` gateway.\nknodin update status|check|explain|apply|rollback consumes only threshold-signed metadata.\n"),S(o,"init","initialize graph, lifecycle hooks, and agent integration",e).option("--scope <scope>","personal, team, or cli-only").addHelpText("after",`
9
- Usage: knodin init [--scope personal|team|cli-only] [--json]
10
-
11
- personal recommended; local/excluded agent adapters and a clean Git status
12
- team commit-ready shared agent configuration
13
- cli-only no agent discovery; AI agents will not know to invoke knodin
14
-
15
- Change later with \`knodin configure --scope <scope>\`.
16
- Tracked files are never added to Git exclude files.
17
- `),S(o,"configure","change or inspect agent integration",e).option("--scope <scope>","personal, team, or cli-only").option("--status","inspect configuration without changing it"),S(o,"index [files...]","index a repository or selected files",e).option("--clean","rebuild selected index state").option("--force","force clean indexing").option("--scip <file>","opt in to a bounded local SCIP protobuf import"),S(o,"doctor","diagnose installation, clients, hooks, graph, and updates",e).option("--client <client>","claude, codex, gemini, or antigravity"),S(o,"status","report graph, lifecycle, integration, and update state",e).option("--deep","run a full graph audit").option("--watch","stream status snapshots").addOption(k("--interval <seconds>","watch interval","number")),S(o,"wait","wait for current graph evidence",e).option("--fresh","wait for a fresh graph").addOption(k("--timeout <seconds>","deadline","number")),S(o,"repair","audit and reconcile graph state",e).option("--plan","report the repair plan without mutation").option("--jsonl","stream JSONL progress").option("--progress <mode>","tty, jsonl, plain, or none").option("--progress-interval <duration>","progress interval such as 750ms, 30s, or 2m"),S(o,"serve","run the one-tool MCP gateway on stdio",e),S(o,"version","print the installed knodin version",e),S(o,"hook-refresh <kind> [values...]","internal Git lifecycle refresh",e),S(o,"refresh-artifacts [event]","refresh external graph artifacts",e),pn(o,e),mn(o,e),fn(o,e);let n=o.command("system").description("inspect declared multi-repository systems");S(n,"list","list configured systems",e);for(let i of["show","validate","query"]){let g=S(n,`${i} <system-id>`,`${i} one configured system`,e);i==="query"&&g.option("--allow-partial","return healthy components")}return o.command("update").description("inspect or apply threshold-signed updates").allowExcessArguments(!1).addArgument(new he("<action>").choices(["status","check","explain","apply","rollback"])).action((...i)=>e(i.at(-1))),S(o,"docs <topic>","read canonical product documentation",e),S(o,"prs [action]","audit pull requests, reviews, and checks",e).option("--state <state>","open, merged, closed, or all").addOption(k("--limit <count>","pull-request limit","integer")).option("--branches <pattern>","branch-name pattern").option("--range <range>","pull-request number range").option("--base <ref>","audit base revision").option("--head <ref>","audit head revision").option("--expected-login <login>","expected GitHub identity"),S(o,"worktrees [action] [path]","inspect, reconcile, or remove managed worktrees",e).option("--dry-run","report removal without mutation"),o.command("telemetry").description("manage local opt-in ROI telemetry").allowExcessArguments(!1).addArgument(new he("<action>").choices(["status","report","export","clear"])).option("--input <path>","telemetry input path").option("--output <path>","dashboard or evidence-bundle output path").addOption(k("--retention-days <count>","retention window in days","integer")).action((...i)=>e(i.at(-1))),o.command("diagnostics").description("manage local troubleshooting diagnostics and support bundles").allowExcessArguments(!1).addArgument(new he("<action>").choices(["enable","status","collect","inspect","clear","disable"])).addArgument(new he("[bundle]")).addOption(k("--retention-days <count>","local event retention in days","integer")).option("--since <duration>","collection window, such as 24h or 7d").option("--output <path>","repository-contained .json.gz bundle path").action((...i)=>e(i.at(-1))),o}function hn(e){return e.flatMap(o=>Array.isArray(o)?o:[o]).filter(o=>typeof o=="string")}function Jt(e){let o,n=Oe(l=>{o=l});if(n.parse([process.execPath,"knodin",...e]),!o)return{commandPath:[],positionals:[],options:n.opts()};let r=o,c=[];for(let l=r;l?.parent;l=l.parent)c.unshift(l.name());return{commandPath:c,positionals:hn(r.processedArgs),options:r.optsWithGlobals()}}function ge(e,o){let r=Oe();for(let l of e){let i=r.commands.find(g=>g.name()===l);if(!i)throw new Error(`knodin: unknown help command ${e.join(" ")}`);r=i}r.configureHelp({helpWidth:Math.max(60,Math.floor(o??100)-1)});let c="";return r.configureOutput({writeOut:l=>c+=l,writeErr:()=>{}}),r.outputHelp(),Vt(c,o)}function Yt(e){let o=Oe(),n=[],r=o;for(let c=0;c<e.length;c++){let l=e[c];if(l==="-h"||l==="--help")break;if(l.startsWith("-")){let g=l.split("=",1)[0],b=r,$;for(;b&&!$;)$=b.options.find(d=>d.short===g||d.long===g),b=b.parent;$?.required&&!l.includes("=")&&c++;continue}let i=r.commands.find(g=>g.name()===l);if(!i)break;r=i,n.push(l)}return n}import{execFileSync as gn}from"node:child_process";import wn from"node:crypto";import K from"node:fs";import j from"node:path";var yn=32768,kn=4096,_e=20;function P(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function bn(e){try{return gn("git",["rev-parse","HEAD"],{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"unavailable"}}function xn(e,o){if(!o||j.extname(o).toLowerCase()!==".html")throw new Error("knodin visualize: --output must be a repository-relative .html path");let n=j.resolve(e,o);if(n===e||!n.startsWith(`${e}${j.sep}`))throw new Error("knodin visualize: output path must stay inside the repository");let r=K.lstatSync(n,{throwIfNoEntry:!1});if(r?.isSymbolicLink())throw new Error("knodin visualize: output path may not be a symlink");if(r&&!K.realpathSync(n).startsWith(`${e}${j.sep}`))throw new Error("knodin visualize: output path may not follow a symlink outside the repository");let c=j.dirname(n);for(;!K.existsSync(c)&&c!==e;)c=j.dirname(c);let l=K.realpathSync(c);if(l!==e&&!l.startsWith(`${e}${j.sep}`))throw new Error("knodin visualize: output path may not traverse a symlink outside the repository");return{absolute:n,relative:j.relative(e,n).replaceAll(j.sep,"/")}}function vn(e){let o=e.communities.map(c=>{let l=(c.files??[]).slice(0,12).join(", ")||"no file detail returned",i=(c.symbols??[]).slice(0,12).join(", ")||"no symbol detail returned";return`<details><summary>${P(c.name)} <span>${c.size} symbols</span></summary><p>Cohesion: ${c.cohesion.toFixed(3)}</p><p><strong>Files:</strong> ${P(l)}</p><p><strong>Symbols:</strong> ${P(i)}</p></details>`}).join(`
18
- `),n=(c,l)=>l.map(i=>{let g="degree"in i?`degree ${i.degree}`:`betweenness ${i.betweenness.toFixed(3)}`;return`<li><strong>${c}</strong> ${P(i.symbol)} \u2014 ${P(g)}; ${P(i.filePath||"unresolved file")} (${P(i.communityId??"unassigned")})</li>`}).join(`
19
- `),r=c=>c.map(l=>`<li data-evidence="${l.label}"><code>${P(l.from)}</code> \u2192 <code>${P(l.to)}</code> <span>${P(l.kind)}</span><br><small>${l.label==="exact"?"Source evidence":"Heuristic source evidence"}: ${P(l.evidence)}</small></li>`).join(`
20
- `)||"<li>No bounded source-evidenced relationships were returned.</li>";return`<!doctype html>
21
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>knodin architecture visualization</title><style>body{font:16px system-ui,sans-serif;margin:2rem auto;max-width:74rem;color:#172033;background:#f6f8fb;line-height:1.45}section{background:#fff;border:1px solid #d9dee7;border-radius:.55rem;padding:1rem 1.25rem;margin:1rem 0}details{border-top:1px solid #e5e9ef;padding:.6rem 0}summary{cursor:pointer;font-weight:650}summary span,small{color:#526170}li{margin:.55rem 0}code{background:#edf1f6;padding:.1rem .3rem;border-radius:.2rem;overflow-wrap:anywhere}[data-evidence=heuristic]{border-left:3px solid #d18a00;padding-left:.55rem}</style></head>
22
- <body><h1>knodin local architecture visualization</h1><p>Generated locally from the persisted knodin graph. Source commit <code>${P(e.commit)}</code>; Index freshness <code>${P(e.freshness)}</code>; ${e.truncated?"some lists are bounded by the artifact budget.":"all displayed lists fit the requested artifact budget."}</p>
23
- <section><h2>Subsystem drill-down</h2>${o||"<p>No communities were returned.</p>"}</section>
24
- <section><h2>Hub and bridge inspection</h2><ul>${n("Hub",e.hubs)}${n("Bridge",e.bridges)}</ul></section>
25
- <section><h2>Architecture relationships</h2><ul>${r(e.architectureEdges)}</ul></section>
26
- <section><h2>Call-flow navigation</h2><p>Static downstream traversal from <code>${P(e.entry)}</code>, depth ${e.depth}; this is not a runtime trace.</p><ul>${r(e.callFlowEdges)}</ul></section>
27
- </body></html>
28
- `}async function Qt(e,o,n){let r=K.realpathSync(o),c=xn(r,n.outputPath),l=n.entry.trim();if(!l)throw new Error("knodin visualize: an entry selector is required");let i=n.depth??3;if(!Number.isInteger(i)||i<1||i>6)throw new Error("knodin visualize: --depth must be an integer from 1 through 6");let g=n.byteBudget??yn;if(!Number.isInteger(g)||g<kn||g>65536)throw new Error("knodin visualize: --max-bytes must be an integer from 4096 through 65536");let[b,$]=await Promise.all([e.map(r,"standard",{topN:12,sort:"name"}),e.query("traverse",l,r,void 0,_e,i,"standard",n.selector,void 0,{direction:"downstream",relationKinds:["call"],includeDataFlow:!0})]);if($.ambiguity||$.count===0)throw new Error("knodin visualize: entry selector did not resolve to a traversable symbol");let d=b.edges.filter(_=>!!_.sourceEvidence).slice(0,_e).map(_=>({from:_.from,to:_.to,kind:_.kind,evidence:_.sourceEvidence??"",label:_.confidenceLabel==="heuristic"||_.provenance!=="EXTRACTED"?"heuristic":"exact"})),O=($.edges??[]).slice(0,_e).map(_=>({from:_.from,to:_.to,kind:_.kind??"call",evidence:_.dataFlow?.evidence??`${_.fromFile??"unresolved file"}:${_.line??"unknown line"}`,label:_.dataFlow?.heuristic||_.provenance==="INFERRED"?"heuristic":"exact"})),C=b.communities.slice(0,12),T=b.hubs.slice(0,10),u=b.bridges.slice(0,10),x=d,w=O,L=!!$.truncated,D="";for(;D=vn({commit:bn(r),freshness:b.staleness??"unknown",communities:C,hubs:T,bridges:u,architectureEdges:x,callFlowEdges:w,entry:l,depth:i,truncated:L}),!(Buffer.byteLength(D)<=g);)if(L=!0,x.length)x=x.slice(0,-1);else if(w.length)w=w.slice(0,-1);else if(C.length)C=C.slice(0,-1);else if(T.length)T=T.slice(0,-1);else if(u.length)u=u.slice(0,-1);else throw new Error("knodin visualize: budget is too small for visualization metadata");return K.mkdirSync(j.dirname(c.absolute),{recursive:!0}),K.writeFileSync(c.absolute,D),{outputPath:c.relative,bytes:Buffer.byteLength(D),sha256:wn.createHash("sha256").update(D).digest("hex"),byteBudget:g,truncated:L,indexFreshness:b.staleness??"unknown",callFlow:{entry:l,depth:i,edgeCount:w.length,truncated:!!$.truncated}}}function we(e){let o=e.indexOf("--scope");if(o>=0){let r=e[o+1];if(!r)throw new Error("knodin: --scope requires a value");return se(r)}let n=e.find(r=>r.startsWith("--scope="));return n?se(n.slice(8)):null}async function Rn(e,o){let n=we(e);if(n)return n;if(xt(o))return process.stderr.write(`[init:scope] Detected tracked knodin team integration; preserving team scope
29
- `),"team";if(!process.stdin.isTTY||!process.stderr.isTTY)return"personal";let r=$n.createInterface({input:process.stdin,output:process.stderr});try{let c=await r.question(["How should knodin integrate with coding agents?"," 1. Personal (recommended) \u2014 all detected agents; Git stays clean"," 2. Team \u2014 create commit-ready shared configuration"," 3. CLI-only \u2014 agents will not discover or invoke knodin automatically","Select [1]: "].join(`
30
- `));if(!c.trim()||c.trim()==="1")return"personal";if(c.trim()==="2")return"team";if(c.trim()==="3"){let l=await r.question("CLI-only requires manual knodin commands. Continue? [y/N] ");if(!/^y(?:es)?$/i.test(l.trim()))throw new Error("knodin init: CLI-only selection cancelled");return"cli-only"}return se(c.trim())}finally{r.close()}}function Xt(e){let o=bt(e)?.agents??[],n=ce(e)?.agents??[];return[...new Set([...Q(),...o,...n])]}function Sn(e){let o=e.paths.scope==="cli-only"?"CLI-only \u2014 AI agents are not configured to discover knodin":e.paths.agentIntegration.configured.length>0?`${e.paths.scope} \u2014 ${e.paths.agentIntegration.configured.join(", ")}`:`${e.paths.scope} \u2014 no supported coding agents detected`,n=e.paths.agentIntegration.failed.map(({agent:c,message:l})=>`
31
- Agent warning (${c}): ${l}`).join(""),r=e.paths.lifecycleRefresh.state==="fresh"?"fresh":`still running (${e.paths.lifecycleRefresh.queuedEvents} queued event(s)); run \`knodin wait --fresh\``;return`${e.message}
32
- Graph: ${e.paths.database}
33
- Git refresh: ${e.paths.gitHooks.length} lifecycle hooks installed; ${r}
34
- Agent integration: ${o}${n}
35
- Background indexer: ${e.paths.backgroundIndexer}
36
- `}function Cn(e){return e.scope==="unconfigured"?"Agent integration: unconfigured.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal`.\n":e.scope==="cli-only"?"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":e.scope==="repository-detected"?`Agent integration: repository-detected${e.agents.length>0?` (${e.agents.join(", ")})`:""}.
37
- Local scope receipt: missing; managed repository integration is present.
38
- `:`Agent integration: ${e.scope}${e.agents.length>0?` (${e.agents.join(", ")})`:""}.
39
- `}function On(e){let o="none detected";e.paths.agentIntegration.configured.length>0?o=e.paths.agentIntegration.configured.join(", "):e.paths.scope==="cli-only"&&(o="none (CLI-only)");let n=e.paths.agentIntegration.failed.map(({agent:i,message:g})=>`
40
- Agent warning (${i}): ${g}`).join(""),r=e.paths.lifecycleRefresh.state==="fresh"?"fresh":`still running (${e.paths.lifecycleRefresh.queuedEvents} queued event(s))`,c=e.paths.filesystemChanges.map(({path:i,action:g})=>`
41
- Filesystem ${g}: ${i}`).join(""),l=e.paths.externalConfigurationOutcomes.map(({system:i,state:g})=>`
42
- External configuration outcome: ${i} ${g} (external mutation not locally observable)`).join("");return`${e.message}
43
- Agent integration: ${e.paths.scope} \u2014 ${o}${n}${c}${l}
44
- Graph initialization: unchanged
45
- Lifecycle refresh: ${r}
46
- Next: ${e.nextAction}
47
- `}function _n(e){let o=e.after.coverage;return e.cancelled?`Repair paused: ${e.remaining??0} file(s) remaining. Run \`knodin repair\` again to finish.
48
- `:e.verified?e.lifecycle?.status==="degraded"?`Repair verified: graph is healthy (${o.indexedFiles} indexed files, ${o.filesWithSymbols} files with symbols), but lifecycle routing is degraded. Run \`knodin init\`, then \`knodin status\`.
49
- `:`Repair verified: graph is healthy (${o.indexedFiles} indexed files, ${o.filesWithSymbols} files with symbols).
50
- `:"Repair finished with remaining issues. Run `knodin status --deep` for details.\n"}function In(e){if(e.indexed.length===0&&e.unchanged.length>0){let r=e.unchanged.length===1?"file":"files";return`Graph already current: ${e.unchanged.length.toLocaleString()} requested ${r} needed no work; health verified.
51
- `}let o=e.unchanged.length>0?`; ${e.unchanged.length.toLocaleString()} already current`:"",n=e.indexed.length===1?"file":"files";return`Index complete: ${e.indexed.length.toLocaleString()} ${n} indexed${o}; graph health verified.
52
- `}function Nn(e){let o=e.verification.missing.files[0]??e.verification.missing.records[0],n=o?` First issue: ${o}.`:"";return`knodin index: requested work completed, but ${e.verification.issueCount.toLocaleString()} graph issue(s) remain.${n} Run \`knodin repair\`.
53
- `}function Tn(e){let o=`${e.coverage.sourceFiles} source files, ${e.coverage.indexedFiles} indexed files, ${e.coverage.filesWithSymbols} files with symbols`;if(e.status==="indexing"&&e.activity){let C=e.activity.phaseTotal===void 0?"":` ${e.activity.phaseCompleted}/${e.activity.phaseTotal}`,T=Math.max(0,Math.floor((Date.now()-Date.parse(e.activity.startedAt))/1e3));return`Graph update in progress: ${e.activity.phase}${C} \u2014 ${e.activity.message} (${T}s elapsed; ${o}).
54
- `}let n=e.integration,r=n?`Agent integration: ${n.scope}${n.agents.length>0?` (${n.agents.join(", ")})`:""}.
55
- `:"Agent integration: unconfigured. AI agents will not discover knodin automatically; run `knodin configure --scope personal`.\n",c=e.lifecycle?e.lifecycle.status==="healthy"?`Hooks: installed and executable.
56
- `:`Lifecycle refresh: ${e.lifecycle.status}; ${e.lifecycle.issues[0]??"refresh capability is not verified"}. Run \`knodin init\`.
57
- `:"",l=C=>C?.slice(0,12)??"unknown",i=e.freshness.commitDistance===null?"":` by ${e.freshness.commitDistance.toLocaleString()} commit(s)`,g=`Freshness: ${e.freshness.state}; indexed ${l(e.freshness.indexedHead)}, current ${l(e.freshness.currentHead)} (${e.freshness.commitRelation}${i}); ${e.freshness.workingTree.pendingPaths??"unknown"} pending path(s).
58
- Last successful refresh: ${e.freshness.lastSuccessfulRefresh??"never"}.
59
- `;if(e.status==="healthy")return`Graph content is healthy: ${o} (knodin ${e.version}; ${e.verification.mode}).
60
- ${g}${c}${r}`;if(e.status==="stale")return`Graph content is intact but evidence is stale (${o}).
61
- ${g}${c}${r}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.
62
- `;let b=e.missing.files.length+e.missing.records.length,$=e.missing.files[0]??e.missing.records[0],d=$?` First issue: ${$}.`:"",O=e.lifecycle?.status==="degraded"&&e.missing.records.every(C=>e.lifecycle?.issues.includes(C))?"Run `knodin init`.":"Run `knodin repair`.";return`Graph or lifecycle needs repair: ${b} issue(s) found (${o}).${d} ${O}
63
- ${c}${r}`}function ie(e){return e.replace(/([a-z])([A-Z])/g,"$1 $2")}function Ae(e,o="",n){if(e===null||typeof e!="object")return[`${o}${n?`${ie(n)}: `:""}${String(e)}`];if(Array.isArray(e)){if(e.length===0)return n?[]:[`${o}(none)`];let l=n?[`${o}${ie(n)}:`]:[];for(let i of e)i===null||typeof i!="object"?l.push(`${o} - ${String(i)}`):l.push(...Ae(i,`${o} - `));return l}let r=Object.entries(e).filter(([l])=>l!=="responseBudget"),c=n?[`${o}${ie(n)}:`]:[];for(let[l,i]of r)i===null||typeof i!="object"?c.push(`${o}${n?" ":""}${ie(l)}: ${String(i)}`):c.push(...Ae(i,`${o}${n?" ":""}`,l));return c}function oe(e,o){return`${[`${ie(e)}:`,...Ae(o," ")].join(`
64
- `)}
65
- `}function An(e){let n=`Compressed output; ${e.omittedRanges.reduce((l,i)=>l+i.lineCount,0)} line(s) omitted.`;e.status==="insufficient-budget"?n=`INSUFFICIENT BUDGET: ${e.fidelity.unpreservedSignals.length} detected signal line(s) are available only through retained drill-down.`:e.complete&&(n="Complete output; nothing omitted.");let r=e.artifact.retained?` Retained artifact: ${e.artifact.id} (${e.artifact.path}).`:" Raw retention disabled; omitted regions cannot be retrieved.";return`${e.content?`${e.content}
66
- `:""}---
67
- ${n} ${e.output.lines}/${e.input.lines} lines, ${e.output.bytes}/${e.input.bytes} bytes; exit=${e.exit.code??"unknown"}, signal=${e.exit.signal??"none"}.${r}
68
- `}function Pn(e){let o=e.content?`${e.content}
69
- `:"",n=e.raw?"UNREDACTED raw view":`${e.secretRedactions} secret(s) redacted`;return`${o}---
70
- Artifact ${e.artifactId}, lines ${e.range.startLine}-${e.range.endLine} of ${e.range.totalLines}; ${e.bytes}/${e.byteBudget} bytes; ${n}.
71
- `}function Ne(e,o){if(o.length===0)return`${e}: none`;let n=o.map(({symbol:r,file:c,line:l})=>{let i=c?` (${c}:${l??"?"})`:"";return`${r}${i}`});return`${e}: ${n.join(", ")}`}function Ln(e){let o=[`Failure diagnosis: ${e.status}; ${e.diagnostics.length} resolved, ${e.unresolved.length} unresolved.`,`Freshness: ${e.freshness.state}; indexed=${e.freshness.indexedHead??"unknown"}; current=${e.freshness.currentHead??"unknown"}.`];for(let n of e.diagnostics){let r=`${n.file}:${n.reference.line??"?"}`,c=n.owner?.identity?` [${n.owner.identity}]`:"",l=`${n.owner?.symbol??"no owning symbol"}${c}`,i=n.package?`${n.package.name??"unnamed"} (${n.package.kind}, ${n.package.manifest})`:"none";o.push("",`${r} -> ${l}`,`Package: ${i}`,Ne("Tests",n.tests),Ne("Upstream",n.upstream),Ne("Downstream",n.downstream));let g=e.contextBundle.snippets.find(({file:b})=>b===n.file);g&&o.push(g.content)}for(let n of e.unresolved){let r=n.candidates?` (${n.candidates.join(", ")})`:"";o.push("",`Unresolved ${n.reference.path}: ${n.reason}${r}`)}return o.push("",...e.limitations.map(n=>`Limitation: ${n}`)),`${o.join(`
72
- `)}
73
- `}async function qn(e){let o=[],n=0;for await(let r of process.stdin){let c=Buffer.isBuffer(r)?r:Buffer.from(String(r));if(n+=c.byteLength,n>e)throw new Error(`knodin compress: input exceeded bounded retention limit of ${e} bytes`);o.push(c)}return Buffer.concat(o).toString("utf-8")}var Mn="\r\x1B[2K",Fn=2e3;function tn(e,o){let n=En(import.meta.url),r=n.endsWith(".ts")?".ts":".js",c=W.dirname(n);for(;!Te.existsSync(W.join(c,"package.json"))&&c!==W.parse(c).root;)c=W.dirname(c);let l=r===".ts"?W.resolve(W.dirname(n),`../src/${e}.ts`):W.resolve(c,`dist/src/${e}.js`),i=r===".ts"&&W.basename(process.execPath).startsWith("node")?[...process.execArgv,l]:[l],g=en(process.execPath,i,{stdio:["pipe","ignore","inherit"],env:{...process.env,KNODIN_FORCE_PROGRESS_TTY:"1"}}),b=!1,$=new Promise(O=>{g.once("close",()=>O()),g.once("error",()=>O())});g.once("error",()=>{b=!0}),g.stdin.once("error",()=>{b=!0});let d=O=>{if(!(b||g.stdin.destroyed))try{g.stdin.write(`${JSON.stringify(O)}
74
- `)}catch{b=!0}};return{start:()=>d(o),onProgress:O=>d({type:"progress",event:O}),stop:async()=>{d({type:"stop"}),g.stdin.end();let O=C=>new Promise(T=>{let u=!1,x=L=>{u||(u=!0,clearTimeout(w),T(L))},w=setTimeout(()=>x(!1),C);w.unref(),$.then(()=>x(!0))});await O(Fn)||(g.kill("SIGKILL"),await O(500)||g.unref()),process.stderr.write(Mn)}}}function Zt(e="init"){if(process.stderr.isTTY!==!0){let o=vt({stderr:process.stderr,operation:e});return{start:()=>o.start(),onProgress:n=>o.onProgress(n),stop:async()=>o.stop()}}return tn("init-progress-worker",{type:"start",operation:e})}async function jn(){let e=process.argv.slice(2);if(e[0]==="__repository-init-worker"){if(e.length!==2||!W.isAbsolute(e[1]))throw new Error("knodin internal repository init worker requires one absolute path");let t=await Te.promises.realpath(e[1]),s=setInterval(()=>{process.send?.({type:"rss",rssBytes:process.memoryUsage().rss})},50);s.unref(),process.send?.({type:"rss",rssBytes:process.memoryUsage().rss});let a=B();try{let p=H(t),h=(await ee([t],{depth:0,include:[t],command:ye(process),index:m=>a.index(m),status:m=>a.status(m),agents:Q(),indexMode:m=>te(p,m)})).results.find(({repository:m})=>m===t);if(!h)throw new Error("repository init worker produced no repository result");process.send?.({type:"rss",rssBytes:process.memoryUsage().rss}),process.send?.({type:"result",result:h}),h.status==="failed"&&(process.exitCode=1)}finally{clearInterval(s),await a.close()}return}let{rest:o}=Be(e),[n,...r]=o;if(!n||n==="-h"||n==="--help"){process.stdout.write(ge([],process.stdout.isTTY?process.stdout.columns:void 0));return}if(n==="-v"||n==="--version"||n==="version"){process.stdout.write(`${F}
75
- `);return}if(e.some(t=>t==="-h"||t==="--help")){process.stdout.write(ge(Yt(e),process.stdout.isTTY?process.stdout.columns:void 0));return}let c=Jt(e),l=t=>t.slice(2).replace(/-([a-z])/g,(s,a)=>a.toUpperCase()),i=t=>{let s=c.options[l(t)];return typeof s=="string"||typeof s=="number"?String(s):void 0},g=i("--repo"),b=ye(process),$=c.options.json===!0||n==="hook-refresh",d=n==="repair"?r:r.filter(t=>t!=="--json"),O={identity:i("--identity"),file:i("--file"),kind:i("--kind"),toIdentity:i("--to-identity"),toFile:i("--to-file"),toKind:i("--to-kind")},C={bytes:i("--bytes")?Number(i("--bytes")):void 0,tokens:i("--tokens")?Number(i("--tokens")):void 0,items:i("--items")?Number(i("--items")):void 0};for(let[t,s,a]of[["--bytes",C.bytes,256],["--tokens",C.tokens,64],["--items",C.items,1]])if(s!==void 0&&(!Number.isInteger(s)||s<a))throw new Error(`knodin: ${t} must be an integer >= ${a}`);if(n==="init"){we(r);let t=r.filter((s,a)=>!(s==="--json"||s.startsWith("--scope=")||s==="--scope"||r[a-1]==="--scope"));if(t.length>0)throw new Error(`knodin init: unknown option: ${t[0]}`)}if(n==="configure"){let t=r.includes("--status"),s=we(r),a=r.filter((p,f)=>!(p==="--json"||p==="--status"||p.startsWith("--scope=")||p==="--scope"||r[f-1]==="--scope"));if(a.length>0)throw new Error(`knodin configure: unknown option: ${a[0]}`);if(t&&s)throw new Error("knodin configure: --status and --scope are mutually exclusive");if(!t&&!s)throw new Error("knodin configure requires --scope personal|team|cli-only or --status")}if(n==="serve"){let{startServer:t}=await import("../chunks/server-7EDF4CBY.js");await t();return}if(n==="fleet"){if(g!==void 0)throw new Error("knodin fleet init accepts discovery roots, not --repo");let t=Ft(r,process.cwd());t.json||process.stderr.write("warning: `knodin fleet init` is deprecated; use `knodin repos init --linked-worktrees=skip|include` (alias retained for two minor releases)\n");let s=B(),a=H(t.roots[0]??process.cwd()),p=await ee(t.roots,{command:b,depth:t.depth,dryRun:t.dryRun,worktrees:t.worktrees,index:f=>s.index(f),status:f=>s.status(f),agents:Q(),indexMode:f=>te(a,f),isolatedInitialize:f=>Ee({repository:f,command:b})});await s.close(),process.stdout.write(t.json?`${JSON.stringify(p)}
76
- `:pe(p)),process.exitCode=p.exitCode;return}if(n==="repos"){if(g!==void 0)throw new Error("knodin repos accepts discovery roots, not --repo");let t=Mt(r,process.cwd());if(t.command==="discover"){let m=await ue(t.roots,{depth:t.depth,linkedWorktrees:t.linkedWorktrees}),y=B(),v=H(t.roots[0]??process.cwd()),R=[],E=t.signals?m.repositories.slice(0,Pt(t)):m.repositories;for(let N of E){let M=await Z(N,{status:async Y=>A(Y,await y.status(Y,{audit:"cached"})),systemMemberships:Y=>X(v,Y)});R.push(qt(M,t.signals,t.signals?await Lt(N.path):{}))}await y.close();let I={schemaVersion:1,command:"discover",linkedWorktrees:t.linkedWorktrees,...m,repositories:R},q=t.signals?le(I,"repositories:discover",{bytes:t.byteBudget,tokens:t.tokenBudget,items:t.itemBudget},{bytes:65536,tokens:16384,items:100}):I;process.stdout.write(`${JSON.stringify(q,null,t.json?0:2)}
77
- `),process.exitCode=m.issues.length>0?1:0;return}if(t.command==="init"){let m=H(t.roots[0]??process.cwd());if(t.dryRun){let N=await ee(t.roots,{command:b,depth:t.depth,dryRun:!0,worktrees:t.linkedWorktrees,manifestPath:t.manifestPath,include:t.include,exclude:t.exclude,index:async()=>{},status:async()=>{throw new Error("dry-run must not inspect graph databases")},agents:Q(),indexMode:U=>te(m,U)}),M=N.results.reduce((U,Le)=>{let qe=Le.plannedStatus??Le.status;return U[qe]=(U[qe]??0)+1,U},{}),Y={schemaVersion:1,command:"init",dryRun:!0,graphDatabasesOpened:0,modelsLoaded:0,selectedRepositories:N.results.filter(({message:U})=>U.startsWith("dry-run:")).length,planned:M,...N,repositories:[]};process.stdout.write(t.json?`${JSON.stringify(Y)}
78
- `:pe(N)),process.exitCode=N.exitCode;return}let y=B(),v=await ee(t.roots,{command:b,depth:t.depth,dryRun:!1,worktrees:t.linkedWorktrees,manifestPath:t.manifestPath,include:t.include,exclude:t.exclude,index:N=>y.index(N),status:N=>y.status(N),agents:Q(),indexMode:N=>te(m,N),isolatedInitialize:N=>Ee({repository:N,command:b})}),R=await ue(t.roots,{depth:t.depth,linkedWorktrees:t.linkedWorktrees}),E=[],I=new Set(v.results.map(({repository:N})=>N));for(let N of R.repositories.filter(({path:M})=>I.has(M)))E.push(await Z(N,{status:async M=>A(M,await y.status(M,{audit:"cached"})),systemMemberships:M=>X(m,M)}));await y.close();let q={schemaVersion:1,command:"init",...v,repositories:E};process.stdout.write(t.json?`${JSON.stringify(q)}
79
- `:pe(v)),process.exitCode=v.exitCode;return}if(t.command==="search"){let m=B(),y=H(t.roots[0]??process.cwd()),v=await jt(t.roots,t.query??"",{depth:t.depth,linkedWorktrees:t.linkedWorktrees,include:t.include,exclude:t.exclude,allowPartial:t.allowPartial,itemBudget:t.itemBudget,byteBudget:t.byteBudget,tokenBudget:t.tokenBudget,cursor:t.cursor,status:async R=>A(R,await m.status(R,{audit:"cached"})),search:(R,E,I,q)=>m.search(R,E,I,{offset:q,includeSource:!0,federate:!1}),systemMemberships:R=>X(y,R)});await m.close(),process.stdout.write(`${JSON.stringify(v,null,t.json?0:2)}
80
- `),process.exitCode=v.status==="unavailable"?1:0;return}let s=await ue(t.roots,{depth:t.depth,linkedWorktrees:t.linkedWorktrees}),a=B(),p=H(t.roots[0]??process.cwd()),f=[];for(let m of s.repositories)try{if(t.command==="doctor"){let y=A(m.path,await a.status(m.path,{audit:"deep"})),v=await Z(m,{status:async()=>y,systemMemberships:R=>X(p,R)});f.push({...v,diagnosis:await ae(m.path,{currentVersion:F,runtimeCommand:b,graph:y})})}else f.push(await Z(m,{status:async y=>A(y,await a.status(y,{audit:"cached"})),systemMemberships:y=>X(p,y)}))}catch(y){f.push({path:m.path,status:"unknown",error:y instanceof Error?y.message:String(y)})}await a.close();let h={schemaVersion:1,command:t.command,linkedWorktrees:t.linkedWorktrees,repositories:f,issues:s.issues,emptyRoots:s.emptyRoots,staleLinkedWorktrees:s.staleLinkedWorktrees};process.stdout.write(`${JSON.stringify(h,null,t.json?0:2)}
81
- `),process.exitCode=s.issues.length>0||f.some(m=>"health"in m&&m.health!=="healthy"||"status"in m&&m.status==="unknown")?1:0;return}if(n==="docs"){if(g!==void 0)throw new Error("knodin docs does not accept --repo");let[t,...s]=r;if(s.length>0)throw new Error(`knodin docs: unknown argument ${s[0]}`);if(!t||t==="list"){process.stdout.write(`${it().join(`
82
- `)}
83
- `);return}let a=st(t);if(!a)throw new Error(`knodin docs: unknown topic ${t}`);process.stdout.write(a.endsWith(`
84
- `)?a:`${a}
85
- `);return}if(n==="update"){if(g!==void 0)throw new Error("knodin update does not accept --repo");let[t,...s]=d;if(!t||!["status","check","explain","apply","rollback"].includes(t))throw new Error("knodin update requires status, check, explain, apply, or rollback");if(s.length>0)throw new Error(`knodin update ${t}: unknown argument ${s[0]}`);let a=re(b),p=async y=>{let[v,...R]=y;if(!v)return{status:1,stdout:"",stderr:"missing manager command"};let E=Ie(v,R,{encoding:"utf-8",timeout:12e4,maxBuffer:4*1024*1024,stdio:["ignore","pipe","pipe"]});return{status:E.status,stdout:E.stdout??"",stderr:E.stderr??E.error?.message??""}},f=async()=>{let[y,...v]=b;if(!y)return!1;let R=Ie(y,[...v,"--version"],{encoding:"utf-8",timeout:1e4,stdio:["ignore","pipe","pipe"]});return R.status===0&&/^\d+\.\d+\.\d+/.test(R.stdout.trim())},h={currentVersion:F,installMethod:a,env:process.env,runManager:p,healthCheck:f},m;if(t==="status")m=ke(h);else if(t==="check")try{m=await be(h)}finally{process.env.KNODIN_UPDATE_BACKGROUND==="1"&&ct()}else t==="explain"?m=dt(h):t==="apply"?m=await lt(h):m=await ut(h);process.stdout.write($?`${JSON.stringify(m)}
86
- `:oe("update",m));return}if(process.stdout.isTTY&&at({currentVersion:F,installMethod:re(b),env:process.env})){let[t,...s]=b;t&&en(t,[...s,"update","check","--json"],{detached:!0,stdio:"ignore",env:{...process.env,KNODIN_UPDATE_BACKGROUND:"1"}}).unref()}let T=De(g,process.cwd());T.ok||(process.stderr.write(`${T.error}
87
- `),process.exit(1));let u=T.repo;if(n==="doctor"){let t=Ie(mt(),["rev-parse","--is-inside-work-tree"],{cwd:u,encoding:"utf-8",stdio:["ignore","pipe","ignore"]});if(t.status!==0||t.stdout.trim()!=="true"){let s={schemaVersion:1,status:"portfolio-root",path:u,graph:"not-inspected",warning:"Target is not a Git worktree; treating a portfolio parent as one repository would be misleading.",remediation:[`knodin repos doctor ${JSON.stringify(u)}`]};process.stdout.write($?`${JSON.stringify(s)}
88
- `:oe("doctor",s)),process.exitCode=1;return}}if(n==="compress"){let t=new Set(["--strategy","--adapter","--lines","--max-output-bytes","--context","--exit-code","--signal","--max-input-bytes","--start","--end","--limit","--offset"]),s=d.filter((y,v)=>!y.startsWith("--")&&!t.has(d[v-1])),a=(y,v,R,E)=>{let I=i(y),q=I===void 0?v:Number(I);if(!Number.isInteger(q)||q<R||q>E)throw new Error(`knodin compress: ${y} must be an integer from ${R} to ${E}`);return q},p=s[0]==="read"||s[0]==="diagnose"||s[0]==="delete"?s[0]:"create",f=p==="create"?void 0:s[1];if(p!=="create"&&!f)throw new Error(`knodin compress ${p} requires an artifact id`);let h,m=!1;if(p==="diagnose"){let y=B();try{let v=await V(u,R=>y.status(R,{audit:"cached"}));v.available?h=await ft(y,u,{artifactId:f??"",maxDiagnostics:a("--limit",10,1,50),diagnosticOffset:a("--offset",0,0,1e6),contextLines:a("--context",2,0,10),contextByteBudget:a("--max-output-bytes",16384,256,128*1024)}):(h=v,m=!0,process.exitCode=1)}finally{await y.close()}}else if(p==="read")h=je(u,f??"",{startLine:a("--start",1,1,Number.MAX_SAFE_INTEGER),endLine:i("--end")?a("--end",200,1,Number.MAX_SAFE_INTEGER):void 0,byteBudget:a("--max-output-bytes",16384,256,4*1024*1024),raw:d.includes("--raw")});else if(p==="delete")h=We(u,f??"");else{let y=i("--strategy")??"smart";if(!["smart","head-tail","errors-only"].includes(y))throw new Error("knodin compress: invalid --strategy");let v=i("--adapter")??"auto";if(!["auto","generic","vitest","jest","pytest","go-test","maven","gradle","dotnet","cargo"].includes(v))throw new Error("knodin compress: invalid --adapter");let R=a("--max-input-bytes",16*1024*1024,1,64*1024*1024),E={exitCode:i("--exit-code")?a("--exit-code",0,0,255):void 0,signal:i("--signal"),strategy:y,adapter:v,lineBudget:a("--lines",200,1,1e4),byteBudget:a("--max-output-bytes",16384,256,4*1024*1024),contextLines:a("--context",1,0,10),maxInputBytes:R,retain:!d.includes("--no-retain"),redactSecrets:!d.includes("--no-redact")},I=s[0]==="create"?s[1]??"-":s[0]??"-";h=I==="-"?Me(u,{...E,text:await qn(R)}):Fe(u,I,E)}p==="diagnose"&&(C.bytes!==void 0||C.tokens!==void 0||C.items!==void 0)&&(h=le(h,"compress:diagnose",C,{bytes:65536,tokens:16384,items:50})),$?process.stdout.write(`${JSON.stringify(h)}
89
- `):p==="diagnose"&&!m?process.stdout.write(Ln(h)):p==="diagnose"?process.stdout.write(oe("compress diagnose",h)):p==="read"?process.stdout.write(Pn(h)):p==="create"?process.stdout.write(An(h)):process.stdout.write(oe("compress delete",h));return}let x=B(),w,L,D=!1,_=0,Pe=!1,G=async t=>{let s=await V(u,async f=>A(f,await x.status(f,{audit:"cached"})));if(!s.available)return process.exitCode=1,s;let a=await t(),p=await V(u,async f=>A(f,await x.status(f,{audit:"cached"})));return p.available?ve(a,p.state,p.graph.freshness):(process.exitCode=1,p)};switch(n){case"doctor":{let t=i("--client");if(t!==void 0&&!["claude","codex","gemini","antigravity"].includes(t))throw new Error("knodin doctor: --client must be claude, codex, gemini, or antigravity");let s=d.filter((f,h)=>f!=="--client"&&d[h-1]!=="--client");if(s.length>0)throw new Error(`knodin doctor: unknown option ${s[0]}`);let a=await ae(u,{currentVersion:F,runtimeCommand:[...b,"serve"],graph:await x.status(u,{audit:"deep"}),client:t}),p=a.manager.name;a.update=await be({currentVersion:F,installMethod:["npm","mise","volta","nvm","fnm","asdf","homebrew"].includes(p??"")?p:"unknown",env:process.env}),w=a;break}case"system":{let t=d.includes("--allow-partial"),[s,a,...p]=d.filter(m=>m!=="--allow-partial");if(!s||!["list","show","validate","query"].includes(s))throw new Error("knodin system requires list, show, validate, or query");if(t&&s!=="query")throw new Error(`knodin system ${s}: --allow-partial applies only to query`);if(p.length>0)throw new Error(`knodin system ${s}: unknown argument ${p[0]}`);let f=await Se(H(u));if(s==="list"){if(a)throw new Error("knodin system list accepts no system id");w={schemaVersion:f.schemaVersion,systems:f.systems.map(({id:m,components:y})=>({id:m,componentCount:y.length}))};break}if(!a)throw new Error(`knodin system ${s} requires <system-id>`);let h=f.systems.find(({id:m})=>m===a);if(!h){w={status:"not-found",systemId:a,available:f.systems.map(({id:m})=>m)},process.exitCode=1;break}if(s==="show")w={status:"ok",system:h,repositories:f.repositories};else if(s==="validate"){let m=await Re(f,a,async y=>A(y,await x.status(y,{audit:"cached"})),u);w={systemId:a,...m},m.valid||(process.exitCode=1)}else{let m=await Re(f,a,async y=>A(y,await x.status(y,{audit:"cached"})),u);w=Wt(f,a,t,m),w.status==="unavailable"&&(process.exitCode=1)}break}case"hook-refresh":{let[t,s,a]=d,p;if(t==="commit")p={kind:t};else if(t==="checkout"&&s&&a)p={kind:t,before:s,after:a};else if(t==="merge"&&s&&a)p={kind:t,before:s,after:a};else if(t==="rewrite"&&s)p={kind:t,inputPath:s};else throw new Error("knodin hook-refresh: invalid lifecycle event");w={indexed:await kt(u,p,(h,m)=>x.index(h,m))};break}case"init":{let t=await Rn(r,u),s=t==="team"?[]:Xt(u),a=Zt(),p=xe(u);p.start(),a.start();let f;try{try{f=await $e(u,{command:b,index:(h,m)=>x.index(h,void 0,!1,m),scope:t,agents:s,onProgress:h=>{p.update(h),a.onProgress(h)}})}finally{await a.stop(),p.stop()}}catch(h){if(!(h instanceof yt))throw h;process.stderr.write(`${h.message}
90
- `),await x.close(),process.exitCode=1;return}w={status:"success",message:"knodin initialized successfully. Git lifecycle hooks configured.",paths:f};break}case"configure":{if(r.includes("--status")){w=ce(u)??{scope:"unconfigured",agents:[],warning:"AI agents are not configured by knodin. Run `knodin configure --scope personal`."};break}let t=we(r);if(!t)throw new Error("knodin configure: missing --scope");if(!Te.existsSync(W.join(u,".knodin","db.sqlite")))throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");let s=t==="team"?[]:Xt(u),a=await $e(u,{command:b,index:async()=>{},scope:t,agents:s,allowTrackedTransition:!0,auditConfigurationChanges:!0});w={status:"success",message:`knodin agent integration changed to ${t}.`,graphInitialization:"unchanged",nextAction:"run `knodin status` and reload the configured client",paths:a};break}case"index":{let t=He(d),s=ze(g,t,process.cwd());s.ok||(process.stderr.write(`${s.error}
91
- `),await x.close(),process.exit(1));let a=d.includes("--clean")||d.includes("--force"),p=i("--scip"),f=Zt("index"),h=xe(s.repo);h.start(),f.start();let m;try{m=await x.index(s.repo,s.files,a,{scip:p?{path:p}:void 0,onProgress:v=>{h.update(v),f.onProgress(v)}})}finally{await f.stop(),h.stop()}w=m;let y=Ge([...m.indexed,...m.unchanged],s.repo);y.ok||(process.stderr.write(`${y.error}
92
- `),await x.close(),process.exit(1)),m.verification.status!=="healthy"&&(process.stderr.write(Nn(m)),await x.close(),process.exit(1));break}case"status":{let t=async()=>({...A(u,await x.status(u,{audit:d.includes("--deep")?"deep":"cached"})),integration:ce(u),update:ke({currentVersion:F,installMethod:re(b),env:process.env})});if(d.includes("--watch")){let s=i("--interval"),a=s===void 0?1:Number(s);if(!Number.isFinite(a)||a<.1||a>60)throw new Error("knodin status: --interval must be between 0.1 and 60 seconds");let p=!0,f=()=>{p=!1};for(process.once("SIGINT",f),process.once("SIGTERM",f);p;){let h=await t();process.stdout.write(`${JSON.stringify({observedAt:new Date().toISOString(),...h})}
93
- `),await new Promise(m=>setTimeout(m,a*1e3))}process.removeListener("SIGINT",f),process.removeListener("SIGTERM",f),Pe=!0,w=null}else w=await t();break}case"wait":{if(!d.includes("--fresh"))throw new Error("knodin wait requires --fresh");let t=i("--timeout"),s=t===void 0?30:Number(t);if(!Number.isFinite(s)||s<0||s>300)throw new Error("knodin wait: --timeout must be between 0 and 300 seconds");w=await Ht(x,u,Math.round(s*1e3)),w.status!=="fresh"&&(process.exitCode=1);break}case"repair":{let t=_t(d);if(t.plan){w=It(await x.status(u,{audit:"deep"})),L=t.output,D=!0;break}let s=Nt(t,process.env,process.stderr.isTTY),a=s==="tty"?tn("repair-progress-worker",{type:"start"}):(()=>{let h=Tt({mode:s,intervalMs:t.progressIntervalMs,stdout:process.stdout,stderr:process.stderr});return{start:()=>h.start(),onProgress:m=>h.onProgress(m),stop:async()=>h.stop()}})(),p=new AbortController,f=()=>p.abort();process.once("SIGINT",f),a.start();try{w=rt(u,await x.repair(u,{signal:p.signal,onProgress:h=>a.onProgress(h)}))}finally{process.removeListener("SIGINT",f),await a.stop()}L=t.output,w.cancelled&&(_=130);break}case"refresh-artifacts":{let t=d[0]??"code-change";t!=="checkout"&&t!=="merge"&&t!=="code-change"&&(process.stderr.write(`knodin refresh-artifacts accepts checkout, merge, or code-change
94
- `),process.exit(1));let s=Gt(u,t);Ut(u,s),w=s;break}case"explain":{let t=d[0];t||(process.stderr.write(`knodin explain requires a <symbol>
95
- `),process.exit(1)),w=await G(async()=>{let s=await x.explain(t,u,d[1]==="minimal"?"minimal":"standard",O);return d[1]==="source"&&!s.ambiguity?{mode:"source",identity:s.identity,symbol:s.symbol,source:s.source,staleness:s.staleness}:s});break}case"review":{let t=Ue(d);w=await G(()=>x.review(t.base,u,t.detailLevel,t.options));break}case"map":w=await G(()=>x.map(u,d.includes("--standard")?"standard":"minimal",{topN:i("--top")?Number(i("--top")):void 0,sort:i("--sort"),relationKinds:i("--relations")?.split(",").filter(Boolean)}));break;case"wiki":{let t=await V(u,async p=>A(p,await x.status(p,{audit:"cached"})));if(!t.available){w=t,process.exitCode=1;break}let s=d.includes("--force"),a=await x.wiki(u,s);await x.close(),process.stdout.write(`wrote ${a.written.length} pages, skipped ${a.skipped.length}
96
- `),process.exitCode=0;return}case"visualize":{let t=d.find((a,p)=>!a.startsWith("--")&&!d[p-1]?.startsWith("--")),s=i("--output");if(!t)throw new Error("knodin visualize requires an <entry> selector");if(!s)throw new Error("knodin visualize requires --output <path.html>");w=await G(()=>Qt(x,u,{entry:t,outputPath:s,depth:i("--depth")?Number(i("--depth")):void 0,byteBudget:i("--max-bytes")?Number(i("--max-bytes")):void 0,selector:{identity:O.identity,file:O.file,kind:O.kind}}));break}case"search":{let t=d[0];t||(process.stderr.write(`knodin search requires a <query>
97
- `),process.exit(1));let s=d[1]&&!d[1].startsWith("--")?d[1]:void 0,a=Number(i("--limit")??s??5);if(!Number.isInteger(a)||a<1)throw new Error("knodin search: limit must be a positive integer");w=await G(()=>x.search(t,u,a,{languages:i("--languages")?.split(",").filter(Boolean),extensions:i("--extensions")?.split(",").filter(Boolean),kinds:i("--kinds")?.split(",").filter(Boolean),path:i("--path"),testScope:d.includes("--tests-only")?"test":d.includes("--production-only")?"production":"all",includeSource:!d.includes("--no-source"),offset:i("--offset")?Number(i("--offset")):0}));break}case"pack":{let t=d[0],s=i("--diff-scope");if(s&&!["unstaged","staged","all","compare"].includes(s))throw new Error("knodin pack: invalid --diff-scope");if(t==="read"){if(!d[1])throw new Error("knodin pack read requires an artifact path");w=Je(u,d[1],Number(i("--start")??1),Number(i("--end")??200),Number(i("--bytes")??16384))}else if(t==="grep"){if(!d[1]||!d[2])throw new Error("knodin pack grep requires an artifact path and regex");w=Ye(u,d[1],d[2],i("--flags")??"",Number(i("--limit")??100))}else{let a={};for(let p of i("--policy")?.split(",")??[]){let[f,h]=p.split("=");if(!f||!["full","summary","structure-only"].includes(h))throw new Error("knodin pack: invalid --policy assignment");a[f]=h}w=Ke(u,{format:i("--format")??"markdown",include:i("--include")?.split(",").filter(Boolean),exclude:i("--exclude")?.split(",").filter(Boolean),policies:a,alreadyPresent:i("--already-present")?.split(",").filter(Boolean),chatFiles:i("--chat-files")?.split(",").filter(Boolean),lineNumbers:d.includes("--line-numbers"),includeTree:d.includes("--tree"),byteBudget:Number(i("--bytes")??65536),tokenBudget:Number(i("--tokens")??16384),outputPath:i("--output"),git:s||i("--log")?{diffScope:s,from:i("--from"),to:i("--to"),log:i("--log")?Number(i("--log")):void 0}:void 0})}break}case"evidence":{let t=d[0],s=d[1];if(!s||!["locate","outline","evidence","expand"].includes(t))throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");w=Ot({repo:u,file:s,level:t,continuation:i("--continuation"),baselineHash:i("--baseline-hash"),baselineBytes:i("--baseline-bytes")?Number(i("--baseline-bytes")):void 0,startLine:i("--start")?Number(i("--start")):void 0,endLine:i("--end")?Number(i("--end")):void 0,byteLimit:C.bytes,tokenLimit:C.tokens,itemLimit:C.items});break}case"query":{let t=d[0],s=pt.includes(t),a=t==="architecture_overview"||t==="import_cycles"||d[1]?.startsWith("--")?"":d[1]??"";t||(process.stderr.write(`knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|batch_outline|project_overview|shortest_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)
98
- `),process.exit(1));let p=i("--direction");p&&!["upstream","downstream","both"].includes(p)&&(process.stderr.write(`knodin query: --direction must be upstream, downstream, or both
99
- `),process.exit(1));let f=i("--facets")?.split(",").filter(Boolean);f?.some(E=>!["packages","layers","boundaries","hotspots","entryPoints","languages"].includes(E))&&(process.stderr.write(`knodin query: --facets contains an unknown architecture facet
100
- `),process.exit(1)),!a&&!s&&(process.stderr.write(`knodin query ${t} requires a <target>
101
- `),process.exit(1));let h=t==="shortest_path"||t==="rename_preview"?d[2]:void 0;t==="rename_preview"&&!h&&(process.stderr.write(`knodin query rename_preview requires <old> and <new>
102
- `),process.exit(1));let m;if(t==="traverse"||t==="feature_path"||t==="impact"){let E=d.indexOf("--depth"),I=E>=0?d[E+1]:d[2];I!==void 0&&I!==""&&Number.isFinite(Number(I))&&(m=Number(I))}let y;t==="architecture_overview"&&d.includes("standard")&&(y="standard");let v=i("--limit");v!==void 0&&(!Number.isInteger(Number(v))||Number(v)<1)&&(process.stderr.write(`knodin query: --limit must be a positive integer
103
- `),process.exit(1));let R=t.startsWith("lsp_")?null:await V(u,async E=>A(E,await x.status(E,{audit:"cached"})));if(R&&!R.available){w=R,process.exitCode=1;break}if(w=await x.query(t,a,u,h,v?Number(v):void 0,m,y,O,t==="impact"?{mode:i("--impact-mode")==="file"?"file":"symbol",direction:["upstream","downstream","both"].includes(i("--direction")??"")?i("--direction"):void 0,relationKinds:i("--relations")?.split(",").map(E=>E.trim()).filter(Boolean),minConfidence:i("--min-confidence")?Number(i("--min-confidence")):void 0,includeTests:!d.includes("--exclude-tests"),includeDataFlow:d.includes("--data-flow")}:void 0,{minLines:i("--min-lines")?Number(i("--min-lines")):void 0,minComplexity:i("--min-complexity")?Number(i("--min-complexity")):void 0,kinds:i("--kinds")?.split(",").filter(Boolean),path:i("--path"),direction:t==="traverse"&&["upstream","downstream","both"].includes(i("--direction")??"")?i("--direction"):void 0,includeDataFlow:t==="traverse"?d.includes("--data-flow"):void 0,flowVariable:t==="flow_analysis"?i("--variable"):void 0,architectureFacets:f,topN:i("--top")?Number(i("--top")):void 0,sort:i("--sort"),relationKinds:t==="impact"?void 0:i("--relations")?.split(",").filter(Boolean),detailLevel:y}),t==="impact"||t==="dead_code"){let E=await Se(H(u));w=Bt(E,u,t,a,w)}if(R?.available){let E=await V(u,async I=>A(I,await x.status(I,{audit:"cached"})));if(!E.available){w=E,process.exitCode=1;break}w=ve(w,E.state,E.graph.freshness)}break}case"rename":{let t=d.filter(h=>!h.startsWith("--")),s=t[0],a=t[1],p=d.includes("--apply"),f=!d.includes("--no-verify");(!s||!a)&&(process.stderr.write(`knodin rename requires <old> and <new>
104
- `),process.exit(1)),w=await G(()=>x.rename(s,a,u,p,f,O));break}case"prs":{try{let t=d[0]==="audit"?d.slice(1):d,s=h=>{let m=t.indexOf(h);return m>=0?t[m+1]:void 0},a=s("--state"),p=s("--limit"),f=p===void 0?50:Number(p);if(!Number.isInteger(f)||f<1)throw new Error("knodin prs: --limit must be a positive integer");w=await Ct(u,x,{state:a,limit:f,branches:s("--branches"),range:s("--range"),base:s("--base"),head:s("--head"),expectedLogin:s("--expected-login")})}catch(t){await x.close(),process.stderr.write(`${t.message}
105
- `),process.exit(1)}break}case"worktrees":{let t=d[0]??"status";if(t==="status")w=await ht(u,s=>x.status(s,{audit:"cached"}));else if(t==="reconcile")w=gt(u);else if(t==="remove"){let s=d[1];if(!s)throw new Error("knodin worktrees remove requires <path>");w=wt(u,s,d.includes("--dry-run"))}else throw new Error(`knodin worktrees: unknown action ${t}`);break}case"telemetry":{let t=d[0],s=i("--retention-days"),a=s===void 0?30:Number(s);if(!Number.isInteger(a)||a<1||a>3650)throw new Error("knodin telemetry: --retention-days must be an integer from 1 to 3650");let p=i("--input");if(t==="status")w=Et(u,p,a);else if(t==="report")w=$t(u,de(u,p,a),i("--output"));else if(t==="export")w=Rt(u,de(u,p,a),i("--output"));else if(t==="clear")w=St(u,p);else throw new Error("knodin telemetry requires status, report, export, or clear");break}case"diagnostics":{let[t,s]=c.positionals,a=i("--retention-days"),p=i("--since"),f=i("--output"),h=a===void 0?14:Number(a);if(!Number.isInteger(h)||h<1||h>365)throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");if(s&&t!=="inspect")throw new Error(`knodin diagnostics ${t}: unexpected bundle argument`);if(a!==void 0&&t!=="enable")throw new Error(`knodin diagnostics ${t}: --retention-days applies only to enable`);if(p!==void 0&&t!=="collect")throw new Error(`knodin diagnostics ${t}: --since applies only to collect`);if(f!==void 0&&t!=="collect")throw new Error(`knodin diagnostics ${t}: --output applies only to collect`);if(t==="enable")w=Qe(u,h);else if(t==="status")w=Ze(u);else if(t==="disable")w=Xe(u);else if(t==="clear")w=tt(u);else if(t==="inspect"){if(!s)throw new Error("knodin diagnostics inspect requires <bundle>");w=ot(u,s)}else if(t==="collect"){let m=p??"24h",y=/^(\d+)(h|d)$/.exec(m);if(!y)throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");let v=Number(y[1])*(y[2]==="d"?24:1),R=A(u,await x.status(u,{audit:"deep"})),E=await ae(u,{currentVersion:F,runtimeCommand:[...b,"serve"],graph:R});w=nt(u,{sinceHours:v,outputPath:f,doctor:E,graph:R,telemetry:de(u,void 0,Math.max(1,Math.ceil(v/24))),knodinVersion:F})}else throw new Error("knodin diagnostics requires enable, status, collect, inspect, clear, or disable");break}case"context":{let t=d[0];t||(process.stderr.write(`knodin context requires a "<task>" description
106
- `),process.exit(1)),w=await G(()=>Ve(x,t,u,d[1]));break}default:process.stderr.write(`unknown command: ${n}
107
-
108
- ${ge([],process.stderr.isTTY?process.stderr.columns:void 0)}`),process.exit(1)}if(await x.close(),Pe)return;let z=le(w,n,C,{bytes:65536,tokens:16384,items:100}),J=Math.max(Number(process.exitCode??0),_);if(n==="init"&&!$){process.stdout.write(Sn(z)),process.exitCode=J;return}if(n==="configure"&&!$&&!r.includes("--status")){process.stdout.write(On(z));return}if(n==="configure"&&!$&&r.includes("--status")){process.stdout.write(Cn(z));return}if(n==="repair"&&L==="human"&&!D){process.stdout.write(_n(z)),process.exitCode=J;return}if(n==="index"&&!$){process.stdout.write(In(w)),process.exitCode=J;return}if(n==="status"&&!$){process.stdout.write(Tn(z)),process.exitCode=J;return}if(!$&&L!=="jsonl"){process.stdout.write(oe(n,z)),process.exitCode=J;return}process.stdout.write(L==="jsonl"?At("result",z):`${JSON.stringify(z)}
109
- `),process.exitCode=J}jn().catch(e=>{let o=process.argv.slice(2),n=o.indexOf("--repo"),r=o.find($=>$.startsWith("--repo=")),c=n>=0&&o[n+1]?o[n+1]:r?r.slice(7):process.cwd(),l=o.find(($,d)=>$.startsWith("-")?!1:!(d>0&&o[d-1]==="--repo")&&$!==c),g=et(c,{surface:"cli",operation:l&&new Set(["init","configure","index","doctor","status","wait","repair","serve","context","explain","review","map","search","query","rename","wiki","visualize","pack","compress","prs","worktrees","telemetry","diagnostics","system","repos","update"]).has(l)?l:"unknown",phase:"dispatch",error:e}),b=g.recorded?` [diagnostic ${g.correlationId}]`:"";console.error(`${e instanceof Error?e.message:String(e)}${b}`),process.exit(1)});
2
+ /**
3
+ * `knodin` CLI the daily driver.
4
+ *
5
+ * knodin explain <symbol> [minimal] edit-ready source + direct call paths + blast radius
6
+ * knodin review [base] risk-scored context with explicit git diff scopes
7
+ * knodin map subsystems + confidence-tagged edges
8
+ * knodin wiki [--force] write .knodin/wiki/ (index.md + per-community pages)
9
+ * knodin serve run the MCP server on stdio
10
+ *
11
+ * The same engine backs both the CLI and the MCP server, so any assistant and
12
+ * a human at a terminal see identical results.
13
+ */
14
+ import { spawn, spawnSync } from "node:child_process";
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import readline from "node:readline/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
20
+ import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
21
+ import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
22
+ import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
23
+ import { buildKnodinContext } from "../src/context.js";
24
+ import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/context-export.js";
25
+ import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
26
+ import { getDocSection, listDocTopics } from "../src/docs-sections.js";
27
+ import { diagnoseInstallation } from "../src/doctor.js";
28
+ import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
29
+ import { resolveDbPath } from "../src/engine/state-paths.js";
30
+ import { diagnoseFailure, } from "../src/failure-diagnosis.js";
31
+ import { gitExecutable } from "../src/git-executable.js";
32
+ import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
33
+ import { createIndexActivityReporter } from "../src/index-activity.js";
34
+ import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
35
+ import { createInitProgressRenderer } from "../src/init-progress.js";
36
+ import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
37
+ import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
38
+ import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
39
+ import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
40
+ import { auditPullRequests } from "../src/pr-triage.js";
41
+ import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
42
+ import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
43
+ import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
44
+ import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
45
+ import { applyResponseBudget } from "../src/response-budget.js";
46
+ import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
47
+ import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
48
+ import { KNODIN_VERSION } from "../src/version.js";
49
+ import { writeVisualization, } from "../src/visualization.js";
50
+ import { waitForFresh } from "../src/wait-for-fresh.js";
51
+ import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../src/worktree-lifecycle.js";
52
+ function explicitScope(args) {
53
+ const index = args.indexOf("--scope");
54
+ if (index >= 0) {
55
+ const value = args[index + 1];
56
+ if (!value)
57
+ throw new Error("knodin: --scope requires a value");
58
+ return parseInitScope(value);
59
+ }
60
+ const equals = args.find((argument) => argument.startsWith("--scope="));
61
+ return equals ? parseInitScope(equals.slice("--scope=".length)) : null;
62
+ }
63
+ async function chooseInitScope(args, repo) {
64
+ const selected = explicitScope(args);
65
+ if (selected)
66
+ return selected;
67
+ if (detectTrackedTeamIntegration(repo)) {
68
+ process.stderr.write("[init:scope] Detected tracked knodin team integration; preserving team scope\n");
69
+ return "team";
70
+ }
71
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
72
+ return "personal";
73
+ const prompt = readline.createInterface({
74
+ input: process.stdin,
75
+ output: process.stderr,
76
+ });
77
+ try {
78
+ const answer = await prompt.question([
79
+ "How should knodin integrate with coding agents?",
80
+ " 1. Personal (recommended) all detected agents; Git stays clean",
81
+ " 2. Team create commit-ready shared configuration",
82
+ " 3. CLI-only — agents will not discover or invoke knodin automatically",
83
+ "Select [1]: ",
84
+ ].join("\n"));
85
+ if (!answer.trim() || answer.trim() === "1")
86
+ return "personal";
87
+ if (answer.trim() === "2")
88
+ return "team";
89
+ if (answer.trim() === "3") {
90
+ const confirmation = await prompt.question("CLI-only requires manual knodin commands. Continue? [y/N] ");
91
+ if (!/^y(?:es)?$/i.test(confirmation.trim()))
92
+ throw new Error("knodin init: CLI-only selection cancelled");
93
+ return "cli-only";
94
+ }
95
+ return parseInitScope(answer.trim());
96
+ }
97
+ finally {
98
+ prompt.close();
99
+ }
100
+ }
101
+ function integrationAgents(repo) {
102
+ const previous = readRepositoryIntegrationConfig(repo)?.agents ?? [];
103
+ const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
104
+ return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
105
+ }
106
+ function formatInitHuman(result) {
107
+ let agents = `${result.paths.scope} — no supported coding agents detected`;
108
+ if (result.paths.scope === "cli-only")
109
+ agents = "CLI-only AI agents are not configured to discover knodin";
110
+ else if (result.paths.agentIntegration.configured.length > 0)
111
+ agents = `${result.paths.scope} — ${result.paths.agentIntegration.configured.join(", ")}`;
112
+ const failures = result.paths.agentIntegration.failed
113
+ .map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
114
+ .join("");
115
+ const refresh = result.paths.lifecycleRefresh.state === "fresh"
116
+ ? "fresh"
117
+ : `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s)); run \`knodin wait --fresh\``;
118
+ return `${result.message}\nGraph: ${result.paths.database}\nGit refresh: ${result.paths.gitHooks.length} lifecycle hooks installed; ${refresh}\nAgent integration: ${agents}${failures}\nBackground indexer: ${result.paths.backgroundIndexer}\n`;
119
+ }
120
+ function formatConfigureStatusHuman(result) {
121
+ if (result.scope === "unconfigured")
122
+ return "Agent integration: unconfigured.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal`.\n";
123
+ if (result.scope === "cli-only")
124
+ 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";
125
+ if (result.scope === "repository-detected")
126
+ return `Agent integration: repository-detected${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\nLocal scope receipt: missing; managed repository integration is present.\n`;
127
+ return `Agent integration: ${result.scope}${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\n`;
128
+ }
129
+ function formatConfigureHuman(result) {
130
+ let configured = "none detected";
131
+ if (result.paths.agentIntegration.configured.length > 0) {
132
+ configured = result.paths.agentIntegration.configured.join(", ");
133
+ }
134
+ else if (result.paths.scope === "cli-only") {
135
+ configured = "none (CLI-only)";
136
+ }
137
+ const failures = result.paths.agentIntegration.failed
138
+ .map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
139
+ .join("");
140
+ const refresh = result.paths.lifecycleRefresh.state === "fresh"
141
+ ? "fresh"
142
+ : `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s))`;
143
+ const filesystemMutationLines = result.paths.filesystemChanges
144
+ .map(({ path: changedPath, action }) => `\nFilesystem ${action}: ${changedPath}`)
145
+ .join("");
146
+ const externalOutcomeLines = result.paths.externalConfigurationOutcomes
147
+ .map(({ system, state }) => `\nExternal configuration outcome: ${system} ${state} (external mutation not locally observable)`)
148
+ .join("");
149
+ return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}${filesystemMutationLines}${externalOutcomeLines}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
150
+ }
151
+ function formatRepairHuman(result) {
152
+ const coverage = result.after.coverage;
153
+ if (result.cancelled) {
154
+ return `Repair paused: ${result.remaining ?? 0} file(s) remaining. Run \`knodin repair\` again to finish.\n`;
155
+ }
156
+ if (result.verified) {
157
+ if (result.lifecycle?.status === "degraded")
158
+ 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`;
159
+ return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols).\n`;
160
+ }
161
+ return `Repair finished with remaining issues. Run \`knodin status --deep\` for details.\n`;
162
+ }
163
+ function formatIndexHuman(result) {
164
+ if (result.indexed.length === 0 && result.unchanged.length > 0) {
165
+ const noun = result.unchanged.length === 1 ? "file" : "files";
166
+ return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.\n`;
167
+ }
168
+ const unchanged = result.unchanged.length > 0
169
+ ? `; ${result.unchanged.length.toLocaleString()} already current`
170
+ : "";
171
+ const noun = result.indexed.length === 1 ? "file" : "files";
172
+ return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.\n`;
173
+ }
174
+ function formatIndexVerificationError(result) {
175
+ const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
176
+ const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
177
+ return `knodin index: requested work completed, but ${result.verification.issueCount.toLocaleString()} graph issue(s) remain.${detail} Run \`knodin repair\`.\n`;
178
+ }
179
+ /**
180
+ * The hooks/lifecycle line of `status`.
181
+ *
182
+ * A mirror has no hooks by design, so neither "installed" nor "degraded" is true
183
+ * of it: claiming they are "installed and executable" would describe a directory
184
+ * containing none, and reporting "degraded" would demand a `knodin init` that
185
+ * must never run there.
186
+ */
187
+ function formatLifecycleLine(lifecycle, isMirror) {
188
+ if (!lifecycle)
189
+ return "";
190
+ if (isMirror)
191
+ return "Hooks: not applicable; a mirror is refreshed explicitly, not by Git events.\n";
192
+ if (lifecycle.status === "healthy")
193
+ return "Hooks: installed and executable.\n";
194
+ const issue = lifecycle.issues[0] ?? "refresh capability is not verified";
195
+ return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin init\`.\n`;
196
+ }
197
+ /** Renders the top extensions of a tally as `.md 180, .yml 74, +3 more`. */
198
+ function formatTally(counts, limit = 4) {
199
+ const entries = Object.entries(counts).sort((left, right) => right[1] - left[1]);
200
+ const shown = entries.slice(0, limit).map(([ext, count]) => `${ext} ${count}`);
201
+ const remaining = entries.length - shown.length;
202
+ if (remaining > 0)
203
+ shown.push(`+${remaining} more`);
204
+ return shown.join(", ");
205
+ }
206
+ /**
207
+ * One clause naming what the graph does not cover. Silence here would let a
208
+ * language gap read as an absence of facts, so this is appended to the coverage
209
+ * line whenever anything was skipped.
210
+ */
211
+ function formatCoverageGaps(skipped) {
212
+ if (!skipped)
213
+ return "";
214
+ // A zero total is only reportable as "no gaps" when the tally was actually
215
+ // read. If it wasn't, staying silent would state full coverage on the
216
+ // strength of a measurement that never happened.
217
+ if (skipped.total === 0 && !skipped.unparsedUnknown)
218
+ return "";
219
+ const clauses = [];
220
+ const byExtension = formatTally(skipped.byExtension);
221
+ if (byExtension)
222
+ clauses.push(`not indexed: ${byExtension}`);
223
+ const unparsed = formatTally(skipped.unparsedByExtension);
224
+ if (unparsed)
225
+ clauses.push(`parsed but empty: ${unparsed}`);
226
+ if (skipped.unparsedUnknown)
227
+ clauses.push("parsed-but-empty tally unavailable — run a full index; this is a lower bound");
228
+ const count = skipped.unparsedUnknown ? `${skipped.total}+` : `${skipped.total}`;
229
+ return `. Not in graph: ${count} files (${clauses.join("; ")})`;
230
+ }
231
+ /**
232
+ * Recursive on-disk size of a directory.
233
+ *
234
+ * Reports unreadable entries rather than treating them as zero. The listing
235
+ * exists so acquired disk is auditable; a mirror that cannot be measured showing
236
+ * a confident `0 B` invites exactly the wrong conclusion — that it is empty and
237
+ * safe to remove — which is the same absence-as-fact error this whole feature
238
+ * is built to avoid.
239
+ */
240
+ function directorySizeBytes(target) {
241
+ let bytes = 0;
242
+ let partial = false;
243
+ let entries;
244
+ try {
245
+ entries = fs.readdirSync(target, { withFileTypes: true });
246
+ }
247
+ catch {
248
+ return { bytes: 0, partial: true };
249
+ }
250
+ for (const entry of entries) {
251
+ const child = path.join(target, entry.name);
252
+ if (entry.isDirectory()) {
253
+ const nested = directorySizeBytes(child);
254
+ bytes += nested.bytes;
255
+ partial = partial || nested.partial;
256
+ }
257
+ else if (entry.isFile()) {
258
+ try {
259
+ bytes += fs.statSync(child).size;
260
+ }
261
+ catch {
262
+ partial = true;
263
+ }
264
+ }
265
+ }
266
+ return { bytes, partial };
267
+ }
268
+ function formatBytes(bytes) {
269
+ if (bytes < 1024)
270
+ return `${bytes} B`;
271
+ const units = ["KiB", "MiB", "GiB"];
272
+ let value = bytes / 1024;
273
+ let unit = 0;
274
+ while (value >= 1024 && unit < units.length - 1) {
275
+ value /= 1024;
276
+ unit++;
277
+ }
278
+ return `${value.toFixed(1)} ${units[unit]}`;
279
+ }
280
+ /**
281
+ * `knodin remote <add|list|remove|refresh>`.
282
+ *
283
+ * Mirrors are reported as snapshots, never as `fresh`: the clone is pinned to
284
+ * the commit it was fetched at, and nothing watches the remote for changes. Size
285
+ * and fetch time are shown for every mirror so acquired disk is auditable rather
286
+ * than silently accumulating.
287
+ */
288
+ function writeMirrorListing() {
289
+ const mirrors = listMirrors();
290
+ if (mirrors.length === 0) {
291
+ process.stdout.write("No mirrors acquired. Add one with `knodin remote add <url>`.\n");
292
+ return;
293
+ }
294
+ let total = 0;
295
+ let anyPartial = false;
296
+ for (const mirror of mirrors) {
297
+ const size = directorySizeBytes(path.dirname(mirror.path));
298
+ total += size.bytes;
299
+ anyPartial = anyPartial || size.partial;
300
+ // "at least" rather than a bare figure when something was unreadable: a
301
+ // mirror reported as 0 B reads as empty, and the obvious next action on an
302
+ // empty mirror is to delete it.
303
+ const shown = size.partial ? `at least ${formatBytes(size.bytes)}` : formatBytes(size.bytes);
304
+ process.stdout.write(`${mirror.identity} ${mirror.url}\n snapshot ${mirror.sha.slice(0, 12)}, fetched ${mirror.fetchedAt}, ${shown}\n ${mirror.path}\n`);
305
+ }
306
+ const totalShown = anyPartial ? `at least ${formatBytes(total)}` : formatBytes(total);
307
+ process.stdout.write(`${mirrors.length} mirror(s), ${totalShown} on disk. Remove one with \`knodin remote remove <identity>\`.\n`);
308
+ }
309
+ async function remoteAdd(url, options) {
310
+ const { record, alreadyPresent } = addMirror(url);
311
+ process.stdout.write(alreadyPresent
312
+ ? `Mirror already present: ${record.identity} (snapshot ${record.sha.slice(0, 12)}). Use \`knodin remote refresh ${record.identity}\` to update it.\n`
313
+ : `Acquired ${record.identity} at snapshot ${record.sha.slice(0, 12)} into ${record.path}\n`);
314
+ if (options.skipIndex || alreadyPresent)
315
+ return;
316
+ await indexMirror(record.path, options.deferSemantic);
317
+ }
318
+ async function remoteRefresh(identity, options) {
319
+ const record = refreshMirror(identity);
320
+ process.stdout.write(`Refreshed ${record.identity} to snapshot ${record.sha.slice(0, 12)} (${record.fetchedAt})\n`);
321
+ if (options.skipIndex)
322
+ return;
323
+ await indexMirror(record.path, options.deferSemantic);
324
+ }
325
+ async function runRemoteCommand(args) {
326
+ const [action, target] = args;
327
+ const options = {
328
+ skipIndex: args.includes("--no-index"),
329
+ deferSemantic: args.includes("--defer-semantic"),
330
+ };
331
+ if (action === "list")
332
+ return writeMirrorListing();
333
+ if (action === "remove") {
334
+ if (!target)
335
+ throw new Error("knodin remote remove requires an <identity>");
336
+ process.stdout.write(removeMirror(target) ? `Removed ${target}\n` : `No mirror named ${target}\n`);
337
+ return;
338
+ }
339
+ if (action === "add") {
340
+ if (!target)
341
+ throw new Error("knodin remote add requires a <url>");
342
+ return remoteAdd(target, options);
343
+ }
344
+ if (action === "refresh") {
345
+ if (!target)
346
+ throw new Error("knodin remote refresh requires an <identity>");
347
+ return remoteRefresh(target, options);
348
+ }
349
+ throw new Error("knodin remote <add|list|remove|refresh>");
350
+ }
351
+ /**
352
+ * Builds a mirror's graph structure-first, then fills embeddings.
353
+ *
354
+ * The embedding pass dominates indexing cost, so structural answers (`explain`,
355
+ * `callers_of`, blast radius) become available in a fraction of the total. The
356
+ * gap is reported rather than hidden: until embeddings land, `search` silently
357
+ * under-returns, so saying nothing here would hand back exactly the confident
358
+ * empty result this whole feature exists to avoid.
359
+ */
360
+ async function indexMirror(source, deferSemantic) {
361
+ const engine = createEngine();
362
+ try {
363
+ await engine.index(source, undefined, true, { skipEmbeddings: true });
364
+ const structural = await engine.status(source, { audit: "deep" });
365
+ process.stdout.write(`Indexed ${structural.coverage.indexedFiles} file(s), ${structural.coverage.filesWithSymbols} with symbols${formatCoverageGaps(structural.coverage.skipped)}\n`);
366
+ process.stdout.write("Structural queries (explain, query, map) are ready now; semantic search is not.\n");
367
+ if (deferSemantic) {
368
+ process.stdout.write("Semantic coverage deferred. Run `knodin index --repo <mirror>` to build it; until then `search` will under-return.\n");
369
+ return;
370
+ }
371
+ process.stdout.write("Building semantic index...\n");
372
+ await engine.index(source, undefined, false);
373
+ const full = await engine.status(source, { audit: "deep" });
374
+ process.stdout.write(`Semantic coverage: ${full.semanticReadiness ?? "unknown"}\n`);
375
+ }
376
+ finally {
377
+ await engine.close();
378
+ }
379
+ }
380
+ function formatStatusHuman(result) {
381
+ // A mirror's `freshness` describes the graph against its local clone and can
382
+ // legitimately read `fresh`. Saying so without also saying the clone is a
383
+ // snapshot would imply knodin is tracking the remote, which it is not.
384
+ const mirrorNote = result.mirror
385
+ ? ` Mirror of ${result.mirror.url}: snapshot ${result.mirror.sha.slice(0, 12)}, fetched ${result.mirror.fetchedAt}; the remote is not watched.`
386
+ : "";
387
+ // Semantic search drops symbols that have no embedding, so an incomplete pass
388
+ // silently shortens results. Say so rather than let it read as "no matches".
389
+ const semanticNote = result.semanticReadiness && result.semanticReadiness !== "ready"
390
+ ? ` Semantic search coverage is ${result.semanticReadiness}: \`search\` will under-return until embedding completes (\`knodin index\`).`
391
+ : "";
392
+ const coverage = `${result.coverage.sourceFiles} source files, ${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
393
+ if (result.status === "indexing" && result.activity) {
394
+ const count = result.activity.phaseTotal === undefined
395
+ ? ""
396
+ : ` ${result.activity.phaseCompleted}/${result.activity.phaseTotal}`;
397
+ const elapsed = Math.max(0, Math.floor((Date.now() - Date.parse(result.activity.startedAt)) / 1_000));
398
+ return `Graph update in progress: ${result.activity.phase}${count} — ${result.activity.message} (${elapsed}s elapsed; ${coverage}).\n`;
399
+ }
400
+ const integration = result.integration;
401
+ const integrationAgents = integration && integration.agents.length > 0 ? ` (${integration.agents.join(", ")})` : "";
402
+ const integrationLine = integration
403
+ ? `Agent integration: ${integration.scope}${integrationAgents}.\n`
404
+ : "Agent integration: unconfigured. AI agents will not discover knodin automatically; run `knodin configure --scope personal`.\n";
405
+ // Replaces main's inline form: a mirror installs no hooks by design, so the
406
+ // inline version reported "needs repair — run knodin init" on every mirror
407
+ // status call, an instruction that must never be followed there.
408
+ const lifecycleLine = formatLifecycleLine(result.lifecycle, result.mirror !== undefined);
409
+ const head = (value) => value?.slice(0, 12) ?? "unknown";
410
+ const distance = result.freshness.commitDistance === null
411
+ ? ""
412
+ : ` by ${result.freshness.commitDistance.toLocaleString()} commit(s)`;
413
+ const freshnessLine = `Freshness: ${result.freshness.state}; indexed ${head(result.freshness.indexedHead)}, ` +
414
+ `current ${head(result.freshness.currentHead)} (${result.freshness.commitRelation}${distance}); ` +
415
+ `${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
416
+ `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n`;
417
+ if (result.status === "healthy")
418
+ return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
419
+ if (result.status === "stale")
420
+ return `Graph content is intact but evidence is stale (${coverage}).\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
421
+ const outstanding = result.missing.files.length + result.missing.records.length;
422
+ const firstIssue = result.missing.files[0] ?? result.missing.records[0];
423
+ const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
424
+ const repairCommand = result.lifecycle?.status === "degraded" &&
425
+ result.missing.records.every((record) => result.lifecycle?.issues.includes(record))
426
+ ? "Run `knodin init`."
427
+ : "Run `knodin repair`.";
428
+ return `Graph or lifecycle needs repair: ${outstanding} issue(s) found (${coverage}).${detail} ${repairCommand}\n${lifecycleLine}${integrationLine}`;
429
+ }
430
+ function humanLabel(key) {
431
+ return key.replace(/([a-z])([A-Z])/g, "$1 $2");
432
+ }
433
+ /** Render bounded CLI data for a terminal without turning it back into JSON. */
434
+ function formatHumanValue(value, indent = "", label) {
435
+ if (value === null || typeof value !== "object") {
436
+ const labelPrefix = label ? `${humanLabel(label)}: ` : "";
437
+ return [`${indent}${labelPrefix}${String(value)}`];
438
+ }
439
+ if (Array.isArray(value)) {
440
+ if (value.length === 0)
441
+ return label ? [] : [`${indent}(none)`];
442
+ const lines = label ? [`${indent}${humanLabel(label)}:`] : [];
443
+ for (const item of value) {
444
+ if (item === null || typeof item !== "object")
445
+ lines.push(`${indent} - ${String(item)}`);
446
+ else
447
+ lines.push(...formatHumanValue(item, `${indent} - `));
448
+ }
449
+ return lines;
450
+ }
451
+ const entries = Object.entries(value).filter(([key]) => key !== "responseBudget");
452
+ const lines = label ? [`${indent}${humanLabel(label)}:`] : [];
453
+ for (const [key, child] of entries) {
454
+ if (child === null || typeof child !== "object") {
455
+ lines.push(`${indent}${label ? " " : ""}${humanLabel(key)}: ${String(child)}`);
456
+ }
457
+ else
458
+ lines.push(...formatHumanValue(child, `${indent}${label ? " " : ""}`, key));
459
+ }
460
+ return lines;
461
+ }
462
+ function formatGenericHuman(cmd, result) {
463
+ const lines = [`${humanLabel(cmd)}:`, ...formatHumanValue(result, " ")];
464
+ return `${lines.join("\n")}\n`;
465
+ }
466
+ function formatCompressionHuman(result) {
467
+ const omitted = result.omittedRanges.reduce((total, range) => total + range.lineCount, 0);
468
+ let fidelity = `Compressed output; ${omitted} line(s) omitted.`;
469
+ if (result.status === "insufficient-budget")
470
+ fidelity = `INSUFFICIENT BUDGET: ${result.fidelity.unpreservedSignals.length} detected signal line(s) are available only through retained drill-down.`;
471
+ else if (result.complete)
472
+ fidelity = "Complete output; nothing omitted.";
473
+ const artifact = result.artifact.retained
474
+ ? ` Retained artifact: ${result.artifact.id} (${result.artifact.path}).`
475
+ : " Raw retention disabled; omitted regions cannot be retrieved.";
476
+ const content = result.content ? `${result.content}\n` : "";
477
+ return (`${content}---\n${fidelity} ` +
478
+ `${result.output.lines}/${result.input.lines} lines, ${result.output.bytes}/${result.input.bytes} bytes; ` +
479
+ `exit=${result.exit.code ?? "unknown"}, signal=${result.exit.signal ?? "none"}.${artifact}\n`);
480
+ }
481
+ function formatCompressionReadHuman(result) {
482
+ const content = result.content ? `${result.content}\n` : "";
483
+ const redactionStatus = result.raw
484
+ ? "UNREDACTED raw view"
485
+ : `${result.secretRedactions} secret(s) redacted`;
486
+ return (`${content}---\nArtifact ${result.artifactId}, lines ${result.range.startLine}-${result.range.endLine}` +
487
+ ` of ${result.range.totalLines}; ${result.bytes}/${result.byteBudget} bytes; ` +
488
+ `${redactionStatus}.\n`);
489
+ }
490
+ function formatFailureRelations(label, relations) {
491
+ if (relations.length === 0)
492
+ return `${label}: none`;
493
+ const formatted = relations.map(({ symbol, file, line }) => {
494
+ const location = file ? ` (${file}:${line ?? "?"})` : "";
495
+ return `${symbol}${location}`;
496
+ });
497
+ return `${label}: ${formatted.join(", ")}`;
498
+ }
499
+ function formatFailureDiagnosisHuman(result) {
500
+ const lines = [
501
+ `Failure diagnosis: ${result.status}; ${result.diagnostics.length} resolved, ${result.unresolved.length} unresolved.`,
502
+ `Freshness: ${result.freshness.state}; indexed=${result.freshness.indexedHead ?? "unknown"}; current=${result.freshness.currentHead ?? "unknown"}.`,
503
+ ];
504
+ for (const diagnostic of result.diagnostics) {
505
+ const location = `${diagnostic.file}:${diagnostic.reference.line ?? "?"}`;
506
+ const identity = diagnostic.owner?.identity ? ` [${diagnostic.owner.identity}]` : "";
507
+ const owner = `${diagnostic.owner?.symbol ?? "no owning symbol"}${identity}`;
508
+ const packageDescription = diagnostic.package
509
+ ? `${diagnostic.package.name ?? "unnamed"} (${diagnostic.package.kind}, ${diagnostic.package.manifest})`
510
+ : "none";
511
+ lines.push("", `${location} -> ${owner}`, `Package: ${packageDescription}`, formatFailureRelations("Tests", diagnostic.tests), formatFailureRelations("Upstream", diagnostic.upstream), formatFailureRelations("Downstream", diagnostic.downstream));
512
+ const snippet = result.contextBundle.snippets.find(({ file }) => file === diagnostic.file);
513
+ if (snippet)
514
+ lines.push(snippet.content);
515
+ }
516
+ for (const unresolved of result.unresolved) {
517
+ const candidates = unresolved.candidates ? ` (${unresolved.candidates.join(", ")})` : "";
518
+ lines.push("", `Unresolved ${unresolved.reference.path}: ${unresolved.reason}${candidates}`);
519
+ }
520
+ lines.push("", ...result.limitations.map((limitation) => `Limitation: ${limitation}`));
521
+ return `${lines.join("\n")}\n`;
522
+ }
523
+ async function readBoundedStdin(maxBytes) {
524
+ const chunks = [];
525
+ let bytes = 0;
526
+ for await (const chunk of process.stdin) {
527
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
528
+ bytes += buffer.byteLength;
529
+ if (bytes > maxBytes)
530
+ throw new Error(`knodin compress: input exceeded bounded retention limit of ${maxBytes} bytes`);
531
+ chunks.push(buffer);
532
+ }
533
+ return Buffer.concat(chunks).toString("utf-8");
534
+ }
535
+ const CLEAR_TERMINAL_LINE = "\r\x1b[2K";
536
+ const PROGRESS_WORKER_CLOSE_TIMEOUT_MS = 2_000;
537
+ /** Resolve true when `closed` settles first, false when the deadline wins. */
538
+ function raceWorkerClose(closed, timeoutMs) {
539
+ return new Promise((resolve) => {
540
+ let settled = false;
541
+ const finish = (closedCleanly) => {
542
+ if (settled)
543
+ return;
544
+ settled = true;
545
+ clearTimeout(timer);
546
+ resolve(closedCleanly);
547
+ };
548
+ const timer = setTimeout(() => finish(false), timeoutMs);
549
+ timer.unref();
550
+ void closed.then(() => finish(true));
551
+ });
552
+ }
553
+ function createProgressWorkerRenderer(workerName, startMessage) {
554
+ const extension = fileURLToPath(import.meta.url).endsWith(".ts") ? ".ts" : ".js";
555
+ const workerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), `../src/${workerName}${extension}`);
556
+ const workerArgs = extension === ".ts" && path.basename(process.execPath).startsWith("node")
557
+ ? [...process.execArgv, workerPath]
558
+ : [workerPath];
559
+ const worker = spawn(process.execPath, workerArgs, {
560
+ stdio: ["pipe", "ignore", "inherit"],
561
+ env: { ...process.env, KNODIN_FORCE_PROGRESS_TTY: "1" },
562
+ });
563
+ let failed = false;
564
+ const closed = new Promise((resolve) => {
565
+ worker.once("close", () => resolve());
566
+ worker.once("error", () => resolve());
567
+ });
568
+ worker.once("error", () => {
569
+ failed = true;
570
+ });
571
+ worker.stdin.once("error", () => {
572
+ failed = true;
573
+ });
574
+ const send = (message) => {
575
+ if (failed || worker.stdin.destroyed)
576
+ return;
577
+ try {
578
+ worker.stdin.write(`${JSON.stringify(message)}\n`);
579
+ }
580
+ catch {
581
+ failed = true;
582
+ }
583
+ };
584
+ return {
585
+ start: () => send(startMessage),
586
+ onProgress: (event) => send({ type: "progress", event }),
587
+ stop: async () => {
588
+ send({ type: "stop" });
589
+ worker.stdin.end();
590
+ const waitForClose = (timeoutMs) => raceWorkerClose(closed, timeoutMs);
591
+ if (!(await waitForClose(PROGRESS_WORKER_CLOSE_TIMEOUT_MS))) {
592
+ // Rendering is observational. A wedged terminal renderer must never
593
+ // hold graph work or its completion summary hostage.
594
+ worker.kill("SIGKILL");
595
+ if (!(await waitForClose(500)))
596
+ worker.unref();
597
+ }
598
+ process.stderr.write(CLEAR_TERMINAL_LINE);
599
+ },
600
+ };
601
+ }
602
+ function createInitRenderer(operation = "init") {
603
+ if (process.stderr.isTTY !== true) {
604
+ const renderer = createInitProgressRenderer({
605
+ stderr: process.stderr,
606
+ operation,
607
+ });
608
+ return {
609
+ start: () => renderer.start(),
610
+ onProgress: (event) => renderer.onProgress(event),
611
+ stop: async () => renderer.stop(),
612
+ };
613
+ }
614
+ return createProgressWorkerRenderer("init-progress-worker", {
615
+ type: "start",
616
+ operation,
617
+ });
618
+ }
619
+ async function main() {
620
+ const argv = process.argv.slice(2);
621
+ if (argv[0] === "__repository-init-worker") {
622
+ if (argv.length !== 2 || !path.isAbsolute(argv[1]))
623
+ throw new Error("knodin internal repository init worker requires one absolute path");
624
+ const repository = await fs.promises.realpath(argv[1]);
625
+ const interval = setInterval(() => {
626
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
627
+ }, 50);
628
+ interval.unref();
629
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
630
+ const engine = createEngine();
631
+ try {
632
+ const systemConfig = loadSystemConfiguration(repository);
633
+ const summary = await initializeRepositories([repository], {
634
+ // A portfolio worker owns exactly one already-discovered worktree.
635
+ // Do not recursively initialize nested repositories inside the same
636
+ // process, which would evade the per-repository memory boundary.
637
+ depth: 0,
638
+ include: [repository],
639
+ command: resolveCliRuntimeCommand(process),
640
+ index: (target) => engine.index(target),
641
+ status: (target) => engine.status(target),
642
+ agents: detectSupportedAgents(),
643
+ indexMode: (target) => indexModeForPath(systemConfig, target),
644
+ });
645
+ const result = summary.results.find(({ repository: target }) => target === repository);
646
+ if (!result)
647
+ throw new Error("repository init worker produced no repository result");
648
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
649
+ process.send?.({ type: "result", result });
650
+ if (result.status === "failed")
651
+ process.exitCode = 1;
652
+ }
653
+ finally {
654
+ clearInterval(interval);
655
+ await engine.close();
656
+ }
657
+ return;
658
+ }
659
+ // Keep a normalized argv for compatibility dispatch while Commander remains
660
+ // the authoritative grammar, option parser, validator, and help source.
661
+ const { rest: cleaned } = extractRepoFlag(argv);
662
+ const [cmd, ...rawRest] = cleaned;
663
+ if (!cmd || cmd === "-h" || cmd === "--help") {
664
+ process.stdout.write(renderCliHelp([], process.stdout.isTTY ? process.stdout.columns : undefined));
665
+ return;
666
+ }
667
+ if (cmd === "-v" || cmd === "--version" || cmd === "version") {
668
+ process.stdout.write(`${KNODIN_VERSION}\n`);
669
+ return;
670
+ }
671
+ if (argv.some((argument) => argument === "-h" || argument === "--help")) {
672
+ process.stdout.write(renderCliHelp(helpCommandPath(argv), process.stdout.isTTY ? process.stdout.columns : undefined));
673
+ return;
674
+ }
675
+ const invocation = parseCliInvocation(argv);
676
+ const optionKey = (flag) => flag.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
677
+ const selectorValue = (flag) => {
678
+ const value = invocation.options[optionKey(flag)];
679
+ return typeof value === "string" || typeof value === "number" ? String(value) : undefined;
680
+ };
681
+ const repoFlag = selectorValue("--repo");
682
+ const runtimeCommand = resolveCliRuntimeCommand(process);
683
+ // hook-refresh is an internal machine-to-machine command. Keep it JSON even
684
+ // when an older installed hook predates the explicit --json argument.
685
+ const jsonOutput = invocation.options.json === true || cmd === "hook-refresh";
686
+ // `--json` is a shared output flag. Repair owns its richer --json/--jsonl
687
+ // parser; all other commands receive their original arguments minus it.
688
+ const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
689
+ const selector = {
690
+ identity: selectorValue("--identity"),
691
+ file: selectorValue("--file"),
692
+ kind: selectorValue("--kind"),
693
+ toIdentity: selectorValue("--to-identity"),
694
+ toFile: selectorValue("--to-file"),
695
+ toKind: selectorValue("--to-kind"),
696
+ };
697
+ const responseBudget = {
698
+ bytes: selectorValue("--bytes") ? Number(selectorValue("--bytes")) : undefined,
699
+ tokens: selectorValue("--tokens") ? Number(selectorValue("--tokens")) : undefined,
700
+ items: selectorValue("--items") ? Number(selectorValue("--items")) : undefined,
701
+ };
702
+ for (const [flag, value, minimum] of [
703
+ ["--bytes", responseBudget.bytes, 256],
704
+ ["--tokens", responseBudget.tokens, 64],
705
+ ["--items", responseBudget.items, 1],
706
+ ]) {
707
+ if (value !== undefined && (!Number.isInteger(value) || value < minimum))
708
+ throw new Error(`knodin: ${flag} must be an integer >= ${minimum}`);
709
+ }
710
+ if (cmd === "init") {
711
+ explicitScope(rawRest);
712
+ const unsupported = rawRest.filter((argument, index) => {
713
+ if (argument === "--json" || argument.startsWith("--scope="))
714
+ return false;
715
+ if (argument === "--scope" || rawRest[index - 1] === "--scope")
716
+ return false;
717
+ return true;
718
+ });
719
+ if (unsupported.length > 0)
720
+ throw new Error(`knodin init: unknown option: ${unsupported[0]}`);
721
+ }
722
+ if (cmd === "configure") {
723
+ const status = rawRest.includes("--status");
724
+ const scope = explicitScope(rawRest);
725
+ const unsupported = rawRest.filter((argument, index) => {
726
+ if (argument === "--json" || argument === "--status" || argument.startsWith("--scope="))
727
+ return false;
728
+ if (argument === "--scope" || rawRest[index - 1] === "--scope")
729
+ return false;
730
+ return true;
731
+ });
732
+ if (unsupported.length > 0)
733
+ throw new Error(`knodin configure: unknown option: ${unsupported[0]}`);
734
+ if (status && scope)
735
+ throw new Error("knodin configure: --status and --scope are mutually exclusive");
736
+ if (!status && !scope)
737
+ throw new Error("knodin configure requires --scope personal|team|cli-only or --status");
738
+ }
739
+ if (cmd === "serve") {
740
+ const { startServer } = await import("../src/server.js");
741
+ await startServer();
742
+ return;
743
+ }
744
+ if (cmd === "fleet") {
745
+ if (repoFlag !== undefined) {
746
+ throw new Error("knodin fleet init accepts discovery roots, not --repo");
747
+ }
748
+ const plan = parseFleetInitArgs(rawRest, process.cwd());
749
+ if (!plan.json) {
750
+ process.stderr.write("warning: `knodin fleet init` is deprecated; use `knodin repos init --linked-worktrees=skip|include` (alias retained for two minor releases)\n");
751
+ }
752
+ const engine = createEngine();
753
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
754
+ const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
755
+ const summary = await initializeRepositories(plan.roots, {
756
+ command: runtimeCommand,
757
+ depth: plan.depth,
758
+ dryRun: plan.dryRun,
759
+ worktrees: plan.worktrees,
760
+ index: (target) => engine.index(target),
761
+ status: (target) => engine.status(target),
762
+ agents: detectSupportedAgents(),
763
+ indexMode: (target) => indexModeForPath(systemConfig, target),
764
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({
765
+ repository: target,
766
+ command: runtimeCommand,
767
+ memoryLimitBytes,
768
+ }),
769
+ });
770
+ await engine.close();
771
+ process.stdout.write(plan.json ? `${JSON.stringify(summary)}\n` : formatRepositoryHuman(summary));
772
+ process.exitCode = summary.exitCode;
773
+ return;
774
+ }
775
+ if (cmd === "remote") {
776
+ await runRemoteCommand(rawRest);
777
+ return;
778
+ }
779
+ if (cmd === "repos") {
780
+ if (repoFlag !== undefined) {
781
+ throw new Error("knodin repos accepts discovery roots, not --repo");
782
+ }
783
+ const plan = parseRepositoryCommandArgs(rawRest, process.cwd());
784
+ if (plan.command === "discover") {
785
+ const discovery = await discoverRepositories(plan.roots, {
786
+ depth: plan.depth,
787
+ linkedWorktrees: plan.linkedWorktrees,
788
+ });
789
+ const engine = createEngine();
790
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
791
+ const repositories = [];
792
+ const discoveryRecords = plan.signals
793
+ ? discovery.repositories.slice(0, repositorySignalInspectionLimit(plan))
794
+ : discovery.repositories;
795
+ for (const record of discoveryRecords) {
796
+ const inventory = await inventoryRepository(record, {
797
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
798
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
799
+ });
800
+ repositories.push(withRepositorySignals(inventory, plan.signals, plan.signals ? await detectRepositorySignals(record.path) : {}));
801
+ }
802
+ await engine.close();
803
+ const output = {
804
+ schemaVersion: 1,
805
+ command: "discover",
806
+ linkedWorktrees: plan.linkedWorktrees,
807
+ ...discovery,
808
+ repositories,
809
+ };
810
+ const boundedOutput = plan.signals
811
+ ? applyResponseBudget(output, "repositories:discover", { bytes: plan.byteBudget, tokens: plan.tokenBudget, items: plan.itemBudget }, { bytes: 65_536, tokens: 16_384, items: 100 })
812
+ : output;
813
+ process.stdout.write(`${JSON.stringify(boundedOutput, null, plan.json ? 0 : 2)}\n`);
814
+ process.exitCode = discovery.issues.length > 0 ? 1 : 0;
815
+ return;
816
+ }
817
+ if (plan.command === "init") {
818
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
819
+ const memoryLimitBytes = plan.memoryLimitBytes ?? configuredRepositoryInitMemoryLimitBytes(systemConfig);
820
+ if (plan.dryRun) {
821
+ const summary = await initializeRepositories(plan.roots, {
822
+ command: runtimeCommand,
823
+ depth: plan.depth,
824
+ dryRun: true,
825
+ worktrees: plan.linkedWorktrees,
826
+ manifestPath: plan.manifestPath,
827
+ include: plan.include,
828
+ exclude: plan.exclude,
829
+ index: async () => undefined,
830
+ status: async () => {
831
+ throw new Error("dry-run must not inspect graph databases");
832
+ },
833
+ agents: detectSupportedAgents(),
834
+ indexMode: (target) => indexModeForPath(systemConfig, target),
835
+ });
836
+ const planned = summary.results.reduce((counts, result) => {
837
+ const key = result.plannedStatus ?? result.status;
838
+ counts[key] = (counts[key] ?? 0) + 1;
839
+ return counts;
840
+ }, {});
841
+ const output = {
842
+ schemaVersion: 1,
843
+ command: "init",
844
+ dryRun: true,
845
+ graphDatabasesOpened: 0,
846
+ modelsLoaded: 0,
847
+ selectedRepositories: summary.results.filter(({ message }) => message.startsWith("dry-run:")).length,
848
+ planned,
849
+ ...summary,
850
+ repositories: [],
851
+ };
852
+ process.stdout.write(plan.json ? `${JSON.stringify(output)}\n` : formatRepositoryHuman(summary));
853
+ process.exitCode = summary.exitCode;
854
+ return;
855
+ }
856
+ const engine = createEngine();
857
+ const summary = await initializeRepositories(plan.roots, {
858
+ command: runtimeCommand,
859
+ depth: plan.depth,
860
+ dryRun: false,
861
+ worktrees: plan.linkedWorktrees,
862
+ manifestPath: plan.manifestPath,
863
+ include: plan.include,
864
+ exclude: plan.exclude,
865
+ index: (target) => engine.index(target),
866
+ status: (target) => engine.status(target),
867
+ agents: detectSupportedAgents(),
868
+ indexMode: (target) => indexModeForPath(systemConfig, target),
869
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({
870
+ repository: target,
871
+ command: runtimeCommand,
872
+ memoryLimitBytes,
873
+ }),
874
+ });
875
+ const discovery = await discoverRepositories(plan.roots, {
876
+ depth: plan.depth,
877
+ linkedWorktrees: plan.linkedWorktrees,
878
+ });
879
+ const repositories = [];
880
+ const selectedPaths = new Set(summary.results.map(({ repository }) => repository));
881
+ for (const record of discovery.repositories.filter(({ path }) => selectedPaths.has(path))) {
882
+ repositories.push(await inventoryRepository(record, {
883
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
884
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
885
+ }));
886
+ }
887
+ await engine.close();
888
+ const output = { schemaVersion: 1, command: "init", ...summary, repositories };
889
+ process.stdout.write(plan.json ? `${JSON.stringify(output)}\n` : formatRepositoryHuman(summary));
890
+ process.exitCode = summary.exitCode;
891
+ return;
892
+ }
893
+ if (plan.command === "search") {
894
+ const engine = createEngine();
895
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
896
+ const output = await searchRepositories(plan.roots, plan.query ?? "", {
897
+ depth: plan.depth,
898
+ linkedWorktrees: plan.linkedWorktrees,
899
+ include: plan.include,
900
+ exclude: plan.exclude,
901
+ allowPartial: plan.allowPartial,
902
+ itemBudget: plan.itemBudget,
903
+ byteBudget: plan.byteBudget,
904
+ tokenBudget: plan.tokenBudget,
905
+ cursor: plan.cursor,
906
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
907
+ search: (query, target, limit, offset) => engine.search(query, target, limit, {
908
+ offset,
909
+ includeSource: true,
910
+ federate: false,
911
+ }),
912
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
913
+ });
914
+ await engine.close();
915
+ process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
916
+ process.exitCode = output.status === "unavailable" ? 1 : 0;
917
+ return;
918
+ }
919
+ const discovery = await discoverRepositories(plan.roots, {
920
+ depth: plan.depth,
921
+ linkedWorktrees: plan.linkedWorktrees,
922
+ });
923
+ const engine = createEngine();
924
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
925
+ const repositories = [];
926
+ for (const repository of discovery.repositories) {
927
+ try {
928
+ if (plan.command === "doctor") {
929
+ const graph = attachLifecycleHealth(repository.path, await engine.status(repository.path, { audit: "deep" }));
930
+ const inventory = await inventoryRepository(repository, {
931
+ status: async () => graph,
932
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
933
+ });
934
+ repositories.push({
935
+ ...inventory,
936
+ diagnosis: await diagnoseInstallation(repository.path, {
937
+ currentVersion: KNODIN_VERSION,
938
+ runtimeCommand,
939
+ graph,
940
+ }),
941
+ });
942
+ }
943
+ else {
944
+ repositories.push(await inventoryRepository(repository, {
945
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
946
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
947
+ }));
948
+ }
949
+ }
950
+ catch (error) {
951
+ repositories.push({
952
+ path: repository.path,
953
+ status: "unknown",
954
+ error: error instanceof Error ? error.message : String(error),
955
+ });
956
+ }
957
+ }
958
+ await engine.close();
959
+ const output = {
960
+ schemaVersion: 1,
961
+ command: plan.command,
962
+ linkedWorktrees: plan.linkedWorktrees,
963
+ repositories,
964
+ issues: discovery.issues,
965
+ emptyRoots: discovery.emptyRoots,
966
+ staleLinkedWorktrees: discovery.staleLinkedWorktrees,
967
+ };
968
+ process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
969
+ process.exitCode =
970
+ discovery.issues.length > 0 ||
971
+ repositories.some((entry) => ("health" in entry && entry.health !== "healthy") ||
972
+ ("status" in entry && entry.status === "unknown"))
973
+ ? 1
974
+ : 0;
975
+ return;
976
+ }
977
+ if (cmd === "docs") {
978
+ if (repoFlag !== undefined)
979
+ throw new Error("knodin docs does not accept --repo");
980
+ const [topic, ...unsupported] = rawRest;
981
+ if (unsupported.length > 0)
982
+ throw new Error(`knodin docs: unknown argument ${unsupported[0]}`);
983
+ if (!topic || topic === "list") {
984
+ process.stdout.write(`${listDocTopics().join("\n")}\n`);
985
+ return;
986
+ }
987
+ const content = getDocSection(topic);
988
+ if (!content)
989
+ throw new Error(`knodin docs: unknown topic ${topic}`);
990
+ process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
991
+ return;
992
+ }
993
+ if (cmd === "update") {
994
+ if (repoFlag !== undefined)
995
+ throw new Error("knodin update does not accept --repo");
996
+ const [action, ...unsupported] = rest;
997
+ if (!action || !["status", "check", "explain", "apply", "rollback"].includes(action)) {
998
+ throw new Error("knodin update requires status, check, explain, apply, or rollback");
999
+ }
1000
+ if (unsupported.length > 0)
1001
+ throw new Error(`knodin update ${action}: unknown argument ${unsupported[0]}`);
1002
+ const method = detectUpdateInstallMethod(runtimeCommand);
1003
+ const runManager = async (argv) => {
1004
+ const [executable, ...arguments_] = argv;
1005
+ if (!executable)
1006
+ return { status: 1, stdout: "", stderr: "missing manager command" };
1007
+ const outcome = spawnSync(executable, arguments_, {
1008
+ encoding: "utf-8",
1009
+ timeout: 120_000,
1010
+ maxBuffer: 4 * 1_024 * 1_024,
1011
+ stdio: ["ignore", "pipe", "pipe"],
1012
+ });
1013
+ return {
1014
+ status: outcome.status,
1015
+ stdout: outcome.stdout ?? "",
1016
+ stderr: outcome.stderr ?? outcome.error?.message ?? "",
1017
+ };
1018
+ };
1019
+ const healthCheck = async () => {
1020
+ const [executable, ...arguments_] = runtimeCommand;
1021
+ if (!executable)
1022
+ return false;
1023
+ const outcome = spawnSync(executable, [...arguments_, "--version"], {
1024
+ encoding: "utf-8",
1025
+ timeout: 10_000,
1026
+ stdio: ["ignore", "pipe", "pipe"],
1027
+ });
1028
+ return outcome.status === 0 && /^\d+\.\d+\.\d+/.test(outcome.stdout.trim());
1029
+ };
1030
+ const options = {
1031
+ currentVersion: KNODIN_VERSION,
1032
+ installMethod: method,
1033
+ env: process.env,
1034
+ runManager,
1035
+ healthCheck,
1036
+ };
1037
+ let output;
1038
+ if (action === "status")
1039
+ output = trustedUpdateStatus(options);
1040
+ else if (action === "check") {
1041
+ try {
1042
+ output = await checkTrustedUpdate(options);
1043
+ }
1044
+ finally {
1045
+ if (process.env.KNODIN_UPDATE_BACKGROUND === "1")
1046
+ releaseScheduledUpdateCheck();
1047
+ }
1048
+ }
1049
+ else if (action === "explain")
1050
+ output = explainTrustedUpdate(options);
1051
+ else if (action === "apply")
1052
+ output = await applyTrustedUpdate(options);
1053
+ else
1054
+ output = await rollbackTrustedUpdate(options);
1055
+ process.stdout.write(jsonOutput ? `${JSON.stringify(output)}\n` : formatGenericHuman("update", output));
1056
+ return;
1057
+ }
1058
+ if (process.stdout.isTTY &&
1059
+ claimScheduledUpdateCheck({
1060
+ currentVersion: KNODIN_VERSION,
1061
+ installMethod: detectUpdateInstallMethod(runtimeCommand),
1062
+ env: process.env,
1063
+ })) {
1064
+ const [executable, ...arguments_] = runtimeCommand;
1065
+ if (executable) {
1066
+ const background = spawn(executable, [...arguments_, "update", "check", "--json"], {
1067
+ detached: true,
1068
+ stdio: "ignore",
1069
+ env: { ...process.env, KNODIN_UPDATE_BACKGROUND: "1" },
1070
+ });
1071
+ background.unref();
1072
+ }
1073
+ }
1074
+ const resolved = resolveRepo(repoFlag, process.cwd());
1075
+ if (!resolved.ok) {
1076
+ process.stderr.write(`${resolved.error}\n`);
1077
+ process.exit(1);
1078
+ }
1079
+ const repo = resolved.repo;
1080
+ if (cmd === "doctor") {
1081
+ const gitProbe = spawnSync(gitExecutable(), ["rev-parse", "--is-inside-work-tree"], {
1082
+ cwd: repo,
1083
+ encoding: "utf-8",
1084
+ stdio: ["ignore", "pipe", "ignore"],
1085
+ });
1086
+ if (gitProbe.status !== 0 || gitProbe.stdout.trim() !== "true") {
1087
+ const portfolio = {
1088
+ schemaVersion: 1,
1089
+ status: "portfolio-root",
1090
+ path: repo,
1091
+ graph: "not-inspected",
1092
+ warning: "Target is not a Git worktree; treating a portfolio parent as one repository would be misleading.",
1093
+ remediation: [`knodin repos doctor ${JSON.stringify(repo)}`],
1094
+ };
1095
+ process.stdout.write(jsonOutput ? `${JSON.stringify(portfolio)}\n` : formatGenericHuman("doctor", portfolio));
1096
+ process.exitCode = 1;
1097
+ return;
1098
+ }
1099
+ }
1100
+ if (cmd === "compress") {
1101
+ const valueFlags = new Set([
1102
+ "--strategy",
1103
+ "--adapter",
1104
+ "--lines",
1105
+ "--max-output-bytes",
1106
+ "--context",
1107
+ "--exit-code",
1108
+ "--signal",
1109
+ "--max-input-bytes",
1110
+ "--start",
1111
+ "--end",
1112
+ "--limit",
1113
+ "--offset",
1114
+ ]);
1115
+ const positionals = rest.filter((argument, index) => !argument.startsWith("--") && !valueFlags.has(rest[index - 1]));
1116
+ const integer = (flag, fallback, minimum, maximum) => {
1117
+ const raw = selectorValue(flag);
1118
+ const value = raw === undefined ? fallback : Number(raw);
1119
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
1120
+ throw new Error(`knodin compress: ${flag} must be an integer from ${minimum} to ${maximum}`);
1121
+ return value;
1122
+ };
1123
+ const action = positionals[0] === "read" || positionals[0] === "diagnose" || positionals[0] === "delete"
1124
+ ? positionals[0]
1125
+ : "create";
1126
+ const artifactId = action === "create" ? undefined : positionals[1];
1127
+ if (action !== "create" && !artifactId)
1128
+ throw new Error(`knodin compress ${action} requires an artifact id`);
1129
+ let compressionResult;
1130
+ let diagnosisUnavailable = false;
1131
+ if (action === "diagnose") {
1132
+ const diagnosisEngine = createEngine();
1133
+ try {
1134
+ const health = await inspectGraphQueryHealth(repo, (target) => diagnosisEngine.status(target, { audit: "cached" }));
1135
+ if (health.available) {
1136
+ compressionResult = await diagnoseFailure(diagnosisEngine, repo, {
1137
+ artifactId: artifactId ?? "",
1138
+ maxDiagnostics: integer("--limit", 10, 1, 50),
1139
+ diagnosticOffset: integer("--offset", 0, 0, 1_000_000),
1140
+ contextLines: integer("--context", 2, 0, 10),
1141
+ contextByteBudget: integer("--max-output-bytes", 16_384, 256, 128 * 1024),
1142
+ });
1143
+ }
1144
+ else {
1145
+ compressionResult = health;
1146
+ diagnosisUnavailable = true;
1147
+ process.exitCode = 1;
1148
+ }
1149
+ }
1150
+ finally {
1151
+ await diagnosisEngine.close();
1152
+ }
1153
+ }
1154
+ else if (action === "read") {
1155
+ compressionResult = readOutputArtifact(repo, artifactId ?? "", {
1156
+ startLine: integer("--start", 1, 1, Number.MAX_SAFE_INTEGER),
1157
+ endLine: selectorValue("--end")
1158
+ ? integer("--end", 200, 1, Number.MAX_SAFE_INTEGER)
1159
+ : undefined,
1160
+ byteBudget: integer("--max-output-bytes", 16_384, 256, 4 * 1024 * 1024),
1161
+ raw: rest.includes("--raw"),
1162
+ });
1163
+ }
1164
+ else if (action === "delete") {
1165
+ compressionResult = deleteOutputArtifact(repo, artifactId ?? "");
1166
+ }
1167
+ else {
1168
+ const strategy = selectorValue("--strategy") ?? "smart";
1169
+ if (!["smart", "head-tail", "errors-only"].includes(strategy))
1170
+ throw new Error("knodin compress: invalid --strategy");
1171
+ const adapter = selectorValue("--adapter") ?? "auto";
1172
+ if (![
1173
+ "auto",
1174
+ "generic",
1175
+ "vitest",
1176
+ "jest",
1177
+ "pytest",
1178
+ "go-test",
1179
+ "maven",
1180
+ "gradle",
1181
+ "dotnet",
1182
+ "cargo",
1183
+ ].includes(adapter))
1184
+ throw new Error("knodin compress: invalid --adapter");
1185
+ const maxInputBytes = integer("--max-input-bytes", 16 * 1024 * 1024, 1, 64 * 1024 * 1024);
1186
+ const request = {
1187
+ exitCode: selectorValue("--exit-code") ? integer("--exit-code", 0, 0, 255) : undefined,
1188
+ signal: selectorValue("--signal"),
1189
+ strategy: strategy,
1190
+ adapter: adapter,
1191
+ lineBudget: integer("--lines", 200, 1, 10_000),
1192
+ byteBudget: integer("--max-output-bytes", 16_384, 256, 4 * 1024 * 1024),
1193
+ contextLines: integer("--context", 1, 0, 10),
1194
+ maxInputBytes,
1195
+ retain: !rest.includes("--no-retain"),
1196
+ redactSecrets: !rest.includes("--no-redact"),
1197
+ };
1198
+ const input = positionals[0] === "create" ? (positionals[1] ?? "-") : (positionals[0] ?? "-");
1199
+ compressionResult =
1200
+ input === "-"
1201
+ ? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
1202
+ : compressOutputFile(repo, input, request);
1203
+ }
1204
+ if (action === "diagnose" &&
1205
+ (responseBudget.bytes !== undefined ||
1206
+ responseBudget.tokens !== undefined ||
1207
+ responseBudget.items !== undefined)) {
1208
+ compressionResult = applyResponseBudget(compressionResult, "compress:diagnose", responseBudget, { bytes: 65_536, tokens: 16_384, items: 50 });
1209
+ }
1210
+ if (jsonOutput)
1211
+ process.stdout.write(`${JSON.stringify(compressionResult)}\n`);
1212
+ else if (action === "diagnose" && !diagnosisUnavailable)
1213
+ process.stdout.write(formatFailureDiagnosisHuman(compressionResult));
1214
+ else if (action === "diagnose")
1215
+ process.stdout.write(formatGenericHuman("compress diagnose", compressionResult));
1216
+ else if (action === "read")
1217
+ process.stdout.write(formatCompressionReadHuman(compressionResult));
1218
+ else if (action === "create")
1219
+ process.stdout.write(formatCompressionHuman(compressionResult));
1220
+ else
1221
+ process.stdout.write(formatGenericHuman("compress delete", compressionResult));
1222
+ return;
1223
+ }
1224
+ const engine = createEngine();
1225
+ let result;
1226
+ let repairOutput;
1227
+ let repairWasPlan = false;
1228
+ let repairExitCode = 0;
1229
+ let statusWasWatched = false;
1230
+ const graphRead = async (run) => {
1231
+ const health = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1232
+ if (!health.available) {
1233
+ process.exitCode = 1;
1234
+ return health;
1235
+ }
1236
+ const value = await run();
1237
+ const verified = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1238
+ if (!verified.available) {
1239
+ process.exitCode = 1;
1240
+ return verified;
1241
+ }
1242
+ return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
1243
+ };
1244
+ switch (cmd) {
1245
+ case "doctor": {
1246
+ const client = selectorValue("--client");
1247
+ if (client !== undefined &&
1248
+ !["claude", "codex", "gemini", "copilot", "antigravity"].includes(client))
1249
+ throw new Error("knodin doctor: --client must be claude, codex, gemini, copilot, or antigravity");
1250
+ const unsupported = rest.filter((argument, index) => argument !== "--client" && rest[index - 1] !== "--client");
1251
+ if (unsupported.length > 0)
1252
+ throw new Error(`knodin doctor: unknown option ${unsupported[0]}`);
1253
+ const diagnosis = await diagnoseInstallation(repo, {
1254
+ currentVersion: KNODIN_VERSION,
1255
+ runtimeCommand: [...runtimeCommand, "serve"],
1256
+ graph: await engine.status(repo, { audit: "deep" }),
1257
+ client,
1258
+ });
1259
+ const manager = diagnosis.manager.name;
1260
+ diagnosis.update = await checkTrustedUpdate({
1261
+ currentVersion: KNODIN_VERSION,
1262
+ installMethod: ["npm", "mise", "volta", "nvm", "fnm", "asdf", "homebrew"].includes(manager ?? "")
1263
+ ? manager
1264
+ : "unknown",
1265
+ env: process.env,
1266
+ });
1267
+ result = diagnosis;
1268
+ break;
1269
+ }
1270
+ case "system": {
1271
+ const allowPartial = rest.includes("--allow-partial");
1272
+ const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial");
1273
+ if (!action || !["list", "show", "validate", "query"].includes(action)) {
1274
+ throw new Error("knodin system requires list, show, validate, or query");
1275
+ }
1276
+ if (allowPartial && action !== "query")
1277
+ throw new Error(`knodin system ${action}: --allow-partial applies only to query`);
1278
+ if (unsupported.length > 0)
1279
+ throw new Error(`knodin system ${action}: unknown argument ${unsupported[0]}`);
1280
+ const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
1281
+ if (action === "list") {
1282
+ if (systemId)
1283
+ throw new Error("knodin system list accepts no system id");
1284
+ result = {
1285
+ schemaVersion: config.schemaVersion,
1286
+ systems: config.systems.map(({ id, components }) => ({
1287
+ id,
1288
+ componentCount: components.length,
1289
+ })),
1290
+ };
1291
+ break;
1292
+ }
1293
+ if (!systemId)
1294
+ throw new Error(`knodin system ${action} requires <system-id>`);
1295
+ const system = config.systems.find(({ id }) => id === systemId);
1296
+ if (!system) {
1297
+ result = {
1298
+ status: "not-found",
1299
+ systemId,
1300
+ available: config.systems.map(({ id }) => id),
1301
+ };
1302
+ process.exitCode = 1;
1303
+ break;
1304
+ }
1305
+ if (action === "show") {
1306
+ result = { status: "ok", system, repositories: config.repositories };
1307
+ }
1308
+ else if (action === "validate") {
1309
+ const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
1310
+ result = { systemId, ...validation };
1311
+ if (!validation.valid)
1312
+ process.exitCode = 1;
1313
+ }
1314
+ else {
1315
+ const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
1316
+ result = queryConfiguredSystem(config, systemId, allowPartial, validation);
1317
+ if (result.status === "unavailable")
1318
+ process.exitCode = 1;
1319
+ }
1320
+ break;
1321
+ }
1322
+ case "hook-refresh": {
1323
+ const [kind, first, second] = rest;
1324
+ let event;
1325
+ if (kind === "commit")
1326
+ event = { kind };
1327
+ else if (kind === "checkout" && first && second) {
1328
+ event = { kind, before: first, after: second };
1329
+ }
1330
+ else if (kind === "merge" && first && second) {
1331
+ event = { kind, before: first, after: second };
1332
+ }
1333
+ else if (kind === "rewrite" && first) {
1334
+ event = { kind, inputPath: first };
1335
+ }
1336
+ else {
1337
+ throw new Error("knodin hook-refresh: invalid lifecycle event");
1338
+ }
1339
+ const indexed = await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1340
+ result = { indexed };
1341
+ break;
1342
+ }
1343
+ case "init": {
1344
+ const scope = await chooseInitScope(rawRest, repo);
1345
+ const agents = scope === "team" ? [] : integrationAgents(repo);
1346
+ const renderer = createInitRenderer();
1347
+ const activity = createIndexActivityReporter(repo);
1348
+ activity.start();
1349
+ renderer.start();
1350
+ let paths;
1351
+ try {
1352
+ try {
1353
+ paths = await initializeRepository(repo, {
1354
+ command: runtimeCommand,
1355
+ index: (target, options) => engine.index(target, undefined, false, options),
1356
+ scope,
1357
+ agents,
1358
+ onProgress: (event) => {
1359
+ activity.update(event);
1360
+ renderer.onProgress(event);
1361
+ },
1362
+ });
1363
+ }
1364
+ finally {
1365
+ await renderer.stop();
1366
+ activity.stop();
1367
+ }
1368
+ }
1369
+ catch (error) {
1370
+ if (!(error instanceof InitializationHealthError))
1371
+ throw error;
1372
+ process.stderr.write(`${error.message}\n`);
1373
+ await engine.close();
1374
+ process.exitCode = 1;
1375
+ return;
1376
+ }
1377
+ result = {
1378
+ status: "success",
1379
+ message: "knodin initialized successfully. Git lifecycle hooks configured.",
1380
+ paths,
1381
+ };
1382
+ break;
1383
+ }
1384
+ case "configure": {
1385
+ if (rawRest.includes("--status")) {
1386
+ result = inspectRepositoryIntegrationStatus(repo) ?? {
1387
+ scope: "unconfigured",
1388
+ agents: [],
1389
+ warning: "AI agents are not configured by knodin. Run `knodin configure --scope personal`.",
1390
+ };
1391
+ break;
1392
+ }
1393
+ const scope = explicitScope(rawRest);
1394
+ if (!scope)
1395
+ throw new Error("knodin configure: missing --scope");
1396
+ if (!fs.existsSync(resolveDbPath(repo))) {
1397
+ throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
1398
+ }
1399
+ const agents = scope === "team" ? [] : integrationAgents(repo);
1400
+ const paths = await initializeRepository(repo, {
1401
+ command: runtimeCommand,
1402
+ index: async () => undefined,
1403
+ scope,
1404
+ agents,
1405
+ allowTrackedTransition: true,
1406
+ auditConfigurationChanges: true,
1407
+ });
1408
+ result = {
1409
+ status: "success",
1410
+ message: `knodin agent integration changed to ${scope}.`,
1411
+ graphInitialization: "unchanged",
1412
+ nextAction: "run `knodin status` and reload the configured client",
1413
+ paths,
1414
+ };
1415
+ break;
1416
+ }
1417
+ case "index": {
1418
+ // Target the repo unambiguously and never report success for a no-op:
1419
+ // a lone directory positional means "index this repo", file positionals
1420
+ // must resolve inside the repo, and a run touching zero files fails loud.
1421
+ const positionals = extractPositionals(rest);
1422
+ const plan = planIndex(repoFlag, positionals, process.cwd());
1423
+ if (!plan.ok) {
1424
+ process.stderr.write(`${plan.error}\n`);
1425
+ await engine.close();
1426
+ process.exit(1);
1427
+ }
1428
+ const clean = rest.includes("--clean") || rest.includes("--force");
1429
+ const scipPath = selectorValue("--scip");
1430
+ const sarifPath = selectorValue("--sarif");
1431
+ // A bound the operator cannot move is just a failure, so every import
1432
+ // ceiling is overridable. Reject junk here rather than letting NaN
1433
+ // silently disable a limit downstream.
1434
+ const importLimit = (flag) => {
1435
+ const raw = selectorValue(flag);
1436
+ if (raw === undefined)
1437
+ return undefined;
1438
+ const parsed = Number(raw);
1439
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1440
+ process.stderr.write(`knodin index: ${flag} must be a positive integer\n`);
1441
+ process.exit(1);
1442
+ }
1443
+ return parsed;
1444
+ };
1445
+ const sarifMaxBytes = importLimit("--sarif-max-bytes");
1446
+ const sarifMaxFindings = importLimit("--sarif-max-findings");
1447
+ const sarifTimeoutMs = importLimit("--sarif-timeout-ms");
1448
+ const scipMaxBytes = importLimit("--scip-max-bytes");
1449
+ const scipMaxFiles = importLimit("--scip-max-files");
1450
+ const scipMaxFacts = importLimit("--scip-max-facts");
1451
+ const scipTimeoutMs = importLimit("--scip-timeout-ms");
1452
+ const renderer = createInitRenderer("index");
1453
+ const activity = createIndexActivityReporter(plan.repo);
1454
+ activity.start();
1455
+ renderer.start();
1456
+ let indexResult;
1457
+ try {
1458
+ indexResult = await engine.index(plan.repo, plan.files, clean, {
1459
+ scip: scipPath
1460
+ ? {
1461
+ path: scipPath,
1462
+ // Only override a default when the operator actually asked.
1463
+ ...(scipMaxBytes !== undefined ? { maxBytes: scipMaxBytes } : {}),
1464
+ ...(scipMaxFiles !== undefined ? { maxFiles: scipMaxFiles } : {}),
1465
+ ...(scipMaxFacts !== undefined ? { maxFacts: scipMaxFacts } : {}),
1466
+ ...(scipTimeoutMs !== undefined ? { timeoutMs: scipTimeoutMs } : {}),
1467
+ }
1468
+ : undefined,
1469
+ sarif: sarifPath
1470
+ ? {
1471
+ path: sarifPath,
1472
+ // Only override a default when the operator actually asked.
1473
+ ...(sarifMaxBytes !== undefined ? { maxBytes: sarifMaxBytes } : {}),
1474
+ ...(sarifMaxFindings !== undefined ? { maxFindings: sarifMaxFindings } : {}),
1475
+ ...(sarifTimeoutMs !== undefined ? { timeoutMs: sarifTimeoutMs } : {}),
1476
+ }
1477
+ : undefined,
1478
+ onProgress: (event) => {
1479
+ activity.update(event);
1480
+ renderer.onProgress(event);
1481
+ },
1482
+ });
1483
+ }
1484
+ finally {
1485
+ await renderer.stop();
1486
+ activity.stop();
1487
+ }
1488
+ result = indexResult;
1489
+ const indexedCheck = checkIndexed([...indexResult.indexed, ...indexResult.unchanged], plan.repo);
1490
+ if (!indexedCheck.ok) {
1491
+ process.stderr.write(`${indexedCheck.error}\n`);
1492
+ await engine.close();
1493
+ process.exit(1);
1494
+ }
1495
+ if (indexResult.verification.status !== "healthy") {
1496
+ process.stderr.write(formatIndexVerificationError(indexResult));
1497
+ await engine.close();
1498
+ process.exit(1);
1499
+ }
1500
+ break;
1501
+ }
1502
+ case "status": {
1503
+ const snapshot = async () => ({
1504
+ ...attachLifecycleHealth(repo, await engine.status(repo, {
1505
+ audit: rest.includes("--deep") ? "deep" : "cached",
1506
+ })),
1507
+ integration: inspectRepositoryIntegrationStatus(repo),
1508
+ update: trustedUpdateStatus({
1509
+ currentVersion: KNODIN_VERSION,
1510
+ installMethod: detectUpdateInstallMethod(runtimeCommand),
1511
+ env: process.env,
1512
+ }),
1513
+ });
1514
+ if (rest.includes("--watch")) {
1515
+ const intervalRaw = selectorValue("--interval");
1516
+ const intervalSeconds = intervalRaw === undefined ? 1 : Number(intervalRaw);
1517
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0.1 || intervalSeconds > 60)
1518
+ throw new Error("knodin status: --interval must be between 0.1 and 60 seconds");
1519
+ let watching = true;
1520
+ const stop = () => {
1521
+ watching = false;
1522
+ };
1523
+ process.once("SIGINT", stop);
1524
+ process.once("SIGTERM", stop);
1525
+ while (watching) {
1526
+ const observed = await snapshot();
1527
+ process.stdout.write(`${JSON.stringify({ observedAt: new Date().toISOString(), ...observed })}\n`);
1528
+ await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1_000));
1529
+ }
1530
+ process.removeListener("SIGINT", stop);
1531
+ process.removeListener("SIGTERM", stop);
1532
+ statusWasWatched = true;
1533
+ result = null;
1534
+ }
1535
+ else
1536
+ result = await snapshot();
1537
+ break;
1538
+ }
1539
+ case "wait": {
1540
+ if (!rest.includes("--fresh"))
1541
+ throw new Error("knodin wait requires --fresh");
1542
+ const timeoutRaw = selectorValue("--timeout");
1543
+ const timeoutSeconds = timeoutRaw === undefined ? 30 : Number(timeoutRaw);
1544
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0 || timeoutSeconds > 300)
1545
+ throw new Error("knodin wait: --timeout must be between 0 and 300 seconds");
1546
+ result = await waitForFresh(engine, repo, Math.round(timeoutSeconds * 1_000));
1547
+ if (result.status !== "fresh")
1548
+ process.exitCode = 1;
1549
+ break;
1550
+ }
1551
+ case "repair": {
1552
+ const options = parseRepairCliArgs(rest);
1553
+ if (options.plan) {
1554
+ result = createRepairPlan(await engine.status(repo, { audit: "deep" }));
1555
+ repairOutput = options.output;
1556
+ repairWasPlan = true;
1557
+ break;
1558
+ }
1559
+ const progressMode = resolveRepairProgressMode(options, process.env, process.stderr.isTTY);
1560
+ const renderer = progressMode === "tty"
1561
+ ? createProgressWorkerRenderer("repair-progress-worker", { type: "start" })
1562
+ : (() => {
1563
+ const direct = createRepairProgressRenderer({
1564
+ mode: progressMode,
1565
+ intervalMs: options.progressIntervalMs,
1566
+ stdout: process.stdout,
1567
+ stderr: process.stderr,
1568
+ });
1569
+ return {
1570
+ start: () => direct.start(),
1571
+ onProgress: (event) => direct.onProgress(event),
1572
+ stop: async () => direct.stop(),
1573
+ };
1574
+ })();
1575
+ const controller = new AbortController();
1576
+ const abortRepair = () => controller.abort();
1577
+ process.once("SIGINT", abortRepair);
1578
+ renderer.start();
1579
+ try {
1580
+ result = attachRepairLifecycle(repo, await engine.repair(repo, {
1581
+ signal: controller.signal,
1582
+ onProgress: (event) => renderer.onProgress(event),
1583
+ }));
1584
+ }
1585
+ finally {
1586
+ process.removeListener("SIGINT", abortRepair);
1587
+ await renderer.stop();
1588
+ }
1589
+ repairOutput = options.output;
1590
+ if (result.cancelled)
1591
+ repairExitCode = 130;
1592
+ break;
1593
+ }
1594
+ case "refresh-artifacts": {
1595
+ const event = rest[0] ?? "code-change";
1596
+ if (event !== "checkout" && event !== "merge" && event !== "code-change") {
1597
+ process.stderr.write("knodin refresh-artifacts accepts checkout, merge, or code-change\n");
1598
+ process.exit(1);
1599
+ }
1600
+ const refreshResult = refreshExternalGraphArtifacts(repo, event);
1601
+ writeArtifactRefreshRecord(repo, refreshResult);
1602
+ result = refreshResult;
1603
+ break;
1604
+ }
1605
+ case "explain": {
1606
+ const symbol = rest[0];
1607
+ if (!symbol) {
1608
+ process.stderr.write("knodin explain requires a <symbol>\n");
1609
+ process.exit(1);
1610
+ }
1611
+ result = await graphRead(async () => {
1612
+ const explained = await engine.explain(symbol, repo, rest[1] === "minimal" ? "minimal" : "standard", selector);
1613
+ return rest[1] === "source" && !explained.ambiguity
1614
+ ? {
1615
+ mode: "source",
1616
+ identity: explained.identity,
1617
+ symbol: explained.symbol,
1618
+ source: explained.source,
1619
+ staleness: explained.staleness,
1620
+ }
1621
+ : explained;
1622
+ });
1623
+ break;
1624
+ }
1625
+ case "review": {
1626
+ const plan = parseReviewArgs(rest);
1627
+ result = await graphRead(() => engine.review(plan.base, repo, plan.detailLevel, plan.options));
1628
+ break;
1629
+ }
1630
+ case "map":
1631
+ result = await graphRead(() => engine.map(repo, rest.includes("--standard") ? "standard" : "minimal", {
1632
+ topN: selectorValue("--top") ? Number(selectorValue("--top")) : undefined,
1633
+ sort: selectorValue("--sort"),
1634
+ relationKinds: selectorValue("--relations")?.split(",").filter(Boolean),
1635
+ }));
1636
+ break;
1637
+ case "wiki": {
1638
+ const health = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1639
+ if (!health.available) {
1640
+ result = health;
1641
+ process.exitCode = 1;
1642
+ break;
1643
+ }
1644
+ const force = rest.includes("--force");
1645
+ const summary = await engine.wiki(repo, force);
1646
+ await engine.close();
1647
+ process.stdout.write(`wrote ${summary.written.length} pages, skipped ${summary.skipped.length}\n`);
1648
+ process.exitCode = 0;
1649
+ return;
1650
+ }
1651
+ case "visualize": {
1652
+ const entry = rest.find((value, index) => !value.startsWith("--") && !rest[index - 1]?.startsWith("--"));
1653
+ const outputPath = selectorValue("--output");
1654
+ const scope = selectorValue("--scope");
1655
+ const granularity = selectorValue("--granularity");
1656
+ // Repo scope draws the whole graph, so it takes no entry selector.
1657
+ if (!entry && scope !== "repo")
1658
+ throw new Error("knodin visualize requires an <entry> selector");
1659
+ if (!outputPath)
1660
+ throw new Error("knodin visualize requires --output <path.html>");
1661
+ result = await graphRead(() => writeVisualization(engine, repo, {
1662
+ entry,
1663
+ outputPath,
1664
+ scope,
1665
+ granularity,
1666
+ depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
1667
+ byteBudget: selectorValue("--max-bytes")
1668
+ ? Number(selectorValue("--max-bytes"))
1669
+ : undefined,
1670
+ selector: {
1671
+ identity: selector.identity,
1672
+ file: selector.file,
1673
+ kind: selector.kind,
1674
+ },
1675
+ }));
1676
+ break;
1677
+ }
1678
+ case "search": {
1679
+ const query = rest[0];
1680
+ if (!query) {
1681
+ process.stderr.write("knodin search requires a <query>\n");
1682
+ process.exit(1);
1683
+ }
1684
+ const positionalLimit = rest[1] && !rest[1].startsWith("--") ? rest[1] : undefined;
1685
+ const limit = Number(selectorValue("--limit") ?? positionalLimit ?? 5);
1686
+ if (!Number.isInteger(limit) || limit < 1)
1687
+ throw new Error("knodin search: limit must be a positive integer");
1688
+ let testScope = "all";
1689
+ if (rest.includes("--tests-only"))
1690
+ testScope = "test";
1691
+ else if (rest.includes("--production-only"))
1692
+ testScope = "production";
1693
+ result = await graphRead(() => engine.search(query, repo, limit, {
1694
+ languages: selectorValue("--languages")?.split(",").filter(Boolean),
1695
+ extensions: selectorValue("--extensions")?.split(",").filter(Boolean),
1696
+ kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
1697
+ path: selectorValue("--path"),
1698
+ testScope,
1699
+ includeSource: !rest.includes("--no-source"),
1700
+ offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : 0,
1701
+ }));
1702
+ break;
1703
+ }
1704
+ case "pack": {
1705
+ const action = rest[0];
1706
+ const diffScope = selectorValue("--diff-scope");
1707
+ if (diffScope && !["unstaged", "staged", "all", "compare"].includes(diffScope))
1708
+ throw new Error("knodin pack: invalid --diff-scope");
1709
+ if (action === "read") {
1710
+ if (!rest[1])
1711
+ throw new Error("knodin pack read requires an artifact path");
1712
+ result = readPackedArtifact(repo, rest[1], Number(selectorValue("--start") ?? 1), Number(selectorValue("--end") ?? 200), Number(selectorValue("--bytes") ?? 16_384));
1713
+ }
1714
+ else if (action === "grep") {
1715
+ if (!rest[1] || !rest[2])
1716
+ throw new Error("knodin pack grep requires an artifact path and regex");
1717
+ result = grepPackedArtifact(repo, rest[1], rest[2], selectorValue("--flags") ?? "", Number(selectorValue("--limit") ?? 100));
1718
+ }
1719
+ else {
1720
+ const policies = {};
1721
+ for (const assignment of selectorValue("--policy")?.split(",") ?? []) {
1722
+ const [glob, policy] = assignment.split("=");
1723
+ if (!glob || !["full", "summary", "structure-only"].includes(policy))
1724
+ throw new Error("knodin pack: invalid --policy assignment");
1725
+ policies[glob] = policy;
1726
+ }
1727
+ result = exportContext(repo, {
1728
+ format: selectorValue("--format") ?? "markdown",
1729
+ include: selectorValue("--include")?.split(",").filter(Boolean),
1730
+ exclude: selectorValue("--exclude")?.split(",").filter(Boolean),
1731
+ policies,
1732
+ alreadyPresent: selectorValue("--already-present")?.split(",").filter(Boolean),
1733
+ chatFiles: selectorValue("--chat-files")?.split(",").filter(Boolean),
1734
+ lineNumbers: rest.includes("--line-numbers"),
1735
+ includeTree: rest.includes("--tree"),
1736
+ byteBudget: Number(selectorValue("--bytes") ?? 65_536),
1737
+ tokenBudget: Number(selectorValue("--tokens") ?? 16_384),
1738
+ outputPath: selectorValue("--output"),
1739
+ git: diffScope || selectorValue("--log")
1740
+ ? {
1741
+ diffScope: diffScope,
1742
+ from: selectorValue("--from"),
1743
+ to: selectorValue("--to"),
1744
+ log: selectorValue("--log") ? Number(selectorValue("--log")) : undefined,
1745
+ }
1746
+ : undefined,
1747
+ });
1748
+ }
1749
+ break;
1750
+ }
1751
+ case "evidence": {
1752
+ const level = rest[0];
1753
+ const file = rest[1];
1754
+ if (!file || !["locate", "outline", "evidence", "expand"].includes(level))
1755
+ throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");
1756
+ result = deliverProgressiveEvidence({
1757
+ repo,
1758
+ file,
1759
+ level,
1760
+ continuation: selectorValue("--continuation"),
1761
+ baselineHash: selectorValue("--baseline-hash"),
1762
+ baselineBytes: selectorValue("--baseline-bytes")
1763
+ ? Number(selectorValue("--baseline-bytes"))
1764
+ : undefined,
1765
+ startLine: selectorValue("--start") ? Number(selectorValue("--start")) : undefined,
1766
+ endLine: selectorValue("--end") ? Number(selectorValue("--end")) : undefined,
1767
+ byteLimit: responseBudget.bytes,
1768
+ tokenLimit: responseBudget.tokens,
1769
+ itemLimit: responseBudget.items,
1770
+ });
1771
+ break;
1772
+ }
1773
+ case "query": {
1774
+ const pattern = rest[0];
1775
+ const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
1776
+ // architecture_overview's only positional is the detail level (minimal|
1777
+ // standard), not a target — keep its target empty so it's not misreported.
1778
+ const target = pattern === "architecture_overview" ||
1779
+ pattern === "import_cycles" ||
1780
+ rest[1]?.startsWith("--")
1781
+ ? ""
1782
+ : (rest[1] ?? "");
1783
+ if (!pattern) {
1784
+ process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|batch_outline|project_overview|shortest_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
1785
+ process.exit(1);
1786
+ }
1787
+ const directionValue = selectorValue("--direction");
1788
+ if (directionValue && !["upstream", "downstream", "both"].includes(directionValue)) {
1789
+ process.stderr.write("knodin query: --direction must be upstream, downstream, or both\n");
1790
+ process.exit(1);
1791
+ }
1792
+ const facetsValue = selectorValue("--facets")?.split(",").filter(Boolean);
1793
+ if (facetsValue?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet))) {
1794
+ process.stderr.write("knodin query: --facets contains an unknown architecture facet\n");
1795
+ process.exit(1);
1796
+ }
1797
+ if (!target && !repoWide) {
1798
+ process.stderr.write(`knodin query ${pattern} requires a <target>\n`);
1799
+ process.exit(1);
1800
+ }
1801
+ // shortest_path: `knodin query shortest_path <from> <to>`
1802
+ // rename_preview: `knodin query rename_preview <old> <new>`
1803
+ const to = pattern === "shortest_path" || pattern === "rename_preview" ? rest[2] : undefined;
1804
+ if (pattern === "rename_preview" && !to) {
1805
+ process.stderr.write("knodin query rename_preview requires <old> and <new>\n");
1806
+ process.exit(1);
1807
+ }
1808
+ // traverse: `knodin query traverse <symbol> [--depth n | <n>]`. Depth is
1809
+ // clamped (1-6) in the engine, so a raw value passes straight through.
1810
+ let depth;
1811
+ if (pattern === "traverse" || pattern === "feature_path" || pattern === "impact") {
1812
+ const flagIdx = rest.indexOf("--depth");
1813
+ const raw = flagIdx >= 0 ? rest[flagIdx + 1] : rest[2];
1814
+ if (raw !== undefined && raw !== "" && Number.isFinite(Number(raw)))
1815
+ depth = Number(raw);
1816
+ }
1817
+ // architecture_overview: `knodin query architecture_overview [minimal|standard]`.
1818
+ // Reuses the same detail knob as explain/review; minimal is the default.
1819
+ let detailLevel;
1820
+ if (pattern === "architecture_overview" && rest.includes("standard")) {
1821
+ detailLevel = "standard";
1822
+ }
1823
+ const queryLimitRaw = selectorValue("--limit");
1824
+ if (queryLimitRaw !== undefined &&
1825
+ (!Number.isInteger(Number(queryLimitRaw)) || Number(queryLimitRaw) < 1)) {
1826
+ process.stderr.write("knodin query: --limit must be a positive integer\n");
1827
+ process.exit(1);
1828
+ }
1829
+ const queryHealth = pattern.startsWith("lsp_")
1830
+ ? null
1831
+ : await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1832
+ if (queryHealth && !queryHealth.available) {
1833
+ result = queryHealth;
1834
+ process.exitCode = 1;
1835
+ break;
1836
+ }
1837
+ result = await engine.query(pattern, target, repo, to, queryLimitRaw ? Number(queryLimitRaw) : undefined, depth, detailLevel, selector, pattern === "impact"
1838
+ ? {
1839
+ mode: selectorValue("--impact-mode") === "file" ? "file" : "symbol",
1840
+ direction: ["upstream", "downstream", "both"].includes(selectorValue("--direction") ?? "")
1841
+ ? selectorValue("--direction")
1842
+ : undefined,
1843
+ relationKinds: selectorValue("--relations")
1844
+ ?.split(",")
1845
+ .map((kind) => kind.trim())
1846
+ .filter(Boolean),
1847
+ minConfidence: selectorValue("--min-confidence")
1848
+ ? Number(selectorValue("--min-confidence"))
1849
+ : undefined,
1850
+ includeTests: !rest.includes("--exclude-tests"),
1851
+ includeDataFlow: rest.includes("--data-flow"),
1852
+ }
1853
+ : undefined, {
1854
+ minLines: selectorValue("--min-lines") ? Number(selectorValue("--min-lines")) : undefined,
1855
+ minComplexity: selectorValue("--min-complexity")
1856
+ ? Number(selectorValue("--min-complexity"))
1857
+ : undefined,
1858
+ kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
1859
+ path: selectorValue("--path"),
1860
+ direction: pattern === "traverse" &&
1861
+ ["upstream", "downstream", "both"].includes(selectorValue("--direction") ?? "")
1862
+ ? selectorValue("--direction")
1863
+ : undefined,
1864
+ includeDataFlow: pattern === "traverse" ? rest.includes("--data-flow") : undefined,
1865
+ flowVariable: pattern === "flow_analysis" ? selectorValue("--variable") : undefined,
1866
+ architectureFacets: facetsValue,
1867
+ topN: selectorValue("--top") ? Number(selectorValue("--top")) : undefined,
1868
+ sort: selectorValue("--sort"),
1869
+ relationKinds: pattern === "impact"
1870
+ ? undefined
1871
+ : selectorValue("--relations")?.split(",").filter(Boolean),
1872
+ detailLevel,
1873
+ });
1874
+ if (pattern === "impact" || pattern === "dead_code") {
1875
+ const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
1876
+ result = incorporateSystemQueryEvidence(config, repo, pattern, target, result);
1877
+ }
1878
+ if (queryHealth?.available) {
1879
+ const verifiedQueryHealth = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1880
+ if (!verifiedQueryHealth.available) {
1881
+ result = verifiedQueryHealth;
1882
+ process.exitCode = 1;
1883
+ break;
1884
+ }
1885
+ result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness);
1886
+ }
1887
+ break;
1888
+ }
1889
+ case "rename": {
1890
+ // knodin rename <old> <new> [--apply] [--no-verify]
1891
+ const positional = rest.filter((a) => !a.startsWith("--"));
1892
+ const oldName = positional[0];
1893
+ const newName = positional[1];
1894
+ const apply = rest.includes("--apply");
1895
+ // Post-apply typecheck is on by default; --no-verify opts out.
1896
+ const verify = !rest.includes("--no-verify");
1897
+ if (!oldName || !newName) {
1898
+ process.stderr.write("knodin rename requires <old> and <new>\n");
1899
+ process.exit(1);
1900
+ }
1901
+ result = await graphRead(() => engine.rename(oldName, newName, repo, apply, verify, selector));
1902
+ break;
1903
+ }
1904
+ case "prs": {
1905
+ try {
1906
+ const args = rest[0] === "audit" ? rest.slice(1) : rest;
1907
+ const flag = (name) => {
1908
+ const index = args.indexOf(name);
1909
+ return index >= 0 ? args[index + 1] : undefined;
1910
+ };
1911
+ const state = flag("--state");
1912
+ const limitValue = flag("--limit");
1913
+ const limit = limitValue === undefined ? 50 : Number(limitValue);
1914
+ if (!Number.isInteger(limit) || limit < 1)
1915
+ throw new Error("knodin prs: --limit must be a positive integer");
1916
+ result = await auditPullRequests(repo, engine, {
1917
+ state,
1918
+ limit,
1919
+ branches: flag("--branches"),
1920
+ range: flag("--range"),
1921
+ base: flag("--base"),
1922
+ head: flag("--head"),
1923
+ expectedLogin: flag("--expected-login"),
1924
+ });
1925
+ }
1926
+ catch (err) {
1927
+ await engine.close();
1928
+ process.stderr.write(`${err.message}\n`);
1929
+ process.exit(1);
1930
+ }
1931
+ break;
1932
+ }
1933
+ case "worktrees": {
1934
+ const action = rest[0] ?? "status";
1935
+ if (action === "status") {
1936
+ result = await inspectWorktrees(repo, (worktree) => engine.status(worktree, { audit: "cached" }));
1937
+ }
1938
+ else if (action === "reconcile") {
1939
+ result = reconcileWorktrees(repo);
1940
+ }
1941
+ else if (action === "remove") {
1942
+ const target = rest[1];
1943
+ if (!target)
1944
+ throw new Error("knodin worktrees remove requires <path>");
1945
+ result = removeManagedWorktree(repo, target, rest.includes("--dry-run"));
1946
+ }
1947
+ else {
1948
+ throw new Error(`knodin worktrees: unknown action ${action}`);
1949
+ }
1950
+ break;
1951
+ }
1952
+ case "telemetry": {
1953
+ const action = rest[0];
1954
+ const rawRetention = selectorValue("--retention-days");
1955
+ const retentionDays = rawRetention === undefined ? 30 : Number(rawRetention);
1956
+ if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 3650)
1957
+ throw new Error("knodin telemetry: --retention-days must be an integer from 1 to 3650");
1958
+ const input = selectorValue("--input");
1959
+ if (action === "status")
1960
+ result = telemetryStatus(repo, input, retentionDays);
1961
+ else if (action === "report")
1962
+ result = writeTelemetryReport(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
1963
+ else if (action === "export")
1964
+ result = exportTelemetry(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
1965
+ else if (action === "clear")
1966
+ result = clearTelemetry(repo, input);
1967
+ else
1968
+ throw new Error("knodin telemetry requires status, report, export, or clear");
1969
+ break;
1970
+ }
1971
+ case "diagnostics": {
1972
+ const [action, bundlePath] = invocation.positionals;
1973
+ const rawRetention = selectorValue("--retention-days");
1974
+ const sinceOption = selectorValue("--since");
1975
+ const outputOption = selectorValue("--output");
1976
+ const previewId = selectorValue("--preview-id");
1977
+ const retentionDays = rawRetention === undefined ? 14 : Number(rawRetention);
1978
+ if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365)
1979
+ throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");
1980
+ if (bundlePath && action !== "inspect")
1981
+ throw new Error(`knodin diagnostics ${action}: unexpected bundle argument`);
1982
+ if (rawRetention !== undefined && action !== "enable")
1983
+ throw new Error(`knodin diagnostics ${action}: --retention-days applies only to enable`);
1984
+ if (sinceOption !== undefined && !["preview", "archive", "collect"].includes(action ?? ""))
1985
+ throw new Error(`knodin diagnostics ${action}: --since applies only to preview or archive`);
1986
+ if (outputOption !== undefined && !["archive", "collect"].includes(action ?? ""))
1987
+ throw new Error(`knodin diagnostics ${action}: --output applies only to archive`);
1988
+ if (previewId !== undefined && !["archive", "collect"].includes(action ?? ""))
1989
+ throw new Error(`knodin diagnostics ${action}: --preview-id applies only to archive`);
1990
+ if (action === "enable")
1991
+ result = enableDiagnostics(repo, retentionDays);
1992
+ else if (action === "status")
1993
+ result = diagnosticsStatus(repo);
1994
+ else if (action === "disable")
1995
+ result = disableDiagnostics(repo);
1996
+ else if (action === "clear")
1997
+ result = clearDiagnostics(repo);
1998
+ else if (action === "inspect") {
1999
+ if (!bundlePath)
2000
+ throw new Error("knodin diagnostics inspect requires <bundle>");
2001
+ result = inspectDiagnosticsBundle(repo, bundlePath);
2002
+ }
2003
+ else if (["preview", "archive", "collect"].includes(action ?? "")) {
2004
+ const since = sinceOption ?? "24h";
2005
+ const match = /^(\d+)([hd])$/.exec(since);
2006
+ if (!match)
2007
+ throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");
2008
+ const sinceHours = Number(match[1]) * (match[2] === "d" ? 24 : 1);
2009
+ let graph;
2010
+ let doctor;
2011
+ try {
2012
+ graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
2013
+ }
2014
+ catch {
2015
+ // Support evidence remains available with an explicit unavailable graph section.
2016
+ }
2017
+ if (graph) {
2018
+ try {
2019
+ doctor = await diagnoseInstallation(repo, {
2020
+ currentVersion: KNODIN_VERSION,
2021
+ runtimeCommand: [...runtimeCommand, "serve"],
2022
+ graph,
2023
+ });
2024
+ }
2025
+ catch {
2026
+ // Installation diagnosis is represented as unavailable in the manifest.
2027
+ }
2028
+ }
2029
+ const options = {
2030
+ sinceHours,
2031
+ doctor,
2032
+ graph,
2033
+ knodinVersion: KNODIN_VERSION,
2034
+ };
2035
+ result =
2036
+ action === "preview"
2037
+ ? persistDiagnosticsPreview(repo, options)
2038
+ : collectDiagnostics(repo, { ...options, outputPath: outputOption, previewId });
2039
+ }
2040
+ else {
2041
+ throw new Error("knodin diagnostics requires enable, status, preview, archive, inspect, clear, or disable");
2042
+ }
2043
+ break;
2044
+ }
2045
+ case "context": {
2046
+ // knodin context "<task>" [base]
2047
+ const task = rest[0];
2048
+ if (!task) {
2049
+ process.stderr.write('knodin context requires a "<task>" description\n');
2050
+ process.exit(1);
2051
+ }
2052
+ result = await graphRead(() => buildKnodinContext(engine, task, repo, rest[1]));
2053
+ break;
2054
+ }
2055
+ default:
2056
+ process.stderr.write(`unknown command: ${cmd}\n\n${renderCliHelp([], process.stderr.isTTY ? process.stderr.columns : undefined)}`);
2057
+ process.exit(1);
2058
+ }
2059
+ await engine.close();
2060
+ if (statusWasWatched)
2061
+ return;
2062
+ const boundedResult = applyResponseBudget(result, cmd, responseBudget, {
2063
+ bytes: 65_536,
2064
+ tokens: 16_384,
2065
+ items: 100,
2066
+ });
2067
+ const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
2068
+ if (cmd === "init" && !jsonOutput) {
2069
+ process.stdout.write(formatInitHuman(boundedResult));
2070
+ process.exitCode = finalExitCode;
2071
+ return;
2072
+ }
2073
+ if (cmd === "configure" && !jsonOutput && !rawRest.includes("--status")) {
2074
+ process.stdout.write(formatConfigureHuman(boundedResult));
2075
+ return;
2076
+ }
2077
+ if (cmd === "configure" && !jsonOutput && rawRest.includes("--status")) {
2078
+ process.stdout.write(formatConfigureStatusHuman(boundedResult));
2079
+ return;
2080
+ }
2081
+ if (cmd === "repair" && repairOutput === "human" && !repairWasPlan) {
2082
+ process.stdout.write(formatRepairHuman(boundedResult));
2083
+ process.exitCode = finalExitCode;
2084
+ return;
2085
+ }
2086
+ if (cmd === "index" && !jsonOutput) {
2087
+ // Human counts must describe the actual operation. The response budget is
2088
+ // a JSON transport constraint and may truncate large `indexed` arrays.
2089
+ process.stdout.write(formatIndexHuman(result));
2090
+ process.exitCode = finalExitCode;
2091
+ return;
2092
+ }
2093
+ if (cmd === "status" && !jsonOutput) {
2094
+ process.stdout.write(formatStatusHuman(boundedResult));
2095
+ process.exitCode = finalExitCode;
2096
+ return;
2097
+ }
2098
+ if (!jsonOutput && repairOutput !== "jsonl") {
2099
+ process.stdout.write(formatGenericHuman(cmd, boundedResult));
2100
+ process.exitCode = finalExitCode;
2101
+ return;
2102
+ }
2103
+ // Budget accounting is over the exact compact serialization written here.
2104
+ process.stdout.write(repairOutput === "jsonl"
2105
+ ? serializeRepairJsonlRecord("result", boundedResult)
2106
+ : `${JSON.stringify(boundedResult)}\n`);
2107
+ process.exitCode = finalExitCode;
2108
+ }
2109
+ try {
2110
+ await main();
2111
+ }
2112
+ catch (err) {
2113
+ const argv = process.argv.slice(2);
2114
+ const repoIndex = argv.indexOf("--repo");
2115
+ const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
2116
+ const explicitRepo = repoIndex >= 0 ? argv[repoIndex + 1] : undefined;
2117
+ let candidate = process.cwd();
2118
+ if (explicitRepo)
2119
+ candidate = explicitRepo;
2120
+ else if (equalsRepo)
2121
+ candidate = equalsRepo.slice("--repo=".length);
2122
+ const command = argv.find((argument, index) => {
2123
+ if (argument.startsWith("-"))
2124
+ return false;
2125
+ return !(index > 0 && argv[index - 1] === "--repo") && argument !== candidate;
2126
+ });
2127
+ const knownCommands = new Set([
2128
+ "init",
2129
+ "configure",
2130
+ "index",
2131
+ "doctor",
2132
+ "status",
2133
+ "wait",
2134
+ "repair",
2135
+ "serve",
2136
+ "context",
2137
+ "explain",
2138
+ "review",
2139
+ "map",
2140
+ "search",
2141
+ "query",
2142
+ "rename",
2143
+ "wiki",
2144
+ "visualize",
2145
+ "pack",
2146
+ "compress",
2147
+ "prs",
2148
+ "worktrees",
2149
+ "telemetry",
2150
+ "diagnostics",
2151
+ "system",
2152
+ "repos",
2153
+ "remote",
2154
+ "update",
2155
+ ]);
2156
+ const diagnostic = recordDiagnosticFailure(candidate, {
2157
+ surface: "cli",
2158
+ operation: command && knownCommands.has(command) ? command : "unknown",
2159
+ phase: "dispatch",
2160
+ error: err,
2161
+ });
2162
+ const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
2163
+ console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
2164
+ process.exit(1);
2165
+ }