Files
solution-erp/scripts/closeout-sentinel.ps1
pqhuy1987 398d343403
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m48s
[CLAUDE] Docs: S149-S150 closeout — bookend lượt đầu end-to-end (hình B) + 2 arc đóng + tally #53 chốt 45
- Arc-1 hội-tụ-bookend (rename 5 vai + 4 máy PS1 + C9-C12) + arc-2 adap-backlog 23/23 + probe Opus-5 khép (S149)
- Bookend @close S150: wave 6 vai (4 đo + 2 KIỂM) → 20 FLAG disposition từng-dòng; ring1 31/34-ĐẠT, ring2 10/10 + M-1 view-stale-role-desc
- Khoá _frozen_until_owner CẮM trước h24-signal-write (frozen-held in thật); 9 class FIRE; JUMP dải {5,4,4,3}+asym{1,4} chờ anh #21
- STATUS bump CURRENT S149-S150 + 2 khối Recently Done; HANDOFF segment mới + re-stamp đủ-slug + (17)(18)(19) lật + (14) viết lại + 6 slot #21-#26
- K2 posture-A @engine PHẦN K + K5 luật TÁCH vòng-đo-mới @session-end (O-2/O-3 OWNER-DELEGATED execute)
- sleep-doc derive-monitor-set (bỏ hardcode 4-tên); spawn-model-audit desc/comment fix; h24-signal-write param-default fix (first-live bug)
- M9 on-behalf ×3 (tooling + inv-cb F3 + reviewer F4); ring1/ring4 nhà seed; ring2 3 cite-dead vá
- Sổ garble #53 derive-from-body chốt 45 + sub-class ngược-#53/skeleton-ruột-rỗng (ghi-đĩa CẦN-KHÔNG-ĐỦ)
- Session-log S149-S150 (Sàn-5 đăng-ký 6 run=) + closeout-synthesis dạng-1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:17:05 +07:00

248 lines
13 KiB
PowerShell

