119 lines
4.7 KiB
Python
119 lines
4.7 KiB
Python
"""_workspace.py — 작업 산출물의 '현재 프로젝트 워크스페이스' 경로 해석(중앙화).
|
|
|
|
org-os = SSOT(정의·계약)만. 생성물/런타임 상태는 프로젝트별 root 폴더로 나간다.
|
|
모든 훅이 하드코딩(org-os/06-agent-work/...) 대신 이 모듈로 경로를 얻는다.
|
|
|
|
현재 워크스페이스 결정 순서:
|
|
1. 환경변수 ORGOS_WORKSPACE (프로젝트명 또는 절대경로)
|
|
2. 포인터 파일 <repo>/.orgos-workspace (첫 non-comment·non-blank 줄: 프로젝트명)
|
|
둘 다 미설정/비어있으면 -> WorkspaceNotSetError 로 중단(과거의 test 프로젝트
|
|
하드코딩 기본값은 제거됨). 운영 실행이 조용히 test 워크스페이스로 떨어지는 것을 막는다.
|
|
|
|
프로젝트 폴더 레이아웃(자기완결):
|
|
<project>/
|
|
completion-records/<wf>/ evidence/<wf>/ reports/ state/ slack-inbox/ slack-outbox/ design-system/
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
)
|
|
|
|
|
|
class WorkspaceNotSetError(RuntimeError):
|
|
"""워크스페이스가 명시되지 않았을 때 raise. 조용한 기본값 대신 명확히 중단."""
|
|
|
|
|
|
def _read_pointer(root):
|
|
"""포인터 파일에서 프로젝트명 해석. 첫 non-comment·non-blank 줄만 사용.
|
|
(# 로 시작하는 줄과 빈 줄은 무시 -> 포인터 파일에 설명 주석 허용.)"""
|
|
ptr = os.path.join(root, ".orgos-workspace")
|
|
try:
|
|
with open(ptr, encoding="utf-8") as fh:
|
|
for raw in fh:
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
return line
|
|
except OSError:
|
|
return None
|
|
return None
|
|
|
|
|
|
def workspace_name():
|
|
ws = os.environ.get("ORGOS_WORKSPACE")
|
|
if ws and ws.strip():
|
|
return ws.strip()
|
|
name = _read_pointer(ROOT)
|
|
if name:
|
|
return name
|
|
raise WorkspaceNotSetError(
|
|
"워크스페이스가 설정되지 않았습니다. "
|
|
"환경변수 ORGOS_WORKSPACE=<project> 를 설정하거나 "
|
|
f"{os.path.join(ROOT, '.orgos-workspace')} 파일에 프로젝트명을 기록하세요. "
|
|
"운영 실행(operational runs)은 test 워크스페이스로 조용히 기본 설정될 수 없습니다 "
|
|
"(set ORGOS_WORKSPACE=<project> or write the project name into "
|
|
"<repo>/.orgos-workspace; operational runs must not default to a test workspace)."
|
|
)
|
|
|
|
|
|
def work_root():
|
|
"""현재 워크스페이스의 절대 경로."""
|
|
name = workspace_name()
|
|
return name if os.path.isabs(name) else os.path.join(ROOT, name)
|
|
|
|
|
|
def require_workspace(hook_name="hook", advisory=False):
|
|
"""운영(operational) 훅의 fail-closed 게이트(finding P0-1).
|
|
|
|
워크스페이스가 설정돼 있으면 work_root 를 돌려주고, 미설정이면:
|
|
- advisory=False(기본): BLOCK 메시지를 stderr 로 쓰고 **exit 2** 로 중단한다.
|
|
상태 게이트·수락 원장·토큰 게이트처럼 '검증 불가면 통과시키면 안 되는' 훅이 쓴다.
|
|
과거엔 미설정 시 degrade(allow)해서 workspace 한 줄만 비우면 모든 게이트가
|
|
연쇄적으로 fail-open 됐다 — 그 구멍을 닫는다.
|
|
- advisory=True: 경고만 쓰고 None 을 돌려준다(호출부가 exit 0 로 degrade).
|
|
receipt 기록기(evidence_ledger)처럼 '게이트가 아닌 계측'이 쓴다.
|
|
|
|
읽기 전용 질의(current/allowed/dashboard 등)는 이 함수 대신 work_root()를 직접
|
|
호출하고 예외를 잡아 degrade 한다 — 스테일 상태로 리포트를 막지 않기 위함이다.
|
|
"""
|
|
try:
|
|
return work_root()
|
|
except WorkspaceNotSetError as e:
|
|
if advisory:
|
|
sys.stderr.write(
|
|
f"[{hook_name}] WARN: 워크스페이스 미설정 — 계측 degrade(allow). ({e})\n"
|
|
)
|
|
return None
|
|
sys.stderr.write(
|
|
f"[{hook_name}] BLOCK: 워크스페이스 미설정 — 운영 훅은 fail-closed(exit 2). "
|
|
f"ORGOS_WORKSPACE=<project> 를 설정하거나 <repo>/.orgos-workspace 에 프로젝트명을 "
|
|
f"기록하세요. 운영 실행은 검증 불가 상태로 통과할 수 없습니다. ({e})\n"
|
|
)
|
|
sys.exit(2)
|
|
|
|
|
|
def records_dir():
|
|
return os.path.join(work_root(), "completion-records")
|
|
|
|
|
|
def evidence_dir():
|
|
return os.path.join(work_root(), "evidence")
|
|
|
|
|
|
def reports_dir():
|
|
return os.path.join(work_root(), "reports")
|
|
|
|
|
|
def state_dir():
|
|
return os.path.join(work_root(), "state")
|
|
|
|
|
|
def slack_outbox():
|
|
return os.path.join(work_root(), "slack-outbox")
|
|
|
|
|
|
def slack_inbox():
|
|
return os.path.join(work_root(), "slack-inbox")
|