11 KiB
date, status, parent, source_buffer, title, type, ingested_at, ingested_by, source_project
| date | status | parent | source_buffer | title | type | ingested_at | ingested_by | source_project |
|---|---|---|---|---|---|---|---|---|
| 2026-05-22 | design-approved | concepts/interns-design.md | .workshop/.brainstorm/interns.md | interns-grep-audit-design | concept | 2026-05-22T04:16:29.503Z | OpeItcLoc03@DESKTOP-NSEF0UK | OpeItcLoc03/workshop |
Interns — grep_audit intern (v0.1.0)
Расширение каталога interns-mcp: добавляет интерн grep_audit, специализированный на структурированном grep по списку путей с матрицей паттернов. Особенность — детерминированный, без LLM-вызова: server применяет re локально, endpoint Ollama Cloud не вызывается ни в каком режиме. Нулевая цена, нулевая hallucination-граница.
Context
Pain-point всплыл при аудите 13 CLAUDE.md на 6 канонических триггер-строк в bootstrap-rollout сессии 2026-05-07. Делалось через bulk_text_read с длинной формулировкой «вот колонки, вот формат, вот сортировка». Шаблонная задача — выдать матрицу N×M по бинарному условию contains/not-contains. Интерн со специализированной сигнатурой убирает 80% текста запроса.
Решение «без LLM вообще» зафиксировано 2026-05-21: семантический матч (если когда-нибудь возникнет pain-point) — отдельный путь через bulk_text_read с вопросом, а не режим grep_audit. Caller не держит в голове «иногда детерминированно, иногда нет» — граница проведена между интернами, не внутри одного.
Это первый LLM-free интерн в каталоге — паттерн для будущих детерминированных тулзов (потенциально path_classify, json_extract, если pain-point всплывёт; те отброшены в текущем раунде как дублирующие jq/grep_audit).
Decisions
| # | Решение | Аргумент |
|---|---|---|
| 1 | Шейп — grep_audit(paths, patterns, output, case_sensitive) → matrix |
Структурированный matrix-output, не Q&A. Симметрия с другими интернами нарушена сознательно — это аудит, не вопрос. |
| 2 | Без LLM на back-end совсем | substring/regex детерминирован, re локально достаточен. Cheaper, точнее, нулевая hallucination. |
| 3 | patterns: list[str | dict] — либо substring, либо {pattern, name, regex?} |
Простой случай (substring) — одна строка; сложный (named regex для читаемой матрицы) — dict. |
| 4 | Always-ask политика единообразна для всех интернов | Несмотря на отсутствие endpoint-вызова, server открывает файл. Caller не должен различать «безопасный/небезопасный» интерн. |
| 5 | Routing в using-interns/SKILL.md явно фиксирует «детерминированный, no LLM, zero cost, zero hallucination» |
Каталог не-гомогенный (один интерн без LLM, остальные с) — это надо явно проговорить чтобы Claude не путался. |
| 6 | output: "table" | "json" — default "table" |
Markdown-таблица для human-readable аудитов; JSON для programmatic consumption. |
Сигнатура
grep_audit(
paths: list[str], # absolute file paths
patterns: list[str | dict], # str = substring; dict = {pattern, name, regex?}
output: Literal["table", "json"] = "table",
case_sensitive: bool = True,
) -> InternResponse
InternResponse = {text: str, usage: {files_scanned, patterns_evaluated, matches_total}}
Output shape:
"table": markdown-таблица, rows = paths, cols = pattern names (илиpatternеслиnameне задан), ячейки ✅/❌."json":{"rows": [{path, matches: {<pattern_name>: bool}}]}.
При файле-не-найден / unreadable — соответствующая ячейка null в JSON, ⚠️ в table; не abort всего вызова (аудит идёт по N путям, partial-result полезнее total fail).
Реализация (interns_mcp/interns/grep_audit.py)
import json
import re
from pathlib import Path
from typing import Literal
from .base import Intern, InternResponse
from .. import safety
class GrepAudit(Intern):
id = "grep_audit"
def run(
self,
paths: list[str],
patterns: list[str | dict],
output: Literal["table", "json"] = "table",
case_sensitive: bool = True,
) -> InternResponse:
safety.check_paths(paths) # raise if always-ask
compiled = _compile_patterns(patterns, case_sensitive)
rows = []
matches_total = 0
for p in paths:
try:
text = Path(p).read_text(encoding="utf-8", errors="replace")
except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
rows.append({"path": p, "matches": {c["name"]: None for c in compiled}, "error": str(exc)})
continue
row_matches = {}
for c in compiled:
hit = bool(c["matcher"](text))
row_matches[c["name"]] = hit
if hit:
matches_total += 1
rows.append({"path": p, "matches": row_matches})
rendered = (
json.dumps({"rows": rows}, ensure_ascii=False, indent=2)
if output == "json"
else _render_table(rows, [c["name"] for c in compiled])
)
return InternResponse(
text=rendered,
usage={
"files_scanned": len(paths),
"patterns_evaluated": len(compiled),
"matches_total": matches_total,
},
)
def _compile_patterns(patterns, case_sensitive):
out = []
for p in patterns:
if isinstance(p, str):
out.append({"name": p, "matcher": _substring(p, case_sensitive)})
continue
name = p.get("name", p["pattern"])
if p.get("regex"):
flags = 0 if case_sensitive else re.IGNORECASE
out.append({"name": name, "matcher": re.compile(p["pattern"], flags).search})
else:
out.append({"name": name, "matcher": _substring(p["pattern"], case_sensitive)})
return out
def _substring(needle, case_sensitive):
if case_sensitive:
return lambda text: needle in text
n = needle.lower()
return lambda text: n in text.lower()
def _render_table(rows, names):
header = "| Path | " + " | ".join(names) + " |"
sep = "|" + "---|" * (len(names) + 1)
lines = [header, sep]
for row in rows:
cells = []
for n in names:
v = row["matches"][n]
cells.append("⚠️" if v is None else ("✅" if v else "❌"))
lines.append(f"| `{row['path']}` | " + " | ".join(cells) + " |")
return "\n".join(lines)
NB по базовому классу: Intern base class должен пропустить интерны без endpoint/model — либо GrepAudit overrides __init__/__call__, либо base поддерживает endpoint=null без инициализации LLM-client. Решение — в impl-таске; рекомендую второй вариант (открывает дорогу другим детерминированным интернам).
Layer 1 — config (.common/config/interns/config.yaml)
interns:
grep_audit:
description: "Deterministic grep matrix over N paths × M patterns. No LLM call, no endpoint cost."
endpoint: null # local, no LLM call
# No model / max_tokens / temperature / system_prompt — этот интерн без LLM.
Layer 3 — skill update (using-interns/SKILL.md)
Routing-подсказки, добавить:
grep_audit— аудит N путей × M паттернов (substring или regex). Детерминированный, без LLM-вызова, zero cost, zero hallucination boundary. Использовать когда нужна матрица contains/not-contains: проверка набора CLAUDE.md / SKILL.md / frontmatter полей на присутствие канонических строк. Возвращает markdown-таблицу (default) или JSON.bulk_text_readvsgrep_audit: первый — Q&A над несколькими файлами (LLM-summary); второй — детерминированная проверка contains/not-contains. Семантический матч — этоbulk_text_readс вопросом, неgrep_audit.- Always-ask paths применяются единообразно с остальными интернами (server всё равно открывает файл, даже без LLM-вызова).
Bump using-interns MINOR (capability added — новый интерн в routing-таблице).
Cross-platform
| Слой | Windows | Linux | macOS |
|---|---|---|---|
Path.read_text(encoding="utf-8", errors="replace") |
✅ | ✅ | ✅ |
re substring/regex |
✅ | ✅ | ✅ |
Always-ask PurePath.match |
✅ POSIX-style globs работают везде | ✅ | ✅ |
Полностью pure-Python, без subprocess/CLI зависимостей.
Open questions
- Cap на size файла (e.g., 5 MB)? Сейчас server читает целиком в память. Если bytecode/blob случайно попадёт в paths — RAM-spike. Добавить если pain-point всплывёт; пока YAGNI.
- Batch-mode (несколько output forms за один вызов)? YAGNI.
- Counting matches (не bool, а number-of-occurrences)? Сейчас shape — boolean matrix. Если понадобится counts — расширение
output: "counts". Решение откладывается.
References
concepts/interns-design.md— parent design (архитектура interns-mcp).concepts/interns-repo-read-design.md— sibling intern (с LLM-вызовом, для сравнения паттерна).using-interns/SKILL.md— target file для routing-добавки..workshop/.archive/2026-05-22-grep-audit-extract.md— process trace (extract из living-catalog).