FBZ_50 — 멀티스토리 증거 게이트 (goals.py)
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 --> ["*"]
쉽게 풀기
이 컴포넌트를 “증거를 받아야만 도장을 찍어주는 깐깐한 검수원” 이라고 생각하면 쉽다.
-
작업을 작은 단위로 쪼갠다 (create). 예를 들어 “로그인 기능 만들기”라는 큰 일을 ① 화면 만들기 ② 서버 연동 ③ 동작 검증 같은 여러 스토리로 나눠 등록한다. 이때 맨 마지막 스토리는 반드시 “검증” 역할을 맡는다.
-
한 번에 하나만 펼친다 (next). 검수원은 책상에 한 건씩만 올려준다.
next를 부르면 그제서야 한 스토리가 “작업 중”으로 켜진다. 아직 켜지지 않은 스토리를 “다 했다”고 보고하면 받아주지 않는다(순차 진행 강제). -
완료 보고에는 증거를 첨부해야 한다 (checkpoint). “끝냈어요”라는 말만으로는 안 된다. 무엇을 했는지 보여주는
--evidence(증거)가 비어 있으면 검수원은 그 자리에서 서류를 돌려보낸다(sys.exit로 프로그램 자체가 멈춤). 증거를 만들어서 다시 와야 한다. -
마지막 스토리는 더 엄격하다 (검증 게이트). 보통 스토리는 증거 한 줄이면 되지만, 최종 스토리는 “실제로 실행해 본 명령(
--verify-cmd)“과 “그 실행 결과(--verify-evidence)“를 둘 다 내야 통과한다. “테스트 돌려봤다고 말만 하는” 가짜 완료를 원천 차단하는 마지막 관문이다. -
기록은 사라지지 않는다 (영속). 모든 진행 상황은
./.fablize/폴더에 저장된다. AI 세션이 중간에 죽어도, 다시 켜고status만 부르면 “어디까지 했는지”를 그대로 이어받는다.
핵심은, 이 모든 게 “AI에게 부탁하는 규칙”이 아니라 코드가 직접 거부하는 강제 장치라는 점이다. AI가 게으름을 피우려 해도 도장이 안 찍힌다.
핵심 정리
| 명령 | 하는 일 | 거부 조건 |
|---|---|---|
create | 작업을 순차 스토리로 분해·등록 | 이미 계획 있으면(--force 없이) |
next | 다음 스토리 1개 활성화 + 안내 출력 | — |
checkpoint | 스토리 완료/실패/막힘 기록 | 증거 없는 완료, 최종 스토리 검증 누락 |
status | 진행 현황 표시(재개의 첫 명령) | 계획 파일 없으면 |
[!note] 4대 불변식 (이걸 깨는 호출은 코드가 막는다)
- 스토리는
next로 활성화돼야(in_progress) 체크포인트 가능complete체크포인트는 비어있지 않은 evidence 필수- 최종 스토리는
--verify-cmd+--verify-evidence없이는 완료 불가- 상태는
./.fablize/에 영속 → 세션이 죽어도status로 재개
[!note] 상태 마크 읽는 법
·pending(미착수) ·▶in_progress(작업 중) ·complete(증거 통과) ·failed(실패) ·■blocked(막힘)
[!note] 두 가지 파일의 역할
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 작업 흐름에 어떻게 끼어드나
- 트리거: 2개 이상 순차 스토리 작업 →
SKILL.md §1/fablize-block.md의[2+ sequential stories]줄이 모델에게 이 스크립트 사용을 지시. - 분해: 모델이
create로 스토리 등록(마지막은 반드시 검증 스토리). - 루프:
next(활성화+핸드오프) → 해당 스토리만 작업 →checkpoint(증거와 함께). 비증거 완료는 엔진이sys.exit로 거부 → 모델이 증거를 만들어 다시 호출. - 검증 게이트: 최종 스토리는 실제 검증 명령과 결과 없이는 완료 거부 → “근거 없는 done” 차단.
- 영속/재개:
goals.json+ledger.jsonl이./.fablize/에 남아 세션 사망 후에도status로 복구. - 주입 경로: 훅 자동 발동이 아니라 모델이 스스로 호출하는 구조 → 규범(스킬/블록)이 호출을 유도.
요약 & 셀프체크
3줄 요약:
- 큰 작업을 순차 스토리로 쪼개 하나씩 처리하고, 완료 보고에는 반드시 증거를 붙이게 강제한다.
- 증거가 없으면 코드가
sys.exit로 거부하고, 특히 최종 스토리는 실제 검증 명령+결과까지 받아야 통과한다. - 진행 상태는
./.fablize/에 영속되어 세션이 죽어도status로 그대로 재개한다.
스스로 답해보기:
- AI가
next를 부르지 않고 곧장checkpoint로 “완료”를 보고하면 어떻게 되는가? (힌트:in_progress가 아닌 상태) - 일반 스토리와 최종 스토리의 완료 조건은 무엇이 다른가?
goals.json과ledger.jsonl은 각각 왜 따로 두는가?
연결
FBZ_개요 · _분석축_루브릭 · FBZ_20_조기종료-차단훅 · FBZ_30_검증접지-팩 · FBZ_60_스킬-운영블록
[!tip] Codex 교차검증 원문 노트의 분석은
/home/seunghyeong/harness-fablize/scripts/goals.py실제 소스와 대조해 4대 불변식, 상태 머신 전이, CLI 형식,./.fablize/영속 구조가 모두 일치함을 확인했다. 핵심 동작은 코드 그대로다: 증거 없는 완료와 최종 스토리 검증 누락은sys.exit로 즉시 거부되며, 이 강제는 훅 자동화가 아니라 모델의 자발적 호출을 규범(SKILL.md / fablize-block.md)이 유도하는 방식으로 작동한다.