[CLAUDE] Docs: S153 closeout — adopt TRỌN Ctx soft-memory + OWNER-VETO 3-vai + RENAME hub-form (squash 22 wal:)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m39s

Pipeline anh lệnh: /check-email + /adap-apply -> /fable-real invest (spec 9-task/24-checklist)
-> hmw Opus-5-MAX 2 đợt + lead-solo T6-T8 -> checklist verify.

Thư ① 9c909007 (model-default Opus 5 MAX): TRỌN — filing 2-tuyến + probe-sau-restart
2-nguồn claude-opus-5[1m] + thư hub d432f5eab5cd + caveat (e) đóng.

Thư ② 0df10df4 (pause-tiep Ctx soft-memory, sàn 7-khoản): EXECUTED
- máy mind-check: scripts/session_ctx.py +542 dòng (7 phép + 2 phụ, 4 mức, draft/closed/--root/--session PIN, exit 0/1/3)
- template .claude/templates/session-mind-template.md + wire 4 lệnh (pause C-bis+2bis+§1-carve-out, tiep §2.6+§3-ter, session-start Phase 3.5, session-end §L.b(l)+6.3-bis)
- memory-budget: mind_ctx_kb=32 + tiep_reload + ĐÓNG ghost-wire session_ctx_kb (H18 class-2 sống S146->S153)
- fault-inject 18/18 + 2 fix hậu-T4 (FI 6/6): GAP-#9 closed-mode E-0-con-trỏ CỜ->TRƯỢT (chặn gradient
  thưởng xoá-con-trỏ-trung-thực, họ bẫy F-1 của chính thư hub) · secrets-sweep +--root parity
- số ĐO THẬT pause +117ms thay ước lệch 2.2-5.4x

OWNER 2 quyết trong-phiên:
- (38) VETO: 1-vai -> hub-form 3-vai (ctx-curator @pause + carve-out pause §1 · ctx-verifier @tiep · ctx-audit @2-bookend)
- RENAME theo chuẩn hub/sisters để audit chéo: ctx-pause-check->ctx-curator, ctx-reload->ctx-verifier
  (đo đĩa 3 nhà; hub≠sister => SE theo HUB-form, tiền lệ S149). Roster 20->23, VALID_ROLES 23.
  Gap còn lại: ring5-audit (V5 truth-floor) hub CÓ, SE+VIPIX chưa -> slot (41).

Bookend 2 đầu (12 spawn) + trio auto 1 vòng:
- @open: H1 7f · H2 4f · H24 5+3 FLAG · ring1 28Đ/4T · ring2 8/8Đ + M-1 MISS thật · trio eval-MIXED-14/refine-3A-7B-4N/audit-25Đ-3T (re-chạy 7 instrument, 24 số byte-exact)
- @close: H1 6f · H2 8f (baseline S154 = 16/10, tự sửa bản ~23/11) · H24 4+2 FLAG · ring1 47Đ/5T/1KC/53 · ring2 6/6Đ + 21 phép phụ
- lead xử 11 mục: 9 FLAG H24 + 3 RESIDUAL ring2 + F-r2 (plugin 3 hard-site) + M-1 cả 2 persona + M9 14 sổ diary + F-3/F-5/F-6 + nit Sàn-5 + BÁC F-4 (fan-out không vào _index)

#53 x14-cận-dưới, 0 mất dữ liệu (WRITE-lane derive-from-body · Agent-resume · SendMessage-resume x6).
Datum: trio return-only hết miễn nhiễm (3/3 CLEAN -> 1/3). 4-A suýt xảy ra thật (bản committed của 2 file H24 là bản cắt 24%/48%).
completeness-gate: 5 vòng đủ · phép ĐẠT 4 / TRƯỢT 0 / vacuous 0.
State: Mig 67 · 89 bảng · 532 test · gotcha 83 · Sub-agents 23 · counter 27 · bundle FROZEN · governance-only 0 prod-code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-26 16:10:54 +07:00
parent c2d01f0dce
commit 11db5efdf8
88 changed files with 5750 additions and 64 deletions

View File

