[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:
@ -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
155
scripts/nhip-no-probe.ps1
Normal 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
|
||||
Reference in New Issue
Block a user