Files
solution-erp/scripts/h24-signal-write.ps1
pqhuy1987 6730eb4d48
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m41s
[CLAUDE] Skill: land 5 slot owner ratify @S172 (68·69·70·71·72)
68 — mở enum class `view-claim-broader-than-sample` (kiểu hỏng thứ tư:
suy-luận vượt mẫu; 3 nhóm class cũ không phủ được). Seed
class_repeat.counts khoá mới = 0 CÙNG NHỊP ⇒ bất-biến counts ≡ enum
giữ 12/12 hai chiều.

69 — wire cổng ép-seed C2 vào /pause.md §2.5 (đếm 2 cột A∖B, seed
on-behalf trước khi commit `wal: pause`). Token cổng-seed 0/5 → 5/5.
Vá cơ chế lead-gap FLAG-6: phiên kết bằng /pause thì phép đếm không
bao giờ chạy ⇒ sót seed là tất yếu cấu trúc, không phải đãng trí.

70 — tách 2 sổ đếm H24: +param `-CoCa` cho h24-signal-write.ps1.
Class CÓ CA nhưng không thành FLAG (lead tự khai ⇒ con-đo xếp ngoài
TOTAL) nay CHẶN RESET nhưng KHÔNG tăng tally. Vá incentive-inversion
ring2-audit A4 bắt: cơ chế cũ thưởng cho việc giấu.
Fault-inject 4/4 (CoCa chặn reset · đối chứng âm vẫn reset · rào enum
exit 2 · happy-path exit 0). Khôi phục view-stale-header 0 → 4.

71 — gom trọn nợ harvest 11 vai / ~306 KB (S168 6 vai · S171 4 vai +
reviewer×3 lens · cicd 2 sha mở từ S162). KHÔNG gom S159-bookend-open
(dương-giả, ring1 chứng 4/4 byte-exact). Sleep probe pending=1/15 ⇒ IM
nên không nén. Ghi uỷ quyền thường trực gom+nén = AUTO, lead tự quyết,
kèm 2 rào không được nới theo.

72 — MFE vào nhịp closeout: gỡ cổng từ-khoá `eval` ⇒ AUTO mỗi closeout.
Tiền đề "MFE tắt" đã ăn trọn lớp-① reinject + E3 của harness-refine;
đo lại: exit 0, 23 role, implementer-frontend 11/13 = 85%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:34:21 +07:00

