[CLAUDE] Docs: S149-S150 closeout — bookend lượt đầu end-to-end (hình B) + 2 arc đóng + tally #53 chốt 45
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m48s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m48s
- 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>
This commit is contained in:
247
scripts/closeout-sentinel.ps1
Normal file
247
scripts/closeout-sentinel.ps1
Normal file
@ -0,0 +1,247 @@
|
||||
<#
|
||||
.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
|
||||
@ -565,7 +565,7 @@ $TitleFreshSkip = @('\docs\_archive\')
|
||||
# explain it. That quote is byte-identical to a live anchor -- being identical is
|
||||
# the POINT of a good example -- so a pattern-matcher flags the teacher.
|
||||
# Measured on disk @S123: 4 H24-1 flags, 2 are this exact case:
|
||||
# .claude/agents/lead-view-auditor.md:45 quotes the anchor as the EXAMPLE
|
||||
# .claude/agents/lead-stale-auditor.md:45 quotes the anchor as the EXAMPLE
|
||||
# for its own class `view-stale-header`
|
||||
# docs/governance/adap-reports/...harness-24...:78 quotes it while EXPLAINING that
|
||||
# very false-positive <- 3rd generation
|
||||
@ -581,7 +581,7 @@ $TitleFreshSkip = @('\docs\_archive\')
|
||||
# not USED (use/mention distinction). Count backticks left of the match; ODD => we are
|
||||
# inside an open span => quoted example => keep looking. Measured 4/4 ON DISK TODAY:
|
||||
# BARE -> docs/rag-setup-plan.md:4 . form-engine/SKILL.md:15 = TRUE POSITIVE, still fires
|
||||
# QUOTED -> lead-view-auditor.md:45 . adap-report...:78 = FALSE POSITIVE, now skipped
|
||||
# QUOTED -> lead-stale-auditor.md:45 . adap-report...:78 = FALSE POSITIVE, now skipped
|
||||
#
|
||||
# 4/4, NOT the "5/5" an earlier draft of this comment claimed (fixed S123 after review).
|
||||
# The would-be 5th case, permission-matrix/SKILL.md:16, IS NOT ON DISK ANY MORE and does
|
||||
@ -1514,6 +1514,399 @@ else {
|
||||
$h245Note = if ($h245Measured) { '' } else { ' -- MEASURED NOTHING (skipped above); a 0 after a skip is not a green' }
|
||||
Write-Host (" H24-5 counter-ancestor flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]{1}" -f $h245Flags, $h245Note) -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C9 - hmw WIDTH mirror-drift (INFORM-only, NEW @S149; count SEPARATE, NOT in TOTAL)
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHY: `hmw_width.cap` in memory-budget.json is CANONICAL, but hmw.js must hardcode the
|
||||
# same number as a MIRROR because the workflow sandbox has no filesystem (hmw.js:5).
|
||||
# Two copies of one number = exactly the drift class B1 exists to prevent. Measured @S149:
|
||||
# `grep -ril 'width' scripts/` = 0 hit => nothing was checking this pair at all.
|
||||
# INFORM-only per repo convention: a NEW net reads separately first; folding into TOTAL
|
||||
# (and raising severity) is post-triage and owner-gated, never the net author's call.
|
||||
Write-Section 'C9 - hmw WIDTH mirror-drift (INFORM-only)'
|
||||
$c9Flags = 0
|
||||
$c9Measured = $false
|
||||
$hmwP = Join-Path $RepoRoot '.claude\workflows\hmw.js'
|
||||
$budP = Join-Path $RepoRoot '.claude\agent-memory\memory-budget.json'
|
||||
if (-not (Test-Path $hmwP)) {
|
||||
Write-Host ' [skip] hmw.js not found' -ForegroundColor DarkGray
|
||||
}
|
||||
elseif (-not (Test-Path $budP)) {
|
||||
Write-Host ' [skip] memory-budget.json not found' -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
$hmwTxt = Get-Content -LiteralPath $hmwP -Raw -Encoding UTF8
|
||||
$mirrorM = [regex]::Match($hmwTxt, 'const\s+WIDTH_CAP\s*=\s*(\d+)')
|
||||
$budObj = $null
|
||||
try { $budObj = (Get-Content -LiteralPath $budP -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { $budObj = $null }
|
||||
$canon = if ($null -ne $budObj -and $null -ne $budObj.hmw_width) { $budObj.hmw_width.cap } else { $null }
|
||||
|
||||
if (-not $mirrorM.Success) {
|
||||
# FAIL-LOUD on absence: a missing mirror is NOT a green. Same discipline as H24-2
|
||||
# (missing key => fail loud, never assume a default).
|
||||
Write-InformFlag (Rel $hmwP) `
|
||||
'hmw WIDTH mirror MISSING: no `const WIDTH_CAP = <n>` found - the cap may have been removed or renamed, which silently drops the runaway guard' `
|
||||
'restore the mirror, or if the guard was intentionally retired, remove hmw_width from memory-budget.json in the SAME change'
|
||||
$c9Flags++; $c9Measured = $true
|
||||
}
|
||||
elseif ($null -eq $canon) {
|
||||
Write-InformFlag (Rel $budP) `
|
||||
('hmw WIDTH canonical MISSING: hmw.js hardcodes {0} but memory-budget.json has no hmw_width.cap - the number has no owner-visible home' -f $mirrorM.Groups[1].Value) `
|
||||
'add hmw_width {cap, mechanism, ratified, _ratified_by} to memory-budget.json (precedent: distill_trigger O-1/O-6)'
|
||||
$c9Flags++; $c9Measured = $true
|
||||
}
|
||||
else {
|
||||
$c9Measured = $true
|
||||
$mirrorN = [int]$mirrorM.Groups[1].Value
|
||||
$canonN = [int]$canon
|
||||
if ($mirrorN -ne $canonN) {
|
||||
Write-InformFlag (Rel $hmwP) `
|
||||
('hmw WIDTH DRIFT: hmw.js mirror={0} but memory-budget.json hmw_width.cap={1} - the running guard does not match the ratified number' -f $mirrorN, $canonN) `
|
||||
'sync hmw.js WIDTH_CAP to the canonical value, then re-run the 2-way fault-inject on the new number (cap+1 => 4 loud lines; cap => silent)'
|
||||
$c9Flags++
|
||||
}
|
||||
else {
|
||||
Write-Host (" [ok] WIDTH mirror {0} == canonical hmw_width.cap {1}" -f $mirrorN, $canonN) -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
$c9Note = if ($c9Measured) { '' } else { ' -- MEASURED NOTHING (skipped above); a 0 after a skip is not a green' }
|
||||
Write-Host (" C9 width-drift flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]{1}" -f $c9Flags, $c9Note) -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C10 - logic-session folder missing / unopened (INFORM-only, NEW @S149)
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHY THIS EXISTS - first-person evidence, not a hypothetical:
|
||||
# /session-start BUOC 0.8 requires opening `.claude/sessions/session-<N>/` at the START of a
|
||||
# session. At S149 the lead ECHOED that very rule verbatim (BUOC 0 prints the whole command
|
||||
# body) and then skipped it anyway - the folder was created ~1.5h late, after 3 large
|
||||
# workflows, and only because the OWNER noticed. Owner's words: "day la ly do ma nghi thuc
|
||||
# nay bat buoc". A rule that lives only in lead memory is not a rule; it is a hope.
|
||||
# Same class as the `trio: skipped` WAL trace (ghost-wire: mandated, never once written).
|
||||
# WHAT IT CHECKS: every session-<N> folder must (a) match ^session-\d+$, (b) carry its
|
||||
# _context-s-<N>.md, and (c) the HIGHEST N must be OPEN (no _end / closed.md) - because a
|
||||
# running session must have an open logic-session to write into. All-closed => nobody opened
|
||||
# one for the session that is running right now.
|
||||
# 🔸 KHAI THAT - what this CANNOT do: it cannot prove the folder was opened EARLY (on time).
|
||||
# A late-but-present folder reads identical to a punctual one. It catches ABSENCE, not
|
||||
# LATENESS. Closing that gap needs a timestamp compare against session start, which the
|
||||
# detector has no reliable source for. Do not read a green C10 as "ritual ran on time".
|
||||
# INFORM-only per repo convention (new net reads separately; folding into TOTAL is owner-gated).
|
||||
Write-Section 'C10 - logic-session folder (INFORM-only)'
|
||||
$c10Flags = 0
|
||||
$c10Measured = $false
|
||||
$sessRoot = Join-Path $RepoRoot '.claude\sessions'
|
||||
if (-not (Test-Path $sessRoot)) {
|
||||
Write-Host ' [skip] .claude/sessions not found' -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
$c10Measured = $true
|
||||
$sessDirs = @(Get-ChildItem -LiteralPath $sessRoot -Directory -ErrorAction SilentlyContinue)
|
||||
if ($sessDirs.Count -eq 0) {
|
||||
Write-InformFlag (Rel $sessRoot) `
|
||||
'no logic-session folder at all: /session-start BUOC 0.8 opens session-<N>/ but none exists' `
|
||||
'create .claude/sessions/session-1/ with _context-s-1.md (STOCK-map + FLOW-START)'
|
||||
$c10Flags++
|
||||
}
|
||||
else {
|
||||
$maxN = -1
|
||||
foreach ($d in $sessDirs) {
|
||||
if ($d.Name -notmatch '^session-(\d+)$') {
|
||||
Write-InformFlag (Rel $d.FullName) `
|
||||
("logic-session folder name '{0}' breaks the machine-scannable form ^session-\d+$" -f $d.Name) `
|
||||
'rename to session-<N> (no L<mm>, no descriptive suffix - tooling scans this pattern)'
|
||||
$c10Flags++
|
||||
continue
|
||||
}
|
||||
$n = [int]$Matches[1]
|
||||
if ($n -gt $maxN) { $maxN = $n }
|
||||
$ctx = Join-Path $d.FullName ("_context-s-{0}.md" -f $n)
|
||||
$legacy = @(Get-ChildItem -LiteralPath $d.FullName -Filter 'pause-*.md' -ErrorAction SilentlyContinue).Count
|
||||
if (-not (Test-Path $ctx) -and $legacy -eq 0) {
|
||||
Write-InformFlag (Rel $d.FullName) `
|
||||
("session-{0} has neither _context-s-{0}.md (hub form) nor legacy pause-*.md - the folder carries no narrative at all" -f $n) `
|
||||
'write _context-s-<N>.md with (a) STOCK-map pointers and (b) FLOW-START'
|
||||
$c10Flags++
|
||||
}
|
||||
}
|
||||
if ($maxN -ge 0) {
|
||||
$topDir = Join-Path $sessRoot ("session-{0}" -f $maxN)
|
||||
$isClosed = (Test-Path (Join-Path $topDir '_end')) -or (Test-Path (Join-Path $topDir 'closed.md'))
|
||||
if ($isClosed) {
|
||||
Write-InformFlag (Rel $sessRoot) `
|
||||
("highest logic-session (session-{0}) is CLOSED and no newer one was opened - if a session is running now, BUOC 0.8 was skipped" -f $maxN) `
|
||||
("open session-{0} with _context-s-{0}.md; only /session-start may mint a new <N>" -f ($maxN + 1))
|
||||
$c10Flags++
|
||||
}
|
||||
else {
|
||||
Write-Host (" [ok] session-{0} is OPEN (no _end/closed.md)" -f $maxN) -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$c10Note = if ($c10Measured) { '' } else { ' -- MEASURED NOTHING (skipped above); a 0 after a skip is not a green' }
|
||||
Write-Host (" C10 logic-session flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]{1}" -f $c10Flags, $c10Note) -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C11 - H24 FLAG-khuon + diary-delta (INFORM-only, NEW @S149; count SEPARATE, NOT in TOTAL)
|
||||
# ---------------------------------------------------------------------------
|
||||
# TWO checks over run-folder H24 artifacts, both born from a MEASURED S148 loss:
|
||||
# (a) FLAG-khuon: every H24 auditor sub-file (name carries lead-stale|lead-gap|ring2-audit)
|
||||
# that RAISES an enumerated FLAG-<n> must tag it as
|
||||
# "## FLAG-<n> <em-dash> `<class>` <em-dash> <SEV>" (class family view-* / gap-*)
|
||||
# A file that carries a FLAG-<n> but has NO conforming tag line => the by-class tally
|
||||
# that reads these files CANNOT count it. That is the exact S148 defect: the vong
|
||||
# dropped the khuon and 2 of 9 FLAGs fell out of the count (spec M4). INFORM, because
|
||||
# the fix is procedural (re-tag), not a build break.
|
||||
# -> WHY the enumerated form "FLAG-<n>" and NOT the bare word "FLAG": a CLEAN auditor
|
||||
# legitimately writes "0 FLAG" / "no FLAG this run" - which contains the word FLAG
|
||||
# but raised nothing to tag. Triggering on the bare word would FLAG a clean run =
|
||||
# false positive on the very role (ring2-audit) that most often reports 0. The
|
||||
# enumerated form appears ONLY when a flag was actually raised. This REFINES the
|
||||
# spec's loose "chu FLAG" to the non-false-positive signal (anti-Goodhart: a 0 must
|
||||
# be a real 0, never a net that cannot tell clean from broken).
|
||||
# -> LIMIT (khai that): file-level. One conforming tag line silences the whole file, so
|
||||
# a file that tags 1 of 2 flags is NOT caught here (partial-tagging is a harder check
|
||||
# the tally itself must do). C11 catches the TOTAL-omission case S148 actually hit
|
||||
# (khuon dropped entirely). A QUOTED example tag also counts as conforming => a doc
|
||||
# quoting the format reads silent; silence is the safe direction (no false alarm).
|
||||
# (b) diary-delta (M9): a run-folder carrying sub-<role>-*.md for an INFORM-only role (list
|
||||
# read LIVE from .claude/agents/*.md frontmatter carrying 'INFORM-only') means that role
|
||||
# RAN and produced an artifact. If the role's diary (.claude/agent-memory/<role>/) has
|
||||
# NO git commit at/after that artifact's mtime, the role ran but nobody seeded its memory
|
||||
# => GAP-3 (spec M9: recurred x4 S124->S149). git is the delta source the spec names; it
|
||||
# is a LOCAL VCS query, not a model/API, so it is within the NO-API mandate. A missing OR
|
||||
# older commit => flag. git absent => say so (do not read "cannot measure" as clean).
|
||||
# *** FROZEN-HISTORY GATE (critical): only run-folders with LastWriteTime >= $C11_LAND_DATE scanned.
|
||||
# Older folders predate this khuon AND the separated lead-stale/lead-gap roles; re-flagging
|
||||
# them is noise about a closed period (spec section 1: "S148 ve truoc = frozen, da biet").
|
||||
# Do NOT "freshen" the constant below - it is the day C11 landed, by design.
|
||||
# INFORM-only per repo convention (new net reads separately; fold into TOTAL is owner-gated).
|
||||
Write-Section 'C11 - H24 FLAG-khuon + diary-delta (INFORM-only)'
|
||||
$c11Flags = 0
|
||||
$c11Measured = $false
|
||||
|
||||
# LAND DATE - the day this detector shipped (S149, spec M4). Run-folders touched BEFORE this
|
||||
# are frozen history. GRANULARITY note (measured @S149): the S148 force-fire folder shares
|
||||
# this calendar day but wrote a COMBINED sub-h24-audit-*.md that matches NONE of the
|
||||
# lead-stale|lead-gap|ring2-audit name patterns, so it is scanned-but-inert. Verified on disk
|
||||
# @S149: `find runs -name '*lead-stale*' -o -name '*lead-gap*' -o -name '*ring2-audit*'` = 0
|
||||
# hits across ALL history => the FIRST files to match will be produced by roles running UNDER
|
||||
# this khuon, i.e. after this lands. A date-level gate therefore suffices (no matching trigger
|
||||
# file exists in any frozen folder, so nothing frozen can fire).
|
||||
$C11_LAND_DATE = [datetime]'2026-07-24'
|
||||
|
||||
$runsRoot = Join-Path $RepoRoot '.claude\workflows\runs'
|
||||
if (-not (Test-Path $runsRoot)) {
|
||||
Write-Host ' [skip] .claude/workflows/runs not found' -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
# INFORM-only role list, read LIVE from agent frontmatter so a NEW monitor auto-joins
|
||||
# (spec M9: "doc song"). Scoped to the frontmatter block (where description lives) so a
|
||||
# body mention of 'INFORM-only' in a non-monitor agent does not falsely enroll it.
|
||||
$informRoles = New-Object System.Collections.Generic.List[string]
|
||||
$agentsDir = Join-Path $RepoRoot '.claude\agents'
|
||||
if (Test-Path $agentsDir) {
|
||||
foreach ($af in (Get-ChildItem -LiteralPath $agentsDir -Filter *.md -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'README.md' })) {
|
||||
$al = @(Get-Content -LiteralPath $af.FullName -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||
if ($al.Count -eq 0 -or $al[0].Trim() -ne '---') { continue }
|
||||
$fmEnd = -1
|
||||
for ($j = 1; $j -lt $al.Count; $j++) { if ($al[$j].Trim() -eq '---') { $fmEnd = $j; break } }
|
||||
if ($fmEnd -le 1) { continue }
|
||||
$fmLines = $al[1..($fmEnd - 1)]
|
||||
if (($fmLines -join "`n") -notmatch 'INFORM-only') { continue }
|
||||
$rname = $null
|
||||
foreach ($fl in $fmLines) {
|
||||
$nm = [regex]::Match($fl, '^\s*name:\s*(.+?)\s*$')
|
||||
if ($nm.Success) { $rname = $nm.Groups[1].Value.Trim(); break }
|
||||
}
|
||||
if (-not $rname) { $rname = $af.BaseName }
|
||||
if (-not $informRoles.Contains($rname)) { $informRoles.Add($rname) | Out-Null }
|
||||
}
|
||||
}
|
||||
|
||||
# git availability probe (part b needs it). Absent git => part (b) cannot measure delta;
|
||||
# we say so per file rather than silently reading a role as clean.
|
||||
$gitOk = $false
|
||||
try { $null = (& git --version 2>$null); $gitOk = ($LASTEXITCODE -eq 0) } catch { $gitOk = $false }
|
||||
|
||||
# class-tag khuon (M4). em-dash from code point ($EM_DASH, :112 - ASCII-source rule,
|
||||
# gotcha #30); backtick optional (roles sometimes drop the code-span); class view-/gap-.
|
||||
$flagEnumRx = 'FLAG-\d+'
|
||||
$flagTagRx = '## FLAG-\d+ ' + $EM_DASH + ' `?(?:view-|gap-)[a-z-]+`? ' + $EM_DASH + ' (?:LOW|MED|HIGH)'
|
||||
$roleNameRx = '(?i)(lead-stale|lead-gap|ring2-audit)'
|
||||
|
||||
$freshFolders = @(Get-ChildItem -LiteralPath $runsRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -ge $C11_LAND_DATE })
|
||||
|
||||
foreach ($rf in $freshFolders) {
|
||||
$subMd = @(Get-ChildItem -LiteralPath $rf.FullName -Filter 'sub-*.md' -File -ErrorAction SilentlyContinue)
|
||||
foreach ($sm in $subMd) {
|
||||
|
||||
# ---- (a) FLAG-khuon: H24 auditor sub-file must class-tag every raised flag ----
|
||||
if ($sm.Name -match $roleNameRx) {
|
||||
$c11Measured = $true
|
||||
$smLines = @(Get-Content -LiteralPath $sm.FullName -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||
$hasEnum = $false; $tagged = $false; $firstEnumLine = 0
|
||||
for ($li = 0; $li -lt $smLines.Count; $li++) {
|
||||
if ($smLines[$li] -cmatch $flagEnumRx) { $hasEnum = $true; if ($firstEnumLine -eq 0) { $firstEnumLine = $li + 1 } }
|
||||
if ($smLines[$li] -match $flagTagRx) { $tagged = $true }
|
||||
}
|
||||
if ($hasEnum -and -not $tagged) {
|
||||
if ($firstEnumLine -eq 0) { $firstEnumLine = 1 }
|
||||
Write-InformFlag ("{0}:{1}" -f (Rel $sm.FullName), $firstEnumLine) `
|
||||
'H24 FLAG-khuon: sub-file raises FLAG-<n> but has NO conforming class-tag line "## FLAG-<n> [em-dash] `<view-/gap-...>` [em-dash] SEV" - the by-class tally cannot count an untagged flag (S148 lost 2/9 flags exactly this way)' `
|
||||
'tag EACH raised flag on its own line "## FLAG-<n> [em-dash=U+2014] `<class>` [em-dash] SEV" (class in view-*/gap-*); one conforming line un-flags the file'
|
||||
$c11Flags++
|
||||
}
|
||||
}
|
||||
|
||||
# ---- (b) diary-delta (M9): INFORM-only role artifact with no fresher diary commit ----
|
||||
foreach ($role in $informRoles) {
|
||||
if ($sm.Name -match ('^sub-' + [regex]::Escape($role) + '[-.]')) {
|
||||
if (-not $gitOk) {
|
||||
Write-Host (" [note] git unavailable - diary-delta for role '{0}' NOT measured (not read as clean)" -f $role) -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
$c11Measured = $true
|
||||
$diaryRel = ".claude/agent-memory/$role/"
|
||||
$gitOut = $null; $code = 1
|
||||
try { $gitOut = & git -C $RepoRoot log -1 --format=%cI -- $diaryRel 2>$null; $code = $LASTEXITCODE } catch { $gitOut = $null; $code = 1 }
|
||||
$diaryTime = $null
|
||||
if ($code -eq 0 -and $gitOut) {
|
||||
$parsed = [datetime]::MinValue
|
||||
if ([datetime]::TryParse(([string]$gitOut).Trim(), [ref]$parsed)) { $diaryTime = $parsed }
|
||||
}
|
||||
if (($null -eq $diaryTime) -or ($diaryTime -lt $sm.LastWriteTime)) {
|
||||
$seen = if ($null -eq $diaryTime) { 'no diary commit found' } else { ('diary last commit ' + $diaryTime.ToString('yyyy-MM-ddTHH:mm')) }
|
||||
Write-InformFlag (Rel $sm.FullName) `
|
||||
("diary-0-delta (GAP-3): role '{0}' produced an artifact (mtime {1}) but its diary has no newer commit ({2}) - role ran, memory not seeded (spec M9)" -f $role, $sm.LastWriteTime.ToString('yyyy-MM-ddTHH:mm'), $seen) `
|
||||
("lead APPEND agent-memory/{0}/ on-behalf IN THE SAME session (khuon B3) and commit, so next session does not enter blind" -f $role)
|
||||
$c11Flags++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$c11Note = if ($c11Measured) { '' } else { ' -- MEASURED NOTHING (no fresh H24 sub-file or INFORM-role artifact in the >= land-date window); a 0 after a skip is not a green' }
|
||||
Write-Host (" C11 FLAG-khuon + diary-delta flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]{1}" -f $c11Flags, $c11Note) -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C12 - raw-engine-bypass (INFORM-only, NEW @S149; count SEPARATE, NOT in TOTAL)
|
||||
# ---------------------------------------------------------------------------
|
||||
# FLOOR SOURCE - hub answer, quoted from
|
||||
# broadcasts/inbox/ai_infra/2026-07-03-ai_infra-to-se-batch13-ack-stamp-and-answers.md:27-29
|
||||
# (section title :27 = "Tra loi cau hoi 1 - detector 'workflow chay ngoai engine chuan' co bat
|
||||
# buoc khong?"; answer :29, verbatim, diacritics stripped for the ASCII-source rule):
|
||||
# "Co, o muc san-chuc-nang; con hinh thuc thi SE tu quyet. San = du an phai CO mot cach phat
|
||||
# hien duoc duong chay workflow khong di qua engine chuan cua minh (vi duong do bo qua toan
|
||||
# bo enforce ve harvest ky uc + vai + checkpoint - day la lop loi tung gay tut chat luong ky
|
||||
# uc toan fleet, da RCA). Hinh thuc dat san thi tu chon: grep transcript, quet ledger-vs-run-
|
||||
# folder, hay 'untracked-run detector nhe' nhu SE de xuat - deu dat, mien chay deu moi cuoi
|
||||
# phien."
|
||||
# => This CLOSES the live residue of broadcast 2026-06-18-Governance-h10-flat-detector-refine
|
||||
# khoan (b), which SE TAILORED-OUT at S72 (docs/governance/adap-reports/
|
||||
# 2026-06-18-Governance-harness-10-flat-refine-checklist-v2.md:46). The tailored-out reason
|
||||
# ("SE runs through the Anthropic Workflow tool, no CLI-launcher bypass-surface") was later
|
||||
# FALSIFIED on disk: S119 measured THREE spawn paths - hmw.js (gated), raw Workflow, raw
|
||||
# Agent - two of them ungated (docs/changelog/sessions/
|
||||
# 2026-07-15-S119-adap-6-broadcast-spec-v3.md:69 and :128, "ke CON-LAI").
|
||||
# FORM CHOSEN = session-log sweep (item 1/3 of the hub menu, the cheapest). It ships inside this
|
||||
# net, which is invoked at session-end => satisfies "chay deu moi cuoi phien".
|
||||
# WHAT IT CHECKS: a session log that MENTIONS a spawn (line carrying 'raw Workflow' / 'raw Agent'
|
||||
# / 'raw-Agent' / a 'wf_' run id) while the WHOLE FILE carries no 'run=' line. 'run=' is the
|
||||
# registration token of the wf: line - form measured on disk: "wf: <label> run=<run-id>
|
||||
# runId=wf_<hex>" (docs/changelog/sessions/2026-07-16-2250-S129-adap-errata-eol.md:8) - and it
|
||||
# is what binds a spawn to a tracked run-folder. Mention-without-registration = the signature
|
||||
# of a workflow that ran outside the standard engine.
|
||||
# USE/MENTION discriminator (citation-trap class): a doc that DESCRIBES this detector inevitably
|
||||
# contains its own trigger tokens. Two enclosure guards, both on the silence side:
|
||||
# (a) lines inside ``` fences are skipped;
|
||||
# (b) a line that also names 'C12' or 'governance-detectors' is skipped (self-reference).
|
||||
# Stated cost: a REAL bypass reported on a line that happens to name C12 reads silent. For a
|
||||
# day-one INFORM net, silence is the safe direction (a false alarm kills the net faster).
|
||||
# [!] KHAI THAT - what this CANNOT do:
|
||||
# (1) FILE-level guard: ONE 'run=' anywhere silences the whole file. A session that registered
|
||||
# run A and then spawned B raw is NOT caught. Per-spawn correlation is not derivable from
|
||||
# the session log's prose.
|
||||
# (2) It reads the session's OWN account. A bypass nobody wrote down stays invisible - this is
|
||||
# a self-report net, not a transcript net (the transcript form on the hub menu covers that,
|
||||
# at much higher cost). Do NOT read a green C12 as "no bypass happened".
|
||||
# (3) It does not prove the run-folder exists; it only reads the registration token.
|
||||
# *** FROZEN-HISTORY GATE (critical): only session logs with LastWriteTime >= $C12_LAND_DATE.
|
||||
# MEASURED reason (dry run 2026-07-25 over all 148 session logs): 36 carry 'wf_' but only 9
|
||||
# carry 'run=' => 29 files would flag on day one. Those predate both the wf:-registration habit
|
||||
# and this net; re-flagging a closed period is noise, and 29 LOW lines on day one is exactly how
|
||||
# a net gets ignored. Same discipline as C11. Do NOT "freshen" the constant below - it is the
|
||||
# day C12 landed, by design.
|
||||
# INFORM-only per repo convention (a new net reads separately first; folding into TOTAL and
|
||||
# raising severity is post-triage and owner-gated, never the net author's call).
|
||||
Write-Section 'C12 - raw-engine-bypass (INFORM-only)'
|
||||
$c12Flags = 0
|
||||
$c12Measured = $false
|
||||
|
||||
# LAND DATE - the day this detector shipped (S149, C-b). Session logs touched BEFORE = frozen.
|
||||
$C12_LAND_DATE = [datetime]'2026-07-25'
|
||||
|
||||
$sessLogDir = Join-Path $RepoRoot 'docs\changelog\sessions'
|
||||
if (-not (Test-Path $sessLogDir)) {
|
||||
Write-Host ' [skip] docs/changelog/sessions not found' -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
# Spawn-mention tokens. 'raw[- ]Workflow|Agent' covers both spellings seen on disk
|
||||
# (S119:69 writes "raw Workflow"/"raw Agent"; S131:17 writes "raw-Agent").
|
||||
$bypassRx = 'raw[-\s]?(?:Workflow|Agent)|wf_'
|
||||
$runRegRx = 'run='
|
||||
$mentionRx = 'C12|governance-detectors'
|
||||
|
||||
$freshLogs = @(Get-ChildItem -LiteralPath $sessLogDir -Filter *.md -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -ge $C12_LAND_DATE })
|
||||
|
||||
if ($freshLogs.Count -eq 0) {
|
||||
Write-Host (' [note] no session log with mtime >= {0} - window EMPTY, nothing measured (not a green)' -f `
|
||||
$C12_LAND_DATE.ToString('yyyy-MM-dd')) -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
foreach ($sl in $freshLogs) {
|
||||
$c12Measured = $true
|
||||
$slLines = @(Get-Content -LiteralPath $sl.FullName -Encoding UTF8 -ErrorAction SilentlyContinue)
|
||||
|
||||
# File-level registration guard: one wf: run= line anywhere means this session DID
|
||||
# register its run-folder(s) -> whole file silent (see LIMIT (1) above).
|
||||
$registered = $false
|
||||
foreach ($ln in $slLines) { if ($ln -match $runRegRx) { $registered = $true; break } }
|
||||
if ($registered) {
|
||||
Write-Host (" [ok] {0} carries a run= registration line" -f $sl.Name) -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
$inFence = $false
|
||||
$hitLine = 0
|
||||
$hitTok = ''
|
||||
for ($li = 0; $li -lt $slLines.Count; $li++) {
|
||||
$ln = $slLines[$li]
|
||||
if ($ln -match '^\s*```') { $inFence = -not $inFence; continue }
|
||||
if ($inFence) { continue }
|
||||
if ($ln -match $mentionRx) { continue }
|
||||
$bm = [regex]::Match($ln, $bypassRx, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
||||
if ($bm.Success) { $hitLine = $li + 1; $hitTok = $bm.Value; break }
|
||||
}
|
||||
|
||||
if ($hitLine -gt 0) {
|
||||
Write-InformFlag ("{0}:{1}" -f (Rel $sl.FullName), $hitLine) `
|
||||
("raw-engine-bypass: session log mentions a spawn ('{0}') but the WHOLE file carries no 'run=' registration line - a workflow that never registered a run-folder skips the harvest/role/checkpoint enforcement (hub floor 2026-07-03 answer, section 3)" -f $hitTok) `
|
||||
"add the wf: line 'wf: <label> run=<run-id> runId=wf_<id>' to this session log, or re-run through .claude/workflows/hmw.js so a tracked run-folder exists; a DELIBERATE raw-Agent monitor is fine but must be declared in a session that also carries run="
|
||||
$c12Flags++
|
||||
}
|
||||
else {
|
||||
Write-Host (" [ok] {0} - no unregistered spawn mention" -f $sl.Name) -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
$c12Note = if ($c12Measured) { '' } else { ' -- MEASURED NOTHING (no session log at/after the land date); a 0 outside the window is not a green' }
|
||||
Write-Host (" C12 raw-engine-bypass flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]{1}" -f $c12Flags, $c12Note) -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary + C4 self-exclusion audit (RUNTIME proof)
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -1537,7 +1930,7 @@ if ($selfInScan -eq 0 -and $leaked -eq 0) {
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ("TOTAL FLAGS: {0}" -f $script:FlagCount) -ForegroundColor Cyan
|
||||
Write-Host ("INFORM-ONLY (new nets C6 cite-2-tier + H24-4 pending-flip + H24-5 counter-ancestor-classify): {0} - counted SEPARATELY, NOT in TOTAL above (anti-Goodhart, owner-set; fold+sev-raise is post-triage)" -f $script:InformCount) -ForegroundColor DarkGray
|
||||
Write-Host ("INFORM-ONLY (new nets C6 cite-2-tier + H24-4 pending-flip + H24-5 counter-ancestor-classify + C9 hmw-width-drift + C11 h24-flag-form + C12 raw-engine-bypass): {0} - counted SEPARATELY, NOT in TOTAL above (anti-Goodhart, owner-set; fold+sev-raise is post-triage)" -f $script:InformCount) -ForegroundColor DarkGray
|
||||
Write-Host 'NOTE: DETECT-only lowering net. Exit 0 always (never fails build). FLAGs are advisory.' -ForegroundColor DarkGray
|
||||
|
||||
exit 0
|
||||
|
||||
286
scripts/h24-signal-write.ps1
Normal file
286
scripts/h24-signal-write.ps1
Normal file
@ -0,0 +1,286 @@
|
||||
#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,
|
||||
|
||||
[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)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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).
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $resetDone) {
|
||||
$resetCandidates = @($enum | Where-Object {
|
||||
($firedUnique -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 (did not repeat this session - 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)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (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
|
||||
250
scripts/session-counter-tick.ps1
Normal file
250
scripts/session-counter-tick.ps1
Normal file
@ -0,0 +1,250 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Harness-24 session-counter TICK - mechanizes the `_contract` block of
|
||||
.claude/governance/.session-counter.json (M2, spec S149 hoi-tu-bookend).
|
||||
|
||||
.DESCRIPTION
|
||||
Turns the hand-run "tick + classify + append-history" ritual into a machine that
|
||||
touches disk deterministically (the S149 root-cause: SE was rules-rich / machine-poor,
|
||||
so ticks were done from memory and drifted: backfill S146, tally-drop S148, x27 != 33).
|
||||
|
||||
Executes the `_contract` VERBATIM - it does NOT invent policy:
|
||||
tick(session, head):
|
||||
1. read HEAD via git (content-addressed: a tick is keyed on the repo HEAD sha).
|
||||
2. OR-guard idempotent: session == last_ticked_session OR head == last_ticked_head
|
||||
=> NO-OP, exit 0, print one line. (The Stop-hook wal-flush.ps1 moves HEAD every
|
||||
turn-boundary, so the head-clause alone would let one real session tick twice; the
|
||||
session-label clause pins ONE label to exactly +1.)
|
||||
3. classify BEFORE tick (two triggers, two verdicts - contract v2):
|
||||
(a) counter REGRESSION (incoming session number < stored) => FAIL-LOUD, exit!=0, no write.
|
||||
(b) last_ticked_head object MISSING (git cat-file fails) => FAIL-LOUD, exit!=0, no write.
|
||||
(c) last_ticked_head NOT reachable from HEAD but object EXISTS and counter did NOT
|
||||
regress => SQUASH-BENIGN: append ONE trace entry to history[] and CONTINUE (no owner
|
||||
alarm; a closeout squash routinely lifts the ticked wal:/session commit out of history).
|
||||
else (reachable + no regress) => CLEAN tick.
|
||||
4. tick: counter+1; update the 4 fields (counter, last_ticked_session, last_ticked_head,
|
||||
last_ticked_at); append ONE history entry classed clean | squash-benign.
|
||||
5. write ATOMICALLY: temp file then Move-Item -Force (khuon hub h17_cadence.py _save_atomic
|
||||
:174-184) - a crash mid-write leaves the original file intact.
|
||||
|
||||
SCOPE GUARDS (per M2): does NOT touch `class_repeat` (that is M3) and does NOT reset anything.
|
||||
|
||||
WRITE STRATEGY - why surgical string-replace, not ConvertTo-Json:
|
||||
Only Windows PowerShell 5.1 is present here (no pwsh 7 / no System.Text.Json). PS 5.1
|
||||
ConvertTo-Json re-serializes the whole file with CRLF + char-escaping (measured: 25900 ->
|
||||
28495 bytes) = a noisy diff on a committed governance file, and risks mangling the 8 emoji.
|
||||
So the file is PARSED with ConvertFrom-Json (for the guard logic) but WRITTEN by anchored
|
||||
line replacement + one array-insert on the raw UTF-8 text - preserving formatting byte-exact
|
||||
(measured surgical write: 25900 -> 25900 + one history entry, LF-only, emoji intact).
|
||||
Read/write both go through explicit UTF-8-no-BOM (E-010 trap: a missing -Encoding reads ANSI).
|
||||
|
||||
.PARAMETER Session
|
||||
Session label, e.g. "S150". Must match ^S\d+ (the leading number is the monotonic guard).
|
||||
|
||||
.PARAMETER RepoRoot
|
||||
Repo root. Default = the parent of this script's folder (the SE repo). Fault-injection (K2)
|
||||
points this at a throwaway git tree holding a copy of the counter json.
|
||||
|
||||
.PARAMETER FaultStopAfterTemp
|
||||
K2(e) FAULT-INJECTION ONLY (default off): perform the real write up to and including the temp
|
||||
file, then STOP before Move-Item - simulating a crash between write and rename so a test can
|
||||
assert the ORIGINAL file is still intact. Never used in production call-sites.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -File scripts/session-counter-tick.ps1 -Session S150
|
||||
|
||||
.NOTES
|
||||
Contract source : .claude/governance/.session-counter.json (_contract, read verbatim)
|
||||
Hub reference : AI_INFRA/scripts/h17_cadence.py (_save_atomic :174-184)
|
||||
Caller (M6) : /session-start + /tiep call this instead of prose.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Session,
|
||||
[string]$RepoRoot,
|
||||
[switch]$FaultStopAfterTemp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
# ------------------------------------------------------------------ output helpers
|
||||
function Write-Line([string]$msg) { [Console]::Out.WriteLine($msg) }
|
||||
|
||||
function Fail-Loud([string]$msg, [int]$code) {
|
||||
# Single clear line to STDERR + non-zero exit (NOT a raw PowerShell exception dump).
|
||||
[Console]::Error.WriteLine("[h24-tick] FAIL-LOUD: $msg")
|
||||
exit $code
|
||||
}
|
||||
|
||||
function Parse-SessionNum([string]$label) {
|
||||
if ([string]::IsNullOrWhiteSpace($label)) { return $null }
|
||||
$m = [regex]::Match($label, '^[Ss]?(\d+)')
|
||||
if ($m.Success) { return [int]$m.Groups[1].Value }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Anchored single-line replace on the raw text. Asserts EXACTLY one match (shape-drift = fail-loud),
|
||||
# and uses a literal MatchEvaluator so no '$' in the replacement is ever re-interpreted.
|
||||
function Replace-LineOnce([string]$text, [string]$pattern, [string]$newLine, [string]$what) {
|
||||
$rx = New-Object System.Text.RegularExpressions.Regex($pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
|
||||
$n = $rx.Matches($text).Count
|
||||
if ($n -ne 1) {
|
||||
Fail-Loud "surgical replace '$what' expected exactly 1 match, found $n - counter-file shape drift; refusing to write." 7
|
||||
}
|
||||
$ev = [System.Text.RegularExpressions.MatchEvaluator]({ param($m) $newLine }.GetNewClosure())
|
||||
return $rx.Replace($text, $ev, 1)
|
||||
}
|
||||
|
||||
function Json-Escape([string]$s) {
|
||||
# Minimal JSON string escaping. Event text is authored quote/backslash-free, but stay safe.
|
||||
$s = $s -replace '\\', '\\'
|
||||
$s = $s -replace '"', '\"'
|
||||
return $s
|
||||
}
|
||||
|
||||
# Run git and return its trimmed stdout + real exit code. Native git writes to stderr on a
|
||||
# missing object / bad ref; under $ErrorActionPreference='Stop' PS 5.1 wraps that stderr as a
|
||||
# TERMINATING NativeCommandError (even with 2>$null), so classification could never run. Drop to
|
||||
# 'Continue' just around the call so a non-zero git exit is DATA we branch on, not a crash.
|
||||
function Invoke-GitLines([string]$root, [string[]]$gitArgs) {
|
||||
$old = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
$out = & git -C $root @gitArgs 2>$null
|
||||
$code = $LASTEXITCODE
|
||||
}
|
||||
finally { $ErrorActionPreference = $old }
|
||||
return [pscustomobject]@{ Out = ("$out").Trim(); Code = $code }
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ resolve paths
|
||||
if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
|
||||
if ([string]::IsNullOrWhiteSpace($PSScriptRoot)) { $RepoRoot = (Get-Location).Path }
|
||||
else { $RepoRoot = Split-Path $PSScriptRoot -Parent }
|
||||
}
|
||||
try { $RepoRoot = (Resolve-Path -LiteralPath $RepoRoot -ErrorAction Stop).Path }
|
||||
catch { Fail-Loud "RepoRoot does not exist: $RepoRoot" 8 }
|
||||
|
||||
$counterFile = Join-Path $RepoRoot ".claude/governance/.session-counter.json"
|
||||
|
||||
# ------------------------------------------------------------------ validate input
|
||||
if ($Session -notmatch '^S\d+') {
|
||||
Fail-Loud "invalid -Session '$Session' - expected an S<nn> label (e.g. S150)." 8
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ read + parse counter file
|
||||
if (-not (Test-Path -LiteralPath $counterFile)) {
|
||||
Fail-Loud "counter file missing: $counterFile" 5
|
||||
}
|
||||
$rawText = [System.IO.File]::ReadAllText($counterFile, [System.Text.Encoding]::UTF8)
|
||||
try { $data = $rawText | ConvertFrom-Json }
|
||||
catch { Fail-Loud "counter file is not valid JSON: $counterFile ($_)" 6 }
|
||||
|
||||
$storedCounter = [int]$data.counter
|
||||
$storedSession = "$($data.last_ticked_session)"
|
||||
$storedHead = "$($data.last_ticked_head)"
|
||||
|
||||
# ------------------------------------------------------------------ (1) HEAD via git
|
||||
$gh = Invoke-GitLines $RepoRoot @('rev-parse', 'HEAD')
|
||||
if ($gh.Code -ne 0 -or [string]::IsNullOrWhiteSpace($gh.Out)) {
|
||||
Fail-Loud "cannot resolve git HEAD in $RepoRoot (not a git repo?)." 4
|
||||
}
|
||||
$headSha = $gh.Out
|
||||
|
||||
# ------------------------------------------------------------------ (2) OR-guard idempotent
|
||||
if ($Session -eq $storedSession -or $headSha -eq $storedHead) {
|
||||
$reason = if ($Session -eq $storedSession) { "session-label '$Session' already ticked" }
|
||||
else { "HEAD $($headSha.Substring(0,7)) already ticked" }
|
||||
Write-Line "[h24-tick] NO-OP: $reason (stored $storedSession @ $($storedHead.Substring(0, [Math]::Min(7,$storedHead.Length)))); counter stays $storedCounter."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ (3) classify BEFORE tick
|
||||
# (3a) counter regression - incoming session number below the stored one = rollback / hand-edit.
|
||||
$inNum = Parse-SessionNum $Session
|
||||
$storedNum = Parse-SessionNum $storedSession
|
||||
if ($null -ne $storedNum -and $null -ne $inNum -and $inNum -lt $storedNum) {
|
||||
Fail-Loud "counter REGRESSION: incoming $Session (n=$inNum) < stored $storedSession (n=$storedNum) - the counter file was rolled back or hand-edited. Refusing to tick or write (contract fail_loud_on_regress trigger-1); report to owner." 2
|
||||
}
|
||||
|
||||
# (3b/3c) reachability classification of the stored head (skip if there is no prior head = fresh seed).
|
||||
$eventClass = 'clean'
|
||||
$reachNote = 'no prior head (fresh tick)'
|
||||
if (-not [string]::IsNullOrWhiteSpace($storedHead) -and $storedHead -ne 'null') {
|
||||
$gc = Invoke-GitLines $RepoRoot @('cat-file', '-t', $storedHead)
|
||||
$objExists = ($gc.Code -eq 0 -and $gc.Out -eq 'commit')
|
||||
if (-not $objExists) {
|
||||
Fail-Loud "last_ticked_head $storedHead is a MISSING object (git cat-file -t failed) - not reachable AND not present = possible tamper/rollback. Refusing to tick (contract fail_loud_on_regress trigger-2, missing branch); report to owner." 3
|
||||
}
|
||||
$gm = Invoke-GitLines $RepoRoot @('merge-base', '--is-ancestor', $storedHead, $headSha)
|
||||
$reachable = ($gm.Code -eq 0)
|
||||
if ($reachable) {
|
||||
$reachNote = "reachable (merge-base --is-ancestor exit 0)"
|
||||
$eventClass = 'clean'
|
||||
}
|
||||
else {
|
||||
# object EXISTS + counter did NOT regress (checked above) => squash-benign, continue.
|
||||
$reachNote = "object EXISTS (cat-file=commit) but NOT reachable (merge-base --is-ancestor exit!=0), counter not regressed"
|
||||
$eventClass = 'squash-benign'
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ (4) tick
|
||||
$newCounter = $storedCounter + 1
|
||||
$gd = Invoke-GitLines $RepoRoot @('log', '-1', '--format=%cd', '--date=short', $headSha)
|
||||
if ($gd.Code -ne 0 -or [string]::IsNullOrWhiteSpace($gd.Out)) {
|
||||
$tickDate = (Get-Date -Format 'yyyy-MM-dd')
|
||||
}
|
||||
else { $tickDate = $gd.Out }
|
||||
$oldShort = $storedHead.Substring(0, [Math]::Min(7, $storedHead.Length))
|
||||
$newShort = $headSha.Substring(0, 7)
|
||||
|
||||
if ($eventClass -eq 'squash-benign') {
|
||||
$event = "squash-benign (session-counter-tick.ps1 M2, contract fail_loud_on_regress trigger-2 BENIGN branch): counter $storedCounter->$newCounter, session $storedSession->$Session, head $oldShort->$newShort. last_ticked_head $oldShort $reachNote => a closeout squash lifted the ticked wal:/session commit out of history (expected drift, not tamper). Trace appended, continue, no owner alarm. Written atomically (temp + Move-Item -Force)."
|
||||
}
|
||||
else {
|
||||
$event = "CLEAN tick (session-counter-tick.ps1 M2): counter $storedCounter->$newCounter, session $storedSession->$Session, head $oldShort->$newShort. Classify-before-tick: no regression (n=$inNum >= stored n=$storedNum); last_ticked_head $reachNote. 4 fields updated, 1 history entry appended, written atomically (temp + Move-Item -Force)."
|
||||
}
|
||||
$eventJson = Json-Escape $event
|
||||
|
||||
# ---- surgical edits on the raw text (preserve formatting / emoji / LF byte-exact) ----
|
||||
$new = $rawText
|
||||
$new = Replace-LineOnce $new '^ "counter": \d+,$' (' "counter": ' + $newCounter + ',') 'counter'
|
||||
$new = Replace-LineOnce $new '^ "last_ticked_session": "[^"]*",$' (' "last_ticked_session": "' + $Session + '",') 'last_ticked_session'
|
||||
$new = Replace-LineOnce $new '^ "last_ticked_head": "[^"]*",$' (' "last_ticked_head": "' + $headSha + '",') 'last_ticked_head'
|
||||
$new = Replace-LineOnce $new '^ "last_ticked_at": "[^"]*",$' (' "last_ticked_at": "' + $tickDate + '",') 'last_ticked_at'
|
||||
|
||||
# ---- append ONE history entry: insert before the array close (anchored at EOF, exactly once) ----
|
||||
$entry = " {`n" +
|
||||
" `"at`": `"$tickDate`",`n" +
|
||||
" `"session`": `"$Session`",`n" +
|
||||
" `"event`": `"$eventJson`"`n" +
|
||||
" }"
|
||||
$histRx = New-Object System.Text.RegularExpressions.Regex('(\n \})(\n \]\n\}\n?)$', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||
$histN = $histRx.Matches($new).Count
|
||||
if ($histN -ne 1) {
|
||||
Fail-Loud "history array-close anchor expected exactly 1 match at EOF, found $histN - counter-file shape drift; refusing to write." 7
|
||||
}
|
||||
$histEv = [System.Text.RegularExpressions.MatchEvaluator]({ param($m) $m.Groups[1].Value + ",`n" + $entry + $m.Groups[2].Value }.GetNewClosure())
|
||||
$new = $histRx.Replace($new, $histEv, 1)
|
||||
|
||||
# ---- validate the result is still parseable BEFORE we touch disk ----
|
||||
try { $null = $new | ConvertFrom-Json }
|
||||
catch { Fail-Loud "post-edit JSON failed to parse - aborting before write (no file touched): $_" 9 }
|
||||
|
||||
# ------------------------------------------------------------------ (5) atomic write
|
||||
$tmp = $counterFile + '.tmp'
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($tmp, $new, $utf8NoBom)
|
||||
|
||||
if ($FaultStopAfterTemp) {
|
||||
[Console]::Error.WriteLine("[h24-tick] FAULT-INJECT: wrote temp then STOPPED before Move-Item (simulated crash). Original intact: $counterFile ; dangling temp: $tmp")
|
||||
exit 42
|
||||
}
|
||||
|
||||
Move-Item -Force -LiteralPath $tmp -Destination $counterFile
|
||||
Write-Line "[h24-tick] ${eventClass}: ticked $storedSession -> $Session, counter $storedCounter -> $newCounter (HEAD $newShort). Wrote $counterFile atomically."
|
||||
exit 0
|
||||
158
scripts/session-scaffold.ps1
Normal file
158
scripts/session-scaffold.ps1
Normal file
@ -0,0 +1,158 @@
|
||||
# session-scaffold.ps1 -- scaffold 1 logic-session folder + _context skeleton (M1, S149).
|
||||
# TAILOR tu hub AI_INFRA/scripts/session_scaffold.py (169 dong) -> SE PowerShell, 2-mode split @S146.
|
||||
#
|
||||
# MODE (exactly one):
|
||||
# -New : allocate a NEW logic-session = max+1 (session-start ONLY). Optional -N pins an
|
||||
# explicit id; if that id already exists (folder OR _context) -> COLLISION
|
||||
# (exit 4, writes NOTHING).
|
||||
# -Ensure : idempotent heal of the CURRENT session (pause/tiep). _context missing -> create;
|
||||
# _context present -> exit 0 NO-OP (mtime preserved). Optional -N pins the id;
|
||||
# default = max existing session.
|
||||
#
|
||||
# INVARIANTS:
|
||||
# - ts-moc = `git log -1 --format=%cI` of RepoRoot HEAD (git-committer). NO wall-clock: git
|
||||
# unavailable -> exit 3 (refuse Get-Date). Deliberate DEVIATION from hub tagged-FALLBACK --
|
||||
# spec M1 line 33 "khong wall-clock"; ts feeds C10 LATENESS measurement, must be deterministic.
|
||||
# - NEVER overwrite an existing _context (ANY mode) -- hard guard in Write-Context (exit 5).
|
||||
# - Pure-ASCII source (gotcha #30/#37 = PS 5.1 diacritics parser-fail). Vietnamese lives ONLY
|
||||
# in the external template .md, never in this .ps1.
|
||||
#
|
||||
# -RepoRoot = fault-inject seam: point at a scratch tree to prove teeth, not just happy-path.
|
||||
# Exit codes: 0 ok/no-op | 2 usage | 3 git-unavailable | 4 collision | 5 defensive-overwrite-block.
|
||||
|
||||
param(
|
||||
[switch]$New,
|
||||
[switch]$Ensure,
|
||||
[int]$N,
|
||||
[string]$RepoRoot = "$PSScriptRoot\.."
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# --- helpers ---
|
||||
function Fail {
|
||||
param([string]$Msg, [int]$Code)
|
||||
Write-Output "session-scaffold: ERROR: $Msg"
|
||||
exit $Code
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
# Run git with stderr discarded; return @{ Out; Code }. EAP=Continue so a non-repo tree
|
||||
# (git exit 128) does NOT throw under the script-wide Stop preference.
|
||||
param([string[]]$GitArgs)
|
||||
$old = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
$out = & git @GitArgs 2>$null
|
||||
return @{ Out = (($out | Out-String)).Trim(); Code = $LASTEXITCODE }
|
||||
} finally {
|
||||
$ErrorActionPreference = $old
|
||||
}
|
||||
}
|
||||
|
||||
function Rel {
|
||||
param([string]$Path, [string]$Root)
|
||||
if ($Path.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $Path.Substring($Root.Length).TrimStart('\', '/')
|
||||
}
|
||||
return $Path
|
||||
}
|
||||
|
||||
function Write-Context {
|
||||
# Render template -> _context, UTF-8 no-BOM + LF. HARD guard: never overwrite an existing
|
||||
# _context (any mode) -- this is the durable owner-evidence file.
|
||||
param([string]$TemplatePath, [string]$OutPath, [int]$SessionN, [string]$Ts, [string]$Head)
|
||||
if (Test-Path -LiteralPath $OutPath) {
|
||||
Fail "REFUSE-OVERWRITE: $OutPath exists (never overwrite _context)" 5
|
||||
}
|
||||
$tpl = [System.IO.File]::ReadAllText($TemplatePath, [System.Text.Encoding]::UTF8)
|
||||
$rendered = $tpl.Replace('{{N}}', "$SessionN").Replace('{{TS}}', $Ts).Replace('{{HEAD}}', $Head)
|
||||
$rendered = $rendered -replace "`r`n", "`n"
|
||||
$outDir = Split-Path -Parent $OutPath
|
||||
if (-not (Test-Path -LiteralPath $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
$enc = New-Object System.Text.UTF8Encoding($false) # $false = emit NO byte-order-mark
|
||||
[System.IO.File]::WriteAllText($OutPath, $rendered, $enc)
|
||||
}
|
||||
|
||||
# --- validate mode (exactly one) ---
|
||||
if ($New -and $Ensure) { Fail "specify exactly one of -New / -Ensure (both given)" 2 }
|
||||
if (-not $New -and -not $Ensure) { Fail "specify exactly one of -New / -Ensure (none given)" 2 }
|
||||
|
||||
$explicitN = $PSBoundParameters.ContainsKey('N')
|
||||
if ($explicitN -and $N -lt 0) { Fail "-N must be a non-negative integer (got $N)" 2 }
|
||||
|
||||
# --- resolve root + template ---
|
||||
if (-not (Test-Path -LiteralPath $RepoRoot)) { Fail "RepoRoot not found: $RepoRoot" 2 }
|
||||
$root = (Resolve-Path -LiteralPath $RepoRoot).Path
|
||||
$templatePath = Join-Path $root '.claude\templates\session-context-template.md'
|
||||
if (-not (Test-Path -LiteralPath $templatePath)) { Fail "template not found: $templatePath" 2 }
|
||||
$sessionsRoot = Join-Path $root '.claude\sessions'
|
||||
|
||||
# --- compute max existing session-<N> ---
|
||||
$maxN = 0
|
||||
if (Test-Path -LiteralPath $sessionsRoot) {
|
||||
Get-ChildItem -LiteralPath $sessionsRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
if ($_.Name -match '^session-(\d+)$') {
|
||||
$k = [int]$Matches[1]
|
||||
if ($k -gt $maxN) { $maxN = $k }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- resolve target N per mode ---
|
||||
if ($New) {
|
||||
if ($explicitN) { $targetN = $N } else { $targetN = $maxN + 1 }
|
||||
} else {
|
||||
if ($explicitN) {
|
||||
$targetN = $N
|
||||
} else {
|
||||
if ($maxN -lt 1) { Fail "-Ensure: no existing session to heal (run -New first)" 2 }
|
||||
$targetN = $maxN
|
||||
}
|
||||
}
|
||||
|
||||
$sessionDir = Join-Path $sessionsRoot "session-$targetN"
|
||||
$contextPath = Join-Path $sessionDir "_context-s-$targetN.md"
|
||||
$folderExists = Test-Path -LiteralPath $sessionDir
|
||||
$contextExists = Test-Path -LiteralPath $contextPath
|
||||
|
||||
# --- ts-moc from git HEAD (NO wall-clock; refuse if git down) ---
|
||||
$r1 = Invoke-Git @('-C', $root, 'log', '-1', '--format=%cI')
|
||||
$r2 = Invoke-Git @('-C', $root, 'log', '-1', '--format=%h')
|
||||
if ($r1.Code -ne 0 -or $r2.Code -ne 0) {
|
||||
Fail "git HEAD unavailable at $root -- refusing wall-clock fallback (spec M1: no wall-clock)" 3
|
||||
}
|
||||
$ts = $r1.Out
|
||||
$head = $r2.Out
|
||||
if ([string]::IsNullOrWhiteSpace($ts)) {
|
||||
Fail "git returned empty committer-ts -- refusing wall-clock fallback" 3
|
||||
}
|
||||
|
||||
# --- mode: New (allocate; collision => write NOTHING) ---
|
||||
if ($New) {
|
||||
if ($folderExists -or $contextExists) {
|
||||
$why = if ($contextExists) { "_context exists" } else { "session folder exists" }
|
||||
Write-Output "session-scaffold: COLLISION session-$targetN ($why) -- writing NOTHING (exit 4)."
|
||||
exit 4
|
||||
}
|
||||
Write-Context -TemplatePath $templatePath -OutPath $contextPath -SessionN $targetN -Ts $ts -Head $head
|
||||
Write-Output "session-scaffold: [New] created session-$targetN (max was $maxN)"
|
||||
Write-Output " folder = $(Rel $sessionDir $root)"
|
||||
Write-Output " context = $(Rel $contextPath $root)"
|
||||
Write-Output " ts-moc = $ts (HEAD $head)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- mode: Ensure (idempotent heal) ---
|
||||
if ($contextExists) {
|
||||
Write-Output "session-scaffold: [Ensure] session-$targetN _context present -- NO-OP (exit 0, mtime preserved)."
|
||||
exit 0
|
||||
}
|
||||
Write-Context -TemplatePath $templatePath -OutPath $contextPath -SessionN $targetN -Ts $ts -Head $head
|
||||
$note = if ($folderExists) { "healed (folder existed, _context missing)" } else { "created (folder was missing)" }
|
||||
Write-Output "session-scaffold: [Ensure] session-$targetN $note"
|
||||
Write-Output " context = $(Rel $contextPath $root)"
|
||||
Write-Output " ts-moc = $ts (HEAD $head)"
|
||||
exit 0
|
||||
@ -6,7 +6,8 @@
|
||||
Owner-decision S119 PA-2 (runs/2026-07-15-S119-adap-6-broadcast/owner-decisions-15-07-2026.md:59-60)
|
||||
splits "anchor the model version" in two, because anchoring AT SPAWN is NOT possible at SE
|
||||
(the spawn param takes an enum alias only; SE has 0 full-id in any definition file - H8 all-inherit):
|
||||
PA-2a .claude/workflows/hmw.js declares TIER2_EXPECTED_FULL_ID = 'claude-opus-4-8'.
|
||||
PA-2a .claude/workflows/hmw.js declares TIER2_EXPECTED_FULL_ID (value lives ONLY there -
|
||||
single-source, do NOT copy it here; stale copy 'claude-opus-4-8' fixed S150).
|
||||
PA-2b THIS script compares the model actually RESOLVED at spawn against that constant.
|
||||
Drift => print FLAG => owner decides re-pin. NO enforcement (H23 section 2(4)).
|
||||
|
||||
@ -93,7 +94,12 @@ $ErrorActionPreference = 'Continue'
|
||||
# Cost, stated plainly: an owner re-pin must touch BOTH files. That cost IS the check.
|
||||
# NOT a fallback: if hmw.js has no constant, section A fails loud - it never borrows this value.
|
||||
# ---------------------------------------------------------------------------
|
||||
$OWNER_RATIFIED_TIER2_FULL_ID = 'claude-opus-4-8'
|
||||
# 🔄 VALUE-update 2026-07-25 (broadcast model-default-opus-5-max sha 59ce1d0f + SE-owner "dam Opus 5 MAX"):
|
||||
# 'claude-opus-4-8' -> 'claude-opus-5'. Khoan 1.4: records cu mang 'claude-opus-4-8*' = LEGACY bucket
|
||||
# trong giai doan chuyen tiep — dem rieng, KHONG co drift (tranh bao canh bao gia tren ban ghi cu).
|
||||
# 2-key: doi DONG THOI voi TIER2_EXPECTED_FULL_ID trong .claude/workflows/hmw.js (cung commit).
|
||||
$OWNER_RATIFIED_TIER2_FULL_ID = 'claude-opus-5'
|
||||
$TIER2_LEGACY_FULL_ID = 'claude-opus-4-8'
|
||||
|
||||
# Family that the 'opus' alias maps to. Alias locks the FAMILY, not the version (H23 section 2(2)):
|
||||
# same family + different version = the drift this audit exists to catch.
|
||||
@ -148,10 +154,11 @@ if (-not (Test-Path $hmwPath)) {
|
||||
else {
|
||||
# STRUCTURAL anchor, NOT a loose string grep (W0.4 lesson 1 - self-reference).
|
||||
# Capture the VALUE of the assignment. A bare grep for the version string is NOT acceptable
|
||||
# evidence here: hmw.js:36 already contains the literal 'claude-opus-4-8' inside a COMMENT
|
||||
# ("KHONG con demote-pin ..."), so a naive grep returns >=1 hit and passes VACUOUSLY even when
|
||||
# the constant is absent or holds a wrong value. The naive count is printed below as a
|
||||
# counter-example only - it is never used as evidence.
|
||||
# evidence here: hmw.js also carries the expected literal inside COMMENT text (measured S150:
|
||||
# naive grep = 2 hits, both comments), so a naive grep returns >=1 hit and passes VACUOUSLY
|
||||
# even when the constant is absent or holds a wrong value. (Old line-pinned example
|
||||
# 'hmw.js:36 demote-pin' died in an edit - keep this description line-number-free, S150.)
|
||||
# The naive count is printed below as a counter-example only - it is never used as evidence.
|
||||
$RX_CONST = 'TIER2_EXPECTED_FULL_ID\s*=\s*[''"]([^''"]+)[''"]'
|
||||
$constHits = @(Select-String -Path $hmwPath -Pattern $RX_CONST -Encoding UTF8 -AllMatches)
|
||||
|
||||
@ -223,6 +230,7 @@ $dispatch = 0 # spawn lanes found (one agent-*.jsonl = one lane)
|
||||
$explicit = 0 # lanes where a resolved model is actually recorded
|
||||
$mismatch = 0 # lanes resolved to the tier-2 FAMILY but NOT the expected version = alias drift
|
||||
$otherFam = 0 # lanes resolved outside the tier-2 family (escape-hatch / inherited lead)
|
||||
$legacy = 0 # lanes resolved the PRE-FLIP default (transition bucket, Khoan 1.4 - counted, not flagged)
|
||||
$modelTally = @{}
|
||||
|
||||
Write-Host (" transcript root: {0}" -f $TranscriptRoot) -ForegroundColor DarkGray
|
||||
@ -302,11 +310,21 @@ else {
|
||||
}
|
||||
else {
|
||||
$bad = @($inFamily | Where-Object { $_ -cne $expected })
|
||||
if ($bad.Count -gt 0) {
|
||||
# Khoan 1.4 broadcast model-default-opus-5-max (2026-07-25): records that resolved the
|
||||
# PREVIOUS default are a LEGACY bucket during the transition, counted separately,
|
||||
# NOT flagged as drift (a value-update flips the expected mid-history; old records
|
||||
# predate the flip - flagging them is a false alarm storm, exactly what 1.4 forbids).
|
||||
# A record that is neither expected NOR legacy is still a REAL mismatch.
|
||||
$legacyOnly = @($bad | Where-Object { $_ -cne $TIER2_LEGACY_FULL_ID })
|
||||
if ($bad.Count -gt 0 -and $legacyOnly.Count -eq 0) {
|
||||
$legacy++
|
||||
$verdict = ("legacy ({0} = pre-flip default; transition bucket Khoan 1.4, not drift)" -f ($bad -join ', '))
|
||||
}
|
||||
elseif ($legacyOnly.Count -gt 0) {
|
||||
$mismatch++
|
||||
$verdict = 'MISMATCH'
|
||||
Write-Flag 'HIGH' ("{0}/{1}" -f $wfName, $lane.Name) `
|
||||
("alias-drift: resolved '{0}' is in the tier-2 family but expected '{1}'" -f ($bad -join ', '), $expected) `
|
||||
("alias-drift: resolved '{0}' is in the tier-2 family but expected '{1}' (and not the legacy bucket)" -f ($legacyOnly -join ', '), $expected) `
|
||||
'owner decides re-pin (H23 section 2(2)); update TIER2_EXPECTED_FULL_ID once ratified'
|
||||
}
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user