<#
.SYNOPSIS
closeout-sentinel.ps1 - M5 (S149 hoi-tu-bookend). Sentinel: "closeout co ghi so
vong soi-lead khong" (did this closeout RECORD the audit round's signal on disk?).
.DESCRIPTION
Given ONE closeout commit (-Commit <sha>), and IF an H24 audit ran that session
(asserted via -AuditRan, OR auto-detected from an H24 run-folder of the same
session), assert the closeout WROTE the audit signal to disk:
(1) role-dir delta -- REQUIRED, drives the verdict:
>=1 file delta (ANY path under the role dir, INCLUDING archive/*.gist.md,
NOT only MEMORY.md) for the closeout audit-pair roles.
Default -Roles = obs-pair {harvest-curator, tooling-auditor} (the pair that
runs UNCONDITIONALLY @session-end and whose disk delta witnesses the closeout).
Default semantics = AND (EVERY role must have a delta, VIPIX-faithful);
-AnyRole relaxes to OR.
(2) .session-counter.json touch -- AUXILIARY, reported as a sub-signal (WARN),
NON-GATING by default. -RequireCounter promotes it to a hard gate (future
state, after M2/M3 mechanize the counter-tick at closeout).
Missing (1) => verdict FLAG "audit do roi nhu chua do".
Audit did NOT run this session => verdict SKIP (nothing to record; no false-flag).
Advisory: exit code is ALWAYS 0. READ the `verdict:` line, NOT the exit code
(mirrors VIPIX --nghi-thuc + spec/invest K6: "doc verdict: line, exit-code luon 0").
---------------------------------------------------------------------------
DEVIATION vs VIPIX .claude/scripts/closeout-sentinel.js (--nghi-thuc, line ~153)
---------------------------------------------------------------------------
D1. SCOPE: VIPIX audits the THREE most-recent closeouts (Hub sec-B rule-of-3) and
auto-detects each closeout via MARKERS (commit touches ALL of STATUS+HANDOFF+
CRYSTALLIZED). SE audits ONE given commit (-Commit) -- like VIPIX's `--commit`
forced mode -- because M5/K5 positive-control is defined per-commit
(S140/S143/S146/S147/S148 individually).
D2. PREDICATE PATH: VIPIX requires delta of `tooling-auditor/MEMORY.md` AND
`harvest-curator/MEMORY.md` -- the MEMORY.md file SPECIFICALLY. SE broadens to
ANY delta under `.claude/agent-memory/<role>/**`. RATIONALE (W2 on-disk measure):
the S140 closeout `b146e2b` touched only harvest-curator/archive/2026-07.gist.md
+ tooling-auditor/archive/2026-07.gist.md (NOT MEMORY.md) -- a MEMORY.md-only
predicate would FALSE-FLAG S140 (expected PASS). This is the spec-K5 fix
("predicate = delta BAT KY duoi agent-memory/<role>/**"), not a loosening: it
still requires a real audit-pair delta, just not one pinned to a single filename.
D3. GATE: VIPIX has no explicit "audit ran" gate (obs-pair runs unconditionally, so
absence IS the signal). SE takes the commit as-given and gates on -AuditRan /
auto-detect so pre-bookend sessions that genuinely skipped the audit are SKIPped,
not false-flagged. In the target bookend world (hinh B) the pair runs every
session, so -AuditRan is effectively always true.
D4. COUNTER: VIPIX does not check `.session-counter.json`; SE surfaces it as an
auxiliary WARN (see deviation-vs-task below).
D5. RUNTIME: SE is Windows PowerShell 5.1, ASCII-only script body (gotcha #30); the
output message 'audit do roi nhu chua do' is ASCII-transliterated for the same
reason. VIPIX is Node.js (keeps Vietnamese diacritics).
---------------------------------------------------------------------------
DEVIATION vs this-task literal wording (documented, NOT loosened -- see K5:
"CAM noi predicate cho vua")
---------------------------------------------------------------------------
T1. ROLE DEFAULT. The task text named the roles `lead-stale-auditor` /
`lead-gap-auditor`. The K5 positive-control expected verdicts, however, are
ONLY reproducible with the OBS-PAIR {harvest-curator, tooling-auditor} as the
audit-pair-of-record. Evidence (all git-verified on the real repo):
- invest sub-MD line 116 (the M5 design) names EXACTLY these two.
- VIPIX :153 (the tailoring source) checks EXACTLY these two.
- file-lists: S140/S143 touch the obs-pair (=> PASS); S146/S147/S148 do NOT
(=> FLAG). CRUCIALLY S148 `932d607` touched lead-view-auditor/ +
lead-omission-auditor/ (the OLD names of the lead-audit pair) AND
.session-counter.json -- so a lead-audit-pair predicate would make S148
PASS, contradicting the expected FLAG.
=> the positive-control role set is the obs-pair EXCLUSIVELY. -Roles defaults to
it; it is CONFIGURABLE, so after M6 bookend-izes the lead-audit pair a caller may
pass `-Roles lead-stale-auditor,lead-gap-auditor`. This is a ground-truth role
CORRECTION, not a loosening (obs-pair AND-both is STRICTER than lead-pair OR).
T2. COUNTER AS GATE. The task text made (2) a hard AND-condition
("... VA (2) counter cham. Thieu => FLAG"). That is mathematically incompatible
with 5/5: S140 `b146e2b` does NOT touch .session-counter.json (git-verified) yet
MUST be PASS. Any predicate [X AND counter-touched] yields S140=FLAG. Hence the
counter cannot be a hard gate while 5/5 holds; spec K5 (the authoritative
"predicate DA SUA theo W2") lists only role-dir-delta + the 5 cases, NOT the
counter. Resolution: counter is computed and REPORTED (WARN if missing) but does
not override a role-delta PASS; -RequireCounter promotes it to a gate for the
mechanized future.
.PARAMETER Commit Closeout commit sha (or any git rev). REQUIRED.
.PARAMETER AuditRan Assert an H24 audit ran this session (skip auto-detect).
.PARAMETER Roles Audit-pair role dir names under .claude/agent-memory/.
Default: harvest-curator, tooling-auditor (obs-pair).
.PARAMETER AnyRole Relax role check to OR (any one role delta). Default = AND.
.PARAMETER RequireCounter Promote the .session-counter.json touch to a hard gate.
.PARAMETER RepoRoot Repo root. Default = scripts/ parent.
.EXAMPLE
powershell.exe -ExecutionPolicy Bypass -File scripts/closeout-sentinel.ps1 -Commit 932d607 -AuditRan
#>
param(
[Parameter(Mandatory = $true)][string]$Commit,
[switch]$AuditRan,
[string[]]$Roles = @('harvest-curator', 'tooling-auditor'),
[switch]$AnyRole,
[switch]$RequireCounter,
[string]$RepoRoot
)
$ErrorActionPreference = 'Continue'
# Resolve RepoRoot robustly: $PSScriptRoot can be empty in PS 5.1 when the script is
# invoked via a RELATIVE -File path, so fall back through $PSCommandPath / MyInvocation
# / cwd (gotcha-adjacent to #30: keep the script self-locating).
if (-not $RepoRoot) {
$scriptDir = $PSScriptRoot
if (-not $scriptDir -and $PSCommandPath) { $scriptDir = Split-Path -Parent $PSCommandPath }
if (-not $scriptDir -and $MyInvocation.MyCommand.Path) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
if (-not $scriptDir) { $scriptDir = (Get-Location).Path }
$RepoRoot = (Resolve-Path (Join-Path $scriptDir '..')).Path
}
$COUNTER = '.claude/governance/.session-counter.json'
$MEMROOT = '.claude/agent-memory'
# Normalize -Roles: when invoked from an EXTERNAL shell as `-Roles a,b`, PS 5.1 -File
# binding delivers the whole "a,b" as ONE string element (comma-split only happens for
# PS-native array literals). Split on commas so both `-Roles a,b` and `-Roles a b` work.
$Roles = @($Roles | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ })
function Invoke-Git {
param([string[]]$GitArgs)
# Capture stdout only; do NOT redirect stderr (PS 5.1 wraps native stderr in
# ErrorRecords). Caller checks $LASTEXITCODE.
& git -C $RepoRoot @GitArgs
}
# --- resolve + validate commit --------------------------------------------
$sha = (Invoke-Git @('rev-parse', '--verify', "$Commit^{commit}"))
if ($LASTEXITCODE -ne 0 -or -not $sha) {
Write-Host "closeout-sentinel: cannot resolve commit '$Commit' (not a git rev in $RepoRoot)"
Write-Host "verdict : ERROR resolve-commit"
exit 0
}
$sha = ($sha | Select-Object -First 1).Trim()
$short = $sha.Substring(0, [Math]::Min(12, $sha.Length))
$subject = (Invoke-Git @('show', '-s', '--format=%s', $sha) | Select-Object -First 1)
$when = (Invoke-Git @('show', '-s', '--format=%cI', $sha) | Select-Object -First 1)
# --- file list touched by the commit (--root => root commits are safe) -----
$files = @(Invoke-Git @('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', $sha) |
ForEach-Object { $_.Trim() } | Where-Object { $_ })
# --- did an H24 audit run this session? ------------------------------------
$sess = $null
if ($subject -match 'S\d+') { $sess = $matches[0] }
$auditRanResolved = $false
$auditSource = ''
if ($AuditRan) {
$auditRanResolved = $true
$auditSource = 'asserted (-AuditRan)'
}
else {
$runsDir = Join-Path $RepoRoot '.claude/workflows/runs'
if ($sess -and (Test-Path $runsDir)) {
$sessRe = '(?i)' + [regex]::Escape($sess) + '(?!\d)'
$h24Marker = '(?i)h24|lead-(view|omission|stale|gap)|sub-lead'
$cand = Get-ChildItem -Path $runsDir -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match $sessRe }
foreach ($c in $cand) {
$nameHit = ($c.Name -match '(?i)h24')
$fileHit = Get-ChildItem -Path $c.FullName -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match $h24Marker } | Select-Object -First 1
if ($nameHit -or $fileHit) {
$auditRanResolved = $true
$auditSource = "auto-detect run-folder: $($c.Name)"
break
}
}
if (-not $auditRanResolved) { $auditSource = "auto-detect: no H24 run-folder for $sess" }
}
else {
$auditSource = 'auto-detect: no session token in subject / runs/ missing'
}
}
# --- role-dir delta (predicate 1, drives verdict) --------------------------
$roleHits = @{}
foreach ($role in $Roles) {
$prefix = "$MEMROOT/$role/"
$hit = @($files | Where-Object { $_.StartsWith($prefix) })
$roleHits[$role] = $hit
}
$rolesWithDelta = @($Roles | Where-Object { $roleHits[$_].Count -gt 0 })
if ($AnyRole) {
$roleOk = ($rolesWithDelta.Count -gt 0)
$roleMode = 'OR (any role)'
}
else {
$roleOk = ($rolesWithDelta.Count -eq $Roles.Count)
$roleMode = 'AND (every role)'
}
# --- counter touch (predicate 2, auxiliary) --------------------------------
$counterTouched = ($files -contains $COUNTER)
# --- verdict ---------------------------------------------------------------
Write-Host "-- closeout-sentinel (M5 / S149) -- commit $short $when"
Write-Host " subject : $subject"
Write-Host " audit : $(if ($auditRanResolved) { 'RAN' } else { 'NOT-RUN' }) [$auditSource]"
if (-not $auditRanResolved) {
Write-Host " => audit did not run this session; nothing to record."
Write-Host "verdict : SKIP audit-not-run (khong co gi de ghi -- no false-flag)"
exit 0
}
Write-Host " roles : [$($Roles -join ', ')] match=$roleMode"
foreach ($role in $Roles) {
$hit = $roleHits[$role]
if ($hit.Count -gt 0) {
Write-Host (" + {0,-18} delta OK ({1} path): {2}" -f $role, $hit.Count, ($hit -join '; '))
}
else {
Write-Host (" - {0,-18} NO delta" -f $role)
}
}
Write-Host " counter : $(if ($counterTouched) { 'touched' } else { 'NOT touched' }) ($COUNTER)$(if ($RequireCounter) { ' [GATING]' } else { ' [auxiliary]' })"
$counterFail = ($RequireCounter -and (-not $counterTouched))
if ($roleOk -and (-not $counterFail)) {
if ((-not $counterTouched) -and (-not $RequireCounter)) {
Write-Host " WARN : .session-counter.json not touched (auxiliary; non-gating -- use -RequireCounter to enforce)"
}
Write-Host "OK: closeout recorded the audit round (role-dir delta present)."
Write-Host "verdict : PASS roles-with-delta=$($rolesWithDelta.Count)/$($Roles.Count) counter=$(if ($counterTouched) { 'y' } else { 'n' })"
exit 0
}
# FLAG
$why = @()
if (-not $roleOk) { $why += "role-dir delta missing ($($rolesWithDelta.Count)/$($Roles.Count) roles have delta, need $roleMode)" }
if ($counterFail) { $why += ".session-counter.json not touched (-RequireCounter)" }
Write-Host "FLAG: audit do roi nhu chua do (audit measured but closeout recorded nothing)."
Write-Host " reason: $($why -join ' ; ')"
Write-Host " note : FLAG = CANDIDATE (lead ground-truth). Stop-hook can split the"
Write-Host " obs-pair delta into a separate 'wal:' commit => a healthy closeout"
Write-Host " may still show missing (known FP, same as VIPIX :153)."
Write-Host "verdict : FLAG roles-with-delta=$($rolesWithDelta.Count)/$($Roles.Count) counter=$(if ($counterTouched) { 'y' } else { 'n' })"
exit 0