341 lines
16 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
H24 signal-write (M3, spec 2026-07-24-S149-hoi-tu-bookend + FIX-1). Writes the
lead-self-audit tally to .claude/governance/.session-counter.json RIGHT AFTER the
monitor pair (lead-stale-auditor + lead-gap-auditor) returns, so an audit that ran
does not read "as if never run" (root-cause of the S146->S148 dropped-tally chain).
.DESCRIPTION
Mechanises the tally step that was previously done by hand (hand-copy dropped half of
the view-* classes at S148, losing 2 FLAGs). Given the classes of the FLAGs that just
fired, it:
(1) validates EVERY class against the CLOSED enum read LIVE from
memory-budget.json -> lead_self_audit.flag_classes. An unknown class => exit != 0
(a monitor may NOT invent a class; report verbatim + escalate to owner). NEVER
hardcodes the enum.
(2) PROCEDURAL max-1-decision/class/logic-session: reads the persisted per-session
ledger (last_audit.signal_*); a class already decided this session is a NO-OP
(one decision/class/session; the two bookend audits @start+@end collapse to one).
(3) FIRE: counts[class] += 1 (deduped within a call). RESET (consecutive semantics):
an enum class that did NOT fire this call AND has >= 1 in counts resets to 0 ONLY
when (a) there is NO _frozen_until_owner key beside the map, and (b) no reset has
happened yet this session. A present freeze key BLOCKS the reset and prints one
line, leaving the owner-owned hung streak intact.
(4) updates last_audit.{light|deep}_at_counter to the current counter.
(5) writes ATOMICALLY (temp + Move-Item -Force) so a crash mid-write cannot wipe the
owner-evidence file.
(6) invariant: the counts map holds ONLY numbers (any note lives OUTSIDE the map;
commit 2f39a7e removed a string that had been wedged into counts).
Pure-ASCII source (gotcha #30: .ps1 stays ASCII-only).
.PARAMETER Session
Logic-session label, e.g. S149. Scopes the per-session decision ledger.
.PARAMETER Flags
Comma-separated list of the flag CLASSES that just fired (may repeat, may be empty).
Example: 'gap-owner-specifics,gap-owner-specifics,view-stale-status'. Empty = a clean
audit that fired nothing.
.PARAMETER AuditKind
'light' or 'deep' - selects which last_audit.*_at_counter to stamp.
.PARAMETER RepoRoot
Repo root; both config paths derive from it. A temp tree here isolates fault-injection
(mirror of governance-detectors.ps1 -RepoRoot convention).
.OUTPUTS
Exit 0 = written. Exit 2 = a class was outside the closed enum. Exit 3 = config/state
error (missing enum, unreadable/unparseable counter, invariant violation).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^S\d+')]
[string]$Session,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Flags,
[Parameter(Mandatory = $true)]
[ValidateSet('light', 'deep')]
[string]$AuditKind,
# SO THU HAI (S172, owner slot 70). Class CO CA trong vong nay nhung KHONG thanh FLAG
# - dien hinh: lead TU KHAI truoc khi con-do soi, nen con-do xep vao §0-bis NGOAI TOTAL.
# Chung KHONG tang tally (khong phai FLAG moi) nhung CHAN RESET (ca co that = chuoi chua dut).
[AllowEmptyString()]
[string]$CoCa,
[string]$RepoRoot
)
$ErrorActionPreference = 'Stop'
# Default RepoRoot in the BODY, not the param block: $PSScriptRoot is EMPTY while param
# defaults evaluate under 'powershell.exe -File' (first-live failure S150; fault-inject K3
# passed only because every test passed -RepoRoot explicitly - happy-path did not cover the
# default path). Mirrors session-counter-tick.ps1, which computes its default in the body.
if (-not $RepoRoot) { $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path }
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
# UTF-8 byte read + BOM strip. Mirrors governance-detectors.ps1:1424 - dodges bug E-010
# (ANSI-decode of UTF-8) AND the leading-BOM "Invalid JSON primitive" that a plain
# UTF8.GetString would leave in front of the JSON.
function Read-JsonFile {
param([string]$Path)
$raw = [Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($Path))
$raw = $raw.TrimStart([char]0xFEFF)
return ($raw | ConvertFrom-Json)
}
# Set a property whether or not it already exists (Add-Member only adds new props).
function Set-Prop {
param($Obj, [string]$Name, $Value)
if ($Obj.PSObject.Properties.Name -contains $Name) {
$Obj.$Name = $Value
}
else {
Add-Member -InputObject $Obj -MemberType NoteProperty -Name $Name -Value $Value
}
}
# Read a counts entry as int; a missing key = 0.
function Get-Count {
param($Counts, [string]$Key)
if ($Counts.PSObject.Properties.Name -contains $Key) { return [int]$Counts.$Key }
return 0
}
# Integer-like guard for the counts invariant. Rejects strings (the actual 2f39a7e bug),
# booleans, doubles and nested objects; accepts integral numeric types.
function Test-IntLike {
param($Value)
if ($Value -is [string]) { return $false }
if ($Value -is [bool]) { return $false }
if ($Value -is [int] -or $Value -is [long] -or $Value -is [int16] -or $Value -is [byte]) { return $true }
return $false
}
# ---------------------------------------------------------------------------
# resolve paths
# ---------------------------------------------------------------------------
$budgetPath = Join-Path $RepoRoot '.claude\agent-memory\memory-budget.json'
$counterPath = Join-Path $RepoRoot '.claude\governance\.session-counter.json'
# ---------------------------------------------------------------------------
# (1) read the LIVE closed enum (never hardcode) + validate every fired class
# ---------------------------------------------------------------------------
if (-not (Test-Path -LiteralPath $budgetPath)) {
Write-Host "FAIL: budget not found: $budgetPath (cannot read the closed enum)"
exit 3
}
try { $budget = Read-JsonFile $budgetPath } catch { $budget = $null }
if ($null -eq $budget -or $null -eq $budget.lead_self_audit -or $null -eq $budget.lead_self_audit.flag_classes) {
Write-Host "FAIL: memory-budget.json is missing lead_self_audit.flag_classes (the CLOSED enum)."
Write-Host " Refusing to invent a default - a hardcoded enum would silently re-create the drift the single-source exists to prevent."
exit 3
}
$enum = @($budget.lead_self_audit.flag_classes | ForEach-Object { [string]$_ })
# parse -Flags (comma list; may repeat; may be empty)
$firedRaw = @()
if (-not [string]::IsNullOrWhiteSpace($Flags)) {
$firedRaw = @($Flags -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
}
$bad = @($firedRaw | Where-Object { $enum -notcontains $_ } | Select-Object -Unique)
if ($bad.Count -gt 0) {
foreach ($b in $bad) {
Write-Host ("FAIL: class '{0}' is NOT in the closed enum lead_self_audit.flag_classes." -f $b)
}
Write-Host (" A monitor may NOT invent a class - report it verbatim and escalate to the owner to extend the list.")
Write-Host (" Closed enum ({0}): {1}" -f $enum.Count, ($enum -join ', '))
exit 2
}
$firedUnique = @($firedRaw | Select-Object -Unique)
# parse -CoCa (SO THU HAI, S172 slot 70). Cung enum DONG, cung luat cam tu-che class.
$cocaRaw = @()
if (-not [string]::IsNullOrWhiteSpace($CoCa)) {
$cocaRaw = @($CoCa -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
}
$badCoCa = @($cocaRaw | Where-Object { $enum -notcontains $_ } | Select-Object -Unique)
if ($badCoCa.Count -gt 0) {
foreach ($b in $badCoCa) {
Write-Host ("FAIL: -CoCa class '{0}' is NOT in the closed enum lead_self_audit.flag_classes." -f $b)
}
Write-Host (" Closed enum ({0}): {1}" -f $enum.Count, ($enum -join ', '))
exit 2
}
$cocaUnique = @($cocaRaw | Select-Object -Unique)
# Giao voi -Flags: mot class vua FLAG vua tu-khai thi FLAG thang (no da tang tally roi).
$cocaOnly = @($cocaUnique | Where-Object { $firedUnique -notcontains $_ })
# ---------------------------------------------------------------------------
# read counter state
# ---------------------------------------------------------------------------
if (-not (Test-Path -LiteralPath $counterPath)) {
Write-Host "FAIL: counter file not found: $counterPath"
exit 3
}
try { $ctr = Read-JsonFile $counterPath } catch { $ctr = $null }
if ($null -eq $ctr) {
Write-Host "FAIL: .session-counter.json is not valid JSON - refusing to write tally onto an unreadable state file."
exit 3
}
$counter = [int]$ctr.counter
# ensure containers exist (defensive; the real file has them)
if ($null -eq $ctr.last_audit) { Set-Prop $ctr 'last_audit' ([pscustomobject]@{}) }
$la = $ctr.last_audit
if ($null -eq $ctr.class_repeat) { Set-Prop $ctr 'class_repeat' ([pscustomobject]@{ counts = [pscustomobject]@{} }) }
$cr = $ctr.class_repeat
if ($null -eq $cr.counts) { Set-Prop $cr 'counts' ([pscustomobject]@{}) }
$counts = $cr.counts
# ---------------------------------------------------------------------------
# freeze detection: _frozen_until_owner key present beside the map (class_repeat level)
# ---------------------------------------------------------------------------
$frozen = ($cr.PSObject.Properties.Name -contains '_frozen_until_owner')
# ---------------------------------------------------------------------------
# per-session decision ledger (persisted under last_audit; OUTSIDE the counts map).
# The two bookend audits of one logic-session are separate script invocations, so the
# ledger MUST persist in the file to collapse them into one decision/class/session.
# ---------------------------------------------------------------------------
$decided = @()
$resetDone = $false
if (([string]$la.signal_session) -eq $Session) {
$sd = $la.signal_decided
if ($null -ne $sd) {
# robust to PS 5.1 array-serialization quirks (empty -> '' , single -> scalar)
$decided = @($sd | Where-Object { ($_ -is [string]) -and ($_ -ne '') } | ForEach-Object { [string]$_ })
}
if ($la.PSObject.Properties.Name -contains 'signal_reset_done') {
$resetDone = [bool]$la.signal_reset_done
}
}
# else: a new logic-session -> fresh ledger (decided empty, resetDone false)
# ---------------------------------------------------------------------------
# (3a) FIRE pass - dedup within a call; NO-OP a class already decided this session
# ---------------------------------------------------------------------------
foreach ($cls in $firedUnique) {
if ($decided -contains $cls) {
Write-Host ("NO-OP fire: class '{0}' already has a decision this session ({1}) - max-1-decision/class/session (procedural)." -f $cls, $Session)
continue
}
$cur = Get-Count $counts $cls
$new = $cur + 1
Set-Prop $counts $cls ([int]$new)
$decided += $cls
Write-Host ("FIRE: class '{0}' counts {1} -> {2}." -f $cls, $cur, $new)
}
# ---------------------------------------------------------------------------
# (3b) RESET pass - consecutive semantics; at most one reset pass per session (FIX-1 p2);
# a present freeze key blocks it entirely (owner-owned hung streak stays intact).
#
# 🔴 SO THU HAI chan reset (S172, owner slot 70 - vá incentive-inversion do ring2-audit A4 bắt).
# BENH: "consecutive semantics" doc tin-hieu tu DUY NHAT danh-sach -Flags. Nhung mot class CO CA
# THAT ma lead TU KHAI TRUOC khi con-do soi thi con-do xep no vao §0-bis NGOAI TOTAL => no vang
# mat khoi -Flags => may doc thanh "khong tai dien" => RESET. Tuc co-che dang THUONG CHO VIEC GIAU:
# im lang thi con-do bat va streak +1; tu thu thi streak ve 0.
# CA THAT @S172: view-stale-header 4 -> 0, trong khi 2 header stale (STATUS:6, HANDOFF:5) CO THAT
# va chinh lead da khai ra chung trong cung phien.
# VA: reset chi khi class VANG MAT o CA HAI so - khong FLAG *VA* khong co ca tu-khai.
# -Flags = "so FLAG moi" (tang tally) ⟂ -CoCa = "class nao co ca trong vong" (chi chan reset).
# ---------------------------------------------------------------------------
if (-not $resetDone) {
$resetCandidates = @($enum | Where-Object {
($firedUnique -notcontains $_) -and ($cocaOnly -notcontains $_) -and ((Get-Count $counts $_) -ge 1) -and ($decided -notcontains $_)
})
if ($resetCandidates.Count -gt 0) {
if ($frozen) {
Write-Host ("(frozen - cho owner xu dai JUMP): _frozen_until_owner present; NOT resetting {0} hung class(es): {1}" -f $resetCandidates.Count, ($resetCandidates -join ', '))
# nothing reset -> leave resetDone false and counts intact
}
else {
foreach ($cls in $resetCandidates) {
$cur = Get-Count $counts $cls
Set-Prop $counts $cls ([int]0)
Write-Host ("RESET: class '{0}' counts {1} -> 0 (khong FLAG VA khong co ca tu-khai - consecutive semantics)." -f $cls, $cur)
}
$resetDone = $true
}
}
}
# ---------------------------------------------------------------------------
# (4) stamp last_audit.{light|deep}_at_counter = current counter
# ---------------------------------------------------------------------------
$floorKey = "${AuditKind}_at_counter"
Set-Prop $la $floorKey ([int]$counter)
# persist the per-session ledger (all OUTSIDE the counts map)
Set-Prop $la 'signal_session' ([string]$Session)
Set-Prop $la 'signal_decided' (@($decided))
Set-Prop $la 'signal_reset_done' ([bool]$resetDone)
Set-Prop $la 'signal_last_kind' ([string]$AuditKind)
# SO THU HAI - vet de doc lai duoc "vong nay class nao co ca ma khong thanh FLAG"
Set-Prop $la 'signal_coca' (@($cocaOnly))
# ---------------------------------------------------------------------------
# (6a) B10 @S152 enum-parity: EVERY enum class must exist as a key (0 if absent) so a
# re-serialize can never silently drop an armed-at-0 class (ring2-deep S151: gap-underfill
# was present-but-toothless - one round-trip from vanishing). Keys OUTSIDE the enum =
# fail-loud, NOT silent delete (hidden data loss; owner extends enum or cleans by hand).
# ---------------------------------------------------------------------------
foreach ($cls in $enum) {
if (-not ($counts.PSObject.Properties.Name -contains $cls)) { Set-Prop $counts $cls ([int]0) }
}
$extraKeys = @($counts.PSObject.Properties.Name | Where-Object { $enum -notcontains $_ })
if ($extraKeys.Count -gt 0) {
Write-Host ("FAIL enum-parity: counts has {0} key(s) OUTSIDE the closed enum: {1} - refusing to write (silent delete hides data; extend lead_self_audit.flag_classes or clean by hand)." -f $extraKeys.Count, ($extraKeys -join ', '))
exit 3
}
if (@($counts.PSObject.Properties).Count -ne $enum.Count) {
Write-Host ("FAIL enum-parity: counts key-count {0} != enum count {1} after ensure - refusing to write." -f @($counts.PSObject.Properties).Count, $enum.Count)
exit 3
}
# ---------------------------------------------------------------------------
# (6) invariant: counts map holds ONLY numbers (note lives OUTSIDE the map; 2f39a7e)
# ---------------------------------------------------------------------------
foreach ($p in $counts.PSObject.Properties) {
if (-not (Test-IntLike $p.Value)) {
$tn = if ($null -eq $p.Value) { 'null' } else { $p.Value.GetType().Name }
Write-Host ("FAIL invariant: counts['{0}'] is not an integer (type {1}) - the counts map must hold ONLY numbers; any note goes OUTSIDE the map (commit 2f39a7e)." -f $p.Name, $tn)
exit 3
}
}
# ---------------------------------------------------------------------------
# (5) atomic write: temp + Move-Item -Force. PS 5.1 Out-File -Encoding utf8 emits a BOM;
# both readers strip it (governance-detectors.ps1 byte+TrimStart, nhip-no-probe.ps1
# Get-Content -Encoding UTF8).
# ---------------------------------------------------------------------------
$json = $ctr | ConvertTo-Json -Depth 20
$tmp = "$counterPath.tmp"
try {
$json | Out-File -LiteralPath $tmp -Encoding utf8
Move-Item -LiteralPath $tmp -Destination $counterPath -Force
}
catch {
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
Write-Host ("FAIL: atomic write failed ({0}) - original file left untouched." -f $_.Exception.Message)
exit 3
}
Write-Host ("OK: {0} | session={1} kind={2} counter={3} | fired=[{4}] decided-this-session=[{5}] reset-done={6} frozen={7}" -f `
$counterPath, $Session, $AuditKind, $counter, ($firedUnique -join '|'), ($decided -join '|'), $resetDone, $frozen)
exit 0