The Making of PavlokPager¶
A living journal of how this tool was built — the decisions, the dead ends, and why it looks the way it does. Newest entries at the bottom. Written to be read by both engineers and non-engineers; technical asides are marked (tech).
Chapter 1 — The idea (2026-07-07)¶
The ask, in Andrew's words: he has a Pavlok shock-alarm tied to his iPhone, and wants a way for Claude to call him while he's asleep — when it finishes all its work or gets stuck and needs guidance. Modeled on the existing "council" pattern (a skill + a tool). Build it in C#, put it in Docker if that's what it takes.
First move: check reality before building. Rather than assume how Pavlok's API worked, we read the current docs. Findings:
- Send a stimulus:
POST https://api.pavlok.com/api/v5/stimulus/sendwith a Bearer token and a body like{ "stimulus": { "stimulusType": "zap", "stimulusValue": 50 } }. Types:vibe/beep/zap, value 1–100. - Auth is a bearer token you get by logging in.
One decision made early: no Docker, no daemon. The instinct might be "put it in a container so it's always up," but this isn't a service — it's a fire-and-forget command. There's nothing to keep running. The only always-on thing is the Claude session itself, which already runs on the machine. A container would only add a thing to babysit (and break Windows-native token encryption). So: a plain command-line program.
Chapter 2 — Shape of the tool (2026-07-07)¶
Built as a single-file .NET 10 console app. Core pieces:
- Verbs:
login,setup,vibe/beep/zap,page,test,log. - Escalation ladders — the heart of it.
gentle,normal,urgent. Each is a list of steps with a strength and a pause after.urgentends in zaps. - Safety rails from day one, because a tool that can shock you needs guardrails before it needs features: a strength cap (
maxZap), a nightly budget (max 3 pages per 8 hours, enforced by counting a log file), and a required reason on every page. - The
pavlok-pageskill — teaches Claude when it's allowed to page (all work done / hard-blocked / critical failure only), and the etiquette (post a summary first, one page, wait, at most one escalation).
(tech) Two bugs surfaced in the first smoke tests: the JSONL log was written pretty-printed but read one-line-per-entry (fixed with a compact serializer), and the budget was counting individual ladder steps instead of page invocations (fixed by logging one page-start marker per run).
Chapter 3 — Andrew's sleep is deeper than the defaults (2026-07-07)¶
Andrew: "usually it takes max shocks to wake me up so usually I set it to 1 to 5." Interpreted as: real wake-ups need several full-strength zaps. So the urgent ladder was rebuilt to a brief warning (vibe 80 → beep 100) then zap 100 ×5, and maxZap raised to 100. A note went into the skill: don't weaken this ladder.
Chapter 4 — Getting it onto Andrew's wrist (2026-07-07)¶
The login puzzle. Andrew opened Pavlok's OAuth "create application" page and asked what to put there. That page is the heavy path — for apps acting on behalf of other users. For a personal tool you just need your own token. So we added a pavlok login command that takes email + password, calls /api/v5/users/login, and stores the returned token — no OAuth app needed.
Making it a real command. First it lived at a long path; then we installed it as a proper .NET global tool, so pavlok works from any terminal.
It works. Andrew ran pavlok login, then pavlok test — the band buzzed. Live.
Chapter 5 — The review gate (2026-07-07)¶
Per house policy, nothing gets committed until it passes the /council-code-review gate. We fanned the diff out to multiple models. Codex and Gemini returned strong reviews; the local model's output was unusable (terminal spinner noise) and the two house-PC councils timed out (they're minutes-slow) — noted as degraded, but the gate held on the two solid reviewers.
Fixes applied from the council: tolerate a corrupted log line so the budget check can't break, share one HTTP connection instead of one-per-zap, stream the log instead of loading it whole, tag dry-run log entries so they can't be mistaken for real shocks, and reject typo'd flags instead of silently firing with a blank reason. One "finding" was dropped as wrong — reviewers thought the login endpoint was a bug, but it was verified correct (it had just fired the band live).
Chapter 6 — Private repo, security gate (2026-07-07)¶
Andrew: everything we build should live in a private git repo, but only after security scans and full gates.
- Security scan. The Semgrep security MCP wasn't connected in this session, so we ran a Codex OWASP-focused pass as the gate. It flagged four things — secrets accepted on the command line, raw API responses being echoed, and a brief window where the token file could be created with loose permissions. All fixed: secrets now come only from a hidden prompt or piped input, error output is sanitized (full detail only behind a debug switch), and the token/log files are created locked-to-owner from the first byte.
- Shipped. Repo created private at
github.com/andrewjonesdev/PavlokPager, three commits pushed.
(tech) A gotcha bit us: dotnet tool update silently does nothing if the version number hasn't changed, so the global pavlok kept running an old build. Fix: bump <Version> on every change (now 1.1.0), or uninstall+reinstall.
Chapter 7 — Docs, diagrams, and a real Semgrep scan (2026-07-07)¶
Brought the repo up to house documentation standard:
- Two-version docs — a technical architecture doc (with C4 context/component diagrams, a send-sequence diagram, and the ladder state machine, all in Mermaid) and a plain-English explainer.
- This journal.
- The real Semgrep gate. With Docker available, we ran actual Semgrep SAST (C#, security-audit, secrets, and OWASP Top Ten rulesets) over the code — 0 findings. The scan record is committed, and it's wired into CI so it re-runs on every push. The Semgrep MCP was also configured so future sessions have it natively.
- CI caught its own supply-chain smell. The first CI run scanned the whole tree (not just the code) and Semgrep flagged the workflow itself: it referenced
actions/checkout@v4andactions/upload-artifact@v4by mutable tag, which an action owner could silently repoint. Fixed by pinning both to commit SHAs. Green after that — the security gate literally hardened its own plumbing.
Chapter 8 — Tests, mutants, and a board (2026-07-07)¶
Andrew: add unit testing and Stryker mutation testing, just like WorkWingman — and do it for every tool as it becomes a private repo. Also project boards for all this work, as a universal standard.
Making it testable. The tool was one static class calling a static HttpClient — hard to test. We added seams without changing behavior: an env-overridable home directory (PAVLOK_HOME) so tests run in a throwaway sandbox, an injectable HttpClient so tests mock the network, injectable delay functions so tests don't actually sleep through retries and ladder pauses, and a pure RecentPageCount helper for the budget.
The tests. xUnit with Bogus-generated data. Two layers: unit tests for the pure logic (arg parsing, budget window, config roundtrip, token encrypt/decrypt, corrupt-log tolerance, send retry/backoff), and end-to-end tests that drive Main() with a mocked transport and piped stdin to cover every command — including the login/setup secret prompts and the page budget refusal.
Mutation testing. Stryker doesn't just check that tests pass — it changes the code (flips a <, blanks a string, negates a condition) and checks the tests notice. First pass: 25.8% — the tests covered the pure seams but not the command handlers. After adding the end-to-end Main() tests: 55.7%. After targeted tests for the real branch logic (zap-only clamping, all-steps-fail, -n limiting, the debug gate, the login validation guard): a solid score, with the remaining survivors being cosmetic console-message mutations not worth pinning. Both dotnet test and Stryker now run in CI.
The board. A WorkWingman-style Kanban (BOARD.md + a live board.html) tracking every PavlokPager ticket (PP-1…PP-17) across Done / In review / Backlog. Cards flip on each transition; nothing reaches Done without passing In review.
All of this — unit + mutation testing, a project board, plus the earlier docs/diagrams/security gates — is now the standing standard for every tool we build into a private repo.
Chapter 9 — Re-audit for the org migration (2026-07-08)¶
An org migration is coming, and the house pipeline's shared build brief tightened since this repo first shipped (self-hosted CI runner labels, a churn/flake job, a fixed docs layout). Rather than rebuild, this pass audited the existing repo against the brief and closed the gaps found:
- CI runners. Both workflows ran on
ubuntu-latest(a mutable hosted label) withsecurity.ymlusing GitHub'scontainer:step to run Semgrep — fine on Linux, but the house standard is[self-hosted, home-ci, windows]everywhere. Moved both jobs there; since Windows has no native Semgrep binary, the security job now shellsdocker runagainst the Semgrep image directly (the same pattern the reference pilot repo uses). - Semgrep image pinning. A follow-up Codex security pass (the tree was otherwise CLEAN — no secrets, injection, or unsafe exec) flagged the Semgrep image as an unpinned mutable tag. Re-pinned to a content digest, re-ran the exact CI command locally against the full tree: still 0 findings.
- Churn job.
tests.ymlhad unit + mutation jobs but no flake-detection job. Added one that runs the suite 5x and fails on any pass/fail disagreement between runs, matching the pilot's PowerShell churn pattern (translated todotnet test). - Doc-set layout. The brief expects
TESTING.mdand.env.exampleat repo root and adocs/plain/README.md; this repo hadTESTING.mdunderdocs/technical/and the plain doc namedHOW-IT-WORKS.md. Moved (not duplicated) both, fixed the cross-links, and added the missing.env.example(documentsPAVLOK_API_KEY,PAVLOK_HOME,PAVLOK_DEBUG— none required for normal use, sincepavlok login/setupalready persist a token). Added an explicit## Securitysection toREADME.md(it previously only linked out todocs/technical/SECURITY.md). - Numbers were stale.
BOARD.md/board.htmlstill said "33 tests" and had PP-14 sitting in "In review" after it had already merged — both a commit behind reality. Fixed, and this pass itself is PP-18. - What didn't need fixing: the secret-handling design (DPAPI/env-var/0600, no argv secrets, no raw-body echo) was already sound — the hard secret grep and the fresh Codex pass both came back clean on that front. Mutation score re-verified at a fresh 63.06%, unchanged from the last committed run — no regression, and per house policy this pass didn't chase the score past the documented ceiling (the interactive
Console.ReadKeypath and cosmetic string mutants).
This journal is living — future changes to PavlokPager should add a chapter here.