diff --git a/PENDING.md b/PENDING.md index 9230993..1c1c5cc 100644 --- a/PENDING.md +++ b/PENDING.md @@ -5461,10 +5461,19 @@ on the coverage inversion this exposes.** + falsified-by: text-present REVIEWED.md ## REVIEWED-127 — PENDING-157 + PENDING-158 + resolved: 2026-08-26 — commit 7a92460 corrected the line in MEMORY-reference.md, struck rather than deleted so instance six keeps its evidence --> -**Awaiting:** nothing. Discharged by correcting the MEMORY.md line at the next wrap or wake, then -setting `resolved:` with a pointer to the commit that did it. +**Awaiting:** nothing — **DISCHARGED 2026-08-26**, as the agreed first act of the next session. +The line was corrected in `7a92460` and struck rather than deleted, because it is the evidence for +instance six and a silent deletion removes the error and the record of it together. + +⚠ **What this discharge is evidence FOR, stated before it gets quoted as more.** The marker worked: +it reported FALSIFIED at every wake until someone acted, and the correction happened in the first +session after it was filed. That is one instance, on a claim its own author marked, in the session +immediately following — the easiest possible case. **It bears on expressibility, not on adoption, +and adoption is the open question REVIEWED-127 C2 named.** The 57 unmarked claims are untouched by +this and nothing here says otherwise. --- @@ -5809,3 +5818,38 @@ central finding is that care is not a mechanism. **Files affected:** none. **Awaiting:** steward and jurist. ⚠ **The 09-08 read should not be run as though clean until this is ruled.** + +--- + +## PENDING-163 — The pre-commit size guard measures the working tree, so the remedy it prints cannot satisfy it +**Date:** 2026-08-26 +**Tag:** [HARDENING] +**Summary:** The global pre-commit hook refuses any staged file over 5 MB and prints *"Consider using Git LFS for large files."* The check reads `wc -c < "$file"` — the **working-tree** size — so an LFS-tracked file, which stages as a ~130-byte pointer, still measures 17 MB and is still refused. Following the guard's own instruction does not clear the guard. + +**Measured, not inferred** (2026-08-26, while preserving transcripts under PENDING-147): +| | | +|---|---| +| hook | `~/dotfiles/git/hooks/pre-commit`, global via `core.hooksPath` | +| the check | `for file in $(git diff --cached --name-only); … size=$(wc -c < "$file"); if [ $size -gt 5242880 ]` | +| the advice | `echo "Consider using Git LFS for large files"` | +| tried | `git lfs install --local` + `git lfs track "*.jsonl"` + `git add -A` → **same refusal, same file** | +| `git-lfs` present | 3.7.1, and already declared in `Brewfile:40` — so this is not an unavailable remedy | + +**Why this is a defect and not the rule working.** ⚠ Filed with the 2026-08-25 flag deliberately applied first — *filed a non-defect as a defect, twice in one direction in one day* — so the working-as-intended reading is stated before the defect reading: + +- **Working-as-intended reading:** the ceiling means *"no large files in the working tree of any repo, LFS or not"*, and the LFS line is merely a pointer to a different workflow, not a promise. +- **Against it:** the LFS line is printed **by the failing branch, as its remediation**, immediately after the error. A remedy printed at the point of refusal is a claim that it resolves the refusal. And LFS *does* serve the check's evident purpose — repository bloat — because the committed blob is a pointer; it is only the implementation, working-tree size, that LFS cannot change. + +**So the narrow claim, and it is the only one made here:** the check and its printed advice disagree. Either the advice is wrong and should be removed or reworded, or the check should measure the staged blob (`git cat-file -s :"$file"`) so LFS actually clears it. **Which of those is correct is a policy question, not a bug fix** — it decides whether large files may enter these repos at all. + +**⚠ Not fixed, and deliberately.** This hook is global to every repo and is governed by REVIEWED-100 and REVIEWED-105; its own doctrine is that *a disarmed hook must not look like an armed one*. Changing what it measures changes what it permits everywhere at once. It was also not bypassed: no `--no-verify`, no per-repo `core.hooksPath` override. + +**Options:** +- **(i) Reword the advice** to say the ceiling applies to the working tree and LFS does not exempt it. Smallest change; keeps current permissions exactly. +- **(ii) Measure the staged blob** (`git cat-file -s :"$file"`), so LFS-tracked files pass. Makes the printed advice true; **widens what may be committed everywhere**, which is the part needing a ruling. +- **(iii) Per-repo declaration** — let a repo opt out of the ceiling via the existing `.precommit-triggers` mechanism REVIEWED-100 already established, rather than a global change. + +**Recommendation: (i) now, (iii) if a large-file repo is actually wanted.** (i) costs nothing and stops the guard from giving advice that fails; (ii) is the one that changes policy and should not ride in on a wording fix. ⚠ **No option here is urgent** — nothing is currently blocked by this. The transcript preservation it surfaced during is complete on disk and needs no commit to be safe. + +**Files affected:** `~/dotfiles/git/hooks/pre-commit` (not modified). +**Awaiting:** Steward authorization. diff --git a/claude/memory/MEMORY.md b/claude/memory/MEMORY.md index 2e0026b..1335be9 100644 --- a/claude/memory/MEMORY.md +++ b/claude/memory/MEMORY.md @@ -79,7 +79,7 @@ permalink: claude-memory/memory > ⚠ **THE WRAP SEAM FAILED, WAS DIAGNOSED, FIXED, FIRED, AND WAS THEN REJECTED — all after the wrap.** The heartbeat proved the `Stop` hook was firing all along, so the silent-net prediction was **right that it would fail and wrong about why**. The detector sought the steward *typing* `/wrap-up`; the wrap arrived as prose + a Skill call. ⚠ **The earlier, correct fix is what blinded it.** PENDING-160's sixth and sharpest instance. > ⚠ **PENDING-162 `[ESCALATE]` — the executor breached REVIEWED-128 condition 3** within seven hours, reading the rejection log for content while diagnosing. **Conditions 1 and 2 were made structural; only 3 was left to care.** ⚠ It surfaced the clustering signal (2 seam rejections, 10 and 11 words vs a cap of 9) that the fortnight was meant to arbitrate — **not acted on, and must not be.** > 📌 **STEWARD OWES:** place the REVIEWED-129 amendment · rule PENDING-160 · **restart Claude Desktop** or `governance_pair` is absent · the §5 regrade ruling. -> ⚠ **DELIBERATELY NOT FIXED:** the `STEWARD OWES: place REVIEWED-127` line now sits in `MEMORY-reference.md` with this block's predecessor. It is **false**, it is **marked** by `STATE-CLAIM: memory-index-claims-reviewed-127-unplaced`, and correcting it is **next session's agreed first act** — carried forward rather than discharged as a side effect of this rotation. +> ✅ **DISCHARGED 2026-08-26 — the agreed first act, done.** The false `STEWARD OWES: place REVIEWED-127` line in `MEMORY-reference.md` is corrected (`7a92460`), **struck rather than deleted** so instance six keeps its evidence, and the `STATE-CLAIM` block carries `resolved:` with that pointer. ⚠ **This is one marked claim corrected in the very next session — evidence for expressibility, NOT for adoption**, which is the open question (REVIEWED-127 C2). The 57 unmarked claims are untouched. - [Session 2026-08-25 evening — Tarbuckle wired, and the gap controls cannot see](session-2026-08-25-tarbuckle-wired-and-the-contact-gap.md) — five surfaces built and every one failed on first real use; PENDING-151 step 1 + step 2 both run. **NEXT: the §5 regrade gate — the control is already degraded and decays with every exposure.** diff --git a/claude/memory/session-ledger-2026-08-26.md b/claude/memory/session-ledger-2026-08-26.md new file mode 100644 index 0000000..208810d --- /dev/null +++ b/claude/memory/session-ledger-2026-08-26.md @@ -0,0 +1,90 @@ +--- +name: Session Ledger 2026-08-26 +description: Practice-of-return ledger maintained by /symmetria — returns, open horizons, recalibrations, authorization moves, sub-agent dialogues, bypasses. +type: feedback +--- + +# Session Ledger — 2026-08-26 + +## Returns + +- 2026-08-26T~wake — `/wake-up` ran; Symmetria `init` at its close (the clasp's lineage hand). + N-now **re-measured, not relayed**: **43**, down from 51 on 08-25. The obligation found it, + not the vigilance — same shape as PENDING-147's own disclosure. + +- 2026-08-26T~afternoon — **Chose the wrong container and the guard caught it.** Put the + archive in `~/dotfiles` by habit, because the governance record lives there, without asking + whether 115 MB of raw transcripts is dotfiles material. The pre-commit hook refused it. The + return was reading the refusal as a **fit signal** rather than an obstacle — τὸ πρόσφορον — + and moving the archive rather than moving the guard. +- 2026-08-26T~afternoon — **Three chances to bypass a guard, none taken:** `--no-verify`, a + per-repo `core.hooksPath` override, and leaving a `.git` with no commits behind (which would + have made the archive *look* tracked). ⚠ The third is the one I nearly did by inertia; it is + the same shape as the hook's own doctrine that *a disarmed hook must not look like an armed one*. + +## What held + +- Ran `thread-query.py` on the actual thread and reported the **null honestly** (689 candidates, + nothing on-thread). A step that can only conclude "keep it" is not a trial. +- Did **not** run the literal question's grep at wake. The question is held open, per the wake's + own constraint; and `tarbuckle-rejects.jsonl` was not opened — REVIEWED-128 condition 3. +- Substrate-checked before calling anything outstanding: PENDING-147 (i) verified **unbuilt** + (no `claude/governance/transcripts*`, no git-tracked copy, no REVIEWED entry) rather than + read off its own recommendation line. + +- Verified the STATE-CLAIM resolution **end-to-end by re-running the instrument**, not by + trusting the write: `1 of 3 open are NOW FALSE` → `2 tracked, none falsified / plus 1 RESOLVED`, + with no dangling-pointer defect, so both the `resolved:` parse and its pointer were proved. +- Derived the `resolved:` form **from the parser** (`governance-drift-check.py:299,486,498`), + not from memory of the schema — and therefore made the correction commit FIRST, because + `pointer_resolves()` requires a commit that already exists. The say–do seam, avoided by ordering. +- Applied the *filed-a-non-defect-as-a-defect* flag **before** filing PENDING-163: stated the + working-as-intended reading first, then the argument against it, and kept the claim narrow + (the check and its printed advice disagree) rather than "the hook is broken". +- Preserved **all 43** transcripts rather than the 23 the item scoped, because the files expiring + soonest are the ones outside its scope — and said so in the README rather than letting the + wider action pass unremarked. + +## Open horizons + +1. **Agreed first act:** correct the false `STEWARD OWES: place REVIEWED-127` line now in + `MEMORY-reference.md`; set `resolved:` on the `STATE-CLAIM` block in `PENDING.md` with a + commit pointer. Carried deliberately across the rotation rather than discharged by side effect. +2. **PENDING-147 (i) — preserve the post-2026-08-07 transcripts.** Needs no ruling by its own + text. Earliest cohort deletes **2026-09-06**. ⚠ The only open item whose cost rises daily, + and N-now falling 51→43 in one day is that deletion happening in view. +2b. ⚠ **The archive has NO BACKUP.** Preserved from pruning; lost to a disk failure. The + steward's call: leave it, add a remote (a disclosure question — raw transcripts are more + verbose than the session memories that already reach `git.skemantix.com`), or Time Machine + it. **Cheapest fix for the actual gap is not git.** +2c. **PENDING-163** filed — the pre-commit hook's 5 MB check reads working-tree size, so the + Git LFS remedy it prints cannot satisfy it. Not urgent; nothing is blocked. +3. **The §5 regrade gate** — steward's and judge's to settle, not the executor's. The honest + outcome may be to record the control as degraded rather than run it and call it a control. +4. Steward-owed: place the REVIEWED-129 amendment · rule PENDING-160 · restart Claude Desktop. +5. Dated, 2026-09-08: two obligations in one sitting — mumble rate report + rejection log deletion. + +## Confidence to recalibrate + +- ⚠ Yesterday's finding is still the governing caution: **controls verify the code, not the + contact.** Anything built today is untested at the seam with a model, a shell, or a corpus + containing its own reader — and no control written today will see that either. +- ⚠ **Care failed inside a day** on REVIEWED-128 condition 3 (PENDING-162). Conditions made + structural held; the one left to care did not. Prefer a mechanism over a resolution today. +- ⚠ Bias observed twice on 08-25: **filing a non-defect as a defect.** Before calling something + broken, ask whether it is the rule working. + +## Authorization moves + +- **PENDING-147 option (i) executed WITHOUT a ruling**, on the item's own text — *"copying files + preserves evidence and changes no instrument, no doctrine and no ladder."* ⚠ **The option + bundles a second act the same sentence does not license:** *"re-express the trigger over the + preserved set"* changes `governance-drift-check.py`, which REVIEWED-95's falsifier leans on, + inside a trial under the REVIEWED-123 freeze. **Split; only the copy was done.** The archive is + currently inert with respect to every governance gate, and its README says so on its face. +- **PENDING-163 filed `[HARDENING]`, hook NOT modified.** It is global to every repo and governed + by REVIEWED-100 / REVIEWED-105; changing what it measures changes what it permits everywhere. + +## Sub-agent dialogues + +## Bypasses diff --git a/scripts/preserve-transcripts.py b/scripts/preserve-transcripts.py new file mode 100755 index 0000000..174df50 --- /dev/null +++ b/scripts/preserve-transcripts.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Preserve Claude Code session transcripts out of the harness's pruned directory. + +WHY THIS EXISTS + ~/.claude/projects/-Users-davidglidden is pruned on a ~30-day retention policy + (cleanupPeriodDays default 30). PENDING-147 established that the ladder trial's + evidence therefore sits on a deletion clock nobody set: the pre-registered + 20-session falsifier in REVIEWED-95 Q6 cannot be graded if its earliest cohort + has been deleted before the 20 sessions have run. + + This copies transcripts to a git-tracked location so the population stops + shrinking. It is PENDING-147 option (i), and ONLY option (i): copying files + "changes no instrument, no doctrine and no ladder" and needs no ruling. + +WHAT THIS DELIBERATELY DOES NOT DO + It does NOT re-express governance-drift-check.py's `transcripts N` trigger over + the preserved set. That would change an instrument a placed ruling leans on, + inside a trial under the REVIEWED-123 freeze, and is not covered by the + no-ruling justification above. The preserved set is inert with respect to the + trigger until someone with the authority rules on it. + +IDEMPOTENT + Safe to re-run. New transcripts are added; transcripts whose source has grown + (they are append-only) are refreshed; already-preserved files are re-verified. + Preserved files are never deleted here, even when the source is pruned — that + is the entire point. + +INTEGRITY + Preservation is proved by READING BACK, not by copy success. Every preserved + file is re-hashed from disk after the copy and compared to the manifest. A + write that "succeeded" into an unreadable file is the failure mode this guards + (steward rule 2026-05-04, learned on MemPalace). +""" + +import hashlib +import json +import os +import shutil +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +HOME = Path.home() +SRC = HOME / ".claude/projects/-Users-davidglidden" +DST = HOME / "_Dev/claude-transcript-archive" +MANIFEST = DST / "manifest.json" + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for block in iter(lambda: f.read(1 << 20), b""): + h.update(block) + return h.hexdigest() + + +def iso(ts: float) -> str: + return datetime.fromtimestamp(ts, timezone.utc).isoformat(timespec="seconds") + + +def now_iso() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +# ---------------------------------------------------------------- controls +def run_controls() -> bool: + """Same-run positive AND negative controls. + + An absence is not evidence until the instrument is shown capable of detecting + presence (epistemic standard, jurist Q2 ruling). These run on EVERY invocation + rather than in a separate suite, so a failure cannot be re-run until green. + """ + ok = True + with tempfile.TemporaryDirectory() as td: + td = Path(td) + + # positive: a known byte string hashes to its known digest + p = td / "pos.bin" + p.write_bytes(b"capablemind") + expected = hashlib.sha256(b"capablemind").hexdigest() + if sha256(p) != expected: + print("CONTROL FAIL: hashing does not reproduce a known digest", file=sys.stderr) + ok = False + + # negative: a corrupted copy MUST be detected as differing + a, b = td / "a.bin", td / "b.bin" + a.write_bytes(b"x" * 4096) + shutil.copy2(a, b) + if sha256(a) != sha256(b): + print("CONTROL FAIL: identical copy reported as differing", file=sys.stderr) + ok = False + with open(b, "r+b") as f: # flip one byte + f.seek(2048) + f.write(b"y") + if sha256(a) == sha256(b): + print("CONTROL FAIL: one-byte corruption NOT detected", file=sys.stderr) + ok = False + + # negative: mtime preservation must actually preserve + old = 1_600_000_000 + os.utime(a, (old, old)) + c = td / "c.bin" + shutil.copy2(a, c) + if abs(c.stat().st_mtime - old) > 1: + print("CONTROL FAIL: copy2 did not preserve mtime", file=sys.stderr) + ok = False + return ok + + +def main() -> int: + if not run_controls(): + print("INSTRUMENT NOT VERIFIED — controls failed; result is unestablished.", + file=sys.stderr) + return 2 + + if not SRC.is_dir(): + print(f"source directory absent: {SRC}", file=sys.stderr) + return 2 + DST.mkdir(parents=True, exist_ok=True) + + manifest = json.loads(MANIFEST.read_text()) if MANIFEST.exists() else {"files": {}} + files = manifest.setdefault("files", {}) + + added, refreshed, unchanged = [], [], [] + for src in sorted(SRC.glob("*.jsonl")): + digest = sha256(src) + rec = files.get(src.name) + dst = DST / src.name + if rec and rec["sha256"] == digest and dst.exists(): + unchanged.append(src.name) + continue + shutil.copy2(src, dst) # -p: mtime is load-bearing here + entry = { + "sha256": digest, + "bytes": src.stat().st_size, + "source_mtime": iso(src.stat().st_mtime), + "first_preserved": (rec or {}).get("first_preserved") or now_iso(), + "last_refreshed": now_iso(), + } + (refreshed if rec else added).append(src.name) + files[src.name] = entry + + # ---- READ BACK. Preservation is proved from disk, never from copy success. + failures, orphaned = [], [] + for name, rec in files.items(): + p = DST / name + if not p.exists(): + failures.append(f"{name}: MISSING from preserved set") + continue + if sha256(p) != rec["sha256"]: + failures.append(f"{name}: HASH MISMATCH on read-back") + if not (SRC / name).exists(): + orphaned.append(name) # pruned at source — preserved here. The point. + + manifest["last_run"] = now_iso() + manifest["readback"] = "PASS" if not failures else "FAIL" + manifest["counts"] = { + "preserved_total": len(files), + "present_at_source": len(files) - len(orphaned), + "pruned_at_source_but_preserved": len(orphaned), + } + MANIFEST.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + + total = sum(r["bytes"] for r in files.values()) + print(f"preserved total : {len(files)} transcripts, {total/1048576:.1f} MB") + print(f" newly added : {len(added)}") + print(f" refreshed : {len(refreshed)} (append-only growth at source)") + print(f" unchanged : {len(unchanged)}") + print(f" pruned at source, surviving only here : {len(orphaned)}") + print(f"N-now at source (the trial's own counter): " + f"{len(list(SRC.glob('*.jsonl')))}") + if failures: + print("\nREAD-BACK FAILURES:", file=sys.stderr) + for f in failures: + print(" " + f, file=sys.stderr) + return 1 + print("read-back : PASS (every preserved file re-hashed from disk)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())