My last post covered day one of wiring up a multi-agent coding workflow with GitHub Projects and Orca — that day it merely ran. This is the five-days-later sequel: it has grown into a complete system. Numbers first: 122 commits, 111 merged PRs, 127 issues filed. The six work packages of V0.1 MVP for my side project (panta-log, a local-first voice journaling tool) are all shipped.
No chronological diary here — this post breaks down the system’s full feature set, layer by layer. Every layer was forced into existence by a real, stepped-in pit, and the pits are included.
Board Layer: From Task List to Automated Task System
The board from the last post was a notepad. This round it grew four automated organs. The foundation is one equation: one issue = one worktree = one PR — a card walks a full loop from start to merge, and everything below orbits it.
Epic cards + sub-issues (official sub-issue API). One epic card per initiative; children attached via the official API; the parent page auto-displays a progress bar; closing a child auto-checks it. Hand-written - [ ] #N tasklists are deprecated — zero automation, and we really did end up hand-closing two epic cards after all their children were done. Then we wrote a 30-line GitHub Action: when the last child closes, the epic auto-closes. Epic lifecycle now requires zero manual bookkeeping.

Before shipping, this Action hit a wall no test can see: for issues events, GitHub only honors the version of a workflow that lives on the default branch. On a PR branch it can be perfectly written and pass every local dry-run — no event fires, silently, no error anywhere. The “close the parent” action itself therefore couldn’t be verified inside a PR; it had to merge to master and wait for the first real epic to graduate. Verification loops for CI-config changes are born half-missing.
Card-creation script. The pitfall of manual issue creation: 14 new cards all missed board enrollment and field values — board filters went blind overnight. Now every card goes through the script:
scripts/new-issue.sh --title "xxx" --body-file card.md \
--type bug --phase 1 --area backend --priority P1 --labels area/hotkey
One command: issue creation (type label + @me assignee) → board enrollment → Status=Todo → Phase/Area/Priority/Milestone. Compliance by construction, not by memory.

Three automated transitions: PR squash-merged → issue auto-closes → card auto-moves to Done; master CI red → an alarm card is auto-opened and auto-closed on recovery; last sub-issue closed → parent epic auto-closes. Plus a stop-line gate: when master is red, other PRs fail fast (a area/ci label bypasses for fix PRs). The board went from “task tombstone” to “live scheduler.”

Claim protocol (anti-collision): before starting, set the card to In Progress as a claim marker and scan for an open PR on the same card. This rule was collided into existence — two sessions started the same card and the second branch got an automatic -2 suffix:

Execution Layer: Multi-Instance Isolation
One problem the last post left open: while the production instance was dogfooding, another session needed its own instance to verify code — but the single-instance lock, port, and data directory were all hard-wired to one copy. The only options were mutual murder.
The fix: give each instance an identity. PANTA_DATA_DIR relocates the entire data directory (locks, database, logs); PANTA_PORT moves the port:
scripts/dev.sh # production
PANTA_DATA_DIR=~/.pantalog-dev PANTA_PORT=9787 scripts/dev.sh # test
Two instances run in parallel, invisible to each other. Without the env vars, behavior is byte-identical to before — single-instance protection intact. Test instances don’t register the global hotkey by default (two instances fighting over a system hotkey has no winners).
CI Layer: Handing “Review Can’t Keep Up” to Machines
This is the most substantial layer. The honest context: with multiple agents delivering in parallel, PRs arrive faster than I can read diffs, and many PRs were merged after a glance at the description — unknown whether tests existed, unknown whether assertions meant anything. The fix is not reviewing harder (diligence doesn’t scale); it’s mechanizing every mechanizable part of review.
One premise first: the repo sits on GitHub’s free tier, where branch protection is unavailable — the API returns 403. With native required checks off the table, “nothing red enters master” can’t be delegated to the platform; it has to be self-built in two layers. Whatever machines can hard-gate goes into CI (all the gates below are that layer). What machines can’t gate — like “did you actually look at the checks before merging” — becomes written red-line protocol in CONTRIBUTING.md, the traffic rules every session reads before starting.
The coverage trio:
- Automatic coverage comments on every PR — visible numbers are themselves a constraint:

- Baseline ratchet:
--cov-fail-underfloors overall coverage — it may only rise (71.5 → 79, pushed up by debt repayment and tested fixes) - diff-cover incremental gate: changed/new lines in a PR require ≥80% test touch rate, or CI goes red and the PR cannot merge. Legacy zero-coverage modules aren’t chased; but new code without tests is physically locked out
Coverage answers “do tests exist,” not “what the tests actually cover.” The most painful one was the database-upgrade path: the suite starts every case from a fresh create_all database — all green — while real users’ old databases had zero upgrade coverage and crashed on launch. A fresh database can never catch a migration bug. The rule since: any schema change must ship an “old-db in-place upgrade” test — build with the old schema, run the migration, assert the result; new-database verification doesn’t count.
Three frontend ratchets (same philosophy: cap the legacy, enforce the new): no new ts-ignore, per-file line caps, i18n key parity between locales. Plus the oxlint+oxfmt baseline — the frontend now matches the backend’s Ruff governance.
Ops details: hard timeout-minutes per job (the 360-minute default would burn per-minute private billing on any hung build), concurrency cancellation of stale runs, ubuntu-only runners (macOS’s 10x rate is unaffordable on per-minute private billing), and the auto-following “master CI red” alarm card:

Red-line protocol: the part tests will never catch. Gates govern code, not process. Of the pits that actually cost work over these five days, not one could have been intercepted by a test:
- Merging on red: master went red twice, both times from merging PRs that were still red. One was genuinely spooky — green locally, red in CI — until it traced back to pytest hitting a UTC boundary that only triggers around 8 a.m. Beijing time. First red line since:
gh pr checksall green before any merge; date-related test failures get the timezone suspected before the code. - Duplicate fixes: master went red once and two sessions started fixing the same problem concurrently; the first to land merged, the second’s work was scrapped wholesale. Since then, check the board and the latest run before touching a fix.
- Fixing inherited errors per-card: branches cut from a red master carry the error into every worktree. The correct move is to fix master once, then rebase each in-flight branch — the inherited error vanishes with the rebase. Fixing the same master bug inside N cards is fixing one bug N times.
- Web-UI bypasses: PR conflicts must be resolved locally — rebase, run green, push with
--force-with-lease. “Resolving” a conflict by editing files in the GitHub web UI skips local tests on the way into master.
All five rules (plus stop-line: master red means no merges and no new cards) live in CONTRIBUTING.md under “CI red-line rules” — required reading before any session starts.
Infra Layer: Infrastructure Taught by Crashes
Disk logs. The trigger was a morning triple crash: 16 backed-up tasks re-queued on startup, each spawning a thread into the local transcription model; MLX’s thread-bound GPU stream threw a C++ exception at the Metal layer; the main process died instantly, bypassing all Python cleanup; the subprocess UI window became an orphan — still open, backend gone. During investigation we discovered logs were terminal-only: a crash left no evidence. The fixed system has three layers: rotating backend logs, frontend error-reporting logs, and test-process isolation (never polluting user logs).

The logs paid for themselves on their first case: diagnosing “transcription feels stuck” — the logs proved three stacked causes (batch serial queuing, same-day aggregation re-triggered N times, GLM occasionally returning malformed JSON), each fixed in its own card.
Transcription worker subprocess. Transcription saturates the GPU and starves the API thread of the Python global lock. Moved into a dedicated subprocess, the main process always responds, and subprocess exit releases the Metal cache wholesale. With one hard constraint: if the parent dies, the child dies (getppid watchdog) — force-quitting the app means everything stops; interrupted tasks are re-queued on next launch, losing nothing.
Import content validation. 23 leftover fake WAVs from testing (2 KB of the letter “x”) got ingested as real recordings and only failed at transcription with a wall of error. Now: file-header magic bytes + ffprobe decodability probing — garbage stops at the door. Alongside it, a duration cap: overlong recordings are stored as too_long awaiting manual transcription, never clogging the queue.
The Most Valuable Lesson: The AI Drifted from Product Intent
The finding worth an entire section — and it isn’t technical. While reviewing, I realized every processed recording triggered one LLM call generating an “AI review draft” (score, mood, next step) plus a same-day aggregation — while my product spec says: recording-level output is transcription plus structured extraction; the daily review is “a synthesis of the day’s multiple recordings,” viewed on the review page. The document is explicit. The implementation had invented an extra layer out of thin air.
The cost list: doubled LLM calls (blew through the daily quota), an unwanted draft block on every timeline card, a misleading “transcribing” state, a lengthened queue. It sat in the codebase for days — neither the agent that wrote it nor me accepting its PR noticed.
Agents don’t self-audit against the spec — that verification is on me. The correction card was opened and closed within the same day: recording-level keeps transcription + extraction only (facts/plans/ideas/problems — the commitment loop’s data source); AI review returns to daily-level aggregation with debounce; prompts are centralized and eval-gated.
Reflection: Quality in the AI Era
Five days in, I have a much more concrete answer to “if AI writes the code, what do I do?” The right answer to “review can’t keep up” is not reviewing harder; diligence does not scale. Mechanize what can be mechanized: coverage ratchets block untested code, diff-cover blocks untested changes, lint and guards block style drift. Machines hold the floor. Humans hold what machines cannot — whether a feature should exist, whether it’s any good, whether it’s what I actually wanted.
Multi-agent parallelism is the same: coordination cost is real. With five sessions running, collisions, stolen issue numbers, and trampled working directories all happen — conventions and scripts are not bureaucracy; they sink coordination cost from human memory into machine execution.
Next step: V0.1’s exit criterion is 14 consecutive days of dogfooding (record → next-day check-off loop). Guardrails are up. Now for the actual using.
Data as of 2026-09-16, at publishing: 122 commits / 111 merged PRs / 127 issues; coverage 71.5% → 79.6% with the baseline ratchet in effect; all work done by multiple Claude Code agents in parallel Orca worktrees.