@ -1,7 +1,10 @@
#!/usr/bin/env python3
"""session_ctx.py -- session-model derive helpers (SE port, S148 2026-07-24).
SE PORT of AI_INFRA scripts/session_ctx.py -- 🔴 TRIMMED ON PURPOSE, KHAI RO:
SE PORT of AI_INFRA scripts/session_ctx.py -- [!] TRIMMED ON PURPOSE, KHAI RO:
(S153 1-char fix: the marker here USED to be a U+1F534 emoji = the ONLY byte that broke this
file's OWN declared "ASCII-only source" invariant at the bottom of this docstring. Docstring
only -- 0 behaviour delta for machine-block / secrets-sweep.)
hub's original (~26 KB) also carries jsonl transcript parsing, anh-message containment
verification, token-overhead accounting and a context-cap getter. SE ports only the TWO
capabilities its rituals actually call:
@ -42,6 +45,19 @@ _RUNID_RE = re.compile(r'\b(wf_[A-Za-z0-9_-]+)')
SECRET_PATTERNS = ('voyage', 'sk-', 'gitea_pat', 'cfut_')
_SECRET_RES = [(p, re.compile(r'\b' + re.escape(p), re.I)) for p in SECRET_PATTERNS]
# CANONICAL SINGLE-TRUTH for the `ts:` KEY-LINE probe (S153, ctx soft-memory 0df10df4).
# [!] ANY future reader that needs "the most recent session marker timestamp" (the C10-LATENESS
# gap in scripts/governance-detectors.ps1 openly declares it has "no reliable source" of ts)
# MUST import THIS constant -- do NOT hand-roll a second ts regex anywhere. Two regexes = two
# truths, which is precisely what rao-2 of the _mind rule-block forbids.
# WHY the probe exists at all: SE markers `_pause-<i>.md` / `_tiep-<i>.md` / `_snapshot-<k>.md`
# carry `ts:` as their FIRST field BY DESIGN, so a future latest-marker reader will grep `_*`
# without filtering by filename => a `ts:` key line inside `_mind-s-<N>.md` would be read as a
# marker timestamp. Hence `_mind` content must never contain one.
# Matches a KEY line only (line-start, optional indent, optional space before colon); a ts that
# lives INSIDE a heading ("## MIND-1 -- 2026-07-26T...") is NOT a key line and must not match.
_TS_KEY_RE = re.compile(r'^[ \t]*ts[ \t]*:', re.I | re.M)
# ---------------------------------------------------------------------------
# git helpers
@ -176,6 +192,497 @@ def secrets_sweep(session_dir):
return hits
# ---------------------------------------------------------------------------
# (c) mind-check -- gate for the SOFT-memory file _mind-s-<N>.md
# S153, adopt of broadcast 0df10df4 (ctx soft-memory). 7 standard checks + 2 aux.
# 4 levels per check: dat / TRUOT / co / bo-qua-co-khai.
# exit 0 = 0 TRUOT | 1 = >=1 TRUOT | 3 = config-or-state (fail-loud, never a default).
# ---------------------------------------------------------------------------
_MIND_RULES_END = '<!-- MIND-RULES-END -->'
_MIND_TOP_MARKER = '<!-- MIND-TOP -->'
# Real blocks only: `^## MIND-<digits>`. The rule-block schema writes `## MIND-<k>` with a
# literal `<k>` placeholder precisely so this parser cannot false-match it (same trick as
# .claude/templates/session-context-template.md ENTRY-SCHEMA).
_MIND_BLOCK_RE = re.compile(r'^##[ \t]+MIND-(\d+)\b', re.M)
# rao-1: anh's VERBATIM words live in ONE place only (_context FLOW `> anh:`); the soft file
# may only POINT at `PAUSE-<k>`. Copying them here would fork the single-source.
_ANH_LINE_RE = re.compile(r'^[ \t]*>[ \t]*anh[ \t]*:', re.I | re.M)
# rao-3 exception: the ONLY accepted masked-secret label, spelled with diacritics exactly as the
# rule-block emits it. Non-ASCII expressed as \uXXXX so the SOURCE stays ASCII (module header).
# `[da che <8-hex>]` (ASCII fallback) is DELIBERATELY NOT accepted: on a secret gate an
# over-strict miss costs an edit, a lenient miss costs a permanent leak (blocks are immutable).
_MASK_LABEL_RE = re.compile(r'\[[ \t]*\u0111\u00e3[ \t]+che[ \t]+[0-9a-fA-F]{8}[ \t]*\]')
# aux-9 structure: A-E section labels. Form FAMILY (not one hardcoded shape) because the template
# is authored in a PARALLEL lane -- this code binds to the SPEC markers (MIND-RULES-*/MIND-TOP/
# `## MIND-<k>`), and stays permissive about how A..E are decorated. Accepted:
# `### A -- x` | `**A.** x` | `- **A** -- x` | `A) x` | `#### A: x`
_SECTION_LETTER_RE = re.compile(
r'^[ \t]{0,3}(?:#{2,6}[ \t]*)?(?:[-*][ \t]+)?(?:\*\*[ \t]*)?([A-E])(?:[ \t]*\*\*)?'
r'[ \t]*[.):\u2014-]',
re.M)
# aux-9: every idea in section D must carry ONE of the 4 stance labels (moi-neu / dang-cai /
# gan-chot / treo-cho-anh) -- \uXXXX-escaped, ASCII source.
_D_LABELS = ('m\u1edbi-n\u00eau', '\u0111ang-c\u00e3i', 'g\u1ea7n-ch\u1ed1t', 'treo-ch\u1edd-anh')
# An empty section must be DECLARED empty -- `(trong ...` with the diacritic o-circumflex-acute.
_EMPTY_DECL_RE = re.compile(r'\([ \t]*tr\u1ed1ng')
_BULLET_RE = re.compile(r'^[ \t]*(?:[-*+][ \t]+|\d+[.)][ \t]+)')
_BACKTICK_RE = re.compile(r'`([^`\n]+)`')
# Self-DECLARED counters (session-end writes them into `_end`). CROSS-CHECK ONLY -- never a
# counting source; disk wins, a mismatch is an INFO flag (hub measured real under-declaration).
_SELF_DECL_RES = (('mind-blocks', re.compile(r'mind-blocks[ \t]*[=:][ \t]*(\d+)')),
('_pause', re.compile(r'_pause[ \t]*[=:][ \t]*(\d+)')))
DAT = 'dat'
TRUOT = 'TRUOT'
CO = 'co'
SKIP = 'bo-qua-co-khai'
class MindConfigError(Exception):
"""Config / state error -> exit 3. Raised INSTEAD of assuming a default value."""
def _ascii_safe(s, limit=110):
"""ASCII-only rendering of an excerpt. A VN-diacritic line printed to a cp1252 console
raises UnicodeEncodeError = the gate would DIE mid-check; backslash-escape instead."""
s = s.strip().replace('\t', ' ')
if len(s) > limit:
s = s[:limit] + '...'
return s.encode('ascii', 'backslashreplace').decode('ascii')
def _is_int(v):
return isinstance(v, int) and not isinstance(v, bool)
def _require_budget_kb(repo_root, keys):
"""{key: cap_in_BYTES} read LIVE from .claude/agent-memory/memory-budget.json (value is KB,
reader multiplies by 1024). Missing file / missing key / non-int -> MindConfigError.
NO DEFAULT, ever (house rule, h24_cadence._note): a hardcoded ceiling would silently
re-create the drift that single-source config exists to prevent. The message NAMES the
missing key -- a nameless 'config error' sends the reader hunting."""
p = Path(repo_root) / '.claude' / 'agent-memory' / 'memory-budget.json'
if not p.is_file():
raise MindConfigError('config absent: %s' % p)
try:
data = json.loads(p.read_text(encoding='utf-8'))
except (OSError, ValueError) as e:
raise MindConfigError('config unreadable: %s (%s)' % (p, e))
missing = [k for k in keys if not _is_int(data.get(k))]
if missing:
raise MindConfigError('missing key(s) %s in %s -- CAM default (khuon H24-2 fail-loud)'
% (', '.join(missing), p))
return {k: int(data[k]) * 1024 for k in keys}
def _split_content_region(text):
"""(region, line_offset, rules_end_found) -- content = everything BELOW <!-- MIND-RULES-END -->.
enclosure use-vs-mention: the rule block ITSELF spells out the forbidden strings in order to
explain them, so a flat grep over the whole file would flag the very rules that forbid them
(citation-trap class). Marker absent => scan the WHOLE file + raise a flag: a file without the
marker is off-template, and the safe failure direction is LOUD, not lenient."""
i = text.find(_MIND_RULES_END)
if i < 0:
return text, 0, False
nl = text.find('\n', i)
start = len(text) if nl < 0 else nl + 1
return text[start:], text.count('\n', 0, start), True
def _parse_blocks(region, line_offset):
"""Ordered-by-POSITION list of {num, line, text}. Newest-on-TOP => blocks[0] is the newest.
(Opposite direction from _context FLOW, which appends newest-at-BOTTOM.)"""
ms = list(_MIND_BLOCK_RE.finditer(region))
out = []
for i, m in enumerate(ms):
end = ms[i + 1].start() if i + 1 < len(ms) else len(region)
out.append({'num': int(m.group(1)),
'line': line_offset + region.count('\n', 0, m.start()) + 1,
'text': region[m.start():end]})
return out
def _block_sections(block_text):
"""{letter: section_text} for the FIRST occurrence of each A-E label (see form family)."""
found = [(m.group(1), m.start()) for m in _SECTION_LETTER_RE.finditer(block_text)]
secs = {}
for i, (letter, pos) in enumerate(found):
end = found[i + 1][1] if i + 1 < len(found) else len(block_text)
secs.setdefault(letter, block_text[pos:end])
return secs
def _pointer_tokens(sec_text):
"""Backticked path-ish tokens in section E (`verdict + con-tro` per line)."""
toks = []
for m in _BACKTICK_RE.finditer(sec_text):
t = m.group(1).strip()
if not t or ' ' in t or '\t' in t or '/' not in t:
continue
if t.startswith('http://') or t.startswith('https://'):
continue
toks.append(t)
return toks
def _classify_pointer(token, repo_root):
"""ok | pending | broken | placeholder (F-1 three-state, + declared 4th for placeholders)."""
if any(c in token for c in '<>*{}'):
return 'placeholder'
raw = token[2:] if token.startswith('./') else token
p = Path(raw)
if not p.is_absolute():
p = Path(repo_root) / raw
if p.exists():
return 'ok'
return 'pending' if p.parent.is_dir() else 'broken'
def _count_pause_on_disk(session_dir):
"""p = DISK count, dual-accept both marker forms.
`_pause-<i>.md` (hub form, S148+) + `pause-*.md` (legacy <=S147). The glob `pause-*` does NOT
match `_pause-1.md` (leading underscore) => adding them cannot double-count; same
life-saving property the ORPHAN-L probe relies on (tiep.md:93)."""
session_dir = Path(session_dir)
if not session_dir.is_dir():
return 0, 0, 0
hub = len([p for p in session_dir.glob('_pause-*.md') if p.is_file()])
legacy = len([p for p in session_dir.glob('pause-*.md') if p.is_file()])
return hub + legacy, hub, legacy
def _self_declared(session_dir):
"""{name: n} parsed from `_end` (e.g. `markers: _pause=1 . mind-blocks=2`). Cross-check ONLY."""
end = Path(session_dir) / '_end'
if not end.is_file():
return None
text = end.read_text(encoding='utf-8', errors='replace')
out = {}
for name, rx in _SELF_DECL_RES:
m = rx.search(text)
if m:
out[name] = int(m.group(1))
return out
def _chk(checks, cid, name, level, detail=()):
checks.append({'id': cid, 'name': name, 'level': level, 'detail': list(detail)})
# --- the three rao (barriers), shared by draft-mode and full-mode -------------------------
def _check_rao(checks, region, line_offset, label):
hits = [(line_offset + region.count('\n', 0, m.start()) + 1,
region[m.start():region.find('\n', m.start()) if region.find('\n', m.start()) >= 0
else len(region)])
for m in _ANH_LINE_RE.finditer(region)]
_chk(checks, '1', 'rao-1 loi-anh-verbatim', TRUOT if hits else DAT,
['%s:%d %s' % (label, ln, _ascii_safe(txt)) for ln, txt in hits]
or ['0 hit `> anh:` duoi RULES-END (verbatim chi song o _context FLOW)'])
ts_hits = [line_offset + region.count('\n', 0, m.start()) + 1
for m in _TS_KEY_RE.finditer(region)]
_chk(checks, '2', 'rao-2 ts-key-line', TRUOT if ts_hits else DAT,
['%s:%d key-line `ts:` (canonical regex session_ctx.py:_TS_KEY_RE)' % (label, ln)
for ln in ts_hits]
or ['0 key-line `ts:` (heading-embedded ts is NOT a key line -- by design)'])
sec_hits = []
for i, line in enumerate(region.splitlines(), 1):
if _MASK_LABEL_RE.search(line):
continue
for pat, rx in _SECRET_RES:
if rx.search(line):
sec_hits.append((line_offset + i, pat))
break
_chk(checks, '3', 'rao-3 secret Category-5', TRUOT if sec_hits else DAT,
# snippet DELIBERATELY not printed: this gate must not itself leak the secret into a log.
['%s:%d [%s] (snippet KHONG in -- chong ro-ri log)' % (label, ln, pat)
for ln, pat in sec_hits]
or ['0 hit / %d pattern; ngoai-le nhan che 8-hex duoc mien' % len(SECRET_PATTERNS)])
def _check_pointer(checks, blocks, repo_root, closed):
"""(4) F-1 three-state on the pointers of section E of the TOP block."""
if not blocks:
_chk(checks, '4', 'con-tro E block-top', SKIP, ['0 block => 0 con-tro de cham'])
return
top = blocks[0]
secs = _block_sections(top['text'])
e_text = secs.get('E')
if e_text is None:
_chk(checks, '4', 'con-tro E block-top', SKIP,
['block MIND-%d: khong tim thay muc E (cau-truc do aux-9 cham)' % top['num']])
return
if _EMPTY_DECL_RE.search(e_text) and not _pointer_tokens(e_text):
_chk(checks, '4', 'con-tro E block-top', SKIP,
['block MIND-%d: muc E khai `(trong ...)` => 0 con-tro (khai, khong xanh-im)'
% top['num']])
return
toks = _pointer_tokens(e_text)
if not toks:
# GAP-#9 (T4 fault-inject @S153): closed-mode MUST fail here. If this stayed CO at close,
# deleting a truthful pending pointer (-> this branch, exit 0) would be CHEAPER than keeping
# it (pending -> TRUOT at close) => the gradient rewards the WRONG path (family of F-1).
lvl = TRUOT if closed else CO
_chk(checks, '4', 'con-tro E block-top', lvl,
['block MIND-%d: muc E co noi-dung ma 0 con-tro backtick-path%s'
% (top['num'],
' => TRUOT o cua DONG (chan duong-re-nhat-de-xanh = xoa con-tro)' if closed else '')])
return
detail, worst = [], DAT
for t in toks:
state = _classify_pointer(t, repo_root)
if state == 'ok':
detail.append('dat %s' % _ascii_safe(t, 90))
elif state == 'placeholder':
detail.append('%s %s (placeholder <>/* -- khong resolve duoc, khai)' % (SKIP, _ascii_safe(t, 90)))
elif state == 'pending':
# anti-Goodhart (PHAN B #9): the cheapest way to go green must be the RIGHT way.
detail.append('%s %s -- cha CO, file chua sinh => CHO file sinh o cua DONG; '
'CAM xoa con-tro de lam xanh'
% ((TRUOT if closed else CO), _ascii_safe(t, 90)))
if closed:
worst = TRUOT
elif worst != TRUOT:
worst = CO
else:
detail.append('%s %s -- cha KHONG co => hong that (moi mode)' % (TRUOT, _ascii_safe(t, 90)))
worst = TRUOT
detail.append('KHAI (nguyen van thu SS4): TEN-TEP-SAI trong thu-muc THAT doc thanh '
'"dang-cho" o cua mo, toi cua dong moi siet -- day la KE, khong phai tinh nang')
_chk(checks, '4', 'con-tro E block-top', worst, detail)
def _check_numbering(checks, blocks):
"""(5) unique + top >= every number below. NO count formula is enforced: the house may change
how numbers are allocated and this check must still work (spec Muc-3 of the broadcast)."""
if not blocks:
_chk(checks, '5', 'so-hieu block', SKIP, ['0 block'])
return
nums = [b['num'] for b in blocks]
dups = sorted({n for n in nums if nums.count(n) > 1})
top = nums[0]
detail = ['day (tren->duoi): %s' % ', '.join(str(n) for n in nums)]
level = DAT
if dups:
level = TRUOT
detail.append('%s so-hieu TRUNG: %s' % (TRUOT, ', '.join(str(d) for d in dups)))
below_max = max(nums[1:]) if len(nums) > 1 else None
if below_max is not None and top < below_max:
level = TRUOT
detail.append('%s block top MIND-%d < so duoi MIND-%d (moi-nhat-o-TREN bi vi pham)'
% (TRUOT, top, below_max))
if level == DAT:
detail.append('duy-nhat + top >= moi so duoi (khong neo cong-thuc dem)')
_chk(checks, '5', 'so-hieu block', level, detail)
def _check_structure(checks, blocks, label):
"""aux-9: 5 sections A-E present (or declared empty) + every D idea carries a stance label."""
if not blocks:
_chk(checks, '9', 'aux cau-truc A-E + nhan D', SKIP, ['0 block'])
return
level, detail = DAT, []
for b in blocks:
secs = _block_sections(b['text'])
missing = [c for c in 'ABCDE' if c not in secs]
if missing:
level = TRUOT
detail.append('%s MIND-%d (%s:%d) thieu muc: %s [dang nhan: `### A --` | `**A.**` | '
'`- **A** --` | `A)` | `#### A:`]'
% (TRUOT, b['num'], label, b['line'], ', '.join(missing)))
continue
d_text = secs['D']
d_lines = d_text.splitlines()[1:]
# An empty-declaration line is a DECLARATION, not an idea -- exempt it even when it is
# written as a bullet (`- (trong -- khai)`), otherwise the rule "muc trong ghi (trong --
# khai)" would itself be un-satisfiable. Caught by the clean fixture, not by reasoning.
ideas = [(i, ln) for i, ln in enumerate(d_lines, 1)
if _BULLET_RE.match(ln) and not _EMPTY_DECL_RE.search(ln)]
if not ideas:
if not _EMPTY_DECL_RE.search(d_text):
level = TRUOT
detail.append('%s MIND-%d muc D: 0 y ma cung KHONG khai `(trong ...)`'
% (TRUOT, b['num']))
continue
bad = [ln for _, ln in ideas if not any(lb in ln for lb in _D_LABELS)]
if bad:
level = TRUOT
detail.append('%s MIND-%d muc D: %d/%d y thieu nhan {moi-neu|dang-cai|gan-chot|'
'treo-cho-anh} -- vd: %s'
% (TRUOT, b['num'], len(bad), len(ideas), _ascii_safe(bad[0], 70)))
if level == DAT:
detail.append('%d block: 5 muc A-E du + moi y muc D co nhan' % len(blocks))
_chk(checks, '9', 'aux cau-truc A-E + nhan D', level, detail)
def _check_context_cap(checks, session_dir, session_n, cap_bytes):
"""aux-8: the FIRST REAL READER of session_ctx_kb (closes the H18 ghost-wire: the key's own
note asked its future reader to (a) read the key (b) x1024 (c) drop the caveat line)."""
ctx = Path(session_dir) / ('_context-s-%d.md' % session_n)
if not ctx.is_file():
_chk(checks, '8', 'aux tran _context', SKIP, ['%s: khong co tep' % ctx.name])
return
size = ctx.stat().st_size
over = size > cap_bytes
_chk(checks, '8', 'aux tran _context', CO if over else DAT,
['%s = %dB vs session_ctx_kb x1024 = %dB%s'
% (ctx.name, size, cap_bytes, ' => VUOT' if over else '')])
def mind_check(session_n, root=None, draft=None, force_closed=False):
"""Gate _mind-s-<N>.md. Returns (rc, lines). Raises MindConfigError for rc=3 cases.
`--session` is PINNED by the caller on purpose (no default-latest): two windows running side
by side would race for "the latest" and audit each other's file."""
repo_root = Path(root or ROOT)
session_dir = repo_root / '.claude' / 'sessions' / ('session-%d' % session_n)
canonical = session_dir / ('_mind-s-%d.md' % session_n)
closed = bool(force_closed) or (session_dir / '_end').is_file()
mode = 'draft' if draft else ('closed' if closed else 'open')
checks = []
out = ['[mind-check] session-%d | mode=%s | root=%s' % (session_n, mode, repo_root)]
if draft:
dpath = Path(draft)
if not dpath.is_file():
alt = repo_root / draft
if not alt.is_file():
raise MindConfigError('draft file absent: %s' % dpath)
dpath = alt
# Draft-mode consumes NO mind_ctx_kb (checks 6/7 are skipped) => requiring it would be a
# fail-loud on a key this run never reads. It DOES read session_ctx_kb (aux-8).
caps = _require_budget_kb(repo_root, ['session_ctx_kb'])
text = dpath.read_text(encoding='utf-8', errors='replace')
region, line_offset, _ = _split_content_region(text)
out.append(' draft : %s (%dB)' % (dpath, dpath.stat().st_size))
out.append(' canon : %s%s' % (canonical, '' if canonical.is_file() else ' (chua co)'))
_check_rao(checks, region, line_offset, dpath.name)
dblocks = _parse_blocks(region, line_offset)
on_disk = 0
if canonical.is_file():
creg, coff, _ = _split_content_region(
canonical.read_text(encoding='utf-8', errors='replace'))
on_disk = len(_parse_blocks(creg, coff))
if not dblocks:
_chk(checks, '5', 'so-hieu draft vs count-on-disk', TRUOT,
['draft khong co heading `## MIND-<n>` nao'])
else:
want = on_disk
got = dblocks[0]['num']
_chk(checks, '5', 'so-hieu draft vs count-on-disk', DAT if got == want else TRUOT,
['draft MIND-%d vs count-on-disk %d%s'
% (got, want, '' if got == want else ' => LECH (so-hieu = DEM block TRUOC chen)')])
_check_pointer(checks, dblocks, repo_root, closed=False)
_chk(checks, '6', 'bat-bien |block| vs p', SKIP, ['draft-mode: cham sau khi chen'])
_chk(checks, '7', 'tran mind_ctx_kb', SKIP,
['draft-mode: do tren tep THAT sau khi chen (khong doc mind_ctx_kb luot nay)'])
_check_context_cap(checks, session_dir, session_n, caps['session_ctx_kb'])
_check_structure(checks, dblocks, dpath.name)
return _render(out, checks, 'draft (kiem NHAP truoc khi chen -- loi khong bao gio vao tep)')
# ---- full mode -----------------------------------------------------------------------
if not canonical.is_file():
cands = sorted(p.name for p in session_dir.glob('*mind*')) if session_dir.is_dir() else []
why = ['thieu: %s' % canonical]
if not session_dir.is_dir():
why.append('thu-muc phien KHONG ton tai: %s' % session_dir)
# tep-sai-phai-NEU-TEN: "khong co gi de kiem" must never hide a typo'd filename.
why.append('glob `*mind*` trong thu-muc: %s'
% (', '.join(cands) if cands else '(0 ung vien)'))
if cands:
why.append('=> co ung vien TEN SAI o tren; canonical dung phai la %s' % canonical.name)
level = TRUOT if closed else SKIP
if closed:
why.append('closed-mode (co `_end`) => TRUOT: phien da dong ma khong co lop MEM')
else:
why.append('open-mode => bo-qua CO KHAI (chua doc memory-budget.json: khong co gi de do)')
_chk(checks, '0', 'ton tai _mind-s-%d.md' % session_n, level, why)
return _render(out, checks, 'full/%s (tep canonical vang)' % mode)
caps = _require_budget_kb(repo_root, ['mind_ctx_kb', 'session_ctx_kb'])
text = canonical.read_text(encoding='utf-8', errors='replace')
size = canonical.stat().st_size
region, line_offset, rules_end = _split_content_region(text)
out.append(' target: %s (%dB)' % (canonical, size))
if not rules_end:
_chk(checks, '0', 'enclosure %s' % _MIND_RULES_END, CO,
['marker VANG => quet TRON tep (huong hong NGHIEM); tep dang off-template'])
else:
_chk(checks, '0', 'enclosure %s' % _MIND_RULES_END, DAT,
['chi quet DUOI marker (use-vs-mention: khoi luat chua chinh chuoi bi cam)'])
if _MIND_TOP_MARKER not in text:
_chk(checks, '0b', 'marker %s' % _MIND_TOP_MARKER, CO,
['vang => khong biet chen block moi o dau (moi-nhat-o-TREN)'])
_check_rao(checks, region, line_offset, canonical.name)
blocks = _parse_blocks(region, line_offset)
_check_pointer(checks, blocks, repo_root, closed)
_check_numbering(checks, blocks)
p, hub, legacy = _count_pause_on_disk(session_dir)
n = len(blocks)
ok = n in (p, p + 1)
detail = ['|block| = %d ; p (DEM DIA) = %d [_pause-*.md=%d + pause-*.md=%d dual-accept]'
% (n, p, hub, legacy),
'ky vong |block| thuoc {%d, %d} (block-0 + 1 refresh/pause)' % (p, p + 1)]
decl = _self_declared(session_dir)
info = []
if decl is None:
detail.append('cross-check tu-khai: khong co `_end` => bo qua (dia van la nguon duy nhat)')
else:
for key, want in (('mind-blocks', n), ('_pause', p)):
if key in decl and decl[key] != want:
info.append('co-INFO lech tu-khai: `_end` %s=%d vs DIA %d (DIA THANG)'
% (key, decl[key], want))
detail.append('cross-check tu-khai `_end`: %s'
% (', '.join('%s=%d' % (k, v) for k, v in sorted(decl.items())) or '(0 field)'))
detail.extend(info)
_chk(checks, '6', 'bat-bien |block| vs p', TRUOT if not ok else (CO if info else DAT), detail)
cap = caps['mind_ctx_kb']
over = size > cap
d7 = ['%dB vs mind_ctx_kb x1024 = %dB (doc LIVE, 0 hardcode)%s'
% (size, cap, ' => VUOT' if over else '')]
if over:
if n < 3:
d7.append('TU-CHOI nen: chi %d block (<3) => giuong co vuot tran, KHONG nen' % n)
else:
d7.append('huong-dan: nen block CU NHAT (tru block-0 va block top); verbatim con o git')
_chk(checks, '7', 'tran mind_ctx_kb', CO if over else DAT, d7)
_check_context_cap(checks, session_dir, session_n, caps['session_ctx_kb'])
_check_structure(checks, blocks, canonical.name)
return _render(out, checks, 'full/%s' % mode)
def _render(out, checks, mode_note):
"""Deterministic 4-level report. rc = 1 iff >=1 TRUOT (co / bo-qua-co-khai never gate)."""
order = {'0': 0, '0b': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6, '6': 7, '7': 8,
'8': 9, '9': 10}
checks = sorted(checks, key=lambda c: order.get(c['id'], 99))
tally = {DAT: 0, TRUOT: 0, CO: 0, SKIP: 0}
for c in checks:
tally[c['level']] = tally.get(c['level'], 0) + 1
tag = '[phu-%s]' % c['id'] if c['id'] in ('8', '9') else '(%s)' % c['id']
out.append(' %-8s %-30s : %s' % (tag, c['name'], c['level']))
for d in c['detail']:
out.append(' - %s' % d)
rc = 1 if tally[TRUOT] else 0
out.append(' mode-note: %s' % mode_note)
out.append(' verdict: dat=%d %s=%d co=%d %s=%d => exit %d'
% (tally[DAT], TRUOT, tally[TRUOT], tally[CO], SKIP, tally[SKIP], rc))
return rc, out
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@ -191,6 +698,22 @@ def main(argv=None):
ss = sub.add_parser('secrets-sweep', help='Category-5 pre-commit gate over a session dir')
ss.add_argument('--session', type=int, required=True, metavar='N')
# S153 T4 #7 spec-drift fix: seam parity with mind-check (--root was mind-check-only, so the
# literal command in PHAN B #7 could not run as written; T4 had to copy the script into the tree).
ss.add_argument('--root', default=None, metavar='PATH',
help='repo root override = fault-inject seam (mirror mind-check --root)')
# S153 ctx soft-memory (0df10df4). --session is REQUIRED with NO default-latest on purpose:
# two windows running in parallel would race over "the latest" session.
mc = sub.add_parser('mind-check', help='gate the soft-memory file _mind-s-<N>.md')
mc.add_argument('--session', type=int, required=True, metavar='N')
mc.add_argument('--draft', default=None, metavar='FILE',
help='check a DRAFT block before inserting it (checks 1-3 + numbering; '
'skips 6/7) -- an error must never reach the immutable file')
mc.add_argument('--closed', action='store_true',
help='force closed-mode (auto-on when `_end` exists); flag = fault-inject seam')
mc.add_argument('--root', default=None, metavar='PATH',
help='repo root override = fault-inject seam (mirror -RepoRoot of the .ps1 family)')
args = ap.parse_args(argv)
@ -201,7 +724,8 @@ def main(argv=None):
return 0
if args.cmd == 'secrets-sweep':
session_dir = DEFAULT_SESSIONS_ROOT / f"session-{args.session}"
base = (Path(args.root) / '.claude' / 'sessions') if args.root else DEFAULT_SESSIONS_ROOT
session_dir = base / f"session-{args.session}"
hits = secrets_sweep(session_dir)
if not hits:
print(f"[secrets-sweep] session-{args.session}: 0 hit "
@ -213,6 +737,20 @@ def main(argv=None):
print(f" {h['file']}:{h['line']} [{h['pattern']}] {h['snippet']}", file=sys.stderr)
return 1
if args.cmd == 'mind-check':
try:
rc, lines = mind_check(args.session, root=args.root, draft=args.draft,
force_closed=args.closed)
except MindConfigError as e:
# exit 3 = config/state. FAIL-LOUD by design: measuring with an assumed ceiling would
# be worse than measuring nothing, because it looks like a pass.
print(f"[mind-check] CONFIG/STATE ERROR: {e}", file=sys.stderr)
return 3
stream = sys.stderr if rc else sys.stdout
for ln in lines:
print(ln, file=stream)
return rc
return 2