[CLAUDE] Docs: S135-S136 adap khung-nhip-nam-vong 7-item — tick da-diem-vao OR-guard+label-pin + force-fire consent + nhip-no-probe 3-ve + 2 detector H25 (fault-inject 9/9, FP W3 tail-adjacency classified) + residual do-token e0bb5bbf + luat non-JSON-args + sleep-khuon; AC A10/10 B6/6 C8/8; reviewer GO 0C/0M/2n

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-17 19:12:41 +07:00
parent 36ccf3bbe8
commit da349fc2c9
10 changed files with 413 additions and 39 deletions

View File

@ -897,6 +897,172 @@ else {
}
}
# ---------------------------------------------------------------------------
# H25-closeout-ritual (GAP-2) : did the last 3 session-close commits each leave a
# full ritual trace? A close that skips STATUS / HANDOFF / a NEW session-log / an
# agent-memory delta is silent governance drift (the memory-loss class).
#
# WINDOW, not per-commit (T4a, measured): the ritual delta does NOT sit inside the
# closeout commit alone. The Stop-hook 'wal: flush' carries agent-memory deltas in
# EARLIER commits, and STATUS/HANDOFF are often bumped a commit or two before the
# close. Scoring the closeout commit by itself FAILs healthy data 2/3. So each close
# is scored over the UNION of every commit since the PREVIOUS close: the half-open
# range (prev-close .. this-close], via git log --name-only.
#
# The NEW-session book uses --diff-filter=A (an ADDED path): editing an old session
# log is not opening this session's log. The other three books accept any touch
# (STATUS/HANDOFF/diaries are appended, not recreated).
#
# HONEST LIMIT (surfaced, NOT silenced): a tail-close commit whose previous close is
# its own parent AND the same session batch yields a 1-commit window; that batch's
# real ritual lives in the excluded lower-bound commit, so the tail reads incomplete
# = a FALSE POSITIVE. The canonical spec baseline calls for per-session-label folding
# of same-batch closes; this build is per-commit as handed. Do NOT special-case it to
# green a report -- classify the flag and let the owner choose the grouping.
#
# git-based, so it honours -RepoRoot (fault-injection runs on a temp git tree). A
# RepoRoot that is not a git work-tree SKIPs with a reason; a probe never crashes.
# ---------------------------------------------------------------------------
Write-Section 'H25-closeout-ritual (GAP-2)'
# Subject shape of a session-close commit. Held in a variable (used by -match on git
# SUBJECTS, never on any scanned file) so no prose copy exists to self-trip a matcher.
$CloseoutSubjectRx = '^\[CLAUDE\] Docs: S\d+.*(?:closeout|session-end)'
$closeouts = @()
$gitTree = Test-Path (Join-Path $RepoRoot '.git')
if ($gitTree) {
try {
$logRaw = & git -C $RepoRoot log --format='%H|%s' --max-count=400
} catch { $logRaw = $null }
if ($logRaw) {
foreach ($ln in $logRaw) {
if ([string]::IsNullOrWhiteSpace($ln)) { continue }
$parts = $ln -split '\|', 2
if ($parts.Count -lt 2) { continue }
if ($parts[1] -match $CloseoutSubjectRx) {
$lab = if ($parts[1] -match 'S(\d+)') { 'S' + $Matches[1] } else { 'S?' }
$closeouts += [pscustomobject]@{ Hash = $parts[0]; Short = $parts[0].Substring(0, 7); Label = $lab }
}
}
}
}
if (-not $gitTree) {
Write-Host ' [skip] RepoRoot is not a git work-tree - cannot score closeouts' -ForegroundColor DarkGray
}
elseif ($closeouts.Count -eq 0) {
Write-Host ' [skip] no commit matches the closeout subject shape - nothing to score' -ForegroundColor DarkGray
}
else {
$take = [Math]::Min(3, $closeouts.Count)
Write-Host (" closeouts found: {0} ; scoring {1} most-recent (union-window per close)" -f $closeouts.Count, $take)
for ($i = 0; $i -lt $take; $i++) {
$cur = $closeouts[$i]
if ($i + 1 -ge $closeouts.Count) {
Write-Host (" [skip] {0} {1}: no earlier close to open the window (oldest close in history) - not scored, not flagged" -f $cur.Short, $cur.Label) -ForegroundColor DarkGray
continue
}
$prev = $closeouts[$i + 1]
$range = ("{0}..{1}" -f $prev.Hash, $cur.Hash)
$union = @()
try { $u = & git -C $RepoRoot log --name-only --pretty=format: $range } catch { $u = $null }
foreach ($p in $u) { if (-not [string]::IsNullOrWhiteSpace($p)) { $union += $p.Trim() } }
$newSess = @()
try { $ns = & git -C $RepoRoot log --diff-filter=A --name-only --pretty=format: $range '--' 'docs/changelog/sessions/' } catch { $ns = $null }
foreach ($p in $ns) { if (-not [string]::IsNullOrWhiteSpace($p)) { $newSess += $p.Trim() } }
$missing = @()
if (-not ($union -contains 'docs/STATUS.md')) { $missing += 'docs/STATUS.md' }
if (-not ($union -contains 'docs/HANDOFF.md')) { $missing += 'docs/HANDOFF.md' }
if ($newSess.Count -eq 0) { $missing += 'docs/changelog/sessions/*(NEW,--diff-filter=A)' }
if (@($union | Where-Object { $_ -match '^\.claude/agent-memory/' }).Count -eq 0) { $missing += '.claude/agent-memory/**' }
if ($missing.Count -eq 0) {
Write-Host (" [ok] {0} {1}: union ({2}..{3}] has all 4 ritual books" -f $cur.Short, $cur.Label, $prev.Short, $cur.Short) -ForegroundColor DarkGray
}
else {
Write-Flag 'MED' ("git:{0} ({1})" -f $cur.Short, $cur.Label) `
("closeout-ritual gap: window ({0}..{1}] missing {2} of 4 books -> {3}" -f $prev.Short, $cur.Short, $missing.Count, ($missing -join ', ')) `
'the session-close (or its window since the prior close) must touch STATUS + HANDOFF + a NEW session-log + an agent-memory diary'
}
}
}
# ---------------------------------------------------------------------------
# H25-role-notebook (GAP-3) : every role that ACTUALLY RAN must leave a diary
# (.claude/agent-memory/<role>/MEMORY.md, byte>0). A role defined-but-never-spawned
# is NOT a defect, so the check is gated on evidence-of-run and measures CONTENT not
# NAME (S122): a sub-file whose prefix is not an exact roster role is LISTED as
# UNMAPPED, never flagged (no name-guessing). A zero-byte diary IS flagged
# (anti-Goodhart: an empty notebook is not a notebook; touching a file to green the
# check must still fail). Byte size via (Get-Item).Length -- Get-Content-count is
# FORBIDDEN (a no-BOM file miscounts VN text x2-3, bug E-010/S130).
# ---------------------------------------------------------------------------
Write-Section 'H25-role-notebook (GAP-3)'
$agentsDir = Join-Path $RepoRoot '.claude\agents'
$roster = @()
if (Test-Path $agentsDir) {
$roster = @(Get-ChildItem -Path $agentsDir -Filter *.md -File -ErrorAction SilentlyContinue |
Where-Object { $_.BaseName -ne 'README' } | ForEach-Object { $_.BaseName })
}
$rosterSet = @{}
foreach ($r in $roster) { $rosterSet[$r] = $true }
if ($roster.Count -eq 0) {
Write-Host ' [skip] no .claude/agents/*.md roster - nothing to check' -ForegroundColor DarkGray
}
else {
# Evidence-of-run: scan runs/*/sub-*.md and map each filename prefix to a roster role.
# Two-step greedy parse: 'sub-<role>-<numeric-idx>.md' first, else 'sub-<rest>.md'.
$runsDir = Join-Path $RepoRoot '.claude\workflows\runs'
$ran = @{} # role -> ran (exact roster match)
$unmapped = @{} # prefix -> seen (not an exact roster role)
if (Test-Path $runsDir) {
$subs = Get-ChildItem -Path $runsDir -Recurse -Filter 'sub-*.md' -File -ErrorAction SilentlyContinue
foreach ($s in $subs) {
$cand = $null
if ($s.Name -match '^sub-(.+)-\d+\.md$') { $cand = $Matches[1] }
elseif ($s.Name -match '^sub-(.+)\.md$') { $cand = $Matches[1] }
if ($null -eq $cand) { continue }
if ($rosterSet.ContainsKey($cand)) { $ran[$cand] = $true } else { $unmapped[$cand] = $true }
}
}
# has-run(role) = a mapped sub-file OR a diary directory already on disk (any size).
$memRoot = Join-Path $RepoRoot '.claude\agent-memory'
$okCount = 0; $flagged = 0; $inert = 0
foreach ($role in ($roster | Sort-Object)) {
$diary = Join-Path (Join-Path $memRoot $role) 'MEMORY.md'
$diaryExists = Test-Path -LiteralPath $diary
$hasRun = ($ran.ContainsKey($role)) -or $diaryExists
if (-not $hasRun) {
$inert++
continue
}
$bytes = if ($diaryExists) { (Get-Item -LiteralPath $diary).Length } else { -1 }
if ($diaryExists -and $bytes -gt 0) {
$okCount++
}
else {
$why = if (-not $diaryExists) { 'MISSING' } else { '0-byte (empty notebook)' }
Write-Flag 'MED' ("agent-memory/{0}/MEMORY.md" -f $role) `
("role-notebook gap: role '{0}' has run-evidence but its diary is {1}" -f $role, $why) `
("write a non-empty agent-memory/{0}/MEMORY.md (harvest the role slice at session-end)" -f $role)
$flagged++
}
}
Write-Host (" roster={0} ; diaries-ok={1} ; flagged={2} ; inert(defined-not-run)={3}" -f $roster.Count, $okCount, $flagged, $inert)
if ($unmapped.Count -gt 0) {
$ulist = ($unmapped.Keys | Sort-Object) -join ', '
Write-Host (" UNMAPPED sub-file prefixes ({0}) [INFORM, not flagged - measure content not name, S122]:" -f $unmapped.Count) -ForegroundColor DarkGray
Write-Host (" {0}" -f $ulist) -ForegroundColor DarkGray
}
}
# ---------------------------------------------------------------------------
# Summary + C4 self-exclusion audit (RUNTIME proof)
# ---------------------------------------------------------------------------

