The steward asked for a daily note in Obsidian explaining in plain terms what we did, decisions taken, and commit references — plus significant insights and exchanges between the parties. This is Obsidian tracker item 5, the actual goal, unstarted since 2026-08-19 while four items of frontmatter hygiene stood in front of it. Two prior attempts at this exact genre were read before anything was designed, honouring the tracker's own flag: daily-log/ (4 entries, Mar-Apr 2026) and sessions/ (3 long-form narratives). Both died inside four weeks. Neither had a trigger — they were written when someone remembered. So the design question was never what the note should say. It was what makes it survive. Answer, steward-chosen at each fork: the note lives at 01. Daily/YYYY-MM-DD.md with project work under its own H2 inside it rather than in project folders; a SessionStart hook creates it so existence stops depending on memory; the executor writes during the session because a wrap-only design inherits the wrap's failure mode (2026-08-19 died unwrapped); and the wrap finalises rather than begins. daybook-ensure.py creates only. It will not touch an existing note — that is the control the design turns on, and it is tested, along with a positive control proving the no-dir report distinguishes a missing directory from a broken function. It also reports failure loudly, because the job it sits beside spent five months failing behind a > /dev/null 2>&1. Not revived: the 2025 analogue template — sleep, supplements, homeopathy. Pen and paper beat it and should keep it. That it failed is a finding, not a gap, and this note is a different genre rather than its replacement. PENDING-155 files the steward's idea for the jurist's side: an append-only MCP surface writing a scratch file the executor folds in. Deliberately NOT a tool on governance-mcp.py, whose read-only guarantee is a live AST control with a positive control rather than a comment. A separate one-tool server leaves that intact, and the vault note stays single-writer — the same lesson the thinking-mirror taught this morning. Correction carried into the record: the executor told the steward mid-session that Claude.app has no filesystem access. False since 2026-08-08. Third instance today of a constraint asserted from recall where the substrate was one file away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQKeKY9T9d95KpvHwwok8T
129 lines
4.9 KiB
Python
Executable File
129 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""daybook-ensure — the day's work-log note exists before anyone remembers to want it.
|
|
|
|
Two previous attempts at this log died inside four weeks (`daily-log/` 4 entries, `sessions/`
|
|
3, both Mar-Apr 2026). Neither had a trigger; both depended on someone remembering. This hook
|
|
removes the remembering: at every session start the day's note exists, skeleton in place, so
|
|
writing into it is an append rather than a decision to begin.
|
|
|
|
Deliberately narrow:
|
|
- CREATES only. An existing note is never opened for writing, never rewritten, never
|
|
reordered. The prose is the executor's and the hook must not be able to damage it.
|
|
- Says so when it fails. The job this replaces died silently for five months behind a
|
|
`> /dev/null 2>&1` (see the 2026-08-23 daily note). A hook that cannot write must
|
|
announce that, not shrug. Constitutional Constraint #4, at hook scale.
|
|
- Always exits 0. Honest reporting must never cost the steward a session.
|
|
"""
|
|
import datetime, os, sys
|
|
|
|
VAULT = os.path.expanduser(
|
|
"~/Library/Mobile Documents/iCloud~md~obsidian/Documents/David, root-and-branch")
|
|
DAILY = os.path.join(VAULT, "01. Daily")
|
|
|
|
SKELETON = """---
|
|
title: "{date} — {dayname}"
|
|
type: daily
|
|
date: {date}
|
|
status: active
|
|
tags:
|
|
- daily
|
|
- worklog
|
|
---
|
|
|
|
# {date} — {dayname}
|
|
|
|
*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.*
|
|
|
|
## The day in short
|
|
|
|
## Decisions taken
|
|
|
|
## Insights and exchanges worth keeping
|
|
|
|
## Open / next
|
|
"""
|
|
|
|
|
|
def note_path(day, daily_dir=DAILY):
|
|
return os.path.join(daily_dir, f"{day.isoformat()}.md")
|
|
|
|
|
|
def ensure(day=None, daily_dir=DAILY):
|
|
"""-> (status, path). status in {'exists','created','no-dir','error:<msg>'}.
|
|
|
|
'exists' is not a failure and 'created' is not a success worth announcing loudly —
|
|
the caller decides what is worth a line of the steward's attention.
|
|
"""
|
|
day = day or datetime.date.today()
|
|
p = note_path(day, daily_dir)
|
|
try:
|
|
if not os.path.isdir(daily_dir):
|
|
return "no-dir", p
|
|
if os.path.exists(p):
|
|
return "exists", p
|
|
body = SKELETON.format(date=day.isoformat(), dayname=day.strftime("%A"))
|
|
# x-mode: refuse to clobber if something raced us between the check and the write.
|
|
with open(p, "x", encoding="utf-8") as fh:
|
|
fh.write(body)
|
|
return "created", p
|
|
except FileExistsError:
|
|
return "exists", p
|
|
except OSError as e:
|
|
return f"error:{e.__class__.__name__}", p
|
|
|
|
|
|
def selftest():
|
|
import tempfile, shutil
|
|
ok = fail = 0
|
|
|
|
def chk(label, cond):
|
|
nonlocal ok, fail
|
|
print(f" [{'ok ' if cond else 'FAIL'}] {label}")
|
|
ok, fail = (ok + (1 if cond else 0), fail + (0 if cond else 1))
|
|
|
|
d = tempfile.mkdtemp()
|
|
try:
|
|
day = datetime.date(2026, 8, 23)
|
|
st, p = ensure(day, d)
|
|
chk("creates the note when absent", st == "created" and os.path.exists(p))
|
|
txt = open(p, encoding="utf-8").read()
|
|
chk("skeleton carries the ISO date and weekday", "2026-08-23" in txt and "Sunday" in txt)
|
|
chk("skeleton is spec-conformant (type/status/tags present)",
|
|
"type: daily" in txt and "status: active" in txt and "worklog" in txt)
|
|
|
|
# THE control that matters: an existing note must survive untouched.
|
|
open(p, "w", encoding="utf-8").write("PROSE THE EXECUTOR WROTE\n")
|
|
st2, _ = ensure(day, d)
|
|
chk("does NOT overwrite an existing note [the whole point]",
|
|
st2 == "exists" and open(p, encoding="utf-8").read() == "PROSE THE EXECUTOR WROTE\n")
|
|
chk("reports 'exists' rather than 'created' on the second run", st2 == "exists")
|
|
|
|
missing = os.path.join(d, "nope")
|
|
st3, _ = ensure(day, missing)
|
|
chk("reports a missing directory instead of creating one [no silent mkdir]",
|
|
st3 == "no-dir" and not os.path.exists(missing))
|
|
|
|
# positive control: the failure path must be reachable, or 'no-dir' proves nothing.
|
|
st4, _ = ensure(day, d)
|
|
chk("the same call succeeds where the directory exists [positive control — proves"
|
|
" 'no-dir' reports the directory, not a broken function]", st4 == "exists")
|
|
finally:
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
|
|
print(f"\n{'SELFTEST PASS' if not fail else 'SELFTEST FAIL'} — {ok} ok, {fail} failed")
|
|
return 0 if not fail else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if "--selftest" in sys.argv:
|
|
sys.exit(selftest())
|
|
status, path = ensure()
|
|
if status == "created":
|
|
print(f"DAYBOOK — created {os.path.basename(path)}")
|
|
elif status.startswith("error") or status == "no-dir":
|
|
# Loud on purpose. The predecessor's silence cost five months.
|
|
print(f"DAYBOOK — COULD NOT WRITE the day's note ({status}). "
|
|
f"Expected at: {path}")
|
|
sys.exit(0)
|