#!/usr/bin/env python3 """Mint an IMMUTABLE report path — 매 실행 = 새 버전 파일(덮어쓰기 아님). 보고서는 한 번 쓰면 불변(guard_tools가 덮어쓰기 차단). 새 결과는 항상 새 파일로 남겨 감사 추적(누가·언제·무엇)을 보존한다. 파일은 워크플로별 폴더에 UTC 타임스탬프로 생성한다. 보고서는 불변 SNAPSHOT이다(상태를 이 파일에서 바꾸지 않는다). "이게 최신 시도인가 / 어느 게 수락됐나 / 무엇을 대체(supersede)했나"는 append-only 이벤트(acceptance_log.py)로 따로 기록한다. 여기서는 각 스냅샷에 계보(lineage) 필드만 심는다: - attempt-id : 이 (workflow, role) 쌍에서 몇 번째 시도인가(1부터, 파일 수로 파생) - supersedes-report-id : (선택) 이 시도가 대체하는 이전 report-id (--supersedes 로 전달) Usage: new_report.py --workflow WF --role ROLE -> completion-records//-.report.yaml (없으면 dir 생성) 경로를 출력 new_report.py --workflow WF --role ROLE --stub -> 위 경로에 report-id/created-at/workflow-id/role-id/attempt-id가 채워진 최소 스텁을 생성까지 new_report.py --workflow WF --role ROLE --stub --supersedes PRIOR-REPORT-ID -> 스텁에 supersedes-report-id 를 추가로 기록(계보 연결) """ import glob import os import sys from datetime import datetime, timezone ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import _workspace as W # noqa: E402 CR = W.records_dir() def mint(workflow, role): wdir = os.path.join(CR, workflow) os.makedirs(wdir, exist_ok=True) # attempt-id: 이 (workflow, role) 쌍의 기존 스냅샷 수 + 1 (몇 번째 시도인가). # 불변 스냅샷 모델 — 파일은 덮어쓰지 않으므로 개수가 곧 시도 횟수. attempt = len(glob.glob(os.path.join(wdir, f"{role}-*.report.yaml"))) + 1 stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") base = f"{role}-{stamp}" path = os.path.join(wdir, base + ".report.yaml") n = 1 while os.path.exists(path): # never overwrite path = os.path.join(wdir, f"{base}-{n}.report.yaml") n += 1 report_id = os.path.basename(path)[:-len(".report.yaml")] return path, report_id, stamp, attempt KNOWN_TYPES = {"decision", "work", "completion", "review", "blocked", "design", "build", "spec", "workflow-artifact"} def main(): args = sys.argv[1:] workflow = role = supersedes = rtype = artifact_kind = stage = None stub = "--stub" in args i = 0 while i < len(args): if args[i] == "--workflow": workflow = args[i + 1]; i += 2 elif args[i] == "--role": role = args[i + 1]; i += 2 elif args[i] == "--supersedes": supersedes = args[i + 1]; i += 2 elif args[i] == "--type": rtype = args[i + 1]; i += 2 elif args[i] == "--artifact-kind": artifact_kind = args[i + 1]; i += 2 elif args[i] == "--stage": stage = args[i + 1]; i += 2 else: i += 1 if not workflow or not role: sys.stderr.write( "usage: new_report.py --workflow WF --role ROLE [--stub] [--type TYPE] " "[--artifact-kind KIND --stage STAGE] " "[--supersedes PRIOR-REPORT-ID]\n" ) sys.exit(1) if artifact_kind: if not stage: sys.stderr.write("[new_report] --artifact-kind 사용 시 --stage 필수\n") sys.exit(1) rtype = "workflow-artifact" # report-type 은 필수(P0-5). --stub 에서 미지정이면 'work'로 두되 경고 — 커맨드는 산출물에 # 맞는 정확한 유형(decision/design/build/spec/completion/review/blocked)을 넘겨야 한다. if rtype and rtype not in KNOWN_TYPES: sys.stderr.write(f"[new_report] 경고: 미지 report-type '{rtype}' — 알려진 유형 {sorted(KNOWN_TYPES)} 권장.\n") if stub and not rtype: rtype = "work" sys.stderr.write("[new_report] 경고: --type 미지정 — 스텁 report-type=work 로 발급. 산출물에 맞는 --type 을 넘겨라.\n") path, report_id, stamp, attempt = mint(workflow, role) if stub: if artifact_kind: lines = [ "report-type: workflow-artifact\n", f"artifact-kind: {artifact_kind}\n", "artifact-version: 1\n", "identity:\n", f" artifact-id: {report_id}\n", f" workflow-id: {workflow}\n", f" stage: {stage}\n", f" producer-role-id: {role}\n", f"created-at: {stamp}\n", f"attempt-id: {attempt}\n", ] else: lines = [ f"report-type: {rtype}\n", f"report-id: {report_id}\n", f"workflow-id: {workflow}\n", f"role-id: {role}\n", f"created-at: {stamp}\n", f"attempt-id: {attempt}\n", ] if supersedes: lines.append(f"supersedes-report-id: {supersedes}\n") if rtype == "work": # work.schema.json requires a top-level work-summary; scaffold it so a --stub # work report is schema-valid out of the box (otherwise the first write BLOCKs # on the missing required field — the exact block hit during a synthesis pass). lines.append('work-summary: ""\n') if artifact_kind: lines.append("payload: {}\n") lines += [ "projection-version: 1\n", "decision-summary:\n", " bottom-line: \"\"\n", " recommendation: \"\"\n", " decision-needed: false\n", " confidence: Med\n", "evidence-index: []\n", "dissent: []\n", "open-risks: []\n", "artifact-refs: []\n", "report-header:\n", " bottom-line: \"\"\n", " decision-needed: { needed: false, approver: }\n", " confidence: { value: Med, derived-from: evidence }\n", " risks: []\n", " evidence: []\n", ] with open(path, "w") as f: f.write("".join(lines)) print(os.path.relpath(path, ROOT)) if __name__ == "__main__": main()