155
scripts/nhip-no-probe.ps1 Normal file
View File

@ -0,0 +1,155 @@
<#
.SYNOPSIS
nhip-no-probe.ps1 - Harness-25 "no hien thi" 3-part debt line (B1 section 3.3 floor).
.DESCRIPTION
ONE-LINE, READ-ONLY, ZERO-MUTATION debt probe. Called (never inlined) by the
session command surfaces (/pause, /tiep, session-start, session-end) so a stop
point always PRINTS the standing debt. Contract:
(1) NO-API, NO-MUTATION - only reads JSON + measures file bytes + lists dirs.
(2) exit 0 ALWAYS - a probe MUST NEVER block a stop point. Any exception ->
prints 'probe-loi (khong chan)' and still exit 0.
(3) PowerShell 5.1 compatible, ASCII-only script body (gotcha #30): every
output token is ASCII (NHIP-NO / kiem / light / deep / OVERDUE / over-cap /
run-chua-gom / CONFIG-THIEU / probe-loi) so a BOM-less .ps1 decoded under the
system ANSI codepage never mojibakes.
(4) NO hardcoded cadence/cap - light_every / deep_every / autoinject_cap_bytes are
READ FROM config keys. A MISSING h24_cadence key prints CONFIG-THIEU(h24_cadence)
IN-LINE (no assumed default, not swallowed into the generic probe-loi) - keeps
the no-default signal H24-2 depends on.
Output contract - EXACTLY one line:
NHIP-NO: kiem light a/e deep b/f <ok|OVERDUE:light|deep> | tran-bo-nho x over-cap | run-chua-gom y
* kiem = audit cadence: a = counter - last_audit.light_at_counter (light debt),
e = h24_cadence.light_every ; b/f = deep pair. OVERDUE when a>=e or b>=f.
* tran-bo-nho = x agent-memory/*/MEMORY.md files whose (Get-Item).Length exceeds
archive_gate.autoinject_cap_bytes (byte-measured FRESH, NOT the stale
'measured' snapshot; Get-Content-count is FORBIDDEN - bay E-010/S130).
* run-chua-gom = y orphan run-folders per the DUAL-ACCEPT rule ported verbatim
from tiep.md:40-53 (flat *-synthesis.md OR C8 harvest/*-synthesis.md,
each non-empty; retired form-3 NOT ported).
.PARAMETER RepoRoot
Repo root. Default = resolved one level up from this script (scripts/ -> repo root).
A non-existent RepoRoot is a probe error -> 'probe-loi (khong chan)' + exit 0.
.EXAMPLE
powershell.exe -ExecutionPolicy Bypass -File scripts/nhip-no-probe.ps1
#>
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
)
$ErrorActionPreference = 'Stop'
# Read a file's byte length via Get-Item (FORBIDDEN: Get-Content line/char count -
# a no-BOM file counted through Get-Content mis-sizes VN text x2-3, bug E-010/S130).
function Get-ByteLen {
param([string]$Path)
return (Get-Item -LiteralPath $Path -ErrorAction Stop).Length
}
try {
if (-not (Test-Path -LiteralPath $RepoRoot)) {
throw "RepoRoot not found: $RepoRoot"
}
# -----------------------------------------------------------------------
# Ve 1 - nhip-kiem (audit cadence debt). Numerators from .session-counter.json,
# denominators from memory-budget.json h24_cadence (READ, never hardcoded).
# -----------------------------------------------------------------------
$counterPath = Join-Path $RepoRoot '.claude\governance\.session-counter.json'
$budgetPath = Join-Path $RepoRoot '.claude\agent-memory\memory-budget.json'
$sc = Get-Content -LiteralPath $counterPath -Raw -Encoding UTF8 | ConvertFrom-Json
$budget = Get-Content -LiteralPath $budgetPath -Raw -Encoding UTF8 | ConvertFrom-Json
$cad = $budget.h24_cadence
if ($null -eq $cad) {
# h24_cadence key ABSENT -> fail-loud IN-LINE (no default, not probe-loi).
$kiemSeg = 'CONFIG-THIEU(h24_cadence)'
}
elseif ($null -eq $cad.light_every) {
$kiemSeg = 'CONFIG-THIEU(h24_cadence.light_every)'
}
elseif ($null -eq $cad.deep_every) {
$kiemSeg = 'CONFIG-THIEU(h24_cadence.deep_every)'
}
else {
$counter = [int]$sc.counter
$lightAt = [int]$sc.last_audit.light_at_counter
$deepAt = [int]$sc.last_audit.deep_at_counter
$lightEvery = [int]$cad.light_every
$deepEvery = [int]$cad.deep_every
$aLight = $counter - $lightAt
$bDeep = $counter - $deepAt
$over = @()
if ($aLight -ge $lightEvery) { $over += 'light' }
if ($bDeep -ge $deepEvery) { $over += 'deep' }
$status = if ($over.Count -gt 0) { 'OVERDUE:' + ($over -join ',') } else { 'ok' }
$kiemSeg = ("light {0}/{1} deep {2}/{3} {4}" -f $aLight, $lightEvery, $bDeep, $deepEvery, $status)
}
# -----------------------------------------------------------------------
# Ve 2 - tran-bo-nho (memory ceiling). Byte-measure agent-memory/*/MEMORY.md
# FRESH vs archive_gate.autoinject_cap_bytes (from key, not hardcoded).
# -----------------------------------------------------------------------
$cap = $budget.archive_gate.autoinject_cap_bytes
if ($null -eq $cap) {
$tranSeg = 'tran-bo-nho CONFIG-THIEU(autoinject_cap_bytes) over-cap'
}
else {
$capN = [int]$cap
$overCap = 0
$memRoot = Join-Path $RepoRoot '.claude\agent-memory'
if (Test-Path $memRoot) {
foreach ($mem in (Get-ChildItem -Path $memRoot -Directory -ErrorAction SilentlyContinue)) {
$mf = Join-Path $mem.FullName 'MEMORY.md'
if (Test-Path $mf) {
if ((Get-ByteLen $mf) -gt $capN) { $overCap++ }
}
}
}
$tranSeg = ("tran-bo-nho {0} over-cap" -f $overCap)
}
# -----------------------------------------------------------------------
# Ve 3 - run-chua-gom (orphan runs). DUAL-ACCEPT ported from tiep.md:40-53:
# form-1 flat <dir>/*-synthesis.md (-s = exists AND non-empty)
# form-2 C8 <dir>/harvest/*-synthesis.md
# form-3 (run.md '## synthesis' heading) = RETIRED @S123, NOT ported.
# -----------------------------------------------------------------------
$orphan = 0
$runsDir = Join-Path $RepoRoot '.claude\workflows\runs'
if (Test-Path $runsDir) {
foreach ($d in (Get-ChildItem -Path $runsDir -Directory -ErrorAction SilentlyContinue)) {
$runMd = Join-Path $d.FullName 'run.md'
if (-not (Test-Path $runMd)) { continue } # no run.md -> not a run -> skip
$found = $false
foreach ($f in (Get-ChildItem -Path $d.FullName -Filter '*-synthesis.md' -File -ErrorAction SilentlyContinue)) {
if ((Get-ByteLen $f.FullName) -gt 0) { $found = $true; break }
}
if (-not $found) {
$harvestDir = Join-Path $d.FullName 'harvest'
if (Test-Path $harvestDir) {
foreach ($f in (Get-ChildItem -Path $harvestDir -Filter '*-synthesis.md' -File -ErrorAction SilentlyContinue)) {
if ((Get-ByteLen $f.FullName) -gt 0) { $found = $true; break }
}
}
}
if (-not $found) { $orphan++ }
}
}
$runSeg = ("run-chua-gom {0}" -f $orphan)
Write-Host ("NHIP-NO: kiem {0} | {1} | {2}" -f $kiemSeg, $tranSeg, $runSeg)
}
catch {
# A probe NEVER blocks a stop point. Any failure prints one generic line.
Write-Host 'probe-loi (khong chan)'
}
exit 0