diff --git a/99 System/Export/agent-tools/AGENT_TOOLS.md b/99 System/Export/agent-tools/AGENT_TOOLS.md new file mode 100644 index 0000000..2ba24bc --- /dev/null +++ b/99 System/Export/agent-tools/AGENT_TOOLS.md @@ -0,0 +1,57 @@ +--- +title: Agent tools для vault +status: stable +type: guide +tags: [agent, tooling, vault] +created: 2026-06-16 +updated: 2026-06-16 +aliases: [] +--- + +# Agent tools для vault + +Набор утилит для ИИ-агентов. Вывод — JSON, чтобы агент мог читать результат без парсинга Markdown-таблиц. + +Запускать из корня vault: + +```bash +python3 "99 System/Export/agent-tools/vault_index.py" dump +``` + +## Команды + +### `vault_index.py` + +- `dump` — вывести все заметки из vault как JSON. +- `query --term "CI/CD" --limit 10` — найти близкие заметки. +- `rebuild` — пересобрать `99 System/INDEX.md`. Это write-команда. + +### `vault_ingest.py` + +- `scan` — показать файлы из `00 Inbox/` и `01 Ideas/`. +- `classify PATH` — эвристически классифицировать источник. +- `plan PATH` — выдать JSON-план ingest: назначение, похожие заметки, возможные wikilinks. + +Команды read-only. Они не заменяют обязательный план и подтверждение перед workflow. + +### `vault_audit.py` + +- `broken-links` — найти wikilinks на несуществующие заметки. +- `orphans` — найти заметки без входящих ссылок. +- `source-links` — проверить wikilinks в `source`. +- `components` — найти компоненты графа ссылок. +- `all` — выполнить все проверки. + +Команды read-only. + +### `vault_frontmatter.py` + +- `check [PATH ...]` — проверить обязательные поля и допустимые `status/type`. +- `normalize PATH ...` — нормализовать frontmatter выбранных файлов. Это write-команда. + +## Правила для агентов + +- Перед `ingest`, `wiki`, `audit`, `remind`, `query` сначала читать `99 System/INDEX.md`; скрипты помогают, но не отменяют правила `AGENTS.md`. +- Write-команды запускать только когда workflow уже подтверждён пользователем или правило явно требует обновить индекс. +- `vault_ingest.py plan` — вспомогательный план, не окончательное решение. Агент обязан проверить содержимое источника сам. +- После создания, удаления, перемещения заметки или изменения frontmatter запускать `vault_index.py rebuild`. diff --git a/99 System/Export/agent-tools/vault_audit.py b/99 System/Export/agent-tools/vault_audit.py new file mode 100644 index 0000000..8122d37 --- /dev/null +++ b/99 System/Export/agent-tools/vault_audit.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from collections import Counter, defaultdict + +from vault_common import all_records, json_print, known_note_names, source_values, wikilinks + + +def broken_links(records: list[dict[str, object]]) -> list[dict[str, object]]: + known = known_note_names(records) + result = [] + for record in records: + links = wikilinks(record["body"]) + missing = sorted({link for link in links if link not in known}) + if missing: + result.append({"path": record["path"], "missing": missing}) + return result + + +def orphans(records: list[dict[str, object]]) -> list[dict[str, object]]: + incoming: Counter[str] = Counter() + aliases: dict[str, str] = {} + path_by_name: dict[str, str] = {} + + for record in records: + path_by_name[record["name"]] = record["path"] + aliases[record["name"]] = record["name"] + aliases[record["path"]] = record["name"] + for alias in record["aliases"]: + aliases[alias] = record["name"] + + for record in records: + for link in wikilinks(record["body"]): + target = aliases.get(link) + if target and target != record["name"]: + incoming[target] += 1 + + result = [] + for name, path in sorted(path_by_name.items(), key=lambda item: item[1]): + if incoming[name] == 0 and not path.startswith("99 System/Archive/"): + result.append({"path": path, "name": name, "incoming": 0}) + return result + + +def source_links(records: list[dict[str, object]]) -> dict[str, object]: + known = known_note_names(records) + linked = [] + missing = [] + plain = [] + + for record in records: + for source in source_values(record["frontmatter"].get("source")): + links = wikilinks(source) + if links: + for link in links: + item = {"path": record["path"], "source": source, "target": link} + if link in known: + linked.append(item) + else: + missing.append(item) + else: + plain.append({"path": record["path"], "source": source}) + + return {"linked": linked, "missing": missing, "plain": plain} + + +def graph_components(records: list[dict[str, object]]) -> list[dict[str, object]]: + known = known_note_names(records) + name_by_link = {} + for record in records: + name_by_link[record["name"]] = record["name"] + name_by_link[record["path"]] = record["name"] + for alias in record["aliases"]: + name_by_link[alias] = record["name"] + + graph: dict[str, set[str]] = defaultdict(set) + path_by_name = {record["name"]: record["path"] for record in records} + for record in records: + name = record["name"] + graph.setdefault(name, set()) + for link in wikilinks(record["body"]): + if link not in known: + continue + target = name_by_link.get(link) + if target and target != name: + graph[name].add(target) + graph[target].add(name) + + seen = set() + components = [] + for name in sorted(graph): + if name in seen: + continue + stack = [name] + component = [] + seen.add(name) + while stack: + current = stack.pop() + component.append(current) + for next_name in graph[current]: + if next_name not in seen: + seen.add(next_name) + stack.append(next_name) + components.append( + { + "size": len(component), + "notes": [path_by_name[item] for item in sorted(component)[:20]], + "truncated": len(component) > 20, + } + ) + + return sorted(components, key=lambda item: item["size"]) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Agent-facing vault audit helper") + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("broken-links") + sub.add_parser("orphans") + sub.add_parser("source-links") + sub.add_parser("components") + sub.add_parser("all") + args = parser.parse_args() + + records = all_records() + if args.command == "broken-links": + json_print({"ok": True, "broken_links": broken_links(records)}) + elif args.command == "orphans": + json_print({"ok": True, "orphans": orphans(records)}) + elif args.command == "source-links": + json_print({"ok": True, "source_links": source_links(records)}) + elif args.command == "components": + json_print({"ok": True, "components": graph_components(records)}) + elif args.command == "all": + json_print( + { + "ok": True, + "broken_links": broken_links(records), + "orphans": orphans(records), + "source_links": source_links(records), + "components": graph_components(records), + } + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/99 System/Export/agent-tools/vault_common.py b/99 System/Export/agent-tools/vault_common.py new file mode 100644 index 0000000..4e03116 --- /dev/null +++ b/99 System/Export/agent-tools/vault_common.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +from datetime import date, datetime +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[3] +INDEX_PATH = ROOT / "99 System" / "INDEX.md" +EXCLUDED_DIRS = {".git", ".trash", ".obsidian", ".smart-env"} +REQUIRED_FRONTMATTER = {"title", "status", "type", "tags", "created", "updated"} +VALID_STATUS = {"seed", "processing", "stable"} +VALID_TYPE = {"concept", "moc", "lab", "knowledge", "guide"} + + +try: + import yaml +except Exception: # pragma: no cover - depends on agent environment + yaml = None + + +def rel(path: Path) -> str: + return path.relative_to(ROOT).as_posix() + + +def json_print(data: Any) -> None: + print(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)) + + +def note_files(include_index: bool = False) -> list[Path]: + files: list[Path] = [] + for path in ROOT.rglob("*.md"): + parts = set(path.relative_to(ROOT).parts) + if parts & EXCLUDED_DIRS: + continue + if not include_index and path == INDEX_PATH: + continue + files.append(path) + return sorted(files, key=lambda p: rel(p).lower()) + + +def split_frontmatter(text: str) -> tuple[dict[str, Any], str, str | None]: + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + return {}, text, None + + end = None + for index, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + end = index + break + + if end is None: + return {}, text, None + + raw = "\n".join(lines[1:end]) + body = "\n".join(lines[end + 1 :]) + return parse_frontmatter(raw), body, raw + + +def parse_frontmatter(raw: str) -> dict[str, Any]: + if yaml is not None: + try: + data = yaml.safe_load(raw) or {} + return data if isinstance(data, dict) else {} + except Exception: + return {} + + data: dict[str, Any] = {} + current_key: str | None = None + + for line in raw.splitlines(): + if not line.strip(): + continue + + list_item = re.match(r"^\s*-\s+(.*)$", line) + if list_item and current_key: + data.setdefault(current_key, []) + if isinstance(data[current_key], list): + data[current_key].append(clean_scalar(list_item.group(1))) + continue + + pair = re.match(r"^([A-Za-zА-Яа-я0-9_-]+):\s*(.*)$", line) + if not pair: + continue + + key, value = pair.group(1), pair.group(2).strip() + current_key = key + if value == "": + data[key] = [] if key in {"aliases", "tags"} else "" + elif value.startswith("[") and value.endswith("]"): + data[key] = parse_inline_list(value) + else: + data[key] = clean_scalar(value) + + return data + + +def parse_inline_list(value: str) -> list[str]: + inner = value.strip()[1:-1].strip() + if not inner: + return [] + return [clean_scalar(item.strip()) for item in inner.split(",") if item.strip()] + + +def clean_scalar(value: Any) -> str: + text = str(value).strip() + if len(text) >= 2 and text[0] == text[-1] and text[0] in {"'", '"'}: + return text[1:-1] + return text + + +def as_list(value: Any) -> list[str]: + if value is None or value == "": + return [] + if isinstance(value, list): + return [clean_scalar(item) for item in value if clean_scalar(item)] + return [clean_scalar(value)] + + +def as_cell(value: Any, default: str = "—") -> str: + if value is None or value == "": + return default + if isinstance(value, (date, datetime)): + return value.isoformat() + return str(value) + + +def tags_cell(value: Any) -> str: + tags = as_list(value) + return "[" + ", ".join(tags) + "]" + + +def note_record(path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + fm, body, raw_fm = split_frontmatter(text) + return { + "path": rel(path), + "name": path.stem, + "title": as_cell(fm.get("title"), ""), + "aliases": as_list(fm.get("aliases")), + "status": as_cell(fm.get("status")), + "type": as_cell(fm.get("type")), + "tags": as_list(fm.get("tags")), + "created": as_cell(fm.get("created"), ""), + "updated": as_cell(fm.get("updated"), ""), + "frontmatter": fm, + "has_frontmatter": raw_fm is not None, + "body": body, + } + + +def all_records() -> list[dict[str, Any]]: + return [note_record(path) for path in note_files()] + + +def wikilinks(text: str) -> list[str]: + links = [] + for match in re.finditer(r"\[\[([^\]]+)\]\]", text): + target = match.group(1).split("|", 1)[0].split("#", 1)[0].strip() + if target: + links.append(target) + return links + + +def known_note_names(records: list[dict[str, Any]]) -> set[str]: + names: set[str] = set() + for record in records: + names.add(record["name"]) + names.add(record["path"]) + names.update(record["aliases"]) + return names + + +def tokenize(value: str) -> set[str]: + return {token.lower() for token in re.findall(r"[A-Za-zА-Яа-я0-9_+-]+", value)} + + +def search_records(records: list[dict[str, Any]], term: str, limit: int = 10) -> list[dict[str, Any]]: + query = tokenize(term) + if not query: + return [] + + scored = [] + for record in records: + haystack = " ".join( + [ + record["name"], + record["title"], + " ".join(record["aliases"]), + " ".join(record["tags"]), + record["path"], + ] + ) + tokens = tokenize(haystack) + score = len(query & tokens) + if term.lower() in haystack.lower(): + score += 3 + if score: + scored.append((score, record)) + + scored.sort(key=lambda item: (-item[0], item[1]["path"])) + return [compact_record(record) | {"score": score} for score, record in scored[:limit]] + + +def compact_record(record: dict[str, Any]) -> dict[str, Any]: + return { + "path": record["path"], + "name": record["name"], + "title": record["title"], + "aliases": record["aliases"], + "status": record["status"], + "type": record["type"], + "tags": record["tags"], + "created": record["created"], + "updated": record["updated"], + } + + +def write_index(records: list[dict[str, Any]]) -> None: + by_dir: dict[str, list[dict[str, Any]]] = {} + for record in records: + directory = str(Path(record["path"]).parent) + if directory == ".": + directory = "." + by_dir.setdefault(directory, []).append(record) + + total = len(records) + lines = [ + "---", + f"generated: {date.today().isoformat()}", + f"total: {total}", + "---", + "", + "# Vault Index", + "", + "## Статистика", + "", + "| Параметр | Значение |", + "|----------|----------|", + f"| Всего заметок | {total} |", + "", + ] + + for directory in sorted(by_dir, key=str.lower): + lines.extend( + [ + f"## {directory}", + "", + "| Заметка | Aliases | Status | Type | Tags |", + "|---------|---------|--------|------|------|", + ] + ) + for record in sorted(by_dir[directory], key=lambda item: item["name"].lower()): + aliases = ", ".join(record["aliases"]) if record["aliases"] else "—" + status = record["status"] or "—" + note_type = record["type"] or "—" + tags = "[" + ", ".join(record["tags"]) + "]" + lines.append(f"| [[{record['name']}]] | {aliases} | {status} | {note_type} | {tags} |") + lines.append("") + + INDEX_PATH.write_text("\n".join(lines), encoding="utf-8") + + +def source_values(value: Any) -> list[str]: + if isinstance(value, list): + return [clean_scalar(item) for item in value if clean_scalar(item)] + if value: + return [clean_scalar(value)] + return [] diff --git a/99 System/Export/agent-tools/vault_frontmatter.py b/99 System/Export/agent-tools/vault_frontmatter.py new file mode 100644 index 0000000..96fc10d --- /dev/null +++ b/99 System/Export/agent-tools/vault_frontmatter.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from datetime import date +from pathlib import Path + +from vault_common import ( + REQUIRED_FRONTMATTER, + ROOT, + VALID_STATUS, + VALID_TYPE, + all_records, + as_list, + json_print, + note_record, + note_files, + rel, + split_frontmatter, +) + + +def resolve_paths(paths: list[str]) -> list[Path]: + if not paths: + return note_files() + result = [] + for item in paths: + path = Path(item) + if not path.is_absolute(): + path = ROOT / path + result.append(path) + return result + + +def validate_record(record: dict[str, object]) -> list[str]: + fm = record["frontmatter"] + issues = [] + + if not record["has_frontmatter"]: + issues.append("missing_frontmatter") + + for field in sorted(REQUIRED_FRONTMATTER): + if field not in fm or fm[field] in (None, ""): + issues.append(f"missing_{field}") + + if fm.get("status") and str(fm["status"]) not in VALID_STATUS: + issues.append("invalid_status") + if fm.get("type") and str(fm["type"]) not in VALID_TYPE: + issues.append("invalid_type") + if "tags" in fm and not isinstance(fm["tags"], list): + issues.append("tags_not_list") + if "aliases" in fm and fm["aliases"] not in (None, "") and not isinstance(fm["aliases"], list): + issues.append("aliases_not_list") + + return issues + + +def check(paths: list[Path]) -> list[dict[str, object]]: + results = [] + for path in paths: + record = note_record(path) + issues = validate_record(record) + if issues: + results.append({"path": rel(path), "issues": issues}) + return results + + +def yaml_list(values: list[str]) -> list[str]: + if not values: + return ["[]"] + return [""] + [f" - {value}" for value in values] + + +def normalize(path: Path) -> dict[str, object]: + text = path.read_text(encoding="utf-8") + fm, body, _ = split_frontmatter(text) + title = fm.get("title") or path.stem + status = fm.get("status") if fm.get("status") in VALID_STATUS else "seed" + note_type = fm.get("type") if fm.get("type") in VALID_TYPE else "concept" + tags = as_list(fm.get("tags")) + aliases = as_list(fm.get("aliases")) + created = fm.get("created") or date.today().isoformat() + updated = date.today().isoformat() + + lines = [ + "---", + f"title: {title}", + f"status: {status}", + f"type: {note_type}", + "tags:" + ("\n".join(yaml_list(tags)) if tags else " []"), + f"created: {created}", + f"updated: {updated}", + "aliases:" + ("\n".join(yaml_list(aliases)) if aliases else " []"), + ] + + for key, value in fm.items(): + if key in {"title", "status", "type", "tags", "created", "updated", "aliases"}: + continue + if isinstance(value, list): + lines.append(f"{key}:") + lines.extend(f" - {item}" for item in value) + elif value in (None, ""): + lines.append(f"{key}:") + else: + lines.append(f"{key}: {value}") + + lines.extend(["---", "", body.lstrip("\n")]) + path.write_text("\n".join(lines), encoding="utf-8") + return {"path": rel(path), "written": True} + + +def main() -> int: + parser = argparse.ArgumentParser(description="Agent-facing frontmatter helper") + sub = parser.add_subparsers(dest="command", required=True) + + check_cmd = sub.add_parser("check") + check_cmd.add_argument("paths", nargs="*") + + normalize_cmd = sub.add_parser("normalize") + normalize_cmd.add_argument("paths", nargs="+") + + args = parser.parse_args() + + if args.command == "check": + issues = check(resolve_paths(args.paths)) + json_print({"ok": True, "issues": issues, "total_with_issues": len(issues)}) + elif args.command == "normalize": + written = [normalize(path) for path in resolve_paths(args.paths)] + json_print({"ok": True, "written": written}) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/99 System/Export/agent-tools/vault_index.py b/99 System/Export/agent-tools/vault_index.py new file mode 100644 index 0000000..b52c6be --- /dev/null +++ b/99 System/Export/agent-tools/vault_index.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse + +from vault_common import all_records, compact_record, json_print, search_records, write_index + + +def main() -> int: + parser = argparse.ArgumentParser(description="Agent-facing vault index helper") + sub = parser.add_subparsers(dest="command", required=True) + + query = sub.add_parser("query", help="Search notes by term") + query.add_argument("--term", required=True) + query.add_argument("--limit", type=int, default=10) + + sub.add_parser("dump", help="Dump note records as JSON") + sub.add_parser("rebuild", help="Rebuild 99 System/INDEX.md") + + args = parser.parse_args() + records = all_records() + + if args.command == "query": + json_print({"ok": True, "results": search_records(records, args.term, args.limit)}) + elif args.command == "dump": + json_print({"ok": True, "total": len(records), "notes": [compact_record(r) for r in records]}) + elif args.command == "rebuild": + write_index(records) + json_print({"ok": True, "written": "99 System/INDEX.md", "total": len(records)}) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/99 System/Export/agent-tools/vault_ingest.py b/99 System/Export/agent-tools/vault_ingest.py new file mode 100644 index 0000000..ad2314c --- /dev/null +++ b/99 System/Export/agent-tools/vault_ingest.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from pathlib import Path + +from vault_common import ROOT, all_records, json_print, rel, search_records, split_frontmatter, tokenize + +INBOX_DIRS = [ROOT / "00 Inbox", ROOT / "01 Ideas"] +LIBRARY_HINTS = { + "ansible": "90 Library/DevOps/Ansible", + "ci": "90 Library/DevOps", + "cd": "90 Library/DevOps", + "cicd": "90 Library/DevOps", + "docker": "90 Library/Containers/Docker", + "git": "90 Library/DevOps", + "gitlab": "90 Library/DevOps", + "jenkins": "90 Library/DevOps", + "kubernetes": "90 Library/Containers/Kubernetes", + "linux": "90 Library/Linux", + "nginx": "90 Library/HomeLab/Proxy", + "rails": "90 Library/Programming/Ruby On Rails", + "ruby": "90 Library/Programming/Ruby", + "terraform": "90 Library/DevOps", +} + + +def read_path(path_arg: str) -> Path: + path = Path(path_arg) + if not path.is_absolute(): + path = ROOT / path + return path + + +def scan() -> list[dict[str, object]]: + items = [] + for directory in INBOX_DIRS: + if not directory.exists(): + continue + for path in sorted(directory.iterdir(), key=lambda item: item.name.lower()): + if path.name == ".gitkeep" or not path.is_file(): + continue + text = path.read_text(encoding="utf-8", errors="replace") if path.suffix == ".md" else "" + fm, body, _ = split_frontmatter(text) + items.append( + { + "path": rel(path), + "folder": rel(directory), + "suffix": path.suffix, + "title": fm.get("title") or path.stem, + "tags": fm.get("tags") or [], + "bytes": path.stat().st_size, + "classification": classify_text(path, body, fm), + } + ) + return items + + +def classify_text(path: Path, body: str, frontmatter: dict[str, object]) -> dict[str, object]: + tags = frontmatter.get("tags") or [] + if not isinstance(tags, list): + tags = [tags] + content = f"{path.stem}\n{' '.join(map(str, tags))}\n{body[:4000]}".lower() + tokens = tokenize(content) + + idea_markers = {"идея", "idea", "business", "product", "стартап", "концепция", "задумка"} + if "01 Ideas" in rel(path) or tokens & idea_markers: + kind = "idea" + destination = "01 Ideas" + else: + kind = "library" + destination = "90 Library/Other" + for marker, folder in LIBRARY_HINTS.items(): + if marker in content: + destination = folder + break + + return { + "kind": kind, + "destination": destination, + "confidence": "heuristic", + } + + +def plan(path: Path) -> dict[str, object]: + text = path.read_text(encoding="utf-8", errors="replace") + fm, body, _ = split_frontmatter(text) + classification = classify_text(path, body, fm) + records = all_records() + terms = " ".join([path.stem, str(fm.get("title") or ""), " ".join(map(str, fm.get("tags") or []))]) + similar = [] + for item in search_records(records, terms, limit=30): + if item["path"] == rel(path): + continue + if item["path"].startswith(("03 Journal/", "99 System/Export/", "99 System/Cache/", "99 System/Template/")): + continue + similar.append(item) + if len(similar) == 8: + break + wikilinks = [item["name"] for item in similar[:5]] + + return { + "path": rel(path), + "classification": classification, + "similar_notes": similar, + "suggested_wikilinks": wikilinks, + "write_policy": "read-only plan; agent must ask before workflow changes", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Agent-facing ingest helper") + sub = parser.add_subparsers(dest="command", required=True) + + sub.add_parser("scan", help="List files in 00 Inbox and 01 Ideas") + classify = sub.add_parser("classify", help="Classify one source file") + classify.add_argument("path") + plan_cmd = sub.add_parser("plan", help="Build JSON ingest plan for one source file") + plan_cmd.add_argument("path") + + args = parser.parse_args() + + if args.command == "scan": + json_print({"ok": True, "items": scan()}) + elif args.command == "classify": + path = read_path(args.path) + text = path.read_text(encoding="utf-8", errors="replace") + fm, body, _ = split_frontmatter(text) + json_print({"ok": True, "path": rel(path), "classification": classify_text(path, body, fm)}) + elif args.command == "plan": + json_print({"ok": True, "plan": plan(read_path(args.path))}) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/99 System/INDEX.md b/99 System/INDEX.md index c597a72..f9fa993 100644 --- a/99 System/INDEX.md +++ b/99 System/INDEX.md @@ -1,6 +1,6 @@ --- generated: 2026-06-16 -total: 515 +total: 516 --- # Vault Index @@ -9,13 +9,13 @@ total: 515 | Параметр | Значение | |----------|----------| -| Всего заметок | 515 | +| Всего заметок | 516 | ## . | Заметка | Aliases | Status | Type | Tags | |---------|---------|--------|------|------| -| [[AGENTS]] | Агент — контекст и инструкции для этого хранилища | — | — | [] | +| [[AGENTS]] | Агент — контекст и инструкции для этого хранилища | stable | guide | [agent, vault] | | [[CLAUDE]] | CLAUDE | — | — | [] | | [[CODEX]] | CODEX | — | — | [] | | [[GEMINI]] | GEMINI | — | — | [] | @@ -821,6 +821,12 @@ total: 515 | [[Drawing 2026-05-17 14.13.00.excalidraw]] | — | — | — | [excalidraw] | | [[infra.excalidraw.md]] | — | — | — | [excalidraw] | +## 99 System/Export/agent-tools + +| Заметка | Aliases | Status | Type | Tags | +|---------|---------|--------|------|------| +| [[AGENT_TOOLS]] | — | stable | guide | [agent, tooling, vault] | + ## 99 System/Template | Заметка | Aliases | Status | Type | Tags | diff --git a/AGENTS.md b/AGENTS.md index 771b080..8632b75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,10 @@ --- created: 2026-04-20 -updated: 2026-06-05 +updated: 2026-06-16 +title: Агент — контекст и инструкции для этого хранилища +status: stable +type: guide +tags: [agent, vault] aliases: ["Агент — контекст и инструкции для этого хранилища"] --- @@ -86,6 +90,16 @@ aliases: ["Агент — контекст и инструкции для это - **Обновлять** после создания/удаления/перемещения заметки или изменения frontmatter. - **Как:** пройтись по всем `.md` (исключая `.trash`, `.obsidian`, `.git`, `.smart-env`, сам `99 System/INDEX.md`), извлечь frontmatter, перезаписать файл в том же формате (frontmatter с `generated`/`total`, таблицы по секциям). +## Agent tools + +Утилиты для ИИ-агентов: `99 System/Export/agent-tools/`. + +- Документация: `99 System/Export/agent-tools/AGENT_TOOLS.md`. +- Вывод скриптов — JSON, чтобы агент мог использовать результат без парсинга Markdown. +- Read-only команды можно использовать для ориентации: поиск по индексу, план ingest, audit, проверка frontmatter. +- Write-команды (`vault_index.py rebuild`, `vault_frontmatter.py normalize`) запускать только после подтверждённого workflow или когда правило явно требует обновить `INDEX.md`. +- Скрипты помогают, но не заменяют чтение исходников и правила этого файла. + ## Workflow: Ingest Триггер: `ingest` или новые файлы в `00 Inbox/`.