Initial Commit

This commit is contained in:
2026-09-16 17:22:14 +09:00
commit 858ee9e9da
335 changed files with 123898 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# 작업로그 · 2026-09-16 (수)
| 시간 | 내용 |
|------|------|
| 17:21 | CodeAssist 백엔드를 OpenCode+Django 로 가는 분석 — 프론트 계약(envelope·쿠키·/chat/stream SSE) vs ABAP_OPENCODE Django(JWT·전역 SSE·OpenCode 미러 없음) 차이표 뽑고, Django 에 어댑터 앱 + OpenCode 컨테이너 분리 안으로 docs/tech/opencode-django-backend.md 작성 |
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python
"""작업하면서 남긴 작은 스텝을 날짜별 MD 표로 쌓는다.
커밋(작업 묶음)보다 작은 단위 로그다. 스텝을 그때그때 한 줄씩 append.
한 파일로 끝: work-log/YYYY-MM/YYYY-MM-DD.md
헤더(제목 + 표 헤더) 없으면 만들고, 스텝을 표 행(| 시간 | 내용 |)으로 파일 끝에 붙인다.
MD 라 에디터·GitHub 어디서나 표로 바로 보임 — 따로 렌더/생성 단계 없음(그래서 멱등 걱정도 없음).
사용법:
python gen_worklog.py add "끊김 재현 테스트부터 짜서 버그 재현함"
→ 지금 시각 찍어 오늘 md 에 표 행 append (파일 없으면 헤더까지 만들고)
python gen_worklog.py --selftest → 자체검증
"""
import sys
from datetime import datetime
from pathlib import Path
OUT = Path(__file__).resolve().parent
WEEKDAY = ["", "", "", "", "", "", ""]
def today():
return datetime.now().astimezone().strftime("%Y-%m-%d")
def now_hm():
return datetime.now().astimezone().strftime("%H:%M")
def md_path(date):
return OUT / date[:7] / f"{date}.md" # 월별 폴더(YYYY-MM) 아래에 쌓음
def header(date):
d = datetime.strptime(date, "%Y-%m-%d")
return f"# 작업로그 · {date} ({WEEKDAY[d.weekday()]})\n\n| 시간 | 내용 |\n|------|------|\n"
def cell(text):
"""표 한 칸용: 줄바꿈·탭은 공백으로, 파이프는 이스케이프(표 안 깨지게)."""
return " ".join(text.split()).replace("|", "\\|")
def add_step(text):
"""오늘 md 표에 '| HH:MM | 내용 |' 한 줄 append. 헤더 없으면 먼저 만든다."""
text = cell(text)
if not text:
return
p = md_path(today())
p.parent.mkdir(parents=True, exist_ok=True)
if not p.exists():
p.write_text(header(today()), encoding="utf-8")
with p.open("a", encoding="utf-8") as f:
f.write(f"| {now_hm()} | {text} |\n")
def selftest():
import tempfile
global OUT
orig = OUT
OUT = Path(tempfile.mkdtemp())
try:
add_step(" 첫 번째\t스텝 ") # 탭·공백 정리 확인
add_step("파이프 | 든 스텝") # 표 깨짐 방지 이스케이프 확인
txt = md_path(today()).read_text(encoding="utf-8")
assert txt.startswith("# 작업로그"), txt
assert "| 시간 | 내용 |" in txt
assert "| 첫 번째 스텝 |" in txt
assert "파이프 \\| 든 스텝" in txt
assert txt.rstrip().count("\n") == 5 # 제목 + 빈줄 + 헤더2행 + 스텝2행
print("selftest ok")
finally:
OUT = orig
def main():
args = sys.argv[1:]
if args and args[0] == "--selftest":
selftest()
elif args and args[0] == "add":
add_step(" ".join(args[1:]))
else:
print('usage: gen_worklog.py add "<한 일 한 줄>" | --selftest')
if __name__ == "__main__":
main()
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python
"""Stop hook 안전망: 이번 세션에서 코드/문서 고쳐놓고 worklog 안 남겼으면 딱 한 번 붙잡는다.
훅이 문장을 대신 못 써준다(그냥 셸이라). 대신 "안 적었으면 멈추지 못하게" 막아서
내가 직접 한 줄 남기게 만든다. gen_worklog.py add 흔적이 마지막 편집보다 뒤에 있으면
통과, 아니면 block(한 턴 더 돌려서 남기게 함).
stdin : Claude Code Stop hook JSON (transcript_path, stop_hook_active)
stdout: 통과면 아무것도. 걸리면 {"decision":"block","reason":...}
python worklog_guard.py --selftest 로 자체검증.
"""
import json
import sys
from pathlib import Path
TAIL = 256 * 1024 # ponytail: 트랜스크립트 꼬리만 스캔. 이보다 오래된 미기재 편집은 못 잡음 —
# 편집은 보통 그 턴에 바로 붙잡히니 실무상 충분. 느려지면 전체읽기로.
REPO = Path(__file__).resolve().parents[2].as_posix().lower() # 레포 루트 — 이 안의 편집만 프로젝트 스텝
def tool_uses(obj):
"""파싱된 한 줄 안의 모든 tool_use dict 를 훑어서 뽑는다(중첩 상관없이)."""
out = []
def walk(x):
if isinstance(x, dict):
if x.get("type") == "tool_use":
out.append(x)
for v in x.values():
walk(v)
elif isinstance(x, list):
for v in x:
walk(v)
walk(obj)
return out
def scan(transcript_path):
"""미기재면 마지막 편집 파일경로, 아니면 None."""
p = Path(transcript_path)
if not p.exists():
return None
data = p.read_bytes()
if len(data) > TAIL:
data = data[-TAIL:]
data = data[data.find(b"\n") + 1 :] # 잘린 첫 줄 버림
last_edit_i, last_edit_file, last_log_i = -1, None, -1
for i, raw in enumerate(data.decode("utf-8", "ignore").splitlines()):
if not raw.strip():
continue
try:
obj = json.loads(raw)
except ValueError:
continue
for tu in tool_uses(obj):
name = tu.get("name", "")
inp = tu.get("input", {}) or {}
if name in ("Edit", "Write", "NotebookEdit"):
fp = str(inp.get("file_path", "")).replace("\\", "/")
low = fp.lower()
if not low.startswith(REPO):
continue # 레포 밖(메모리·스크래치패드 등)은 프로젝트 스텝 아님
if "z-my-docs" in low:
continue # worklog 자체 편집은 self-trigger 방지로 제외
last_edit_i, last_edit_file = i, fp
elif name in ("Bash", "PowerShell") and "gen_worklog.py add" in str(
inp.get("command", "")
):
# 윈도우 세션은 Bash 대신 PowerShell 툴로 돎 — 둘 다 인정해야 헛붙잡기 안 함
last_log_i = i
if last_edit_i >= 0 and last_edit_i > last_log_i:
return last_edit_file or "코드"
return None
def main():
try:
payload = json.load(sys.stdin)
except (ValueError, OSError):
return
if payload.get("stop_hook_active"):
return # 이미 한 번 붙잡았음 — 두 번은 안 함(덫·무한루프 방지). 그래도 안 적으면 그냥 통과.
edited = scan(payload.get("transcript_path", ""))
if not edited:
return
hint = Path(edited).name if edited != "코드" else "코드"
print(
json.dumps(
{
"decision": "block",
"reason": (
f"이번에 {hint} 고쳤는데 worklog 를 안 남겼어. "
f'`python z-my-docs/work-log/gen_worklog.py add "<한 일 한 줄>"` 로 '
f"한 줄 남기고 끝내. 진짜 남길 것 없으면 그냥 다시 멈추면 통과됨."
),
},
ensure_ascii=False,
)
)
def _selftest():
import tempfile
def w(objs):
f = tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False, encoding="utf-8")
for o in objs:
f.write(json.dumps(o) + "\n")
f.close()
return f.name
def tu(name, **inp):
return {"message": {"content": [{"type": "tool_use", "name": name, "input": inp}]}}
root = Path(__file__).resolve().parents[2]
edit = tu("Edit", file_path=str(root / "1_backend/src/x.py"))
log = tu("Bash", command='python z-my-docs/work-log/gen_worklog.py add "x"')
pslog = tu("PowerShell", command='python z-my-docs/work-log/gen_worklog.py add "x"')
logedit = tu("Edit", file_path=str(root / "z-my-docs/work-log/2026-07/x.tsv"))
memedit = tu("Edit", file_path="C:/Users/user/.claude/projects/foo/memory/bar.md")
r = scan(w([edit]))
assert r and r.endswith("x.py"), r # 레포 안 편집만 → 미기재
assert scan(w([edit, log])) is None # 편집 뒤 로그 → 통과
assert scan(w([edit, pslog])) is None # PowerShell 툴로 남긴 로그도 인정(윈도우 세션)
assert scan(w([log, edit])) is not None # 로그 뒤 편집 → 미기재
assert scan(w([logedit])) is None # worklog 편집만 → 무시
assert scan(w([memedit])) is None # 레포 밖(메모리) 편집 → 무시
assert scan(w([])) is None # 빈 트랜스크립트 → 통과
print("selftest ok")
if __name__ == "__main__":
if sys.argv[1:2] == ["--selftest"]:
_selftest()
else:
main()