Weekly Work Log
Builds and refreshes the Weekly Work Log at brycewatson.com/weekly-report: a per-project feed of the specific work Bryce did last week, each item with an honest status badge and an expandable evidence drawer, a git-derived commits/day chart for the week, and per-project archive pages. Each week's report is snapshotted; brycewatson.com/log is the index of all past weekly reports.
Routes: /weekly-report (live, current week) · /weekly-report/<project> (per-project commit-history archive) · /log (index of weekly reports) · /log/<week> (an archived week).
Target project: C:\Users\Bryce\Projects\brycewatson.com. The page + build scripts live there; this skill is the reusable orchestrator + the weekly scheduler.
The window: the last completed week
The report always covers the most recently completed Monday–Sunday week (a week completes on Sunday). Run on a Sunday night, that is the week that just ended; run any other day, it is the previous full week. Discovery (handoffs + git), the chart, and the per-project metrics all use this same week, resolved by the single shared resolveWeek() in scripts/lib/work-log-sessions.mjs. It is anchored on the calendar week, never a rolling window walking back from today.
Backfill (--week YYYY-MM-DD) regenerates a PAST completed week's /log archive (snapshot + index + re-aggregated goals) and leaves the live work-log.json untouched. Any day inside the target week resolves to that whole Mon–Sun window; a future date or the current in-progress week aborts. Backfill is opt-in / operator-confirmed and regenerates exactly one completed week per invocation (blast radius bounded to one PR); it is never fired by the scheduled run (the unattended agent always targets the last completed week — one PR, not N).
What runs where
Everything runs LOCALLY (it reads ~/.claude handoffs + the sibling repos under C:\Users\Bryce\Projects\); GitHub Pages CI cannot. The committed JSON is what CI builds, exactly like art.json/books.json.
<repo>/src/data/work-log.source.json— approval-gated content (the human gate): Claude distills it from the week's handoffs + redacted session digest + git; you review and approve the PR; the generator verifies every number. Prose is distilled and human-reviewed, not number-verified.<repo>/src/data/work-log.json— GENERATED + committed (every number re-derived + git-verified).<repo>/src/data/reports/<weekStart>.json+reports/index.json— committed per-week snapshots (the/logarchive series); the generator writes these.<repo>/src/data/goals.json— GENERATED + committed (the/goalscross-week aggregation).<repo>/src/components/ReportPanel.astro— the report panel, shared by/weekly-reportand/log/<week>.<repo>/src/pages/weekly-report.astro+weekly-report/[project].astro+log.astro+log/[week].astro+goals.astro— the pages.<repo>/scripts/draft-work-log-from-handoffs.mjs— discover (handoffs → redacted digest).<repo>/scripts/draft-work-log-sessions.mjs— mine the week's interactive sessions → redacted digest.<repo>/scripts/draft-work-log-proposed.mjs— ranked.proposeddistil scaffold (interactive; the builder NEVER reads it).<repo>/scripts/work-log-harvest-nouns.mjs— propose redactor denylist additions → gitignored sidecar (count surfaced, never raw nouns).<repo>/scripts/work-log-validate-source.mjs— voice/leak gate on the DRAFTED source prose (fails loud on a dash / denylisted token / bad badge / display-role git-leak / a claimingheadlinewith noheadlineWeekstamp, anextUpwith nonextUpWeekstamp), run after distillation and before the build (also chained inpnpm build).<repo>/scripts/lib/work-log-headline.mjs— the ONE decision on whether this week'sheadline(resolveHeadline) andnextUp(resolveNextUp) may publish; shared by both generators + the PR-body composer so they cannot drift.<repo>/scripts/work-log-via-honestweek.mjs— build (the honestweek engine: verify-or-abort every commit, re-derive + number-fence every number, redact, then write work-log.json + the /log snapshot + index + goals.json).<repo>/scripts/build-work-log.mjsis retained as the parity oracle (scripts/work-log-parity.mjsproves the two agree on all four artifacts).<repo>/scripts/work-log-weekly.mjs— open the review PR (--advisory <path>splices the fail-open advisory; never main, never deploy).<repo>/scripts/lib/work-log-redact.mjs— shared secret/leak scrubber (the ONE canonical redactor; extend its term lists, never duplicate).~/.claude/skills/weekly-work-log/run-state.cjs— atomically records a privacy-saferunning/succeeded/failedstatus for the Sunday run. The Monday preview reads this instead of treating every missing PR as a quiet no-op.
Modes
/weekly-work-log(interactive, a human in the loop): the full build — discover → distil candidates → build → preview. New items get WRITTEN here (judgment). The optional.proposedscaffold (draft-work-log-proposed.mjs) gives you a ranked, mirror-shaped starting point to fill in the public voice; you then MOVE accepted items by hand intosource.json(the gate).- Unattended (the Sunday-night Claude scheduled task) — curates the whole week, then opens one PR:
- Isolated checkout: the user's
brycewatson.comcheckout is never switched, reset, stashed, or cleaned. The task fetchesorigin/mainand works in the dedicated sibling worktreebrycewatson.com-weekly-work-log. A clean registered stale worktree may be replaced; a dirty or unregistered directory is preserved and surfaced as a failed run. - Curate every session (distillation): distil EVERY interactive Claude Code session of the week into
source.jsonitems so the page shows the full week (the original "curate every session" directive, automated). Done in-context from the redacted digest (not subagents). Private/sensitive sessions are summarized through the privacy filter, not dropped or stubbed (display roles: Akaya/Personal/ShopForge — generalized to the kind of work, never git-read; "personal finance is fine, we just need iron clad rules"). Each item is linked to the SPECIFIC goal it advances viaobjectiveId(readobjectives.public.json) — never leave a multi-goal project like Command to fall through to its catch-all/parent goal, or the goal lens collapses to one bucket; client/private items get noobjectiveId(they stay off/goals). The drafted prose is gated bywork-log-validate-source.mjs(fails loud on a dash/leak/bad-badge) + a claim-falsification self-check BEFORE the build. - Deterministic backstop:
work-log-via-honestweek.mjs(honestweek) git-verifies + number-fences every number (aborts on any unresolved/non-Bryce commit, so NO PR on bad data). Items the job drafts carry"drafted": "auto-<date>". - Fail-open advisory: judgment that ASSISTS the reviewer (badge-vs-prose, coverage gaps, badge-vs-git, a downgrade-only privacy adjudicator, reversal-coverage) spliced into the PR body, plus the noun-harvester (count only). Any LLM/network/
ghfailure → a single "advisory unavailable" line; never blocks the PR. Advisory may only DOWNGRADE/FLAG. - The PR is the gate: auto-DRAFT, human-APPROVE. The run never pushes
main, never deploys, opens exactly one PR.
- Isolated checkout: the user's
--week YYYY-MM-DD: backfill a past completed week's archive (see above). Manual only.--days N: override the window to a rolling N-day scan (manual broader discovery).
The full flow (interactive)
- Discover —
node scripts/draft-work-log-from-handoffs.mjs(and/ordraft-work-log-sessions.mjs). Scans the week's session-end handoffs / interactive sessions across the allowlisted repos, extracts tagged claims + reversals + cited commits, redacts, writes the gitignoredsrc/data/work-log.drafts.jsondigest. - Curate every session —
node scripts/draft-work-log-sessions.mjsfor the redacted per-session digest, then distil EVERY session intosource.jsonitems in the plain public voice (private/sensitive sessions summarized through the privacy filter, never dropped — see the rules + the unattended promptscheduled-task-prompt.md).draft-work-log-proposed.mjscan give a ranked scaffold to fill. Thennode scripts/work-log-validate-source.mjs(fails loud on a dash/leak/bad-badge) before building. Obey every rule below; accuracy is the floor even for private. - Build —
node scripts/work-log-via-honestweek.mjs(the honestweek engine). Re-derives every date/number from git, rewrites commit snippets to the real subject, verify-or-aborts all cited commits, number-fences every number, redacts. Writeswork-log.json(+ snapshot + index +goals.json).build-work-log.mjsis the parity oracle;node scripts/work-log-parity.mjsproves they agree. - Preview —
pnpm dev, openhttp://localhost:4321/weekly-report(archive index at/log, goals at/goals). - Ship — commit the data files. A merge to
maindeploys via CI. (Unattended:node scripts/work-log-weekly.mjs --advisory <sidecar>opens the PR instead of committing to main.)
The rules (load-bearing — every run, especially unattended, must honor these)
- The page headline is written fresh, for THIS week, and stamped:
source.headlineis the<h1>— an editorial claim about the week it sits above ("The counts were wrong, and the entries beneath them gave it away"). It is AUTHORED, never computed, so distil it from THIS week's entries like any other prose, and setheadlineWeekto the MondayYYYY-MM-DDof the week being reported. Write the two fields together, always. A headline stamped for another week (or unstamped) is DROPPED by the build to the neutral title"What I worked on this week."and flagged in the PR body — it is never carried forward, because a claim about last week is not true of this one. If no week-specific headline is warranted, author the neutral title deliberately; the build never invents one for you. (This existed as a silent gap: the generators passedheadlinestraight through, so one claim rode the weeks of 2026-07-13, -07-20 and -07-27, and each/logarchive kept its own copy. Seedocs/work-log.md→ "The page headline".) - The "next up" line is written fresh and stamped too:
source.nextUpis the closing line under the feed, and it had the SAME defect as the headline — measured onorigin/main, one line rode 2026-07-13, -07-20 and -07-27, another rode 2026-06-08 and -06-15, and a third rode 2026-06-22 and -06-29. SetnextUpWeekto the MondayYYYY-MM-DDof the week being reported, and write the two fields together, always. An unstamped or mis-stamped line is DROPPED entirely (the page renders no next-up block at all) and flagged in the PR body. Two deliberate differences from the headline: there is no neutral fallback (there is no honest generic "next up" text, and inventing one would put words in Bryce's mouth about his own plans), and re-stating an unchanged plan is legitimate — a forward-looking line may genuinely still be what is next, so it publishes as long as it is re-stamped for this week. Both fields resolve through the samescripts/lib/work-log-headline.mjs. Seedocs/work-log.md→ "The 'next up' line". - Voice: plain, concrete, no marketing flourishes. Subject-led headlines — lead with the work or the finding, NOT "I"/"My" (headlines must not all start the same way or read self-focused). First-person belongs in the body. No em dashes. Never announce the page's own honesty ("honest", "keeping myself honest", "proof I don't fake it") — show it through the badges + receipts. This includes generalized/private entries: state the KIND of work concretely (like any public entry) and NEVER narrate the withholding itself ("keeping it sealed", "recording only the kind of work", "surfaced here as its own thread", "keeping it generic here", "belongs in an honest log") — model on the Akaya display-role rows.
- Honest status badges:
shipped(built, merged, verified),in progress,designed, not proven(machinery exists, no real result yet). The most interesting work is often the least finished; let it wear the badge openly. Map a handoff's[verified]claim → shipped;[derived]/[assumed]/[unverified]/[handoff-claimed]→ designed-not-proven ([derived]is an inference the handoff drew, not a result it observed — it never earnsshipped). - Every number git-verified: re-derived at build, never hardcoded. A cited commit that does not resolve (or is not Bryce's:
bryceewatson@gmail.com/bryceewatson@users.noreply.github.com) aborts the build. Item dates come from the commit, not the prose. - Privacy (default-deny): only Bryce's OWN repos are read (Command, DemandForge, claude-global-skills, brycewatson.com). Client / no-remote repos (dropKnowledge, etc.) are never read or named. The shared redactor scrubs codenames (plumagedispatch), client names (dropKnowledge / castgryff), vendor + niche names, session UUIDs, home paths, and secrets. Draft-and-distil, never lift: handoff prose is operational and leaky; distil it into plain public-voice items, never paste it verbatim. The
.proposedscaffold and the harvester sidecar are gitignored; never commit them, and never route raw harvested nouns into the PR body (count only). - Structure: the feed is grouped by project into containers; each project is a card with git-derived metrics (commits this week, active days, entries) + an
archive →link to/weekly-report/<project>(6-month commit history). Dark console aesthetic; the shared site header usesheaderVariant="dark-canvas". - The approval gate is the PR: the unattended run never pushes to
mainand never deploys. It only opens a PR. Bryce reviews + merges; CI deploys the merge. Advisory judgment never gates — it only assists.
Scheduling (every Sunday night) — Claude scheduled task
The unattended weekly run is a Claude scheduled task (mcp__scheduled-tasks), NOT a Windows Task Scheduler job. It runs as a local Claude agent with full filesystem access and catches up on next app launch if the app/machine was closed at the scheduled time — fixing the old schtasks job's silent-drop failure (it had StartWhenAvailable=False / WakeToRun=False / DisallowStartIfOnBatteries=True and no catch-up).
- The task:
taskId: weekly-work-log,cronExpression: "0 22 * * 0"(Sunday 22:00 local). Its prompt is authored inscheduled-task-prompt.md(this skill dir) — keep that file in sync with the live task. Its Monday preview prompt is authored inpreview-scheduled-task-prompt.mdand accepts only a verifiedwork-log/weekly-*PR that changes both the authored source and generated report. - Create / update it (the runtime swap; done by an operator session after the repo seams are on
main):
Confirm withmcp__scheduled-tasks__create_scheduled_task({ taskId: "weekly-work-log", cronExpression: "0 22 * * 0", description: "Sunday-night Weekly Work Log: deterministic build + verify, fail-open advisory, opens one review PR (never deploys)", prompt: <contents of scheduled-task-prompt.md> })mcp__scheduled-tasks__list_scheduled_tasks. - Retire the legacy Windows task at the same swap:
node uninstall.cjs(orschtasks /Delete /TN ClaudeWeeklyWorkLog /F).install.cjs/weekly-run.cmdare the legacy schtasks launchers, retired at the swap; do not runinstall.cjsgoing forward. - Failure visibility: every run starts by writing
last-run.jsonunder the live scheduled-task directory and must write one terminal state. Fetch/worktree setup and a non-data PR-open failure retry exactly once; data verification never retries into a PR. - Verification debt: the run-on-next-launch catch-up is documented, not yet behaviorally proven here. Worth a one-time controlled catch-up test (close the app over a due time, relaunch, confirm fire) before fully trusting it — the
demandforge-verify-cron-first-firingtask is precedent. - It activates once the page is shipped (the data files are committed). Requires:
ghauthenticated and git push credentials. The shared repo may be on any branch and may contain the user's work because the scheduled run operates fromorigin/mainin its own worktree.
Safety invariants
- Never push to
main; never deploy; only ever open a PR. The unattended run opens exactly one PR. - Never mutate the user's shared checkout. All unattended writes happen in the dedicated worktree; every setup or terminal failure is persisted before the task exits.
- Abort (no PR) if the generator fails verification — never open a PR with unverified data.
- Advisory is fail-open, but never silently: any LLM/network/
ghfailure → the "advisory unavailable" line PLUS a line naming which failure it was (not requested / did not run / empty / withheld by the privacy screen). The PR still opens with deterministic data + the human checklist intact. Advisory may only DOWNGRADE/FLAG — never clear-for-publish, never suppress the human leak/voice checklist. A reviewer must always be able to tell a failed advisory from a clean one; an absent advisory is not a clean bill of health. - The cron NEVER fires
--weekbackfill; the harvester's raw nouns NEVER enter the PR body (count only). - Reuse the review-loop
--mode claimengine/rubric verbatim for the badge-vs-prose checks — read itsagents/claim-*.mdlenses at runtime; do not fork them. - Curate every session, redacted — not dropped, not stubbed. Private/sensitive sessions are summarized through the privacy filter (display roles never git-read; client→"Akaya", finances→"Personal"; generalized to the kind of work; accuracy is the hard floor even for private). A session you cannot distil confidently gets the most generic HONEST entry, never a fabricated one.
- The drafted prose is gated before it builds:
work-log-validate-source.mjsfails loud on a dash/leak/bad-badge/git-leak; the iron-clad redactor + the dist-PII scan are the deterministic backstop; the PR is the human gate. Auto-DRAFT, human-APPROVE — never auto-publish in Bryce's voice. - The unattended run writes ONLY
work-log.source.*(its drafted items) + gitignored sidecars; it NEVER hand-writeswork-log.json/reports/*/goals.json(the build does, deterministically), nevermain, never deploy. - The drafts digest and harvester sidecar are gitignored and redacted; never commit them.