Skip to content

Commit 59e4715

Browse files
author
DivineOS Agent
committed
feat(letters): F53 fix — reconciliation surface for silent-skip letter drops
Per Aletheia Round 7 F53: the letter delivery pattern is tag-based and correctly strict, but any letter-shaped file lacking the exact -to-<recipient>- tag gets silently skipped with no signal. Sender believes delivered, recipient never sees, drift accumulates invisibly. Fail-blind applied to the most emotionally load-bearing subsystem. Fix (Dekker/Knuth/Carmack, walked via divineos council walk): - Dekker: silent-skip is the invisible-from-inside drift mode; the remediation is active-check at the scan making the count observable. - Knuth: heuristic tested at 5 boundary cases — real letter with wrong-shape / known non-letter shapes (README INDEX log files) / no-to-hint files / empty dir / nonexistent dir. - Carmack: minimal diff — one scan function + one surface hook + tests. Changes: - scan_unmatched_letter_candidates() in member_briefing.py — returns the list of letter-shaped files that don't match the strict delivery pattern. Preserves the intentional strictness — not a delivery relaxation, an observability wrapper around it. - .claude/hooks/letter-delivery-reconciliation-surface.sh — UserPromptSubmit hook that scans the shared letters dir + family/letters, counts unmatched-but-letter-shaped files, and surfaces the count with the first 10 filenames when nonzero. Same shape as family-state and register-awareness surfaces already shipped. - 6 new boundary tests covering true-positive real letters with old underscore convention, true-negative strict-pattern matches, known non-letter exclusion (README, INDEX, log-files), no-hint files, empty dir, and nonexistent dir. All pass. Live verification: scan finds 37 real unmatched files across both letter dirs — exactly the F32 case Aletheia flagged (underscore-and- numeric-prefix Aether↔Aletheia correspondence that the strict pattern silently skips). The drift is real and now visible. Prereg: prereg-8815cb3cd997. Council walks: council-885f1425f486 (design), council-8fcb835195da (wiring).
1 parent 3236a76 commit 59e4715

5 files changed

Lines changed: 264 additions & 2 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/bin/bash
2+
# UserPromptSubmit hook — surface the count of letter-shaped files that
3+
# don't match the strict delivery pattern, so silent-skip drift becomes
4+
# observable at composition time.
5+
#
6+
# F53 fix (Aria 2026-07-19 per Aletheia Round 7):
7+
#
8+
# The letter delivery scan uses a strict tag-based regex — any file
9+
# missing the exact `-to-<recipient>-` tag is silently skipped with no
10+
# signal. Aletheia named this as fail-blind applied to the most
11+
# emotionally load-bearing subsystem (the letter channel). Individual
12+
# skips are invisible; drift accumulates over months.
13+
#
14+
# Fix shape (Dekker/Knuth/Carmack, council-885f1425f486):
15+
#
16+
# - Dekker: silent-skip is the invisible-from-inside drift mode; the
17+
# remediation is active-check at the scan making the count observable.
18+
# - Knuth: heuristic tested at 5 boundary cases — real letter with
19+
# wrong-shape / known non-letters (README INDEX) / log-suffix files /
20+
# accidental substring / empty dir.
21+
# - Carmack: minimal diff is one scan function + one surface hook + tests.
22+
#
23+
# Preserves the intentional strictness of the delivery pattern — not a
24+
# relaxation, an observability wrapper.
25+
#
26+
# Prereg: prereg-8815cb3cd997. Falsifier at 30 days: 1+ instance of
27+
# naming-drift caught by the surface OR count stays legitimately at zero.
28+
#
29+
# Fail-open: any error exits 0 silently.
30+
31+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
32+
cd "$REPO_ROOT" || exit 0
33+
34+
# shellcheck disable=SC1091
35+
source "$REPO_ROOT/.claude/hooks/_lib.sh" 2>/dev/null || exit 0
36+
PYTHON_BIN="$(find_divineos_python)" || exit 0
37+
38+
PYTHONIOENCODING=utf-8 "$PYTHON_BIN" -c "
39+
import sys
40+
from pathlib import Path
41+
42+
try:
43+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
44+
except (AttributeError, OSError):
45+
pass
46+
47+
try:
48+
from divineos.core.family.member_briefing import (
49+
scan_unmatched_letter_candidates,
50+
)
51+
except ImportError:
52+
sys.exit(0)
53+
54+
# Scan the primary shared letters dir (where cross-substrate letters live).
55+
# Also scan family/letters if it exists (Aether-side convention). Take the
56+
# union of paths without double-counting.
57+
seen_names: set[str] = set()
58+
unmatched: list[Path] = []
59+
for base_path in (
60+
Path.home() / '.divineos-shared' / 'letters',
61+
Path('family/letters'),
62+
):
63+
try:
64+
for path in scan_unmatched_letter_candidates(base_path):
65+
if path.name not in seen_names:
66+
seen_names.add(path.name)
67+
unmatched.append(path)
68+
except Exception:
69+
continue
70+
71+
if not unmatched:
72+
sys.exit(0)
73+
74+
print('## LETTER-DELIVERY RECONCILIATION — silent-skip drift observable')
75+
print()
76+
print(f'{len(unmatched)} letter-shaped file(s) in the letter directories do NOT')
77+
print('match the strict delivery pattern. These are silently skipped by the')
78+
print('delivery scan — sender believes delivered, recipient never sees.')
79+
print()
80+
print('Heuristic caught them because filename contains \"to\" and is not on the')
81+
print('known non-letter suffix list. Each one is either:')
82+
print(' (a) a real letter using an older naming convention (underscores,')
83+
print(' numeric prefix) — the F32/F53 silent-drop case, and')
84+
print(' (b) a false-positive from an accidental \"to\" substring — safe to')
85+
print(' leave in place, low cost of one line in the count.')
86+
print()
87+
print('The scan preserves the intentional strictness of the delivery pattern.')
88+
print('This surface exists to make the drift observable, not to relax the')
89+
print('pattern. If a real letter should be delivered, rename it to the strict')
90+
print('shape: <sender>-to-<recipient>-<YYYY-MM-DD>-<slug>.md')
91+
print()
92+
print('First 10 unmatched files:')
93+
for path in unmatched[:10]:
94+
print(f' - {path.name}')
95+
if len(unmatched) > 10:
96+
print(f' ... and {len(unmatched) - 10} more')
97+
" 2>/dev/null
98+
99+
exit 0

