<# .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 | tran-bo-nho x over-cap | run-chua-gom y | pull-cach N ngay * 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). * pull-cach = N days since the most recent INBOUND receive-date in broadcasts/_index.md (col-1 of rows ABOVE the OUTBOUND header). WHY THIS VE EXISTS (S143): the standing instruction was "watch broadcasts/inbox" - but inbox only ever holds mail ALREADY pulled, so it can NEVER surface un-pulled mail. It stayed silent by design while SE waited on a hub reply that had been sitting in the hub outbox since 18-07, until the hub had to send a nudge. This ve measures the thing that was actually blind. THRESHOLD IS DELIBERATELY ABSENT: prints the raw number only. If key h24_cadence.pull_warn_days is present it also appends OVERDUE:pull - but SE does NOT invent that number (owner's call). Precedent for that discipline: session_ctx_kb sat DELIBERATELY EMPTY until the owner set it explicitly at S146 - the number was never guessed by SE, it was waited for. Same rule applies here. Unparseable/absent index -> 'pull-cach n-a' (NEVER kills ve 1-3). .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++ } } } } # B1/H3 @S152: ALSO measure archive/_INDEX.md vs tiers.l2_index.cap_bytes. # Before this, the probe printed "0 over-cap" while the cicd _INDEX sat at 110.5% # of its cap (S152 eval S2) -> an ACTIVE false claim; and cap_bytes had ZERO # runtime consumers (H18 ghost-wire class-2). This block is now the consumer. $idxOver = 0 $idxCap = $budget.tiers.l2_index.cap_bytes if ($null -eq $idxCap) { $tranSeg = ("tran-bo-nho {0} over-cap l2idx CONFIG-THIEU(cap_bytes)" -f $overCap) } else { $idxCapN = [int]$idxCap if (Test-Path $memRoot) { foreach ($mem in (Get-ChildItem -Path $memRoot -Directory -ErrorAction SilentlyContinue)) { $ix = Join-Path $mem.FullName 'archive\_INDEX.md' if (Test-Path $ix) { if ((Get-ByteLen $ix) -gt $idxCapN) { $idxOver++ } } } } $tranSeg = ("tran-bo-nho {0} over-cap l2idx {1} over" -f $overCap, $idxOver) } } # ----------------------------------------------------------------------- # Ve 3 - run-chua-gom (orphan runs). DUAL-ACCEPT ported from tiep.md:40-53: # form-1 flat /*-synthesis.md (-s = exists AND non-empty) # form-2 C8 /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) # ----------------------------------------------------------------------- # Ve 4 - pull-cach (broadcast pull lag). Own try/catch ON PURPOSE: a NEW ve # must never be able to blind the 3 ves that already work. Any failure here # degrades to 'pull-cach n-a', it does NOT fall through to probe-loi. # ----------------------------------------------------------------------- $pullSeg = 'pull-cach n-a' try { $idxPath = Join-Path $RepoRoot 'broadcasts\_index.md' if (Test-Path -LiteralPath $idxPath) { $lines = Get-Content -LiteralPath $idxPath -Encoding UTF8 # Rows ABOVE the OUTBOUND header are INBOUND. Match on the ASCII token # only - the real heading carries an emoji this script must not contain. $cut = $lines.Count for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^#{1,6}.*OUTBOUND') { $cut = $i; break } } $newest = $null for ($i = 0; $i -lt $cut; $i++) { if ($lines[$i] -match '^\|\s*(\d{4}-\d{2}-\d{2})\s*\|') { $d = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd', $null) if ($null -eq $newest -or $d -gt $newest) { $newest = $d } } } if ($null -ne $newest) { $days = [int]([datetime]::Now.Date - $newest.Date).TotalDays $pullSeg = ("pull-cach {0} ngay" -f $days) # Threshold is OPTIONAL and owner-owned. Absent key = stay INFORM, # do NOT assume a default (that would be inventing owner's number). $warn = $null if ($null -ne $cad) { $warn = $cad.pull_warn_days } if ($null -ne $warn -and $days -ge [int]$warn) { $pullSeg = ("pull-cach {0}/{1} ngay OVERDUE:pull" -f $days, [int]$warn) } } } } catch { $pullSeg = 'pull-cach n-a' } # ----------------------------------------------------------------------- # (5) SO YEU-CAU CHU DU AN (C4-06 / W4, S181) - hai ve: dang-treo + nhac-tu-lan-2. # # (!) PHAN BIET HAI THU KHAC NHAU, in HAI CHUOI KHAC NHAU: # 'chua-co-du-lieu' = so vang / khong doc duoc / 0 dong YC -> phep do KHONG TON TAI # 'treo 0 / nhac2+ 0' = da doc duoc n dong, dem ra bang 0 -> phep do CO, ket qua 0 # Gop hai cai nay lam mot chinh la 'den xanh gia' ma hub goi ten (A4). # (!) Dong khong tach duoc cot thi DEM RIENG va IN RA, khong bo im lang # (loi cam 4c 'liet-khong-co' / '[skip] im lang'). # (!) Cot trang thai mang chu co DAU ('mo' co dau moc). File nay BAT BUOC pure-ASCII # (gotcha #30) nen dung CODE-POINT, khong go chu co dau vao day. $ycSeg = 'so-yc chua-co-du-lieu' try { $ycPath = Join-Path $RepoRoot 'docs/governance/so-yeu-cau-chu-du-an.md' if (Test-Path -LiteralPath $ycPath) { # -Encoding UTF8 BAT BUOC: PS 5.1 mac dinh doc theo codepage ANSI, so do # chu co dau trong cot trang thai bi bien dang TRUOC khi so sanh => dem ra 0 # trong khi so co dong MO that. Da xay ra that o lan cam dau tien (S181). $ycLines = Get-Content -LiteralPath $ycPath -Encoding UTF8 $OPEN = 'm' + [char]0x1EDF # "mo" voi dau moc = trang thai MO cua yeu-cau $tot = 0; $treo = 0; $nhac2 = 0; $bad = 0; $dup = 0 foreach ($L in $ycLines) { if ($L -notmatch '^\|\s*`YC-\d+`') { continue } $c = $L -split '\|' if ($c.Count -lt 9) { $bad++; continue } # (!) DEM THEO **MUC**, KHONG THEO **DONG** (va @S181 sau khi vai tang-2 bat loi). # Cot cuoi 'nhac-lai-cua' co gia tri => dong nay la LAN NHAC LAI cua mot muc # da dem o ma goc => BO QUA. Dem ca no la dem theo LAN, dung don vi luat CAM. # Dau van tay cua loi do: ca "nhac 5 lan" ma hoa thanh 5 dong se cho 4 dong # count>=2 - dung con so ma luat goi la SAI. if (($c[8] -replace '[*` ]','') -ne '') { $dup++; continue } $tot++ # (!) Strip dau nhan markdown truoc khi so: o bang hay duoc viet **mo** cho noi bat. # Ban dau so thang chuoi => '**mo**' truot => in 'treo 0' trong khi so co muc MO. # Da xay ra that ngay luot cam dau tien (S181). if (($c[4] -replace '[*` ]','') -eq $OPEN) { $treo++ } $m = [regex]::Match($c[5], '\d+') if ($m.Success -and [int]$m.Value -ge 2) { $nhac2++ } } if ($tot -gt 0) { # in ca MAU SO muc + so dong nhac-lai da gop, de nguoi doc kiem duoc don vi $ycSeg = ("so-yc treo {0} / nhac2+ {1} (muc {2}, gop {3} dong nhac-lai)" -f $treo, $nhac2, $tot, $dup) if ($bad -gt 0) { $ycSeg += (" / {0} dong KHONG TACH DUOC COT" -f $bad) } } } } catch { $ycSeg = 'so-yc chua-co-du-lieu' } Write-Host ("NHIP-NO: kiem {0} | {1} | {2} | {3} | {4}" -f $kiemSeg, $tranSeg, $runSeg, $pullSeg, $ycSeg) } catch { # A probe NEVER blocks a stop point. Any failure prints one generic line. Write-Host 'probe-loi (khong chan)' } exit 0