Co-authored-by: claude-code <claude-code@javagil.de>
8.4 KiB
PR#41: Werkregie Keeps the Work of Each Day
WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR documentation is not maintained along with new PRs; treat it as a snapshot.
Problem
The user wants a proposed import file for Werkjournal per calendar day, built from what Werkregie knows about the work of that day.
Werkregie keeps no history: each poll reads the sources again, at most windowHours back,
and several sources forget much earlier:
- a Claude Code terminal session disappears as soon as its process ends;
- the Gitea feed gives only the last 50 actions, Matrix keeps 200 messages, and Taiga lists only open items;
- an item has one point in time, no start and no active time.
Werkjournal books whole hours per day and project. For that, Werkregie needs the active time of each session per day, and a record of each day that outlives the sources.
Non-Goals
- No proposal, no mapping to Werkjournal customers and projects, and no page yet; these follow in a second PR.
- No recovery of days before the deployment.
- No change to the items the page shows.
Specification
Feature: Werkregie keeps the work of each day
Scenario#41.01: a pause splits the active time of a session
… so that a session open for hours counts only the time it was worked in.
- Given a Claude Code transcript with entries at 09:00, 09:10, 09:20 and 10:00, and
journal.gapMinutes15 - When the relay reads the transcripts
- Then it reports the session with the active spans 09:00–09:20 and 10:00–10:00
Verified by
- TranscriptActivityTest: "a pause longer than the gap splits the active time of a session".
- Manual: on think, the 64 sessions of 7 days gave 345 spans; the first read took 4.8 s, each later one 18 ms.
Scenario#41.02: an ended terminal session still counts
… so that the work of a closed terminal is not lost.
- Given a Claude Code terminal session whose process has ended, with its transcript written today
- When the relay reads the transcripts
- Then it reports the session with its active spans, its working directory, its repository and its branches
Verified by
- TranscriptActivityTest: "a transcript counts without a running process, with its directory, repository, branches and title", "reads a growing transcript on, and a line still being written only once it is complete".
- RelayClientTest: "reports the active time of the local sessions along".
Scenario#41.03: the active time of a Codex thread
… so that Codex work counts like Claude Code work.
- Given a Codex thread whose rollout file has entries at 14:00 and 14:05
- When the relay reads the threads
- Then it reports the thread with the active span 14:00–14:05 and its name
Verified by
- TranscriptActivityTest: "the active time of a Codex thread comes from its rollout file".
Scenario#41.04: the server files the spans by local day
… so that a day of the user's calendar holds exactly its own work.
- Given a relay report with a span from 23:30 to 00:20 local time
- When the server records the report
- Then the day file of the first day holds 23:30–24:00, and that of the second day 00:00–00:20
Verified by
- JournalArchiveTest: "a span over midnight falls into the files of both days".
Scenario#41.05: a recorded span stays
… so that a day keeps its work after the relay forgets it, e.g. after windowHours.
- Given a day file with the span 09:00–09:20 of a session
- When the relay reports the session with the span 10:00–10:30 only
- Then the day file holds both spans
Verified by
- JournalArchiveTest: "a recorded span stays when the relay no longer reports it".
Scenario#41.06: the items of the sources are kept by day
… so that pull requests, builds, reviews, tasks and messages of a day remain after the sources forget them.
- Given a merge in the Gitea feed, a Werkator build, a werkrevisor review, a Taiga task and a Matrix message, each of today
- When the server records the items
- Then the day file of today holds each of them, and a later state of an item replaces its earlier one
Verified by
- JournalArchiveTest: "keeps the items of each day, a later state replacing the earlier one, the sessions apart".
Scenario#41.07: the record needs no viewer
… so that a day is complete even when nobody opened the page.
- Given a running server that nobody polls
- When a minute passes
- Then the server has recorded the current items and spans
Verified by
- JournalArchiveTest: "the recorder fills the archive every interval, without anybody asking for the page".
Scenario#41.08: old days are deleted
… so that the record does not grow without end.
- Given
journal.keepDays400 and a day file 401 days old - When the server records
- Then the old day file is deleted
Verified by
- JournalArchiveTest: "deletes the days older than keepDays".
Scenario#41.09: a day only with the access token
… so that the record of the user's work stays private.
- Given a server with a recorded day
- When a request asks for
/api/journal/<day>with and without the access token - Then it gets the day with the token, and HTTP 401 without it
Verified by
- WebServerTest: "gives the record of a day only with the token".
Solution
TranscriptActivityreads the time stamps of the Claude Code transcripts,claude.projectsDir, and of the Codex rollout files of the threads instate_5.sqlite. It parses only the top-level fields of a line and reads each file on from where it stopped, at most every 30 s. A session's title is its lastcustomTitle, for Codex the thread's name; the branches come fromgitBranchandgit_branch.RelayReportgetsactivity, the sessions with their spans withinwindowHours; the registry names them after the relay, as the items. A server older than 0.22.0 skips the field, an older relay sends none.JournalArchivekeeps one file per local day in<stateDir>/journal/, with the sessions and the items. It joins spans with those recorded before and replaces an item by its later state; the key of an item is source, origin, kind and URL or title, for a Gitea activity also its operation. Session items stay out, since the sessions with their active time stand there.JournalRecorderrecords every minute in the modesserverandlocal; the relay only reports.GET /api/journal/YYYY-MM-DDreturns a day; on a server only with the access token, as cookie or bearer.- Configuration
journalwithenabled,zone,gapMinutesandkeepDays; the server on mih09 getszone: Europe/Berlin. - Version 0.22.0.
Open Questions
- Which entries form a Werkjournal entry? One per Taiga story and day where a Taiga ID is known, else one per project and day; decided by the human, for the second PR.
- Where do the hours come from? From the active time of the sessions, split by a pause over 15 min; decided by the human.
- How far back must a day be available? Each day since the deployment, from the record of this PR; decided by the human.
- Does the record keep the text of Matrix messages? Only what the relay already sends the server: room, sender, time and the start of the text.
Follow-up PRs
- The proposal of a Werkjournal import file for a selected day, with the mapping in
journal:ofdashboard.yml, a day picker, and copying and downloading the file.