Session 2026-08-24: grounding gates ruled, daybook rule changed, two censuses run
PENDING-95: Amendment 2 (jurist ruling — (a) rejected, (d) discharged, (b)/(c) deferred) plus the (b)/(c) census that discharges the deferral's condition. 60 guarded / 32 marked (record said 59/31); date 75%, sections 97%, quoting 47%. Date broadly present => the jurist's cheaper third form is the live option. PENDING-156 opened (kind (c): mechanisms off the path the work takes) and its option (b) census run the same session. PENDING-109 prior confirmed by direct read. PENDING-89: two docket entries, one same-direction miss and one cross-direction catch, filed the same day and at the same speed. Mechanisms: daybook-cue.py rewritten on steward ruling — the daily note must be appended to until end of day, so the trigger is staleness, not note size, and the matcher now includes Bash (it had never fired once). daybook-ensure.py and /wrap-up 7.5 gain a standing Corrections slot per REVIEWED-126. governance-mcp.py gains two read-only keys so the jurist can read the artifacts it rules on; the doctrine that read-surface changes should arrive as rulings is adopted, and the next key is proposed rather than added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQKeKY9T9d95KpvHwwok8T
This commit is contained in:
co-authored by
Claude Opus 5
parent
e28451c826
commit
3e828fe3f2
+92
-30
@@ -19,6 +19,27 @@ written. So this fires there.
|
||||
REMINDER, NOT BLOCK — unlike verify-before-compose, whose failure is unrecoverable.
|
||||
This one's is recoverable, and a gate that interrupts every write would be removed
|
||||
within a day. Fail-open everywhere: any unexpected condition exits 0.
|
||||
|
||||
REVISED 2026-08-24, steward ruling, verbatim: "the daily note needs to be appended to
|
||||
until the end of the day. Period. There is no design choice to make."
|
||||
|
||||
Two defects were fixed together, both found by censusing the hooks rather than by a
|
||||
selftest -- the suite was 16/16 green throughout:
|
||||
|
||||
1. WRONG CONDITION. v1 cued only while the note was under SKELETON_CEILING (1200 B),
|
||||
i.e. it watched "was the note ever started", not "is it current". A session that
|
||||
appends nothing to an already-filled note was invisible to it: on the day it was
|
||||
built, the note stood at 15,688 B and the cue would have stayed silent all
|
||||
afternoon while work landed elsewhere. Size is not the question and the ceiling
|
||||
is gone. The question is STALENESS: has the note been appended to lately?
|
||||
2. UNREACHABLE. v1 was wired PostToolUse | Write|Edit -- the same matcher as
|
||||
verify-before-compose, copied from the hook whose blindness was already recorded
|
||||
in PENDING-95 since 2026-08-19. Work routes through Bash heredocs, so it never
|
||||
fired once: no stamp, ~/.claude/state/ did not exist. Bash is now matched, and
|
||||
any tool use counts as the session being live.
|
||||
|
||||
It self-silences on compliance: appending resets the note's mtime, which clears the
|
||||
staleness condition. It insists only while the note is actually behind.
|
||||
"""
|
||||
import datetime, json, os, sys, time
|
||||
|
||||
@@ -28,7 +49,7 @@ VAULT = os.path.join(HOME, "Library/Mobile Documents/iCloud~md~obsidian/Document
|
||||
DAILY_DIR = os.path.join(VAULT, "01. Daily")
|
||||
STAMP = os.path.join(HOME, ".claude/state/daybook-cue.stamp")
|
||||
|
||||
SKELETON_CEILING = 1200 # the generated skeleton is ~558 bytes
|
||||
STALE_SECONDS = 1800 # the note may lag the work by 30 min, no longer
|
||||
QUIET_SECONDS = 900 # cue at most once per 15 minutes; a nag gets disabled
|
||||
|
||||
# Writes that are not "work landing".
|
||||
@@ -55,23 +76,46 @@ def note_path(today=None):
|
||||
return os.path.join(DAILY_DIR, f"{d.isoformat()}.md")
|
||||
|
||||
|
||||
def should_cue(path, note_size, note_exists, seconds_since_last, today=None):
|
||||
"""Pure decision, so it can be tested without a filesystem or a clock."""
|
||||
if not is_substantive(path):
|
||||
def should_cue(activity, note_exists, note_age, seconds_since_last):
|
||||
"""Pure decision, so it can be tested without a filesystem or a clock.
|
||||
|
||||
activity -- did substantive work just land? (bool)
|
||||
note_age -- seconds since the daily note was last modified
|
||||
"""
|
||||
if not activity:
|
||||
return False
|
||||
if not note_exists: # daybook-ensure owns creation, not us
|
||||
return False
|
||||
if note_size >= SKELETON_CEILING: # it is being written; say nothing
|
||||
if note_age < STALE_SECONDS: # appended to recently; it is current
|
||||
return False
|
||||
if seconds_since_last < QUIET_SECONDS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
MESSAGE = """DAILY NOTE — still a skeleton, and work is landing elsewhere.
|
||||
def activity_from(tool_input):
|
||||
"""Substantive work, from EITHER a file_path (Write/Edit) or a command (Bash).
|
||||
|
||||
For Bash the written path is not recoverable without parsing arbitrary shell, so
|
||||
any Bash call counts as the session being live -- which is the right reading of
|
||||
the rule: the note must be current while work is happening, whatever the tool.
|
||||
A command that touches the daily note itself is not a trigger for writing it.
|
||||
"""
|
||||
path = tool_input.get("file_path") or ""
|
||||
if path:
|
||||
return is_substantive(path)
|
||||
command = tool_input.get("command") or ""
|
||||
if not command:
|
||||
return False
|
||||
if os.path.basename(DAILY_DIR) in command or "01. Daily" in command:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
MESSAGE = """DAILY NOTE — behind the work. Append to it now.
|
||||
|
||||
The convention is FILL IT AS THE WORK HAPPENS, not at the wrap:
|
||||
01. Daily/{date}.md (currently {size} bytes)
|
||||
01. Daily/{date}.md (last touched {mins} min ago; {size} bytes)
|
||||
|
||||
Give what just landed its own `##` heading now — plain language, for the steward on a
|
||||
day he wants to know what happened without reading git log. /wrap-up §7.5 finalises;
|
||||
@@ -87,11 +131,12 @@ def main():
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
path = (data.get("tool_input") or {}).get("file_path") or ""
|
||||
activity = activity_from(data.get("tool_input") or {})
|
||||
np = note_path()
|
||||
try:
|
||||
exists = os.path.exists(np)
|
||||
size = os.path.getsize(np) if exists else 0
|
||||
note_age = (time.time() - os.path.getmtime(np)) if exists else 0
|
||||
except OSError:
|
||||
sys.exit(0)
|
||||
|
||||
@@ -101,7 +146,7 @@ def main():
|
||||
last = 0
|
||||
since = time.time() - last
|
||||
|
||||
if not should_cue(path, size, exists, since):
|
||||
if not should_cue(activity, exists, note_age, since):
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
@@ -110,7 +155,8 @@ def main():
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print(MESSAGE.format(date=datetime.date.today().isoformat(), size=size), file=sys.stderr)
|
||||
print(MESSAGE.format(date=datetime.date.today().isoformat(), size=size,
|
||||
mins=int(note_age // 60)), file=sys.stderr)
|
||||
sys.exit(2) # PostToolUse: tool already ran; stderr is surfaced as feedback
|
||||
|
||||
|
||||
@@ -126,30 +172,46 @@ def selftest():
|
||||
|
||||
vault_md = os.path.join(VAULT, "08. Notes/Something.md")
|
||||
daily_md = os.path.join(DAILY_DIR, "2026-08-24.md")
|
||||
STALE = STALE_SECONDS + 1
|
||||
|
||||
# positive control — it fires when it should
|
||||
check("cues on a real vault write while the note is a skeleton",
|
||||
should_cue(vault_md, 558, True, 9999) is True)
|
||||
check("cues on a repo write too",
|
||||
should_cue(os.path.join(HOME, "_Dev/x/y.py"), 558, True, 9999) is True)
|
||||
# positive controls — it fires when it should
|
||||
check("cues when the note is stale and work lands",
|
||||
should_cue(True, True, STALE, 9999) is True)
|
||||
check("cues on a LARGE but stale note (the v1 defect: size is not the question)",
|
||||
should_cue(True, True, STALE, 9999) is True)
|
||||
|
||||
# negative controls — each suppression works in isolation
|
||||
check("silent once the note is being written", should_cue(vault_md, 5000, True, 9999) is False)
|
||||
check("silent when the note does not exist", should_cue(vault_md, 0, False, 9999) is False)
|
||||
check("silent inside the quiet window", should_cue(vault_md, 558, True, 10) is False)
|
||||
check("silent on the daily note itself", should_cue(daily_md, 558, True, 9999) is False)
|
||||
check("silent on scratchpad", should_cue("/private/tmp/claude-501/x/scratchpad/a.md", 558, True, 9999) is False)
|
||||
check("silent on /tmp", should_cue("/tmp/a.md", 558, True, 9999) is False)
|
||||
check("silent on .obsidian config", should_cue(os.path.join(VAULT, ".obsidian/x.json"), 558, True, 9999) is False)
|
||||
check("silent on backups", should_cue(vault_md + ".bak", 558, True, 9999) is False)
|
||||
check("silent on jsonl logs", should_cue(os.path.join(HOME, "a/b.jsonl"), 558, True, 9999) is False)
|
||||
check("silent on empty path", should_cue("", 558, True, 9999) is False)
|
||||
check("silent when the note was just appended to",
|
||||
should_cue(True, True, 60, 9999) is False)
|
||||
check("silent when the note does not exist", should_cue(True, False, STALE, 9999) is False)
|
||||
check("silent inside the quiet window", should_cue(True, True, STALE, 10) is False)
|
||||
check("silent when no substantive activity", should_cue(False, True, STALE, 9999) is False)
|
||||
|
||||
# boundary
|
||||
check("threshold is exclusive at the ceiling",
|
||||
should_cue(vault_md, SKELETON_CEILING, True, 9999) is False)
|
||||
check("just under the ceiling still cues",
|
||||
should_cue(vault_md, SKELETON_CEILING - 1, True, 9999) is True)
|
||||
# boundary on staleness
|
||||
check("threshold is exclusive at STALE_SECONDS",
|
||||
should_cue(True, True, STALE_SECONDS, 9999) is True)
|
||||
check("one second under the threshold is silent",
|
||||
should_cue(True, True, STALE_SECONDS - 1, 9999) is False)
|
||||
|
||||
# activity_from — the reachability half (the v1 defect: Bash was invisible)
|
||||
check("Write/Edit: a real vault write is activity",
|
||||
activity_from({"file_path": vault_md}) is True)
|
||||
check("Write/Edit: a repo write is activity",
|
||||
activity_from({"file_path": os.path.join(HOME, "_Dev/x/y.py")}) is True)
|
||||
check("BASH counts as activity (v1 never fired because it did not)",
|
||||
activity_from({"command": "python3 - <<'EOF'\nopen('x','w')\nEOF"}) is True)
|
||||
check("bash touching the daily note is NOT a trigger for writing it",
|
||||
activity_from({"command": "cat '01. Daily/2026-08-24.md'"}) is False)
|
||||
check("empty tool_input is not activity", activity_from({}) is False)
|
||||
check("silent on the daily note itself", activity_from({"file_path": daily_md}) is False)
|
||||
check("silent on scratchpad",
|
||||
activity_from({"file_path": "/private/tmp/claude-501/x/scratchpad/a.md"}) is False)
|
||||
check("silent on /tmp", activity_from({"file_path": "/tmp/a.md"}) is False)
|
||||
check("silent on .obsidian config",
|
||||
activity_from({"file_path": os.path.join(VAULT, ".obsidian/x.json")}) is False)
|
||||
check("silent on backups", activity_from({"file_path": vault_md + ".bak"}) is False)
|
||||
check("silent on jsonl logs",
|
||||
activity_from({"file_path": os.path.join(HOME, "a/b.jsonl")}) is False)
|
||||
|
||||
# fail-open on malformed input
|
||||
import subprocess
|
||||
|
||||
@@ -51,7 +51,9 @@ tags:
|
||||
**{nav}**
|
||||
|
||||
*A plain-language record of the day's work, so the history is readable without reading `git log`.
|
||||
Project work sits under its own heading; decisions, insights and open threads are collected at the end.*
|
||||
Project work sits under its own heading; decisions, insights and open threads are collected at the end.
|
||||
Corrections are a standing slot, not an occasional one: an empty `## Corrections` on a working day
|
||||
is itself a claim, and usually a false one.*
|
||||
|
||||
## The day in short
|
||||
|
||||
@@ -59,6 +61,8 @@ Project work sits under its own heading; decisions, insights and open threads ar
|
||||
|
||||
## Insights and exchanges worth keeping
|
||||
|
||||
## Corrections
|
||||
|
||||
## Open / next
|
||||
"""
|
||||
|
||||
|
||||
@@ -72,6 +72,18 @@ FILES = {
|
||||
"claude-md": (os.path.join(D, "CLAUDE.md"), "the executor's governing document"),
|
||||
"memory-index": (os.path.join(wd.MEM, "MEMORY.md"), "wake-loaded memory index"),
|
||||
"app-brief": (wd.BRIEF_PATH, "last generated .app Standing Context block"),
|
||||
# PENDING-95 AMENDMENT 1, 2026-08-24. Same shape as the PENDING-86 (a) precedent
|
||||
# below: the jurist is asked to rule options (a)-(d) on the grounding gate, and the
|
||||
# two artifacts the ruling GOVERNS are the two it could not read. Ruling from the
|
||||
# record alone would make the subject the executor's description of the hooks rather
|
||||
# than the hooks -- the defect recorded inside REVIEWED-125 ("the ruling's subject
|
||||
# was the pasted text, not the filed artifact"). Read-only, keyed, enum unchanged.
|
||||
"grounding-hook-pretool": (os.path.join(HOME, ".claude", "hooks",
|
||||
"verify-before-compose.sh"),
|
||||
"PreToolUse grounding gate (Write|Edit route)"),
|
||||
"grounding-hook-commit": (os.path.join(HOME, "_Dev", "chamber-library", ".githooks",
|
||||
"pre-commit"),
|
||||
"pre-commit grounding gate (tool-agnostic route)"),
|
||||
# PENDING-86 option (a), steward-authorized 2026-08-05. The jurist design-gates
|
||||
# constitutional supersessions of documents it could not read; three distinct
|
||||
# instances are recorded on that item. These two are the ones the loop actually
|
||||
|
||||
Reference in New Issue
Block a user