A weekly review file existed exactly where the automation expected it. The week still had not been reviewed.
The file was created on Monday with a plan. By Sunday it contained intentions, outcomes, and evidence—but no retrospective. A startup check based on filename existence reported nothing wrong.
That is the trap: existence is not completion.
This matters whenever one artifact is filled progressively. A file created at the start of a period can prove that the period was opened, but it cannot also prove that the period was closed.
The naive check
The first design looked reasonable:
| Cadence | Expected artifact | When to nudge |
|---|---|---|
| Weekly | weekly/YYYY-Www.md |
Current ISO-week file is missing |
| Monthly | monthly/YYYY-MM.md |
Previous month’s file is missing |
| Quarterly | quarterly/YYYY-QN.md |
Previous quarter’s file is missing |
The monthly and quarterly rules work because their rollups are written after those periods end. A missing file means a missing close.
Weekly is different. One file holds two state transitions:
## Plan: Week of ...
...written when the week opens...
## Retrospective: Week of ...
...appended after the week closes...
Checking only weekly/2026-W31.md answers one question:
Was week 31 opened?
It does not answer:
Was week 31 closed?
The false negative appeared at the boundary. On the last day of W31, its file existed, so the hook stayed quiet. On Monday, W32 could be opened while W31 remained unreviewed. The system had confused a container with a completed process.
Model transitions, not files
The corrected weekly rule has two independent checks:
- Opening: the current Monday-first ISO week has a file.
- Closure: the previous ISO week’s file contains a retrospective heading.
They can both fire on the same session start:
No weekly review for 2026-W32 — run /weekly-review to close it out.
No weekly retrospective for 2026-W31 — run /weekly-review to close it out.
That is not duplicate noise. The lines describe two different missing transitions: open this week, close last week.
The complete cadence model became:
| Check | Period examined | Completion evidence |
|---|---|---|
| Daily close | Previous calendar day | Journal contains ## Daily Close |
| Weekly open | Current Monday-first ISO week | Weekly file exists |
| Weekly close | Previous ISO week | Weekly file contains a retrospective heading |
| Monthly close | Previous calendar month | Monthly rollup exists |
| Quarterly close | Previous quarter | Quarterly rollup exists |
The asymmetry is deliberate. You cannot demand that the current day, week, month, or quarter be closed while it is still in progress.
The date math is small, but boundary-sensitive
JavaScript’s default week assumptions are easy to get wrong, especially around Sundays and year boundaries. The hook converts the local calendar date to a UTC date used only for ISO-week arithmetic:
function isoWeek(today: Date): { year: number; week: number } {
const date = new Date(Date.UTC(
today.getFullYear(),
today.getMonth(),
today.getDate(),
));
const day = date.getUTCDay() || 7; // Monday=1, Sunday=7
date.setUTCDate(date.getUTCDate() + 4 - day);
const year = date.getUTCFullYear();
const yearStart = new Date(Date.UTC(year, 0, 1));
const week = Math.ceil(
((date.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7,
);
return { year, week };
}
Moving to Thursday determines the ISO week-year correctly. That detail matters around New Year’s Day, where the calendar year and ISO week-year can differ.
The previous weekly period is not “week number minus one.” It is the ISO week containing the date seven days earlier:
const previousWeekDate = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() - 7,
12,
);
const previousWeek = isoWeek(previousWeekDate);
Subtracting seven calendar days lets the date implementation handle month and year rollover.
Match a semantic marker, not the whole heading
The close check needs a stable marker, but exact heading text is too brittle. A retrospective may legitimately be titled:
## Retrospective: Week of Jul 20–26
The implementation therefore uses a case-insensitive prefix match:
/^##\s+Retrospective\b/im
This tolerates capitalization and descriptive suffixes without accepting unrelated sections such as ## End-of-week evidence.
There is a useful boundary here: automation should tolerate harmless formatting variation, but it should not silently redefine the convention. If the team wants another heading to mean “closed,” that is a process decision, not a regex improvisation.
Startup nudges should fail open
This check runs during an agent’s session_start lifecycle. A broken productivity reminder must never prevent the session it is supposed to help.
The hook follows four rules:
- Read local files only; no service dependency.
- Catch unreadable-directory and unreadable-file errors.
- Emit nothing when all checks pass.
- Display missing checks without triggering an agent turn.
In an Oh My Pi extension, the final delivery is intentionally small:
pi.on("session_start", async () => {
const nudges = await getCadenceNudges();
if (nudges.length === 0) return;
pi.sendMessage({
customType: "cadence-nudge",
content: nudges.join("\n"),
display: true,
}, { triggerTurn: false });
});
This is discoverable, not enforced. The person can ignore the nudge. The system does not open an interactive wizard, block startup, or print an “all clear” message every session.
A reminder that constantly announces its own health becomes another maintenance burden.
Verify boundaries, not just helpers
The smallest useful verification matrix covered three states:
| Scenario | Expected output |
|---|---|
| Sunday, current weekly file exists, previous week has retrospective | No weekly nudge |
| Monday, new weekly file missing, previous week lacks retrospective | Two weekly nudges |
| All daily/weekly/monthly/quarterly evidence exists | Zero output bytes |
The Monday case is the regression test. It fails if the implementation collapses weekly opening and closure back into one existence check.
The no-op case matters equally. Non-blocking automation should prove that success is silent, not merely that failure is loud.
The general pattern
This bug is not specific to review notes. It appears anywhere a durable object is created before its workflow completes:
- a pull request exists but has not been reviewed;
- an incident document exists but has no resolution section;
- a migration file exists but has not been applied;
- a backup file exists but has not been restored in a test;
- a project page exists but has no decision or owner.
The reliable question is not:
Does the artifact exist?
It is:
What observable transition proves that this stage completed?
Sometimes the answer is another file. Sometimes it is a heading, status field, timestamp, database constraint, or successful restore. Whatever the marker is, check the marker that represents completion—not the container that happens to hold it.