Files
dotfiles/scripts/daybook-ensure.py
David F GliddenandClaude Opus 5 3e828fe3f2 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
2026-08-24 18:36:34 +02:00

288 lines
12 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")
# The steward's own folders and formats, in use since 2025 — 01. Weekly/YYYY-Www.md and
# 02. Monthly/YYYY.MM.md (dots, not dashes). Reused rather than reinvented: the vault already
# holds 2025-W15..W21 and 2025.05, and [[2025-W15]]-style links already resolve against them.
WEEKLY = os.path.join(VAULT, "02. Reviews & Planning", "01. Weekly")
MONTHLY = os.path.join(VAULT, "02. Reviews & Planning", "02. Monthly")
def week_id(day):
y, w, _ = day.isocalendar()
return f"{y}-W{w:02d}"
def month_id(day):
return day.strftime("%Y.%m")
SKELETON = """---
title: "{date} — {dayname}"
type: daily
date: {date}
week: {week}
month: {month}
status: active
tags:
- daily
- worklog
---
# {date} — {dayname}
**{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.
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
## Decisions taken
## Insights and exchanges worth keeping
## Corrections
## 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
# Upward links only. A "tomorrow" link on every note manufactures ~365 unresolved
# links a year, and an unresolved-link census on 2026-08-23 found 4,799 already —
# 56% of notes isolated. The week note is created below, so [[week]] always resolves;
# yesterday is linked only if it actually exists.
nav = [f"[[{week_id(day)}|Week]]", f"[[{month_id(day)}|Month]]", "[[Daily index]]"]
prev = day - datetime.timedelta(days=1)
if os.path.exists(note_path(prev, daily_dir)):
nav.insert(0, f"[[{prev.isoformat()}|Previous]]")
body = SKELETON.format(date=day.isoformat(), dayname=day.strftime("%A"),
week=week_id(day), month=month_id(day), nav=" · ".join(nav))
# 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
WEEK_SKELETON = """---
type: weekly
week: {week}
start_date: {start}
status: active
tags:
- weekly
- review
---
# Weekly Review — {week}
*Week of {start} to {end}.*
> The daily notes for this week link up to here, so Obsidian's **backlinks** pane below lists
> exactly the days that have one. Nothing is hard-linked downward on purpose: a link to a day that
> never happened is an unresolved link, and the vault already carries thousands of those.
## Work — the week in review
*Written at the end of the week, from the dailies. What moved, what was decided, what is still open.
Plain language, and shorter than the sum of its days — a week's worth of perspective, not a digest.*
## Threads that carried across days
*The point of the weekly view: what appeared on more than one day. A problem that kept returning, a
question that stayed open, an idea that grew. These are invisible from inside a single day.*
## Personal review
*The 2025 weekly practice lived here — intentions, patterns, task migration. Kept as its own
heading; the work rollup above does not displace it.*
"""
MONTH_SKELETON = """---
type: monthly
month: {month}
status: active
tags:
- monthly
- review
---
# Monthly Review — {label}
## Work — the month in perspective
*Written from the weeks, not from the days. The question this answers is the one no daily can:
**did the month go anywhere?** Name the two or three things that actually advanced, and what
consumed time without advancing.*
## Threads that carried across weeks
## Personal review
"""
def rollup(day, kind, weekly_dir=None, monthly_dir=None):
"""-> (status, path) for the week or month note. Same create-only contract as the daily."""
try:
if kind == "week":
d, name = (weekly_dir or WEEKLY), week_id(day)
start = day - datetime.timedelta(days=day.isoweekday() - 1)
body = WEEK_SKELETON.format(week=name, start=start.isoformat(),
end=(start + datetime.timedelta(days=6)).isoformat())
else:
d, name = (monthly_dir or MONTHLY), month_id(day)
body = MONTH_SKELETON.format(month=name, label=day.strftime("%B %Y"))
if not os.path.isdir(d):
return "no-dir", os.path.join(d, name + ".md")
p = os.path.join(d, name + ".md")
if os.path.exists(p):
return "exists", p
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__}", os.path.join(d, name + ".md")
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")
# --- upward-link controls. The census that motivated them: 4,799 unresolved link
# targets, 56% of notes isolated. A nav line that manufactures links is the disease.
day2 = datetime.date(2026, 8, 24) # the day AFTER one that now exists
st5, p5 = ensure(day2, d)
txt2 = open(p5, encoding="utf-8").read()
chk("links Previous when yesterday's note EXISTS", "[[2026-08-23|Previous]]" in txt2)
chk("never links tomorrow [no manufactured unresolved link]", "2026-08-25" not in txt2)
chk("links up to the week and month, which the hook creates",
"[[2026-W35|Week]]" in txt2 and "[[2026.08|Month]]" in txt2)
chk("carries the steward's own week/month frontmatter vocabulary",
"week: 2026-W35" in txt2 and "month: 2026.08" in txt2)
d3 = tempfile.mkdtemp()
st6, p6 = ensure(datetime.date(2026, 9, 1), d3) # no previous note in a fresh dir
chk("OMITS Previous when yesterday's note does NOT exist [negative control — proves"
" the Previous check reads the disk, not the calendar]",
"Previous" not in open(p6, encoding="utf-8").read())
shutil.rmtree(d3, ignore_errors=True)
# --- rollups: same create-only contract, the steward's own folders and formats
wd, md = tempfile.mkdtemp(), tempfile.mkdtemp()
try:
stw, pw = rollup(day, "week", wd, md)
chk("creates the weekly note as YYYY-Www.md [his 2025 format]",
stw == "created" and os.path.basename(pw) == "2026-W34.md")
stm, pm = rollup(day, "month", wd, md)
chk("creates the monthly note as YYYY.MM.md [dots, as his 2025.05]",
stm == "created" and os.path.basename(pm) == "2026.08.md")
wtxt = open(pw, encoding="utf-8").read()
chk("weekly carries a Work heading AND keeps his personal-review heading",
"## Work — the week in review" in wtxt and "## Personal review" in wtxt)
chk("weekly hard-links NO daily notes [backlinks instead; zero manufactured links]",
"2026-08-2" not in wtxt.split("start_date")[1].split("## Personal")[0]
.replace("2026-08-23", "").replace("2026-08-17", ""))
open(pw, "w", encoding="utf-8").write("STEWARD PROSE\n")
stw2, _ = rollup(day, "week", wd, md)
chk("does NOT overwrite an existing weekly note",
stw2 == "exists" and open(pw, encoding="utf-8").read() == "STEWARD PROSE\n")
finally:
shutil.rmtree(wd, ignore_errors=True); shutil.rmtree(md, ignore_errors=True)
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())
made, failed = [], []
for st, pth in (ensure(), rollup(datetime.date.today(), "week"),
rollup(datetime.date.today(), "month")):
if st == "created":
made.append(os.path.basename(pth))
elif st != "exists":
failed.append(f"{os.path.basename(pth)} ({st})")
if made:
print("DAYBOOK — created " + ", ".join(made))
if failed:
# Loud on purpose. The predecessor's silence cost five months.
print("DAYBOOK — COULD NOT WRITE: " + "; ".join(failed))
sys.exit(0)