FBZ_50 — 멀티스토리 증거 게이트 (goals.py)

한 줄 요약

큰 작업을 여러 개의 “이야기(스토리)“로 쪼개 하나씩 처리하되, 증거 없는 “다 했어요”는 코드가 아예 거부하는 상태 엔진이다. 왜 배우나: AI가 검증을 건너뛰고 “완료”를 선언하지 못하도록, 권고가 아니라 실행 차단으로 강제하는 fablize의 핵심 장치이기 때문이다.

그림

flowchart TD
    A["create<br/>작업을 순차 스토리로 분해<br/>(마지막=검증 스토리)"] --> B["next<br/>다음 스토리 1개 활성화"]
    B --> C["그 스토리만 작업"]
    C --> D{"checkpoint<br/>완료 보고"}
    D -- "증거 없음" --> X["거부 (sys.exit)<br/>다시 시도하라"]
    X --> C
    D -- "증거 있음" --> E{"최종 스토리인가?"}
    E -- "아니오" --> F["완료 기록 → 다음 next로"]
    F --> B
    E -- "예" --> G{"검증 명령+결과 제출했나?"}
    G -- "아니오" --> Y["거부 (검증 게이트)"]
    Y --> C
    G -- "예" --> H["전체 완료 "]
stateDiagram-v2
    ["*"] --> pending: create
    pending --> in_progress: next (활성화)
    in_progress --> complete: checkpoint + 증거
    in_progress --> failed: checkpoint (실패)
    in_progress --> blocked: checkpoint (막힘)
    complete --> ["*"]
    failed --> ["*"]
    blocked --> ["*"]

쉽게 풀기

이 컴포넌트를 “증거를 받아야만 도장을 찍어주는 깐깐한 검수원” 이라고 생각하면 쉽다.

  1. 작업을 작은 단위로 쪼갠다 (create). 예를 들어 “로그인 기능 만들기”라는 큰 일을 ① 화면 만들기 ② 서버 연동 ③ 동작 검증 같은 여러 스토리로 나눠 등록한다. 이때 맨 마지막 스토리는 반드시 “검증” 역할을 맡는다.

  2. 한 번에 하나만 펼친다 (next). 검수원은 책상에 한 건씩만 올려준다. next를 부르면 그제서야 한 스토리가 “작업 중”으로 켜진다. 아직 켜지지 않은 스토리를 “다 했다”고 보고하면 받아주지 않는다(순차 진행 강제).

  3. 완료 보고에는 증거를 첨부해야 한다 (checkpoint). “끝냈어요”라는 말만으로는 안 된다. 무엇을 했는지 보여주는 --evidence(증거)가 비어 있으면 검수원은 그 자리에서 서류를 돌려보낸다(sys.exit로 프로그램 자체가 멈춤). 증거를 만들어서 다시 와야 한다.

  4. 마지막 스토리는 더 엄격하다 (검증 게이트). 보통 스토리는 증거 한 줄이면 되지만, 최종 스토리는 “실제로 실행해 본 명령(--verify-cmd)“과 “그 실행 결과(--verify-evidence)“를 둘 다 내야 통과한다. “테스트 돌려봤다고 말만 하는” 가짜 완료를 원천 차단하는 마지막 관문이다.

  5. 기록은 사라지지 않는다 (영속). 모든 진행 상황은 ./.fablize/ 폴더에 저장된다. AI 세션이 중간에 죽어도, 다시 켜고 status만 부르면 “어디까지 했는지”를 그대로 이어받는다.

핵심은, 이 모든 게 “AI에게 부탁하는 규칙”이 아니라 코드가 직접 거부하는 강제 장치라는 점이다. AI가 게으름을 피우려 해도 도장이 안 찍힌다.

핵심 정리

명령하는 일거부 조건
create작업을 순차 스토리로 분해·등록이미 계획 있으면(--force 없이)
next다음 스토리 1개 활성화 + 안내 출력
checkpoint스토리 완료/실패/막힘 기록증거 없는 완료, 최종 스토리 검증 누락
status진행 현황 표시(재개의 첫 명령)계획 파일 없으면

4대 불변식 (이걸 깨는 호출은 코드가 막는다)

  • 스토리는 next로 활성화돼야(in_progress) 체크포인트 가능
  • complete 체크포인트는 비어있지 않은 evidence 필수
  • 최종 스토리--verify-cmd + --verify-evidence 없이는 완료 불가
  • 상태는 ./.fablize/에 영속 → 세션이 죽어도 status로 재개

상태 마크 읽는 법

· pending(미착수) · in_progress(작업 중) · complete(증거 통과) · failed(실패) · blocked(막힘)

두 가지 파일의 역할

  • goals.json — 현재 계획의 “스냅샷”(지금 상태). 계속 덮어써진다.
  • ledger.jsonl — “감사 로그”. 한 줄에 사건 하나씩, 덧붙이기만(append-only) 한다. 무슨 일이 언제 일어났는지 추적용.

실제 예시

상태 파일과 로그 (stdlib만 사용)

# scripts/goals.py
DIR    = Path(".fablize")            # 리포 루트에서 실행
GOALS  = DIR / "goals.json"          # 현재 계획(plan) 스냅샷
LEDGER = DIR / "ledger.jsonl"        # append-only 감사 로그(이벤트마다 1줄)
 
def log(event, **kw):                # 모든 사건을 ledger.jsonl에 타임스탬프와 함께 적재
    DIR.mkdir(exist_ok=True)
    with open(LEDGER, "a", encoding="utf-8") as f:
        f.write(json.dumps({"ts": now(), "event": event, **kw}, ensure_ascii=False) + "\n")

스토리 스키마 (create)

