ki-manager 2.3.1.dev1__tar.gz → 2.3.2.dev2__tar.gz

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 (34) hide show
  1. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/CHANGELOG.md +23 -1
  2. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/PKG-INFO +1 -1
  3. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/pyproject.toml +1 -1
  4. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/__init__.py +1 -1
  5. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/cli.py +6 -3
  6. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/check_ki_drift.py +70 -14
  7. ki_manager-2.3.2.dev2/src/ki_manager/scripts/find_unmapped_files.py +143 -0
  8. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/ki_search.py +32 -5
  9. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/ki_utils.py +187 -23
  10. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/server.py +57 -9
  11. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/tools/facade.py +43 -1
  12. ki_manager-2.3.2.dev2/src/ki_manager/workflows/agent-rules.md +152 -0
  13. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/workflows/expand-knowledge.md +4 -2
  14. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/workflows/scaffold-knowledge.md +4 -0
  15. ki_manager-2.3.1.dev1/src/ki_manager/scripts/find_unmapped_files.py +0 -101
  16. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/.gitignore +0 -0
  17. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/LICENSE +0 -0
  18. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/README.md +0 -0
  19. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
  20. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/analyze_module.py +0 -0
  21. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/audit_coverage.py +0 -0
  22. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/finalize_ki_scaffolds.py +0 -0
  23. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/generate_dir_index.py +0 -0
  24. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/generate_ki_scaffolds.py +0 -0
  25. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/git_diff_secured.py +0 -0
  26. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
  27. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
  28. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/knowledge_engine.py +0 -0
  29. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/scripts/sync_agents_md.py +0 -0
  30. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/templates/KI_template.md +0 -0
  31. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/tools/__init__.py +0 -0
  32. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/tools/scaffold.py +0 -0
  33. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/workflows/create-adr.md +0 -0
  34. {ki_manager-2.3.1.dev1 → ki_manager-2.3.2.dev2}/src/ki_manager/workflows/sync-knowledge.md +0 -0
@@ -9,7 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
- ## [2.3.1.dev1] — 2026-10-16
12
+ ## [2.3.2.dev2] — 2026-11-01
13
+
14
+ ### Added
15
+ - **`ki_list` Navigation Tool**:
16
+ - Direct navigation tool to list registered Knowledge Items and Architecture Decision Records (ADRs) with summaries and covered modules without filesystem scans.
17
+ - Supports filtering by type (`scope: "all" | "ki" | "adr"`), output formats (`format: "markdown" | "json"`), and search query (`filter: "<text>"`).
18
+ - Available as a direct top-level tool in both full mode (35 tools) and compact facade mode (7 tools).
19
+ - **BM25 Search Pagination (`ki_search`)**:
20
+ - Added 1-based `page` parameter for intuitive result pagination alongside `offset`.
21
+ - Prominent pagination header and next-page action hints (`[Page X/Y]`, `Next page: ki_search(query="...", page=X+1)`) in search outputs to prevent AI models from getting stuck in repeat queries.
22
+
23
+ ## [2.3.2.dev1] — 2026-10-20
24
+
25
+ ### Fixed
26
+ - **`find_unmapped_files` Traversal & Scoping**:
27
+ - Automatically loads and enforces `.gitignore` patterns and `doc_config.json` `exclude` rules (in addition to `exclude_patterns`) during repository discovery.
28
+ - When scanning repository root by default, scopes file discovery to root segments of configured `tracked_modules`, avoiding timeouts and runaway directory walks across large external data trees.
29
+ - Made the CLI `path` argument optional, defaulting to root / tracked modules.
30
+ - **`check_ki_drift` Markdown Table Header Validation**:
31
+ - Validates Markdown table headers explicitly for file path columns (`File`, `Path`, `Source`, `Location`) before inspecting cells for broken paths.
32
+ - Tables without file columns (e.g. `| Tool | Access | Purpose |`) are cleanly bypassed, eliminating false-positive `Missing file: Access` errors.
33
+
34
+ ## [2.3.1] — 2026-10-18
13
35
 
14
36
  ### Security & Hardening
15
37
  - **Path Traversal Protection**: Enforced strict directory boundary containment across all file discovery and inspection tools (`find_unmapped_files`, `analyze_module`, `ki_search`, and custom agent paths) to prevent prefix-based sandbox traversal attacks.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ki-manager
3
- Version: 2.3.1.dev1
3
+ Version: 2.3.2.dev2
4
4
  Summary: Knowledge Item (KI) management MCP server for AI-assisted development
5
5
  Project-URL: Homepage, https://github.com/Laeryid/KI-base
6
6
  Project-URL: Repository, https://github.com/Laeryid/KI-base
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ki-manager"
7
- version = "2.3.1.dev1"
7
+ version = "2.3.2.dev2"
8
8
  description = "Knowledge Item (KI) management MCP server for AI-assisted development"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,3 +1,3 @@
1
1
 
2
2
  """ki-manager — Knowledge Item MCP server for AI-assisted development."""
3
- __version__ = "2.3.1.dev1"
3
+ __version__ = "2.3.2.dev2"
@@ -144,11 +144,14 @@ def main():
144
144
  if command == "install-skills":
145
145
  install_skills(sys.argv[2:])
146
146
  elif command in ("--version", "-v"):
147
- import importlib.metadata
148
147
  try:
149
- v = importlib.metadata.version("ki-manager")
148
+ from . import __version__ as v
150
149
  except Exception:
151
- v = "unknown"
150
+ import importlib.metadata
151
+ try:
152
+ v = importlib.metadata.version("ki-manager")
153
+ except Exception:
154
+ v = "unknown"
152
155
  print(f"ki-manager {v}")
153
156
  sys.exit(0)
154
157
  else:
@@ -76,31 +76,87 @@ def parse_ki_file(ki_path: Path) -> Dict[str, Any]:
76
76
 
77
77
  # Extract Key Components table symbols
78
78
  # Expected table format: | `SymbolName` | `rel/path/file.ext` | Purpose |
79
+ # Only tables that explicitly contain a File/Path column header are inspected.
79
80
  table_symbols: List[Dict[str, str]] = []
80
81
  in_key_components = False
81
- table_row_pattern = re.compile(r"\|\s*`?([A-Za-z0-9_]+)`?\s*\|\s*`?([^`|\s]+)`?\s*\|")
82
+ pending_header: Optional[List[str]] = None
83
+ current_table_active = False
84
+ sym_idx = -1
85
+ file_idx = -1
82
86
 
83
87
  for line in lines:
84
88
  if line.startswith("## Key Components"):
85
89
  in_key_components = True
90
+ pending_header = None
91
+ current_table_active = False
86
92
  continue
87
93
  elif in_key_components and line.startswith("## "):
88
94
  in_key_components = False
95
+ pending_header = None
96
+ current_table_active = False
89
97
  continue
90
98
 
