DESIGN.md@CLAUDE/Plan/Completed/00543-journal-file-per-entry · git:20261021.36fe7a3 · 2026-10-21 · sha256 c2132f938e8580ca
DESIGN.md@CLAUDE/Plan/Completed/00543-journal-file-per-entry git:20261021.36fe7a3A
Immutable. This exact content never changes and is served at /api/v1/blob/c2132f938e8580ca.
# Plan 00543 design: one journal file per entry
Task 1.1 output. The map was read from the source on branch `worktree-journal-per-entry` at `48aebe158`.
## Map: what knows the day-file name or path today
| Component | File | What it does with day-files | Change needed |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Writer, `--journal` | `CLAUDE/Plan/mkplan.bash` = `install/templates/mkplan.bash` (identical copies) | Opens `JOURNAL/NNNNN-Journal-YY-MM-DD.md` from the template preamble, appends `## HH:MM · cat · REF` and the body | Write one new entry file; never open an existing file |
| Writer, plan scaffold | same script, "scaffold the plan JOURNAL/" block | Writes today's day-file with the template preamble and the seeded `plan scaffolded` entry | Write the seeded entry as an entry file |
| Correction lookup | `_journal_has_entry_at`, `JOURNAL_CORRECTION_REF_PATTERN` | `--ref HH:MM` or `YY-MM-DD/HH:MM` must name an entry in that day's day-file | Search that day's legacy day-file AND that day's entry folder; CLI unchanged |
| Conflict repair | `--resolve-conflict`, `JOURNAL_DAYFILE_NAME_PATTERN` | Union of two sides of a day-file | Unchanged; legacy day-files only (entry files cannot conflict) |
| Template | `_JOURNAL_TEMPLATE_.md` (plan dir and `install/templates`) | Preamble + seeded creation entry; presence = "this project journals" | Preamble prose describes the folder layout; still the presence marker and the source of the creation entry body. UTC sentinel kept (pinned by `test_journal_sentinel_sync.py`) |
| Plan template comment | `_TEMPLATE_.md` and mkplan heredoc line ~866 | Names `JOURNAL/$padded-Journal-YY-MM-DD.md` | Name `JOURNAL/YYYY-MM-DD/<entry>.md` |
| Name grammar | `plan_qa/model.py` `_JOURNAL_DAYFILE_RE`, `parse_journal_dayfile_name` | Day-file parse | Add `parse_journal_entry_path` (folder date + file name) beside it |
| Freshness input | `model._scan_journal` | Latest date from day-file NAMES only | Also count date folders holding a well-formed entry file |
| Classifier | `plan_qa/paths.py` `classify`, `is_journal_file` | Nested files are `JOURNAL_OTHER` | New kind `JOURNAL_ENTRY` for `JOURNAL/<YYYY-MM-DD>/*.md`; still `is_journal` |
| Edit/sweep targets | `checks/common.py` `journal_edit_target`, `journal_tree_targets` | Only `.md` directly in `JOURNAL/` | Also yield files in date folders, marked as entry files |
| `journal-dayfile-naming` | checks | Grammar of direct children | Entry files: folder is a real date, name matches `HHMMSSZ-<category>-<6 hex>.md`, category legal |
| `journal-dayfile-is-today` | checks | Blocks an edit to a day-file not dated today | Reframed: an EXISTING journal file is never edited. Legacy day-files are closed (any edit), entry files any edit once they exist; plus a COMMIT half: a staged M or D of an entry file. Check id and `today_only_mode` knob kept for config compatibility |
| `journal-append-only` | checks | Advisory on a history rewrite of a day-file | Unchanged for day-files; entry files belong to the reframed check above (no double finding) |
| `journal-entry-ordering` | checks | Times increase down a day-file (EDIT + SWEEP), cross-day correction voids | Entry file: exactly one heading, and its `HH:MM` equals the file name's. Corrections in entry files feed the same `(date, HH:MM)` voids, a same-day `HH:MM` ref in an entry file voids that date's label |
| `journal-entry-future-dated` | checks | Date from day-file name, UTC if the sentinel is present | Entry file: date from folder, always UTC |
| `journal-freshness` | checks | `latest_journal_date` | Works once `_scan_journal` reads folders |
| `journal-folder-present` | checks | `has_journal` | Unchanged |
| `journal-entry-with-progress`, `journal-completion-entry` | `common.has_staged_journal_entry` | Any Added/Modified file under `<plan>/JOURNAL/` | Already satisfied by a staged new entry file (prefix match is depth-blind); add a test that pins it |
| `same-commit-plan-doc` | checks | Keys on `PLAN.md` | Unaffected |
| `command_journal.py` | plan_qa | Reads `mkplan.bash --journal N` in the commit's own command | Unaffected |
| Guard `R-JOURNAL-HAND-WRITTEN-ENTRY` | `handlers/pre_tool_use/plan_journal_guard.py` | `_dayfile_target` requires parent `JOURNAL/` + day-file name; unplaced Bash destinations judged by `_could_name_dayfile` | `_dayfile_target` also accepts `JOURNAL/<anything>/<file>`; `_could_name_dayfile` fails closed when ANY component after `JOURNAL/` is built at run time (`JOURNAL/$(date +%F)/x.md`). Remedy text, `get_claude_md` |
| Docs | `CLAUDE/PlanJournalling.md` (+ `install/templates/PlanJournalling.md`), `core/PlanWorkflow.core.md`, `CLAUDE/DirectoryRoles.md`, `pipe_blocker` guidance ("tail a day-file") | Describe day-files | Describe the folder layout and reading order; regenerate the CLAUDE.md block the supported way |
Not changed: `plan_links`, `archival_links_resolve`, `plan_promotion_disposition`, `markdown_organization`, `index_no_log`, `plan_doc_size` only ask "is this journal territory", which `is_journal` still answers for the new kind.
## Chosen entry file name
```
<plan>/JOURNAL/YYYY-MM-DD/HHMMSSZ-<category>-<6 hex>.md
```
- One `date -u +%Y-%m-%d %H%M%S` call gives the folder and the time, so a midnight straddle cannot split them.
- 6 hex from `/dev/urandom` (`od`): two writers in the same second with the same category collide with odds 1 in 16.7M,
and the file is created with `noclobber`, so even that collision fails loudly instead of overwriting.
- Four-digit year in the folder (brief), so a folder never reads as a plan folder (`NNNNN-<letter>`).
- The file holds `## HH:MM · category · REF — title`, a blank line, the body. The heading grammar is unchanged, so
`journal_entry_headings` reads it as one entry. No preamble per file: the grammar is documented once.
- `--ref` is unchanged (`HH:MM` today, `YY-MM-DD/HH:MM` earlier). A minute may now match several entry files; a
correction voids that minute on that date, as it already did inside a day-file.
## Reading order
Sort every entry of a plan by `(UTC date, HH:MM, source rank, name)`:
- legacy day-file entry: date from the file name, `HH:MM` from the heading, source rank 0, file order kept;
- entry file: date from the folder, `HH:MM:SS` from the name, source rank 1, then file name.
Worked example, plan 00543, both layouts on one day:
```
JOURNAL/00543-Journal-26-10-21.md ## 16:40 action (legacy, written before the switch)
JOURNAL/00543-Journal-26-10-21.md ## 17:05 finding
JOURNAL/2026-10-21/170500Z-action-3fa2c1.md ## 17:05 (same minute: legacy first)
JOURNAL/2026-10-21/171233Z-decision-0b9e44.md ## 17:12
JOURNAL/2026-10-22/090101Z-handoff-77d0aa.md ## 09:01 (newest; the next agent's entry point)
```
The newest entry is the last file of the newest date folder, unless a legacy day-file is dated later (only possible on a
branch cut before the switch).
## Does the map make the design wrong?
No blocker, three adjustments:
1. Checks only see `JOURNAL/` direct children today, and `classify` puts nested files in `JOURNAL_OTHER`. Keeping entry
files one folder down means an OLDER daemon sees them as `JOURNAL_OTHER` and reports nothing false (issue's point),
but the new checks must opt them in explicitly. Only date-shaped folders count, so an existing non-date subfolder of
`JOURNAL/` is not suddenly judged.
2. The reframed today-only rule closes legacy day-files to every edit, including removing conflict markers by hand.
That was already blocked for any day-file not dated today; the route stays `mkplan.bash --resolve-conflict`.
3. The commit-stage half of "never edited" must not flag the `git mv` into `Completed/` (a content-identical rename),
so it judges `M` and `D`, and an `R` only when content changed.