# scripts/goals.py — cmd_create
goals.append({"id": f"G{i:03d}", "title": title.strip(), "objective": obj.strip(),
              "status": "pending", "evidence": None})   # --goal "title::objective" 형식
# 저장 형태: {"brief": ..., "created": ..., "goals": [...]}

증거 게이트의 심장 (checkpoint)

# scripts/goals.py — cmd_checkpoint
def cmd_checkpoint(a):
    plan = load()
    g = next((x for x in plan["goals"] if x["id"] == a.id), None)
    if not g: sys.exit(f"fablize: {a.id} not found.")
    if g["status"] != "in_progress":
        sys.exit(f"fablize: {a.id} is not active ({g['status']}) — activate it with `next` first.")
    if a.status == "complete":
        if not (a.evidence and a.evidence.strip()):                   # ① 증거 없는 완료 거부
            sys.exit("fablize: a complete checkpoint requires non-empty --evidence.")
        if g["id"] == plan["goals"][-1]["id"]:                        # 최종 스토리인가?
            if not (a.verify_cmd and a.verify_cmd.strip() and a.verify_evidence and a.verify_evidence.strip()):
                sys.exit("fablize: the final story cannot complete without --verify-cmd and --verify-evidence (verification gate).")  # ② 검증 게이트
    g["status"] = a.status; g["evidence"] = a.evidence
    save(plan)
    log("checkpoint", id=g["id"], status=a.status, evidence=a.evidence,
        verify_cmd=a.verify_cmd, verify_evidence=a.verify_evidence)

최종 스토리 핸드오프 안내 (next)

# scripts/goals.py — cmd_next
is_final = g["id"] == plan["goals"][-1]["id"]
# ...
if is_final:
    print("Final story — the complete checkpoint requires --verify-cmd and --verify-evidence (verification gate).")

실제 사용 흐름 (CLI)

# 1) 작업 분해 — 마지막 --goal이 검증 스토리
goals.py create --brief "..." --goal "title::objective" [--goal ...]
 
# 2) 다음 스토리 활성화 + 핸드오프 출력
goals.py next
 
# 3) 증거와 함께 완료 보고 (증거 필수)
goals.py checkpoint --id G001 --status complete --evidence "..."
 
# 3-최종) 최종 스토리는 검증 명령+결과까지 필수
goals.py checkpoint --id G003 --status complete --evidence "..." \
                    --verify-cmd "..." --verify-evidence "..."
 
# 4) 재개할 때 첫 명령 — 어디까지 했는지 복구
goals.py status

직접 만들 최소 골격

# mygoals.py — 증거 게이트 최소 골격(stdlib only)
import json, sys
from pathlib import Path
P = Path(".mygoals/plan.json")
def checkpoint(gid, status, evidence, is_final, verify):
    if status == "complete":
        if not evidence.strip(): sys.exit("증거 없는 완료 거부")
        if is_final and not verify.strip(): sys.exit("최종 스토리: 검증 결과 필수")
    # ... 상태 저장 + append-only 로그 ...

설계 포인트: ① 완료=증거 강제 ② 최종=실행검증 강제 ③ 순차 활성화(next 없이 체크포인트 불가) ④ 영속+감사로그(jsonl) ⑤ 의존성 0.

AI 작업 흐름에 어떻게 끼어드나

  1. 트리거: 2개 이상 순차 스토리 작업 → SKILL.md §1 / fablize-block.md[2+ sequential stories] 줄이 모델에게 이 스크립트 사용을 지시.
  2. 분해: 모델이 create로 스토리 등록(마지막은 반드시 검증 스토리).
  3. 루프: next(활성화+핸드오프) → 해당 스토리만 작업 → checkpoint(증거와 함께). 비증거 완료는 엔진이 sys.exit로 거부 → 모델이 증거를 만들어 다시 호출.
  4. 검증 게이트: 최종 스토리는 실제 검증 명령과 결과 없이는 완료 거부 → “근거 없는 done” 차단.
  5. 영속/재개: goals.json+ledger.jsonl./.fablize/에 남아 세션 사망 후에도 status로 복구.
  6. 주입 경로: 훅 자동 발동이 아니라 모델이 스스로 호출하는 구조 → 규범(스킬/블록)이 호출을 유도.

요약 & 셀프체크

3줄 요약:

  • 큰 작업을 순차 스토리로 쪼개 하나씩 처리하고, 완료 보고에는 반드시 증거를 붙이게 강제한다.
  • 증거가 없으면 코드가 sys.exit로 거부하고, 특히 최종 스토리는 실제 검증 명령+결과까지 받아야 통과한다.
  • 진행 상태는 ./.fablize/에 영속되어 세션이 죽어도 status로 그대로 재개한다.

스스로 답해보기:

  1. AI가 next를 부르지 않고 곧장 checkpoint로 “완료”를 보고하면 어떻게 되는가? (힌트: in_progress가 아닌 상태)
  2. 일반 스토리와 최종 스토리의 완료 조건은 무엇이 다른가?
  3. goals.jsonledger.jsonl은 각각 왜 따로 두는가?

연결

FBZ_개요 · _분석축_루브릭 · FBZ_20_조기종료-차단훅 · FBZ_30_검증접지-팩 · FBZ_60_스킬-운영블록

Codex 교차검증

원문 노트의 분석은 /home/seunghyeong/harness-fablize/scripts/goals.py 실제 소스와 대조해 4대 불변식, 상태 머신 전이, CLI 형식, ./.fablize/ 영속 구조가 모두 일치함을 확인했다. 핵심 동작은 코드 그대로다: 증거 없는 완료와 최종 스토리 검증 누락은 sys.exit로 즉시 거부되며, 이 강제는 훅 자동화가 아니라 모델의 자발적 호출을 규범(SKILL.md / fablize-block.md)이 유도하는 방식으로 작동한다.