Session-model (owner chốt "đối ứng đúng chính xác như hub"):
- Form hub: _context-s-<N>.md (STOCK-map + FLOW append-only + STOCK-touched)
+ _pause-<i>/_tiep-<i> marker 5-trường + _snapshot-<i> + _end (thay closed.md)
- Port /snapshot + scripts/session_scaffold.py (near-verbatim)
+ scripts/session_ctx.py TRIMMED CÓ KHAI (chỉ machine-block + secrets-sweep;
KHÔNG port jsonl/overhead/cap-getter vì chưa có caller = ghost-wire)
- session-2 migrate sang form hub; session-1 giữ legacy (FROZEN)
Sàn-3 ORPHAN-L:
- DUAL-ACCEPT hub + legacy; glob pause-* KHÔNG khớp _pause-1.md nên không đếm đôi
- VÁ bug có sẵn từ S146: chốt-kết ĐÓNG TRỌN thư-mục (bản cũ c=1 chỉ tha 1 pause,
lệch chính câu session-end §6.3-bis vẫn nói "mọi pause")
- Fault-inject 10/10 hai chiều + anti-Goodhart
/check-email:
- Wire 4 CỬA phiên (session-start/tiep/session-end/pause), 2 CHẾ-ĐỘ:
DÒ ~5ms ở cửa dừng-nối (ràng buộc BINDING hub goi-chot §3) ⟂ KÉO ở bookend
- DÒ quét 2 kênh + định tuyến: outbox/se -> /check-email · outbox/all -> /adap-apply
- STAGE-2: 10 thư fan-out verify 2 tuyến 10/10 -> inbox/ai_infra/; backlog root = 0
HANDOFF re-stamp: #1 ĐÓNG (trio đã chạy S144) · #2 đổi trục · #3 anh chốt (a)
+ 4 mục mới (13)-(16); carry #15/#17 đóng, #16 đóng nửa (khai rõ vế còn hở)
H24 tick S147->S148: counter 21->22 CLEAN
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""session_scaffold.py -- scaffold 1 logic-session folder + _context skeleton.
|
|
|
|
SE PORT of AI_INFRA scripts/session_scaffold.py (adopted S148, 2026-07-24, owner
|
|
"doi ung dung chinh xac nhu hub"). Near-verbatim: the logic is repo-agnostic; only
|
|
the template CONTENT differs (SE STOCK-map rows). Kept stdlib-only + same CLI so a
|
|
future hub change can be re-pulled by diff instead of re-derived.
|
|
|
|
Tach LOGIC-session khoi VAT-LY window: tao `.claude/sessions/session-<N>/` +
|
|
`_context-s-<N>.md` tu template `.claude/templates/session-context-template.md`.
|
|
|
|
Design pins:
|
|
- path HARD-PIN: `.claude/sessions/session-<N>/` (KHONG configurable -- uniform cross-repo).
|
|
- ts-moc = `git log -1 --format=%cI` (committer ISO-8601 cua HEAD), KHONG datetime.now
|
|
tuy-tien; nguon-ts ghi thang vao file (audit). Fallback datetime.now CHI khi git down,
|
|
tag ro "FALLBACK".
|
|
- Idempotent: folder ton tai -> bao + KHONG de `_context` (chi tao neu thieu).
|
|
- Cap so <N> KHONG phai viec cua script nay -- caller truyen vao. Doc-quyen cap so =
|
|
/session-start (session-start.md BUOC 0.8); /pause va /tiep chi tao folder cho <N> dang mo.
|
|
|
|
stdlib-only . Python 3.11+ . UTF-8 no-BOM . LF.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
SESSIONS_SUBPATH: tuple[str, ...] = (".claude", "sessions")
|
|
TEMPLATE_SUBPATH: tuple[str, ...] = (".claude", "templates", "session-context-template.md")
|
|
|
|
|
|
def repo_root() -> Path:
|
|
"""Repo root via `git rev-parse --show-toplevel`; fallback = parent-of-scripts/."""
|
|
try:
|
|
out = subprocess.run(
|
|
["git", "rev-parse", "--show-toplevel"],
|
|
capture_output=True, text=True, check=True,
|
|
)
|
|
root = out.stdout.strip()
|
|
if root:
|
|
return Path(root)
|
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
pass
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def head_ts() -> tuple[str, str]:
|
|
"""Return (ts, source). Prefer HEAD committer ISO-8601, tagged with short-sha for audit."""
|
|
try:
|
|
out = subprocess.run(
|
|
["git", "log", "-1", "--format=%cI%x09%h"],
|
|
capture_output=True, text=True, check=True,
|
|
).stdout.strip()
|
|
if out:
|
|
ts, _, sha = out.partition("\t")
|
|
ts, sha = ts.strip(), sha.strip()
|
|
if ts:
|
|
return ts, f"git log -1 --format=%cI @ HEAD {sha}"
|
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
pass
|
|
return (
|
|
datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
"datetime.now(UTC) FALLBACK (git unavailable -- non-deterministic)",
|
|
)
|
|
|
|
|
|
def render(template_text: str, n: int, ts: str, ts_source: str) -> str:
|
|
"""Fill placeholders with str.replace (NOT str.format) so literal `{`/`}` in the
|
|
template's JSON schema example survive untouched."""
|
|
return (
|
|
template_text
|
|
.replace("{{N}}", str(n))
|
|
.replace("{{TS}}", ts)
|
|
.replace("{{TS_SOURCE}}", ts_source)
|
|
)
|
|
|
|
|
|
def _rel(p: Path, root: Path) -> str:
|
|
try:
|
|
return p.relative_to(root).as_posix()
|
|
except ValueError:
|
|
return str(p)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(
|
|
prog="session_scaffold.py",
|
|
description="Scaffold 1 logic-session folder + _context skeleton "
|
|
"(.claude/sessions/session-<N>/). Idempotent; safe to re-run.",
|
|
)
|
|
ap.add_argument("--session", type=int, required=True, metavar="N",
|
|
help="logic-session ID N (folder = session-<N>, regex ^session-\\d+$)")
|
|
ap.add_argument("--dry-run", action="store_true", help="print planned actions, write NOTHING")
|
|
args = ap.parse_args(argv)
|
|
|
|
n: int = args.session
|
|
if n < 0:
|
|
print(f"ERROR: --session must be a non-negative integer (got {n})", file=sys.stderr)
|
|
return 2
|
|
|
|
root = repo_root()
|
|
session_dir = root.joinpath(*SESSIONS_SUBPATH) / f"session-{n}"
|
|
context_path = session_dir / f"_context-s-{n}.md"
|
|
template_path = root.joinpath(*TEMPLATE_SUBPATH)
|
|
|
|
if not template_path.is_file():
|
|
print(f"ERROR: template not found: {template_path}", file=sys.stderr)
|
|
return 2
|
|
|
|
ts, ts_source = head_ts()
|
|
folder_exists = session_dir.exists()
|
|
context_exists = context_path.exists()
|
|
|
|
if args.dry_run:
|
|
print(f"[dry-run] session N = {n}")
|
|
print(f"[dry-run] ts (moc) = {ts}")
|
|
print(f"[dry-run] ts-source = {ts_source}")
|
|
print(f"[dry-run] template = {_rel(template_path, root)}")
|
|
print(f"[dry-run] session folder = {_rel(session_dir, root)} "
|
|
f"({'EXISTS' if folder_exists else 'would mkdir'})")
|
|
print(f"[dry-run] context file = {_rel(context_path, root)} "
|
|
f"({'EXISTS -> would SKIP (idempotent)' if context_exists else 'would write'})")
|
|
print("[dry-run] NOTHING written.")
|
|
return 0
|
|
|
|
created_folder = False
|
|
if not folder_exists:
|
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
created_folder = True
|
|
|
|
if context_exists:
|
|
print(f"[idempotent] {_rel(context_path, root)} EXISTS -- NOT overwriting.")
|
|
if created_folder:
|
|
print(f"[note] folder {_rel(session_dir, root)} was missing -> created "
|
|
f"(unusual: context present without folder).")
|
|
return 0
|
|
|
|
template_text = template_path.read_text(encoding="utf-8")
|
|
rendered = render(template_text, n, ts, ts_source)
|
|
with open(context_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
fh.write(rendered)
|
|
|
|
print(f"[created] folder = {_rel(session_dir, root)} ({'new' if created_folder else 'existed'})")
|
|
print(f"[created] context = {_rel(context_path, root)}")
|
|
print(f"[created] ts(moc) = {ts} (source: {ts_source})")
|
|
print("[note] NOT committed (commit theo nghi-thuc /pause | /snapshot | /session-end).")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|