Skip to content

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/send with 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. urgent ends 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-page skill — 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@v4 and actions/upload-artifact@v4 by 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) with security.yml using GitHub's container: 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 shells docker run against 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.yml had 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 to dotnet test).
  • Doc-set layout. The brief expects TESTING.md and .env.example at repo root and a docs/plain/README.md; this repo had TESTING.md under docs/technical/ and the plain doc named HOW-IT-WORKS.md. Moved (not duplicated) both, fixed the cross-links, and added the missing .env.example (documents PAVLOK_API_KEY, PAVLOK_HOME, PAVLOK_DEBUG — none required for normal use, since pavlok login/setup already persist a token). Added an explicit ## Security section to README.md (it previously only linked out to docs/technical/SECURITY.md).
  • Numbers were stale. BOARD.md/board.html still 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.ReadKey path and cosmetic string mutants).

This journal is living — future changes to PavlokPager should add a chapter here.