91
- if in_key_components and line.strip().startswith("|"):
92
- lower = line.lower()
93
- if any(k in lower for k in ["class / function", "component", "|---", "|:---", "| — |", "| - |"]):
94
- continue
95
- m = table_row_pattern.search(line)
96
- if m:
97
- sym_name = m.group(1).strip()
98
- file_rel = m.group(2).strip()
99
- header_keywords = {"component", "file", "purpose", "class", "function", "description"}
100
- if sym_name.lower() in header_keywords or file_rel.lower() in header_keywords:
101
- continue
102
- if sym_name and file_rel and not file_rel.startswith("—"):
103
- table_symbols.append({"symbol": sym_name, "file": file_rel})
99
+ if not in_key_components:
100
+ continue
101
+
102
+ stripped = line.strip()
103
+ if not stripped.startswith("|"):
104
+ pending_header = None
105
+ current_table_active = False
106
+ continue
107
+
108
+ raw_cells = [c.strip() for c in stripped.strip("|").split("|")]
109
+ # Check if separator row (e.g. |---|---| or |:---|---:|)
110
+ is_separator = (
111
+ len(raw_cells) > 0
112
+ and all(re.match(r"^:?-{2,}:?$", c) for c in raw_cells if c)
113
+ )
114
+
115
+ if is_separator:
116
+ if pending_header:
117
+ file_idx = -1
118
+ for i, col in enumerate(pending_header):
119
+ col_lower = col.lower()
120
+ if any(w in col_lower for w in ["file", "path", "source", "location"]):
121
+ file_idx = i
122
+ break
123
+
124
+ sym_idx = -1
125
+ for i, col in enumerate(pending_header):
126
+ if i == file_idx:
127
+ continue
128
+ col_lower = col.lower()
129
+ if any(w in col_lower for w in ["class", "function", "symbol", "component", "name", "element", "method", "tool"]):
130
+ sym_idx = i
131
+ break
132
+
133
+ if sym_idx == -1 and file_idx != -1:
134
+ sym_idx = 0 if file_idx != 0 else 1
135
+
136
+ current_table_active = (file_idx != -1 and sym_idx != -1)
137
+ pending_header = None
138
+ continue
139
+
140
+ if pending_header is None and not current_table_active:
141
+ # Possible header row
142
+ pending_header = raw_cells
143
+ continue
144
+
145
+ if current_table_active:
146
+ if len(raw_cells) > max(sym_idx, file_idx):
147
+ sym_val = raw_cells[sym_idx].strip(" `").strip()
148
+ file_val = raw_cells[file_idx].strip(" `").strip()
149
+ header_keywords = {"component", "file", "path", "purpose", "class", "function", "description", "tool", "access"}
150
+ if (
151
+ sym_val
152
+ and file_val
153
+ and not sym_val.startswith("—")
154
+ and not file_val.startswith("—")
155
+ and not file_val.startswith("-")
156
+ and sym_val.lower() not in header_keywords
157
+ and file_val.lower() not in header_keywords
158
+ ):
159
+ table_symbols.append({"symbol": sym_val, "file": file_val})
104
160
 
105
161
  # Extract wiki-links [[KI_*.md]]
106
162
  wiki_links = []
@@ -0,0 +1,143 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ # Add path to ki_utils
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+ import ki_utils
11
+
12
+
13
+ def _scan_path(target_path: str, project_root: str, mapped_paths: set, exclude_patterns: set, ignored_exts: set) -> List[str]:
14
+ abs_target = os.path.abspath(os.path.join(project_root, target_path))
15
+ if not ki_utils.is_subpath(abs_target, project_root) or not os.path.exists(abs_target):
16
+ return []
17
+
18
+ unmapped = []
19
+ if os.path.isfile(abs_target):
20
+ rel_to_project = os.path.relpath(abs_target, project_root).replace("\\", "/")
21
+ fname = os.path.basename(abs_target)
22
+ if fname.startswith(".") or any(fname.endswith(ext) for ext in ignored_exts):
23
+ return []
24
+ if ki_utils.should_exclude(fname, rel_to_project, exclude_patterns):
25
+ return []
26
+ if abs_target in mapped_paths:
27
+ return []
28
+ for mapped in mapped_paths:
29
+ if os.path.isdir(mapped) and abs_target.startswith(os.path.join(mapped, "")):
30
+ return []
31
+ return [rel_to_project]
32
+
33
+ for root, dirs, files in os.walk(abs_target):
34
+ dirs[:] = [
35
+ d for d in dirs
36
+ if not d.startswith(".")
37
+ and not ki_utils.should_exclude(d, os.path.relpath(os.path.join(root, d), project_root), exclude_patterns)
38
+ ]
39
+
40
+ for f in files:
41
+ if f.startswith(".") or any(f.endswith(ext) for ext in ignored_exts):
42
+ continue
43
+
44
+ full_path = os.path.abspath(os.path.join(root, f))
45
+ rel_to_project = os.path.relpath(full_path, project_root).replace("\\", "/")
46
+ if ki_utils.should_exclude(f, rel_to_project, exclude_patterns):
47
+ continue
48
+
49
+ if full_path in mapped_paths:
50
+ continue
51
+
52
+ is_parent_mapped = False
53
+ for mapped in mapped_paths:
54
+ if os.path.isdir(mapped):
55
+ if full_path.startswith(os.path.join(mapped, "")):
56
+ is_parent_mapped = True
57
+ break
58
+
59
+ if not is_parent_mapped:
60
+ unmapped.append(rel_to_project)
61
+
62
+ return unmapped
63
+
64
+
65
+ def get_unmapped_files(target_path: str = ".") -> List[str]:
66
+ """Returns a sorted list of unmapped files relative to project root."""
67
+ project_root = ki_utils.get_project_root()
68
+ doc_config = ki_utils.get_doc_config()
69
+
70
+ if not project_root or not doc_config:
71
+ return []
72
+
73
+ # 1. Collect all files that are already mapped in doc_config.json
74
+ mapped_paths = set()
75
+ knowledge_items = doc_config.get("knowledge_items", {})
76
+
77
+ for ki in knowledge_items.values():
78
+ depends_on = ki.get("depends_on", [])
79
+ for p in depends_on:
80
+ abs_p = os.path.abspath(os.path.join(project_root, p))
81
+ mapped_paths.add(abs_p)
82
+
83
+ ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
84
+ exclude_patterns = ki_utils.get_exclude_patterns()
85
+
86
+ norm_target = (target_path or ".").strip()
87
+
88
+ # If target is root/empty, scope to tracked_modules if defined
89
+ if norm_target in (".", ""):
90
+ tracked_modules = doc_config.get("coverage_settings", {}).get("tracked_modules") or doc_config.get("tracked_modules", [])
91
+ target_dirs = set()
92
+ if isinstance(tracked_modules, list) and tracked_modules:
93
+ for m in tracked_modules:
94
+ mod_path = m[0] if isinstance(m, (list, tuple)) else m
95
+ if isinstance(mod_path, str) and mod_path.strip():
96
+ norm_p = mod_path.replace("\\", "/").strip("/")
97
+ top_segment = norm_p.split("/")[0] if "/" in norm_p else norm_p
98
+ if top_segment:
99
+ target_dirs.add(top_segment)
100
+
101
+ if target_dirs:
102
+ unmapped_set = set()
103
+ for td in sorted(target_dirs):
104
+ unmapped_set.update(_scan_path(td, project_root, mapped_paths, exclude_patterns, ignored_exts))
105
+ return sorted(unmapped_set)
106
+ else:
107
+ return sorted(_scan_path(".", project_root, mapped_paths, exclude_patterns, ignored_exts))
108
+
109
+ return sorted(_scan_path(norm_target, project_root, mapped_paths, exclude_patterns, ignored_exts))
110
+
111
+
112
+ def find_unmapped_files(target_path: str = "."):
113
+ project_root = ki_utils.get_project_root()
114
+ doc_config = ki_utils.get_doc_config()
115
+
116
+ if not project_root or not doc_config:
117
+ print("Error: Could not resolve project root or doc_config.json")
118
+ return
119
+
120
+ display_target = (target_path or ".").strip()
121
+ if display_target != ".":
122
+ abs_target = os.path.abspath(os.path.join(project_root, display_target))
123
+ if not os.path.exists(abs_target):
124
+ print(f"Error: Target path '{display_target}' does not exist.")
125
+ return
126
+
127
+ unmapped = get_unmapped_files(display_target)
128
+ if unmapped:
129
+ print(f"### Unmapped Files in '{display_target}':")
130
+ for f in unmapped:
131
+ print(f"- {f}")
132
+ else:
133
+ print(f"All files in '{display_target}' are already mapped in doc_config.json.")
134
+
135
+
136
+ if __name__ == "__main__":
137
+ if sys.platform == "win32":
138
+ sys.stdout.reconfigure(encoding="utf-8")
139
+ parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
140
+ parser.add_argument("path", nargs="?", default=".", help="Relative path from project root to scan (default: tracked_modules or .)")
141
+ args = parser.parse_args()
142
+
143
+ find_unmapped_files(args.path)
@@ -198,6 +198,7 @@ def search_knowledge(
198
198
  scope: str = "all",
199
199
  limit: int = 5,
200
200
  offset: int = 0,
201
+ page: Optional[int] = None,
201
202
  k1: float = 1.5,
202
203
  b: float = 0.75,
203
204
  ) -> Dict[str, Any]:
