diff --git a/.agents/skills/analyzing-codebase-for-tech-log/SKILL.md b/.agents/skills/analyzing-codebase-for-tech-log/SKILL.md index 8acd15e..f152f7d 100644 --- a/.agents/skills/analyzing-codebase-for-tech-log/SKILL.md +++ b/.agents/skills/analyzing-codebase-for-tech-log/SKILL.md @@ -1,6 +1,9 @@ --- name: analyzing-codebase-for-tech-log description: Use when a project under <분석 대상 저장소> must be deeply analyzed and documented under docs, especially when the repository is too large for one pass and analysis must proceed by bounded module or subsystem. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Analyzing Codebase For Tech Log diff --git a/.agents/skills/deriving-tech-log-root-tree/SKILL.md b/.agents/skills/deriving-tech-log-root-tree/SKILL.md index e7ba951..d73d691 100644 --- a/.agents/skills/deriving-tech-log-root-tree/SKILL.md +++ b/.agents/skills/deriving-tech-log-root-tree/SKILL.md @@ -1,6 +1,9 @@ --- name: deriving-tech-log-root-tree description: Use when a completed or substantially completed docs project analysis must be decomposed into grounded Tech Log Topics and candidate Case, Concept, Reference, Open Question, and Decision records. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Deriving Tech Log Root Tree diff --git a/.agents/skills/publishing-tech-log-to-studio/SKILL.md b/.agents/skills/publishing-tech-log-to-studio/SKILL.md index 4acdbd6..bedbdf6 100644 --- a/.agents/skills/publishing-tech-log-to-studio/SKILL.md +++ b/.agents/skills/publishing-tech-log-to-studio/SKILL.md @@ -1,6 +1,9 @@ --- name: publishing-tech-log-to-studio description: Use when a finished Tech Log record .md must be put into Tech Log Studio through the browser with Playwright MCP — creating or opening the working copy, uploading assets, filling the per-kind fields, and saving. Save only; this skill never publishes. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Studio 반입 — 저장까지만 diff --git a/.agents/skills/refactoring-from-analysis/SKILL.md b/.agents/skills/refactoring-from-analysis/SKILL.md index 0ec7244..a946352 100644 --- a/.agents/skills/refactoring-from-analysis/SKILL.md +++ b/.agents/skills/refactoring-from-analysis/SKILL.md @@ -1,6 +1,9 @@ --- name: refactoring-from-analysis description: Use when a completed codebase analysis should be turned into one bounded, evidence-backed refactoring WorkItem and implemented in isolation. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Refactoring From Analysis diff --git a/.agents/skills/rewriting-technical-prose-naturally/SKILL.md b/.agents/skills/rewriting-technical-prose-naturally/SKILL.md index d45c96a..27c5420 100644 --- a/.agents/skills/rewriting-technical-prose-naturally/SKILL.md +++ b/.agents/skills/rewriting-technical-prose-naturally/SKILL.md @@ -1,6 +1,9 @@ --- name: rewriting-technical-prose-naturally description: Use when an existing Korean technical document, section, or heading already contains grounded facts but sounds AI-written, slogan-like, overly polished, abstract, compressed, or mechanically contrasted and must be rewritten without changing its technical meaning. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Rewriting Technical Prose Naturally diff --git a/.agents/skills/running-tech-log-pipeline/SKILL.md b/.agents/skills/running-tech-log-pipeline/SKILL.md index 9e2e81c..9097b05 100644 --- a/.agents/skills/running-tech-log-pipeline/SKILL.md +++ b/.agents/skills/running-tech-log-pipeline/SKILL.md @@ -1,6 +1,9 @@ --- name: running-tech-log-pipeline description: Use when a codebase must go all the way to a saved Tech Log Studio draft — running the seven stages (SSOT, tree, record, diagram, prose, voice, Studio save) as separate subagents, one skill per stage, with a run ledger that records which skill each stage actually used and which gate it passed. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Tech Log 파이프라인 실행 diff --git a/.agents/skills/technical-visualizer/SKILL.md b/.agents/skills/technical-visualizer/SKILL.md index a7d8e8b..5cfe1f4 100644 --- a/.agents/skills/technical-visualizer/SKILL.md +++ b/.agents/skills/technical-visualizer/SKILL.md @@ -1,6 +1,9 @@ --- name: technical-visualizer description: Create source-grounded, diagram-only technical visuals from nearby documentation context. Select a logical composition grammar, compile VizSpec 1.1 into SVG and editable formats, and reject disconnected-card output. +metadata: + version: "1.0.0" + language: "ko-KR" --- # Technical Visualizer diff --git a/.agents/skills/writing-as-the-person-who-did-it/SKILL.md b/.agents/skills/writing-as-the-person-who-did-it/SKILL.md index 1b7a131..f52663e 100644 --- a/.agents/skills/writing-as-the-person-who-did-it/SKILL.md +++ b/.agents/skills/writing-as-the-person-who-did-it/SKILL.md @@ -1,6 +1,9 @@ --- name: writing-as-the-person-who-did-it description: Use when a Korean technical document is accurate, well-ordered and well-evidenced but reads like a report produced by nobody — no one chose anything, nothing surprised anyone, and the limits are an inventory instead of an admission. +metadata: + version: "1.0.0" + language: "ko-KR" --- # 일한 사람이 쓴 글로 만들기 diff --git a/scripts/capture-evidence.py b/scripts/capture-evidence.py new file mode 100644 index 0000000..756c531 --- /dev/null +++ b/scripts/capture-evidence.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""명령을 실제로 돌려 원문과 실행 메타를 함께 적립한다. + +`final/evidence/raw/` 는 정본이고 `final/evidence/meta/` 는 그 실행의 +command·cwd·executedAt·exitCode·revision 이다. 둘을 사람이 따로 적으면 갈라진다 — +`verify-project-layout.py` 가 「raw 는 있는데 meta 가 없다」로 세는 자리가 그것이다. + +이 도구는 **종료 코드를 손으로 적을 수 없게 만든다.** 명령을 여기서 돌리고, 그 프로세스의 +반환값을 그대로 meta 에 적는다. 돌리지 않은 검증을 완료로 적는 경로가 없어야 한다. + +기존 경로를 지우지 않는다 — 손으로 만든 raw/meta 도 그대로 유효하고, 이 도구는 선택적으로 쓴다. + + python3 scripts/capture-evidence.py <프로젝트> <증거 id> -- <명령...> + python3 scripts/capture-evidence.py <프로젝트> <증거 id> --cwd <경로> \ + --proves "<이 출력이 뒷받침하는 것>" --does-not-prove "<뒷받침하지 못하는 것>" -- <명령...> +""" +from __future__ import annotations + +import argparse +import datetime +import hashlib +import json +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _revision(cwd: str) -> str | None: + """그 작업 디렉터리 저장소의 HEAD. 저장소가 아니면 None 이다.""" + try: + out = subprocess.run(["git", "rev-parse", "HEAD"], cwd=cwd, + capture_output=True, text=True, timeout=15) + except (OSError, subprocess.SubprocessError): + return None + return out.stdout.strip() if out.returncode == 0 else None + + +def _dirty(cwd: str) -> bool | None: + """작업 트리에 커밋 안 된 변경이 있나. 있으면 revision 이 출력을 설명하지 못한다.""" + try: + out = subprocess.run(["git", "status", "--porcelain"], cwd=cwd, + capture_output=True, text=True, timeout=15) + except (OSError, subprocess.SubprocessError): + return None + return bool(out.stdout.strip()) if out.returncode == 0 else None + + +def capture(project: str, eid: str, command: list[str], cwd: str, + proves: str, does_not_prove: str, kind: str, + timeout: int, subdir: str) -> int: + base = os.path.join(ROOT, "docs", project, "final", "evidence") + raw_dir = os.path.join(base, "raw", subdir) if subdir else os.path.join(base, "raw") + meta_dir = os.path.join(base, "meta") + os.makedirs(raw_dir, exist_ok=True) + os.makedirs(meta_dir, exist_ok=True) + + started = datetime.datetime.now().astimezone() + try: + proc = subprocess.run(command, cwd=cwd, capture_output=True, + text=True, timeout=timeout) + exit_code, out = proc.returncode, proc.stdout + proc.stderr + except subprocess.TimeoutExpired as e: + exit_code = 124 + out = (e.stdout or "") + (e.stderr or "") + f"\n[timeout {timeout}s]\n" + except OSError as e: + print(f"명령을 실행하지 못했다: {e}", file=sys.stderr) + return 2 + + raw_rel = os.path.join("raw", subdir, f"{eid}.txt") if subdir else os.path.join("raw", f"{eid}.txt") + raw_path = os.path.join(base, raw_rel) + with open(raw_path, "w", encoding="utf-8") as fh: + fh.write(out) + + meta = { + "id": eid, + "kind": kind, + "sourceRevision": _revision(cwd), + "sourceDirty": _dirty(cwd), + "executedAt": started.isoformat(timespec="seconds"), + "executedAtSource": "이 도구가 명령을 실행한 시각", + "command": " ".join(command), + "cwd": os.path.relpath(cwd, ROOT) if cwd.startswith(ROOT) else cwd, + "exitCode": exit_code, + "exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다", + "rawPath": f"evidence/{raw_rel}", + "presentationPath": None, + "proves": proves, + "doesNotProve": does_not_prove, + "sha256": hashlib.sha256(out.encode("utf-8")).hexdigest(), + "bytes": len(out.encode("utf-8")), + } + meta_path = os.path.join(meta_dir, f"{eid}.json") + tmp = meta_path + ".tmp" + with open(tmp, "w", encoding="utf-8") as fh: + json.dump(meta, fh, ensure_ascii=False, indent=2) + fh.write("\n") + os.replace(tmp, meta_path) + + print(f"{os.path.relpath(raw_path, ROOT)} exit={exit_code} {meta['bytes']}B") + print(f"{os.path.relpath(meta_path, ROOT)}") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser( + description="명령을 돌려 raw 원문과 실행 메타를 함께 적립한다.") + ap.add_argument("project") + ap.add_argument("evidence_id") + ap.add_argument("--cwd", default=ROOT) + ap.add_argument("--subdir", default="", help="raw/ 아래 하위 폴더") + ap.add_argument("--kind", default="terminal", + choices=["terminal", "browser", "query-plan", "benchmark", "other"]) + ap.add_argument("--proves", default="", help="이 출력이 뒷받침하는 것 (경계까지)") + ap.add_argument("--does-not-prove", default="", help="이 출력이 뒷받침하지 못하는 것") + ap.add_argument("--timeout", type=int, default=600) + + # `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다 + argv = sys.argv[1:] + if "-h" in argv or "--help" in argv: + ap.parse_args(["--help"]) + if "--" not in argv: + ap.error("돌릴 명령이 없다. `-- <명령...>` 으로 준다") + cut = argv.index("--") + args = ap.parse_args(argv[:cut]) + command = argv[cut + 1:] + if not command: + ap.error("돌릴 명령이 없다. `-- <명령...>` 으로 준다") + return capture(args.project, args.evidence_id, command, + os.path.abspath(args.cwd), args.proves, args.does_not_prove, + args.kind, args.timeout, args.subdir) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/skill-versions.py b/scripts/skill-versions.py new file mode 100644 index 0000000..7595c6c --- /dev/null +++ b/scripts/skill-versions.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""스킬과 검사기의 버전을 한 장으로 뽑는다. + +통과 판정은 「어느 문서를 어느 검사기로 봤는가」에 묶여야 한다. 스킬은 `SKILL.md` 의 +`metadata.version` 이 그 값이고, 스크립트 검사기는 버전 칸이 없으므로 파일 내용의 sha256 +앞 12자를 쓴다. 버전을 올리는 것을 잊어도 sha 는 따라 움직인다. + + python3 scripts/skill-versions.py # 사람이 읽는 표 + python3 scripts/skill-versions.py --json # 판정에 붙일 값 +""" +from __future__ import annotations + +import argparse +import glob +import hashlib +import json +import os +import re + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +GATE_SCRIPTS = [ + "scripts/verify-tech-log-tree.py", "scripts/verify-project-layout.py", + "scripts/verify-pipeline-run.py", "scripts/verify-pipeline.py", + "scripts/audit-records.py", "scripts/check-figure-text.py", + "scripts/check-figure-overlap.py", "scripts/build-tech-log-tree.py", + "scripts/studio-body.py", "scripts/capture-evidence.py", +] + + +def _sha12(path: str) -> str | None: + try: + with open(path, "rb") as fh: + return hashlib.sha256(fh.read()).hexdigest()[:12] + except OSError: + return None + + +def collect() -> dict: + skills = {} + for path in sorted(glob.glob(os.path.join(ROOT, ".agents/skills/*/SKILL.md"))): + name = os.path.basename(os.path.dirname(path)) + text = open(path, encoding="utf-8").read() + m = re.search(r"^metadata:\n(?: .*\n)*? version:\s*\"?([^\"\n]+)\"?", + text, re.M) + skills[name] = {"version": m.group(1).strip() if m else None, + "sha12": _sha12(path)} + + checkers = {} + for rel in GATE_SCRIPTS: + checkers[rel] = {"version": None, "sha12": _sha12(os.path.join(ROOT, rel))} + for path in sorted(glob.glob(os.path.join(ROOT, ".agents/skills/*/scripts/*.mjs"))): + rel = os.path.relpath(path, ROOT) + checkers[rel] = {"version": None, "sha12": _sha12(path)} + return {"skills": skills, "checkers": checkers} + + +def main() -> int: + ap = argparse.ArgumentParser(description="스킬·검사기의 버전과 내용 해시를 뽑는다.") + ap.add_argument("--json", action="store_true") + args = ap.parse_args() + + data = collect() + if args.json: + print(json.dumps(data, ensure_ascii=False, indent=2)) + return 0 + + missing = 0 + print("스킬") + for name, v in data["skills"].items(): + mark = " " if v["version"] else "✗" + if not v["version"]: + missing += 1 + print(f" {mark} {name:<40} {v['version'] or '버전 없음':<10} {v['sha12']}") + print("\n검사기 — 버전 칸이 없어 내용 해시로 묶는다") + for name, v in data["checkers"].items(): + print(f" {name:<70} {v['sha12']}") + print(f"\nSKILL VERSIONS: {'FAIL' if missing else 'PASS'} — " + f"스킬 {len(data['skills'])} · 버전 없음 {missing} · 검사기 {len(data['checkers'])}") + return 1 if missing else 0 + + +if __name__ == "__main__": + raise SystemExit(main())