[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

- 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:
pqhuy1987
2026-07-25 12:17:05 +07:00
parent 2f39a7e22c
commit 398d343403
122 changed files with 7666 additions and 250 deletions

View 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