@@ -209,6 +210,7 @@ def search_knowledge(
209
210
  :param scope: "all", "ki", or "adr".
210
211
  :param limit: Max number of results to return (default: 5).
211
212
  :param offset: Offset index for pagination (default: 0).
213
+ :param page: 1-based page number for pagination (calculates offset if provided).
212
214
  :param k1: BM25 k1 parameter.
213
215
  :param b: BM25 b parameter.
214
216
  """
@@ -217,6 +219,14 @@ def search_knowledge(
217
219
  if not project_root:
218
220
  return {"error": "No project root found. Run ki_init_project or specify workspace.", "results": []}
219
221
 
222
+ if page is not None:
223
+ try:
224
+ p = int(page)
225
+ if p > 0:
226
+ offset = (p - 1) * limit
227
+ except (ValueError, TypeError):
228
+ pass
229
+
220
230
  knowledge_root = ki_utils.get_knowledge_root()
221
231
  if not knowledge_root or not os.path.exists(knowledge_root) or not ki_utils.is_subpath(knowledge_root, project_root):
222
232
  for cand in [
@@ -231,7 +241,7 @@ def search_knowledge(
231
241
 
232
242
  q_tokens = tokenize(query)
233
243
  if not q_tokens:
234
- return {"query": query, "total": 0, "results": []}
244
+ return {"query": query, "total": 0, "offset": offset, "limit": limit, "page": 1, "total_pages": 1, "results": []}
235
245
 
236
246
  docs: List[Dict[str, Any]] = []
237
247
 
@@ -261,7 +271,7 @@ def search_knowledge(
261
271
 
262
272
  total_docs = len(docs)
263
273
  if total_docs == 0:
264
- return {"query": query, "total": 0, "results": []}
274
+ return {"query": query, "total": 0, "offset": offset, "limit": limit, "page": 1, "total_pages": 1, "results": []}
265
275
 
266
276
  avg_dl = sum(d["weighted_len"] for d in docs) / total_docs
267
277
 
@@ -298,14 +308,19 @@ def search_knowledge(
298
308
  })
299
309
 
300
310
  scored_results.sort(key=lambda x: x["score"], reverse=True)
311
+ total_count = len(scored_results)
301
312
  safe_offset = max(0, offset)
302
313
  final_results = scored_results[safe_offset : safe_offset + limit]
314
+ current_page = (safe_offset // limit) + 1 if limit > 0 else 1
315
+ total_pages = max(1, math.ceil(total_count / limit)) if limit > 0 else 1
303
316
 
304
317
  return {
305
318
  "query": query,
306
- "total": len(scored_results),
319
+ "total": total_count,
307
320
  "offset": safe_offset,
308
321
  "limit": limit,
322
+ "page": current_page,
323
+ "total_pages": total_pages,
309
324
  "results": final_results,
310
325
  }
311
326
 
@@ -317,13 +332,24 @@ def format_search_markdown(search_dict: Dict[str, Any]) -> str:
317
332
  results = search_dict.get("results", [])
318
333
  offset = search_dict.get("offset", 0)
319
334
  limit = search_dict.get("limit", 5)
335
+ page = search_dict.get("page")
336
+ total_pages = search_dict.get("total_pages")
320
337
 
321
338
  if total == 0 or not results:
322
339
  return f'No documentation matching "{query}" found.'
323
340
 
341
+ if page is None and limit > 0:
342
+ page = (offset // limit) + 1
343
+ if total_pages is None and limit > 0:
344
+ total_pages = max(1, math.ceil(total / limit))
345
+
324
346
  start_idx = offset + 1
325
347
  end_idx = offset + len(results)
326
- out = [f'Found {total} result(s) for "{query}" (showing {start_idx}-{end_idx}):\n']
348
+ out = [f'Found {total} result(s) for "{query}" [Page {page}/{total_pages} (showing {start_idx}-{end_idx})]:\n']
349
+
350
+ if end_idx < total:
351
+ next_page = (page or 1) + 1
352
+ out.append(f'> 💡 **Next page**: `ki_search(query="{query}", page={next_page})` (or `offset={end_idx}`)\n')
327
353
 
328
354
  for i, r in enumerate(results, start=start_idx):
329
355
  doc_type = r.get("type", "ki").upper()
@@ -337,6 +363,7 @@ def format_search_markdown(search_dict: Dict[str, Any]) -> str:
337
363
  out.append(f" > {snippet}")
338
364
 
339
365
  if end_idx < total:
340
- out.append(f"\n*Showing {end_idx} of {total} results. Call ki_search(query='{query}', offset={end_idx}) to view more.*")
366
+ next_page = (page or 1) + 1
367
+ out.append(f"\n*Showing {end_idx} of {total} results. Call `ki_search(query='{query}', page={next_page})` (or `offset={end_idx}`) to view more.*")
341
368
 
342
369
  return "\n".join(out)
@@ -18,6 +18,7 @@ import tempfile
18
18
  import argparse
19
19
  import fnmatch
20
20
  from pathlib import Path
21
+ from typing import Optional, Dict, Any, List, Union
21
22
 
22
23
  # Global state — set by MCP server on initialize
23
24
  ACTIVE_WORKSPACE_PATH = None
@@ -304,15 +305,20 @@ def get_project_root() -> str:
304
305
 
305
306
  def get_doc_config_path() -> str:
306
307
  root = get_knowledge_root()
307
- return os.path.join(root, "doc_config.json") if root else ""
308
+ cand = os.path.join(root, "doc_config.json") if root else ""
309
+ if cand and os.path.exists(cand):
310
+ return cand
311
+ proj = get_project_root()
312
+ if proj:
313
+ for p in [os.path.join(proj, ".ki-base", "doc_config.json"), os.path.join(proj, "doc_config.json")]:
314
+ if os.path.exists(p):
315
+ return p
316
+ return cand
308
317
 
309
318
 
310
319
  def get_doc_config() -> dict:
311
- root = get_knowledge_root()
312
- if not root:
313
- return {}
314
- path = os.path.join(root, "doc_config.json")
315
- if os.path.exists(path):
320
+ path = get_doc_config_path()
321
+ if path and os.path.exists(path):
316
322
  try:
317
323
  with open(path, "r", encoding="utf-8") as f:
318
324
  return json.load(f)
@@ -335,6 +341,34 @@ def get_local_config() -> dict:
335
341
  return {}
336
342
 
337
343
 
344
+ def load_gitignore_patterns(project_root: str = None) -> set:
345
+ """Parses .gitignore file in project_root if present, returning a set of pattern strings."""
346
+ if not project_root:
347
+ project_root = get_project_root()
348
+ if not project_root or not os.path.isdir(project_root):
349
+ return set()
350
+
351
+ gitignore_path = os.path.join(project_root, ".gitignore")
352
+ if not os.path.isfile(gitignore_path):
353
+ return set()
354
+
355
+ patterns = set()
356
+ try:
357
+ with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
358
+ for line in f:
359
+ line = line.strip()
360
+ if not line or line.startswith("#") or line.startswith("!"):
361
+ continue
362
+ norm = line.replace("\\", "/").rstrip("/")
363
+ if norm.startswith("/"):
364
+ norm = norm.lstrip("/")
365
+ if norm:
366
+ patterns.add(norm)
367
+ except Exception:
368
+ pass
369
+ return patterns
370
+
371
+
338
372
  DEFAULT_EXCLUDED_DIRS = {
339
373
  ".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build",
340
374
  ".mypy_cache", ".pytest_cache", ".ruff_cache",
@@ -344,33 +378,45 @@ DEFAULT_EXCLUDED_DIRS = {
344
378
  def get_exclude_patterns() -> set:
345
379
  """
346
380
  Returns the set of directory/file exclude patterns.
347
- Combines default excluded dirs with exclude_patterns from:
381
+ Combines default excluded dirs with exclude_patterns and exclude from:
348
382
  1. config.json (machine-specific)
349
- 2. doc_config.json (coverage_settings.exclude_patterns or top-level exclude_patterns)
383
+ 2. doc_config.json (coverage_settings.exclude_patterns, coverage_settings.exclude,
384
+ top-level exclude_patterns, top-level exclude)
350
385
  3. ki_config.json
386
+ 4. .gitignore in project root
351
387
  """
352
388
  patterns = set(DEFAULT_EXCLUDED_DIRS)
353
389
 
390
+ def _add_from_cfg(cfg_dict, key):
391
+ val = cfg_dict.get(key, [])
392
+ if isinstance(val, list):
393
+ for p in val:
394
+ if p:
395
+ patterns.add(str(p))
396
+ elif isinstance(val, str) and val:
397
+ patterns.add(val)
398
+
354
399
  # 1. config.json
355
400
  local_cfg = get_local_config()
356
- for p in local_cfg.get("exclude_patterns", []):
357
- if p:
358
- patterns.add(str(p))
401
+ _add_from_cfg(local_cfg, "exclude_patterns")
402
+ _add_from_cfg(local_cfg, "exclude")
359
403
 
360
404
  # 2. doc_config.json
361
405
  doc_cfg = get_doc_config()
362
- for p in doc_cfg.get("exclude_patterns", []):
363
- if p:
364
- patterns.add(str(p))
365
- for p in doc_cfg.get("coverage_settings", {}).get("exclude_patterns", []):
366
- if p:
367
- patterns.add(str(p))
406
+ _add_from_cfg(doc_cfg, "exclude_patterns")
407
+ _add_from_cfg(doc_cfg, "exclude")
408
+ cov_cfg = doc_cfg.get("coverage_settings", {})
409
+ if isinstance(cov_cfg, dict):
410
+ _add_from_cfg(cov_cfg, "exclude_patterns")
411
+ _add_from_cfg(cov_cfg, "exclude")
368
412
 
369
413
  # 3. ki_config.json
370
414
  ki_cfg = get_ki_cfg()
371
- for p in ki_cfg.get("exclude_patterns", []):
372
- if p:
373
- patterns.add(str(p))
415
+ _add_from_cfg(ki_cfg, "exclude_patterns")
416
+ _add_from_cfg(ki_cfg, "exclude")
417
+
418
+ # 4. .gitignore
419
+ patterns.update(load_gitignore_patterns())
374
420
 
375
421
  return patterns
376
422
 
@@ -389,15 +435,17 @@ def should_exclude(name: str, rel_path: str = "", patterns: set = None) -> bool:
389
435
  norm_rel = rel_path.replace("\\", "/").strip("/") if rel_path else ""
390
436
 
391
437
  for pat in patterns:
392
- if fnmatch.fnmatch(name, pat):
438
+ norm_pat = pat.replace("\\", "/").strip("/")
439
+ if not norm_pat:
440
+ continue
441
+ if fnmatch.fnmatch(name, norm_pat):
393
442
  return True
394
443
  if norm_rel:
395
- norm_pat = pat.replace("\\", "/").strip("/")
396
444
  if norm_rel == norm_pat or norm_rel.startswith(norm_pat + "/"):
397
445
  return True
398
446
  if fnmatch.fnmatch(norm_rel, norm_pat):
399
447
  return True
400
- if any(fnmatch.fnmatch(comp, pat) for comp in norm_rel.split("/")):
448
+ if any(fnmatch.fnmatch(comp, norm_pat) for comp in norm_rel.split("/")):
401
449
  return True
402
450
 
403
451
  return False
@@ -458,6 +506,122 @@ def get_ki_list_table() -> str:
458
506
  return "\n".join(rows)
459
507
 
460
508
 
509
+ def get_ki_list_data(scope: str = "all", filter_query: Optional[str] = None) -> Dict[str, Any]:
510
+ """Collects structured list of Knowledge Items and ADRs with optional filtering."""
511
+ jail = get_knowledge_root()
512
+ proj = get_project_root()
513
+ dec_dirs = get_decisions_dirs(proj, jail)
514
+ doc_config = get_doc_config() or {}
515
+ items = doc_config.get("knowledge_items", {})
516
+
517
+ flt = filter_query.lower().strip() if filter_query else None
518
+
519
+ ki_list = []
520
+ if scope in ("all", "ki"):
521
+ for name, info in sorted(items.items()):
522
+ summary = info.get("summary", info.get("description", "—"))
523
+ covers = info.get("covers", [])
524
+ display_name = name
525
+ if "path" in info and info["path"]:
526
+ display_name = info["path"]
527
+
528
+ if flt:
529
+ covers_str = " ".join(covers).lower() if isinstance(covers, list) else str(covers).lower()
530
+ if flt not in name.lower() and flt not in summary.lower() and flt not in covers_str and flt not in display_name.lower():
531
+ continue
532
+
533
+ ki_list.append({
534
+ "name": name,
535
+ "path": display_name,
536
+ "summary": summary,
537
+ "covers": covers,
538
+ "type": "ki",
539
+ })
540
+
541
+ adr_list = []
542
+ if scope in ("all", "adr"):
543
+ adr_records = []
544
+ seen_files = set()
545
+ for c in dec_dirs:
546
+ if os.path.exists(c) and os.path.isdir(c):
547
+ for f in sorted(os.listdir(c)):
548
+ if f.endswith(".md") and f not in seen_files:
549
+ seen_files.add(f)
550
+ full_p = os.path.join(c, f)
551
+ adr_records.append(parse_adr_file(full_p, proj))
552
+
553
+ def _sort_key(r):
554
+ num = int(r["id"]) if r["id"].isdigit() else 999999
555
+ return (num, r["file"])
556
+ adr_records.sort(key=_sort_key)
557
+
558
+ for r in adr_records:
559
+ if flt:
560
+ target_text = f"{r['id']} {r['title']} {r['summary']} {r['file']} {r['status']}".lower()
561
+ if flt not in target_text:
562
+ continue
563
+ adr_list.append({
564
+ "id": r["id"],
565
+ "title": r["title"],
566
+ "status": r["status"],
567
+ "date": r["date"],
568
+ "summary": r["summary"],
569
+ "file": r["file"],
570
+ "rel_path": r["rel_path"],
571
+ "type": "adr",
572
+ })
573
+
574
+ return {
575
+ "scope": scope,
576
+ "filter": filter_query,
577
+ "total_ki": len(ki_list),
578
+ "total_adr": len(adr_list),
579
+ "knowledge_items": ki_list,
580
+ "adrs": adr_list,
581
+ }
582
+
583
+
584
+ def format_ki_list_markdown(data: Dict[str, Any]) -> str:
585
+ """Formats KI and ADR list into clean Markdown tables."""
586
+ scope = data.get("scope", "all")
587
+ flt = data.get("filter")
588
+ ki_list = data.get("knowledge_items", [])
589
+ adr_list = data.get("adrs", [])
590
+
591
+ sections = []
592
+ flt_msg = f" matching '{flt}'" if flt else ""
593
+
594
+ if scope in ("all", "ki"):
595
+ if ki_list:
596
+ rows = [
597
+ f"### Knowledge Items ({len(ki_list)}){flt_msg}\n",
598
+ "| File | Summary / Topic | Covered Modules |",
599
+ "|------|-----------------|-----------------|"
600
+ ]
601
+ for item in ki_list:
602
+ covers_str = ", ".join(f"`{c}`" for c in item.get("covers", [])) if item.get("covers") else "—"
603
+ rows.append(f"| `{item['path']}` | {item['summary']} | {covers_str} |")
604
+ sections.append("\n".join(rows))
605
+ else:
606
+ sections.append(f"No Knowledge Items{flt_msg} found.")
607
+
608
+ if scope in ("all", "adr"):
609
+ if adr_list:
610
+ rows = [
611
+ f"### Architecture Decision Records ({len(adr_list)}){flt_msg}\n",
612
+ "| ID | Title | Status | Date | File |",
613
+ "|----|-------|--------|------|------|"
614
+ ]
615
+ for r in adr_list:
616
+ link = f"[{r['file']}]({r['rel_path']})"
617
+ rows.append(f"| {r['id']} | {r['title']} | {r['status']} | {r['date']} | {link} |")
618
+ sections.append("\n".join(rows))
619
+ elif scope == "adr":
620
+ sections.append(f"No ADRs{flt_msg} found.")
621
+
622
+ return "\n\n".join(sections)
623
+
624
+
461
625
  def save_doc_config(config: dict) -> str:
462
626
  """Atomically saves doc_config.json for the active project."""
463
627
  path = get_doc_config_path()
@@ -80,12 +80,33 @@ CURRENT_TOOL_MODE: str = os.environ.get("KI_TOOL_MODE", "full").lower()
80
80
  GLOBAL_INSTRUCTIONS = """\
81
81
  # Global AI Instructions for ki-manager
82
82
 
83
- ## 1. Project Navigation
83
+ ## 1. Project Navigation & Architecture
84
+ `{{KI_DIR}}` (knowledge_root) is the **system container** for the entire knowledge infrastructure (analogous to `.git/` in Git).
85
+
86
+ ```
87
+ <project_root>/
88
+ ├── {{KI_DIR}}/ ← SYSTEM CONTAINER (do not pollute with .ki.md)
89
+ │ ├── config.json ← Machine-specific environment config (venv path, etc.)
90
+ │ ├── ki_config.json ← Project knowledge configuration
91
+ │ ├── doc_config.json ← Manifest of tracked modules and Knowledge Items
92
+ │ ├── DIR_INDEX.md ← Auto-generated project directory tree
93
+ │ ├── coverage_matrix.md ← Audit report (from audit_coverage)
94
+ │ ├── knowledge/ ← STRICTLY ALL Knowledge Items (*.ki.md) live HERE!
95
+ │ │ ├── _OVERVIEW.ki.md ← Project overview stub (MUST NOT be deleted)
96
+ │ │ └── <module>.ki.md ← Module Knowledge Items
97
+ │ └── decisions/ ← Architectural Decision Records (*.md ADRs)
98
+ ├── AGENTS.md ← Project instructions for AI agents in harness
99
+ └── CLAUDE.md ← Instructions for Claude Code / Anthropic
100
+ ```
101
+
84
102
  - **`DIR_INDEX.md`** (`{{KI_DIR}}/DIR_INDEX.md`) — project directory tree.
85
103
  - **`doc_config.json`** (`{{KI_DIR}}/doc_config.json`) — manifest of tracked artifacts.
86
- - All Knowledge Items (KI) live in `{{KI_DIR}}/knowledge/`.
87
- - Architecture Decision Records (ADR) live in `{{KI_DIR}}/decisions/` or `decisions/`.
88
- - Start here: `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md`
104
+ - **`_OVERVIEW.ki.md`** (`{{KI_DIR}}/knowledge/_OVERVIEW.ki.md`) — start here for high-level architecture.
105
+ - **Harness & Agent Instructions Guide**: see `ki://agent-rules.md` or `ki_instructions(document="agent-rules")` for recommended snippets for `AGENTS.md` and `CLAUDE.md`.
106
+
107
+ > [!CAUTION]
108
+ > 1. NEVER place `*.ki.md` files directly in the root of `{{KI_DIR}}/`. All Knowledge Items must reside in `{{KI_DIR}}/knowledge/`.
109
+ > 2. NEVER delete `{{KI_DIR}}/knowledge/`. The initial `_OVERVIEW.ki.md` (~640 bytes) is a bootstrap stub, not a duplicate. Deleting `knowledge/` breaks audit, scaffolding, and dependency analysis.
89
110
 
90
111
  ## 2. Forced Efficiency (Anti-Hallucinations)
91
112
  1. **Mandatory Planning Template**:
@@ -97,12 +118,12 @@ GLOBAL_INSTRUCTIONS = """\
97
118
  2. **Strict Adherence**:
98
119
  - Always read relevant KIs before modifying a module.
99
120
  <!-- if-compact -->
100
- - Use `ki_search(query="...")` and `ki_read(rel_path="...")` to discover and read documentation.
121
+ - Use `ki_list()` to view all documented items, `ki_search(query="...")` to find topics, and `ki_read(rel_path="...")` to read documentation.
101
122
  - For read-only analysis tools (`audit_coverage`, `generate_dir_index`, `ki_scaffold_status`, `git_diff_secured`), call `ki_call(tool="...")`.
102
123
  - For state-modifying operations (`git_checkpoint`, `write_know_file`, `edit_know_file`, `ki_scaffold`, `create_adr`), call `ki_mutate(tool="...", args={...})`.
103
124
  - To inspect schemas of available tools, call `ki_tools(name="<tool_name>")`.
104
125
  <!-- else-compact -->
105
- - Use `ki_search(query="...")` and `ki_read(rel_path="...")` or `read_know_file(rel_path="...")` to explore documentation.
126
+ - Use `ki_list()` to view all documented items, `ki_search(query="...")` to find topics, and `ki_read(rel_path="...")` or `read_know_file(rel_path="...")` to explore documentation.
106
127
  - After significant changes, run `audit_coverage` via MCP.
107
128
  - Use `git_checkpoint` to save knowledge snapshots.
108
129
  <!-- /if-compact -->
@@ -1038,7 +1059,8 @@ MCP_TOOLS = [
1038
1059
  "idempotentHint": True,
1039
1060
  },
1040
1061
  },
1041
- # ─── Search & Read ───
1062
+ # ─── Navigation, Search & Read ───
1063
+ facade.KI_LIST_TOOL,
1042
1064
  facade.KI_SEARCH_TOOL,
1043
1065
  facade.KI_READ_TOOL,
1044
1066
  ]
@@ -1049,6 +1071,7 @@ facade.sync_dispatch_schemas(MCP_TOOLS)
1049
1071
  _INSTRUCTIONS_TOOL = next(t for t in MCP_TOOLS if t["name"] == "ki_instructions")
1050
1072
  COMPACT_TOOLS = [
1051
1073
  _INSTRUCTIONS_TOOL,
1074
+ facade.KI_LIST_TOOL,
1052
1075
  facade.KI_SEARCH_TOOL,
1053
1076
  facade.KI_READ_TOOL,
1054
1077
  facade.KI_TOOLS_TOOL,
@@ -1970,14 +1993,29 @@ def handle_tool_call(name: str, args: dict) -> Any:
1970
1993
  return ke.restore_mapping()
1971
1994
  return str(ke.save_state(ke.capture_full_state()))
1972
1995
 
1973
- # ─── Search & Read ───
1996
+ # ─── Navigation, Search & Read ───
1997
+ if name == "ki_list":
1998
+ scope = args.get("scope", "all")
1999
+ fmt = args.get("format", "markdown")
2000
+ flt = args.get("filter")
2001
+ data = ki_utils.get_ki_list_data(scope=scope, filter_query=flt)
2002
+ if fmt == "json":
2003
+ return json.dumps(data, ensure_ascii=False, indent=2)
2004
+ return ki_utils.format_ki_list_markdown(data)
2005
+
1974
2006
  if name == "ki_search":
1975
2007
  query = args.get("query", "").strip()
1976
2008
  scope = args.get("scope", "all")
1977
2009
  limit = int(args.get("limit", 5))
1978
2010
  offset = int(args.get("offset", 0))
2011
+ page = args.get("page")
2012
+ if page is not None:
2013
+ try:
2014
+ page = int(page)
2015
+ except (ValueError, TypeError):
2016
+ page = None
1979
2017
  fmt = args.get("format", "markdown")
1980
- res = ki_search.search_knowledge(query, project_root=get_project_root(), scope=scope, limit=limit, offset=offset)
2018
+ res = ki_search.search_knowledge(query, project_root=get_project_root(), scope=scope, limit=limit, offset=offset, page=page)
1981
2019
  if fmt == "json":
1982
2020
  return json.dumps(res, ensure_ascii=False, indent=2)
1983
2021
  return ki_search.format_search_markdown(res)
@@ -2273,6 +2311,7 @@ def main():
2273
2311
  jail = get_jail_dir()
2274
2312
  resources = [
2275
2313
  {"uri": "ki://instructions.md", "name": "instructions.md (Global AI Rules)", "mimeType": "text/markdown"},
2314
+ {"uri": "ki://agent-rules.md", "name": "agent-rules.md (Harness & Agent Instructions Guide)", "mimeType": "text/markdown"},
2276
2315
  {"uri": "ki://knowledge-items.md", "name": "knowledge-items.md (Dynamic KI List)", "mimeType": "text/markdown"},
2277
2316
  {"uri": "ki://adr-list.md", "name": "adr-list.md (Dynamic ADR List)", "mimeType": "text/markdown"},
2278
2317
  ]
@@ -2319,6 +2358,15 @@ def main():
2319
2358
  if uri == "ki://instructions.md":
2320
2359
  know_name = os.path.basename(ki_utils.get_knowledge_root()) or ".ki-base"
2321
2360
  content = facade.render_instruction(GLOBAL_INSTRUCTIONS, CURRENT_TOOL_MODE, know_name)
2361
+ elif uri == "ki://agent-rules.md":
2362
+ wf_path = (_WORKFLOWS_DIR / "agent-rules.md").resolve()
2363
+ if wf_path.exists():
2364
+ with open(wf_path, "r", encoding="utf-8") as f:
2365
+ raw = f.read()
2366
+ know_name = os.path.basename(ki_utils.get_knowledge_root()) or ".ki-base"
2367
+ content = facade.render_instruction(raw, CURRENT_TOOL_MODE, know_name)
2368
+ else:
2369
+ content = "agent-rules.md not found."
2322
2370
  elif uri == "ki://knowledge-items.md":
2323
2371
  ki_table = ki_utils.get_ki_list_table()
2324
2372
  adr_table = ki_utils.get_adr_table()
@@ -49,6 +49,7 @@ TOOL_GROUPS: Dict[str, List[str]] = {
49
49
  "create_adr",
50
50
  ],
51
51
  "files": [
52
+ "ki_list",
52
53
  "read_know_file",
53
54
  "write_know_file",
54
55
  "edit_know_file",
@@ -66,6 +67,41 @@ TOOL_GROUPS: Dict[str, List[str]] = {
66
67
 
67
68
  # ─── Tool Schemas ─────────────────────────────────────────────────────────────
68
69
 
70
+ KI_LIST_TOOL = {
71
+ "name": "ki_list",
72
+ "description": (
73
+ "List Knowledge Items (KI) and Architecture Decision Records (ADRs) registered in the active project. "
74
+ "Provides a structured overview of all documented modules, topics, and architectural decisions."
75
+ ),
76
+ "inputSchema": {
77
+ "type": "object",
78
+ "properties": {
79
+ "scope": {
80
+ "type": "string",
81
+ "enum": ["all", "ki", "adr"],
82
+ "default": "all",
83
+ "description": "Filter by document type: 'all' (default), 'ki' (Knowledge Items), or 'adr' (Architecture Decision Records).",
84
+ },
85
+ "format": {
86
+ "type": "string",
87
+ "enum": ["markdown", "json"],
88
+ "default": "markdown",
89
+ "description": "Output format: 'markdown' (structured tables, default) or 'json'.",
90
+ },
91
+ "filter": {
92
+ "type": "string",
93
+ "description": "Optional substring to filter by filename, title, summary, or covered module.",
94
+ },
95
+ },
96
+ },
97
+ "annotations": {
98
+ "title": "List Knowledge Items & ADRs",
99
+ "readOnlyHint": True,
100
+ "destructiveHint": False,
101
+ "idempotentHint": True,
102
+ },
103
+ }
104
+
69
105
  KI_SEARCH_TOOL = {
70
106
  "name": "ki_search",
71
107
  "description": (
@@ -96,6 +132,12 @@ KI_SEARCH_TOOL = {
96
132
  "default": 0,
97
133
  "description": "Result offset for pagination (default: 0).",
98
134
  },
135
+ "page": {
136
+ "type": "integer",
137
+ "minimum": 1,
138
+ "default": 1,
139
+ "description": "Page number for pagination (1-based, alternative to offset).",
140
+ },
99
141
  "format": {
100
142
  "type": "string",
101
143
  "enum": ["markdown", "json"],
@@ -245,7 +287,7 @@ KI_MUTATE_TOOL = {
245
287
 
246
288
 
247
289
  # Direct facade tools that cannot be called recursively via ki_call / ki_mutate
248
- DIRECT_FACADE_TOOLS = {"ki_instructions", "ki_search", "ki_read", "ki_tools", "ki_call", "ki_mutate"}
290
+ DIRECT_FACADE_TOOLS = {"ki_instructions", "ki_list", "ki_search", "ki_read", "ki_tools", "ki_call", "ki_mutate"}
249
291
 
250
292
 
251
293
  def sync_dispatch_schemas(all_tools: List[Dict[str, Any]]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: ki-manager-agent-rules
3
+ description: Recommendations and reference snippets for configuring project-level AI instruction files (AGENTS.md, CLAUDE.md, .cursorrules) with ki-manager
4
+ metadata:
5
+ author: ki-manager
6
+ version: "1.0"
7
+ <!-- if-compact -->
8
+ allowed-tools: ki_instructions ki_read ki_search ki_tools ki_call ki_mutate
9
+ <!-- else-compact -->
10
+ allowed-tools: ki_instructions ki_read sync_agents_md
11
+ <!-- /if-compact -->
12
+ ---
13
+
14
+ # /agent-rules — AI Rules & Guidelines for Project Harnesses (AGENTS.md, CLAUDE.md, IDE Rules)
15
+
16
+ > [!IMPORTANT]
17
+ > **Purpose of this Document**:
18
+ > This document is **NOT a system file or magical hook** that automatically or invisibly directs AI agent behavior by its mere presence.
19
+ > Rather, it is a **reference guide and collection of actionable snippets and recommendations** for developers and agents to configure project-level instruction files defined in the project harness (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, `.windsurfrules`).
20
+ > Its goal is to ensure external AI agents understand the `ki-manager` architecture and use the available tools properly.
21
+
22
+ ---
23
+
24
+ ## 1. Knowledge Base Directory Layout
25
+
26
+ Every AI agent working with the repository must distinguish between the **system container** and the **knowledge items directory**:
27
+
28
+ ```
29
+ <project_root>/
30
+ ├── {{KI_DIR}}/ ← SYSTEM CONTAINER (like .git/ for Git)
31
+ │ ├── config.json ← Environment config (Python venv path, etc.)
32
+ │ ├── ki_config.json ← Knowledge base configuration (knowledge_root: "{{KI_DIR}}")
33
+ │ ├── doc_config.json ← Manifest and registry of tracked modules & KIs
34
+ │ ├── DIR_INDEX.md ← Auto-generated project directory tree
35
+ │ ├── coverage_matrix.md ← Coverage audit report (generated by audit_coverage)
36
+ │ ├── knowledge/ ← STRICTLY ALL Knowledge Items (*.ki.md) live HERE!
37
+ │ │ ├── _OVERVIEW.ki.md ← High-level project architecture stub (DO NOT DELETE!)
38
+ │ │ └── <module>.ki.md ← Knowledge Items for individual modules
39
+ │ └── decisions/ ← Architecture Decision Records (*.md ADRs)
40
+ ├── AGENTS.md ← Instructions for Antigravity & general AI agents in harness
41
+ └── CLAUDE.md ← Instructions for Claude Code / Anthropic
42
+ ```
43
+
44
+ > [!CAUTION]
45
+ > **Common Agent Pitfalls:**
46
+ > 1. **`knowledge_root: "{{KI_DIR}}"` points to the root of the system container, NOT the folder for `.ki.md` files.**
47
+ > NEVER write `*.ki.md` files directly into the root of `{{KI_DIR}}/`.
48
+ > 2. **NEVER delete `{{KI_DIR}}/knowledge/`!**
49
+ > The initial `_OVERVIEW.ki.md` (~640 bytes) is NOT a stale duplicate. It is the initial bootstrap stub created for enrichment. Deleting `knowledge/` breaks all analysis and scaffolding tools (`audit_coverage`, `analyze_dependencies`, `ki_scaffold`, `finalize_ki_scaffolds`).
50
+
51
+ ---
52
+
53
+ ## 2. Reference Snippet for `AGENTS.md` (Antigravity & General AI Agents)
54
+
55
+ Add the following block to your project's `AGENTS.md` (or use `sync_agents_md` to keep tables synchronized):
56
+
57
+ ```markdown
58
+ # AI Agent Guidelines (AGENTS.md)
59
+
60
+ This project uses **ki-manager** (an MCP server for architectural knowledge management).
61
+
62
+ ## Knowledge Base Architecture
63
+ - System container: `{{KI_DIR}}/`
64
+ - Knowledge Items (KIs): strictly inside `{{KI_DIR}}/knowledge/*.ki.md`
65
+ - Architecture Decision Records (ADRs): in `{{KI_DIR}}/decisions/*.md` (or `decisions/`)
66
+ - Project overview & entry point: `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md`
67
+ - Project directory tree: `{{KI_DIR}}/DIR_INDEX.md`
68
+
69
+ ## Agent Operating Rules
70
+ 1. **Context Before Changes**:
71
+ - Inspect `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md` first.
72
+ - Read relevant module KIs via `ki_read(rel_path="...")` or `read_know_file` before writing code.
73
+ - List read KIs and architectural constraints in your Implementation Plan.
74
+ 2. **File Safety**:
75
+ - NEVER delete `{{KI_DIR}}/knowledge/` and never save `.ki.md` files directly in `{{KI_DIR}}/` root.
76
+ - Use `ki-manager` tools (`write_know_file`, `edit_know_file`) rather than generic filesystem tools when modifying documentation.
77
+ 3. **Synchronization**:
78
+ - When modifying module logic, update the corresponding KI and run `sync_agents_md`.
79
+
80
+ ## Knowledge Items
81
+ <!-- Auto-synchronized by sync_agents_md -->
82
+ | File | Topic / Summary |
83
+ |------|-----------------|
84
+ | `_OVERVIEW.ki.md` | Project overview |
85
+
86
+ ## Architecture Decision Records (ADRs)
87
+ <!-- Auto-synchronized by sync_agents_md -->
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 3. Reference Snippet for `CLAUDE.md` (Claude Code / Anthropic)
93
+
94
+ Add the following block to your project's `CLAUDE.md`:
95
+
96
+ ```markdown
97
+ # Project Guidelines for Claude Code
98
+
99
+ This project uses `ki-manager` MCP server for knowledge management.
100
+
101
+ ## Knowledge Base Architecture
102
+ - System container: `{{KI_DIR}}/`
103
+ - Knowledge Items (KIs): strictly inside `{{KI_DIR}}/knowledge/*.ki.md`
104
+ - Architecture Decision Records (ADRs): inside `{{KI_DIR}}/decisions/*.md`
105
+ - Project overview: `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md`
106
+
107
+ ## Core Protocols
108
+ - **Always read context first**: Check `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md` and relevant KIs before implementing changes.
109
+ - **Never mutate container root**: Do NOT write `*.ki.md` directly into `{{KI_DIR}}/`. Never delete `{{KI_DIR}}/knowledge/`.
110
+ - **Tooling**: Prefer MCP tools (`ki_read`, `ki_search`, `write_know_file`) when inspecting or updating documentation.
111
+ - **Verification**: Run tests via project virtual environment (`.venv`).
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 4. Reference Snippet for Cursor (`.cursorrules` or `.cursor/rules/ki-manager.mdc`)
117
+
118
+ ```markdown
119
+ ---
120
+ description: Rules for navigating and updating ki-manager knowledge base
121
+ globs: ["**/*"]
122
+ alwaysApply: true
123
+ ---
124
+
125
+ # ki-manager Integration Rules
126
+
127
+ - Project architecture is documented via Knowledge Items (KIs) located in `{{KI_DIR}}/knowledge/`.
128
+ - Start exploring codebase with `{{KI_DIR}}/knowledge/_OVERVIEW.ki.md` and `{{KI_DIR}}/DIR_INDEX.md`.
129
+ - When writing or updating documentation, always keep `*.ki.md` files inside `{{KI_DIR}}/knowledge/`.
130
+ - Do NOT delete or modify the container directory structure (`{{KI_DIR}}/`).
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 5. Tooling Interaction
136
+
137
+ Depending on the active `--tool-mode` of the `ki-manager` MCP server, use the corresponding tools:
138
+
139
+ <!-- if-compact -->
140
+ - **Search documentation**: `ki_search(query="<keywords>")`
141
+ - **Read KI or ADR**: `ki_read(rel_path="<filename>.ki.md")`
142
+ - **Inspect tool catalog**: `ki_tools()`
143
+ - **Coverage audit**: `ki_call(tool="audit_coverage")`
144
+ - **Write / update KI**: `ki_mutate(tool="write_know_file", args={"rel_path": "<name>.ki.md", "content": "..."})`
145
+ - **Sync AGENTS.md**: `ki_mutate(tool="sync_agents_md")`
146
+ <!-- else-compact -->
147
+ - **Search documentation**: `ki_search(query="<keywords>")`
148
+ - **Read KI or ADR**: `ki_read(rel_path="<filename>.ki.md")` or `read_know_file(rel_path="<filename>.ki.md")`
149
+ - **Coverage audit**: `audit_coverage()`
150
+ - **Write / update KI**: `write_know_file(rel_path="<name>.ki.md", content="...")`
151
+ - **Sync AGENTS.md**: `sync_agents_md()`
152
+ <!-- /if-compact -->
@@ -54,7 +54,9 @@ Select **one** target with the highest priority in the following order:
54
54
 
55
55
  ## Safety & Tooling Rules (Mandatory)
56
56
  > [!IMPORTANT]
57
- > When working with this workflow, it is **STRICTLY PROHIBITED** to use general file editing tools (e.g., `filesystem.edit_file`) for files inside the <knowledge_root> directory.
57
+ > - `{{KI_DIR}}` (knowledge_root) is the system container. All Knowledge Items (`*.ki.md`) live strictly in `{{KI_DIR}}/knowledge/`.
58
+ > - **NEVER** write `.ki.md` files directly into `{{KI_DIR}}/` root, and **NEVER** delete `{{KI_DIR}}/knowledge/`.
59
+ > - When working with this workflow, it is **STRICTLY PROHIBITED** to use general file editing tools (e.g., `filesystem.edit_file`) for files inside the `{{KI_DIR}}` directory.
58
60
  >
59
61
  <!-- if-compact -->
60
62
  > You **MUST** use the following MCP tools from the `ki-manager` server:
@@ -94,7 +96,7 @@ Select **one** target with the highest priority in the following order:
94
96
 
95
97
  ## Step 4 — Create or Update a KI File
96
98
 
97
- Write the file using `write_know_file` or `edit_know_file`:
99
+ Write the file using `write_know_file` or `edit_know_file` (passing `rel_path="<filename>.ki.md"` automatically writes inside `{{KI_DIR}}/knowledge/`):
98
100
  <!-- if-compact -->
99
101
  // turbo
100
102
  `ki_mutate(tool="write_know_file", args={"rel_path": "<filename>.ki.md", "content": "..."})`
@@ -135,6 +135,10 @@ Replace the stub with the enriched version:
135
135
  `write_know_file(rel_path="<filename>.ki.md", content="...")`
136
136
  <!-- /if-compact -->
137
137
 
138
+ > [!NOTE]
139
+ > All Knowledge Items live in `{{KI_DIR}}/knowledge/`. Calling `write_know_file(rel_path="<filename>.ki.md", ...)` automatically targets `{{KI_DIR}}/knowledge/<filename>.ki.md`.
140
+ > **Never** write `.ki.md` files to the root of `{{KI_DIR}}/`, and **never** delete `{{KI_DIR}}/knowledge/` (including `_OVERVIEW.ki.md`, which is the project bootstrap stub).
141
+
138
142
  > [!IMPORTANT]
139
143
  > The **first line must be `<!-- scaffold: enriched -->`**. This marker tells `ki_finalize_scaffolds` to process the file in Phase 3 (clean markers, update doc_config summary). The `Related KIs` section is left empty — it will be populated automatically in Phase 3.
140
144
 
@@ -1,101 +0,0 @@
1
- import os
2
- import sys
3
- import argparse
4
- import json
5
- from pathlib import Path
6
- from typing import List
7
-
8
- # Add path to ki_utils
9
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
- import ki_utils
11
-
12
- def get_unmapped_files(target_path: str = ".") -> List[str]:
13
- """Returns a sorted list of unmapped files relative to project root."""
14
- project_root = ki_utils.get_project_root()
15
- doc_config = ki_utils.get_doc_config()
16
-
17
- if not project_root or not doc_config:
18
- return []
19
-
20
- # 1. Collect all files that are already mapped in doc_config.json
21
- mapped_paths = set()
22
- knowledge_items = doc_config.get("knowledge_items", {})
23
-
24
- for ki in knowledge_items.values():
25
- depends_on = ki.get("depends_on", [])
26
- for p in depends_on:
27
- abs_p = os.path.abspath(os.path.join(project_root, p))
28
- mapped_paths.add(abs_p)
29
-
30
- # 2. Scan the target directory
31
- abs_target = os.path.abspath(os.path.join(project_root, target_path))
32
- if not ki_utils.is_subpath(abs_target, project_root):
33
- return []
34
- if not os.path.exists(abs_target):
35
- return []
36
-
37
- unmapped = []
38
- ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
39
- exclude_patterns = ki_utils.get_exclude_patterns()
40
-
41
- for root, dirs, files in os.walk(abs_target):
42
- dirs[:] = [
43
- d for d in dirs
44
- if not d.startswith(".")
45
- and not ki_utils.should_exclude(d, os.path.relpath(os.path.join(root, d), project_root), exclude_patterns)
46
- ]
47
-
48
- for f in files:
49
- if f.startswith(".") or any(f.endswith(ext) for ext in ignored_exts):
50
- continue
51
-
52
- full_path = os.path.abspath(os.path.join(root, f))
53
- rel_to_project = os.path.relpath(full_path, project_root).replace("\\", "/")
54
- if ki_utils.should_exclude(f, rel_to_project, exclude_patterns):
55
- continue
56
-
57
- if full_path in mapped_paths:
58
- continue
59
-
60
- is_parent_mapped = False
61
- for mapped in mapped_paths:
62
- if os.path.isdir(mapped):
63
- if full_path.startswith(os.path.join(mapped, "")):
64
- is_parent_mapped = True
65
- break
66
-
67
- if not is_parent_mapped:
68
- unmapped.append(rel_to_project)
69
-
70
- return sorted(unmapped)
71
-
72
-
73
- def find_unmapped_files(target_path: str):
74
- project_root = ki_utils.get_project_root()
75
- doc_config = ki_utils.get_doc_config()
76
-
77
- if not project_root or not doc_config:
78
- print("Error: Could not resolve project root or doc_config.json")
79
- return
80
-
81
- abs_target = os.path.abspath(os.path.join(project_root, target_path))
82
- if not os.path.exists(abs_target):
83
- print(f"Error: Target path '{target_path}' does not exist.")
84
- return
85
-
86
- unmapped = get_unmapped_files(target_path)
87
- if unmapped:
88
- print(f"### Unmapped Files in '{target_path}':")
89
- for f in unmapped:
90
- print(f"- {f}")
91
- else:
92
- print(f"All files in '{target_path}' are already mapped in doc_config.json.")
93
-
94
- if __name__ == "__main__":
95
- if sys.platform == "win32":
96
- sys.stdout.reconfigure(encoding="utf-8")
97
- parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
98
- parser.add_argument("path", help="Relative path from project root to scan")
99
- args = parser.parse_args()
100
-
101
- find_unmapped_files(args.path)
File without changes