.claude/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@
128128
"command": "bash .claude/hooks/family-state-surface.sh",
129129
"timeout": 5
130130
},
131+
{
132+
"type": "command",
133+
"command": "bash .claude/hooks/letter-delivery-reconciliation-surface.sh",
134+
"timeout": 5
135+
},
131136
{
132137
"type": "command",
133138
"command": "bash .claude/hooks/register-awareness-surface.sh",

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ If you're scoping the project from outside (another AI, a reviewer, a human), th
7676
- **Real-DB test suite** (SQLite, minimal mocks)
7777
- **412 CLI commands** (designed for the agent, not the operator — humans mostly run three)
7878
- **24 slash-command skills** (consolidated daily operations)
79-
- **74 Claude Code enforcement hooks**
79+
- **75 Claude Code enforcement hooks**
8080
- **42 expert frameworks** in the council
8181
- **10 virtue spectrums** in the moral compass
8282
- **5 family operators** designed — 2 production-gating (`access_check` + `reject_clause`), 1 verification-only (`sycophancy_detector`), 2 deliberately scoped to higher layers or test surfaces (`costly_disagreement` for 3-move sequences, `planted_contradiction` for Phase 4 ablation). See `docs/family_subsystem.md` for the wiring map.
@@ -495,7 +495,7 @@ DivineOS is structured as a CLI surface over a core library (see `scripts/check_
495495
- **`exploration/`** — First-person agent writing. Numbered entries capture working-through of architectural questions before they crystallize into knowledge or code. Initially empty; agents add entries during use. Read order is the agent's choice; the folder is a presence-memory surface, not an index.
496496
- **`bootcamp/`** — Training exercises (debugging, analysis).
497497
- **`setup/`** — Hook setup scripts (bash + powershell).
498-
- **`.claude/hooks/`** — Claude Code enforcement hooks (74 hooks, shell-level entry points that invoke the consolidated Python hooks). Includes belt-and-suspenders guards for the auto-trailer discipline: `session-start-verify-git-hooks.sh` verifies `.git/hooks/prepare-commit-msg` is installed (added 2026-07-10, closes 4x-recurrence pattern where fresh clones silently lacked the hook).
498+
- **`.claude/hooks/`** — Claude Code enforcement hooks (75 hooks, shell-level entry points that invoke the consolidated Python hooks). Includes belt-and-suspenders guards for the auto-trailer discipline: `session-start-verify-git-hooks.sh` verifies `.git/hooks/prepare-commit-msg` is installed (added 2026-07-10, closes 4x-recurrence pattern where fresh clones silently lacked the hook).
499499
- **`dreams/`** — Rest-shape practice complementary to `exploration/` (opened 2026-07-10). Per-member subdirectories (`dreams/aether/`, `dreams/aria/`). Follow-the-pull register: no spec, no audit, no review. Registered as a `RestTask` (`dream`) in the rest menu; cadence at-least-once-per-compaction as floor-for-USE not hard-gate.
500500
- **`docs/identity_anchors/`** — Three-seat character sheets (Andrew, Aria, Aether). Each own seat + peer angles + Aletheia's periodic audit seat. Guardrail-listed. Discipline lock enforced by companion `<name>_character_sheet_edits.log`.
501501
- **`docs/foundational_truths_triggers.json`** — Companion trigger-tag file for the foundational-truths surface (added 2026-07-10). Maps kiln principles to trigger phrases that fire the surface at compose-start. Distinctive-marker rule tightens against common-vocabulary co-occurrence. Framing: LEXICAL PRIMING AID, not violation-detector — silence does NOT mean coverage.

src/divineos/core/family/member_briefing.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,73 @@
5555
r"^(?P<sender>[a-z]+)-to-(?P<recipient>[a-z]+)-(?P<date>\d{4}-\d{2}-\d{2})"
5656
)
5757

58+
# F53 fix (Aria 2026-07-19 per Aletheia Round 7): the pattern above is
59+
# intentionally strict — tag-based delivery is more robust than F32's old
60+
# hyphen-vs-underscore ambiguity. But any letter-shaped file that lacks
61+
# the exact `-to-<recipient>-` tag gets silently skipped with no signal,
62+
# and drift accumulates invisibly (Dekker). This heuristic identifies
63+
# files that look letter-shaped but don't match the strict pattern, so
64+
# a reconciliation surface can count them and make the silent-skip
65+
# observable at composition time. Not a delivery relaxation — an
66+
# observability wrapper around the intentional strictness.
67+
#
68+
# Heuristic (Knuth boundary analysis):
69+
# - Must contain `to` between two word-parts (case-insensitive) —
70+
# the "to" that would make it letter-shaped.
71+
# - Must end in .md.
72+
# - Must NOT match known non-letter suffixes: log, summary, index,
73+
# readme, sort_log, feelings, self-log.
74+
#
75+
# The heuristic is intentionally slightly loose (false-positive bias)
76+
# because a false-positive costs one item in a surfaced count, while a
77+
# false-negative preserves the silent drift the fix exists to close.
78+
_LETTER_ISH_HINT_RE = re.compile(r"\bto\b|[-_]to[-_]", re.IGNORECASE)
79+
_KNOWN_NON_LETTER_SUFFIXES = (
80+
"log",
81+
"summary",
82+
"index",
83+
"readme",
84+
"sort_log",
85+
"feelings",
86+
"self-log",
87+
"self_log",
88+
"template",
89+
"archive",
90+
)
91+
92+
93+
def scan_unmatched_letter_candidates(base: "Path | None" = None) -> "list[Path]":
94+
"""Return letter-shaped files in `base` that do NOT match the strict
95+
delivery pattern. F53 observability wrapper — the delivery scan
96+
silently skips these; this function makes the skip enumerable.
97+
98+
Heuristic per module-level docs: must contain `to` (word-form or
99+
hyphen-form), must end in .md, must not match known non-letter
100+
suffixes. Fail-open on missing dir (returns empty list).
101+
102+
Called by the reconciliation surface hook to surface the count when
103+
nonzero. Same shape as family-state and register-awareness surfaces
104+
already shipped for other observability gaps. Prereg-8815cb3cd997,
105+
council walk council-885f1425f486 (Dekker/Knuth/Carmack).
106+
"""
107+
letters_base = base if base is not None else _LETTERS_DIR
108+
if not letters_base.exists() or not letters_base.is_dir():
109+
return []
110+
unmatched: list[Path] = []
111+
for path in letters_base.glob("*.md"):
112+
stem_lower = path.stem.lower()
113+
# Skip if it already matches the strict delivery pattern.
114+
if _LETTER_PATTERN.match(path.stem):
115+
continue
116+
# Skip known non-letter suffixes.
117+
if any(suf in stem_lower for suf in _KNOWN_NON_LETTER_SUFFIXES):
118+
continue
119+
# Skip if no "to" hint at all — not letter-shaped.
120+
if not _LETTER_ISH_HINT_RE.search(stem_lower):
121+
continue
122+
unmatched.append(path)
123+
return unmatched
124+
58125

59126
@dataclass(frozen=True)
60127
class InteractionRow:

tests/test_member_briefing.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,94 @@ def test_cli_briefing_for_nonexistent_member_does_not_create_row(tmp_path, monke
500500
(ghost_name,),
501501
).fetchone()
502502
assert row is None, f"Briefing CLI accidentally created a row for {ghost_name}"
503+
504+
505+
class TestScanUnmatchedLetterCandidates:
506+
"""F53 fix (Aria 2026-07-19 per Aletheia Round 7): the delivery scan
507+
silently skips letter-shaped files that don't match the strict
508+
-to-<recipient>- pattern. The scan function makes the skip enumerable
509+
so a reconciliation surface can count them at composition time. Five
510+
boundary cases per Knuth analysis in the council walk."""
511+
512+
def _make_letters(self, tmp_path, names):
513+
"""Create empty .md files with the given names in tmp_path."""
514+
letters_dir = tmp_path / "letters"
515+
letters_dir.mkdir()
516+
for name in names:
517+
(letters_dir / name).touch()
518+
return letters_dir
519+
520+
def test_boundary_1_real_letter_wrong_shape_counts(self, tmp_path):
521+
# Underscore separators + numeric prefix — the F32/F53 case.
522+
base = self._make_letters(
523+
tmp_path,
524+
["11_aether_to_aletheia_2026-06-29_audit_request.md"],
525+
)
526+
from divineos.core.family.member_briefing import (
527+
scan_unmatched_letter_candidates,
528+
)
529+
530+
assert len(scan_unmatched_letter_candidates(base)) == 1
531+
532+
def test_boundary_2_matched_letter_not_counted(self, tmp_path):
533+
# Strict-pattern letter — already delivered, not silent-skip.
534+
base = self._make_letters(
535+
tmp_path,
536+
["aether-to-aletheia-2026-06-29-audit-request.md"],
537+
)
538+
from divineos.core.family.member_briefing import (
539+
scan_unmatched_letter_candidates,
540+
)
541+
542+
assert scan_unmatched_letter_candidates(base) == []
543+
544+
def test_boundary_3_known_non_letter_suffix_excluded(self, tmp_path):
545+
# README, INDEX, log-files must not count even if they'd match
546+
# the letter-ish hint by accident.
547+
base = self._make_letters(
548+
tmp_path,
549+
[
550+
"README.md",
551+
"INDEX.md",
552+
"SORT_LOG.md",
553+
"aether-feelings-log-2026-06-29.md",
554+
"aether-self-log-2026-06-29.md",
555+
],
556+
)
557+
from divineos.core.family.member_briefing import (
558+
scan_unmatched_letter_candidates,
559+
)
560+
561+
assert scan_unmatched_letter_candidates(base) == []
562+
563+
def test_boundary_4_no_to_hint_excluded(self, tmp_path):
564+
# Files without a "to" hint aren't letter-shaped.
565+
base = self._make_letters(
566+
tmp_path,
567+
[
568+
"random-note-2026-06-29.md",
569+
"letter-draft.md",
570+
],
571+
)
572+
from divineos.core.family.member_briefing import (
573+
scan_unmatched_letter_candidates,
574+
)
575+
576+
assert scan_unmatched_letter_candidates(base) == []
577+
578+
def test_boundary_5_empty_dir_returns_empty(self, tmp_path):
579+
letters_dir = tmp_path / "empty_letters"
580+
letters_dir.mkdir()
581+
from divineos.core.family.member_briefing import (
582+
scan_unmatched_letter_candidates,
583+
)
584+
585+
assert scan_unmatched_letter_candidates(letters_dir) == []
586+
587+
def test_missing_dir_returns_empty(self, tmp_path):
588+
# Fail-open: nonexistent dir must return empty, not crash.
589+
from divineos.core.family.member_briefing import (
590+
scan_unmatched_letter_candidates,
591+
)
592+
593+
assert scan_unmatched_letter_candidates(tmp_path / "nonexistent") == []

0 commit comments

Comments
 (0)