tapflow
AGENTS.md1.8k starsChanged 5 months agoMITView on GitHub
Package-specific rules are referenced via INDEX.md. tapflow is an open-source self-hosted library that lets the entire team — PO, PM, designers, backend engineers, and QA — test iOS/Android apps directly from a browser. It uses the Mac you already own — no external cloud dependency. Remove friction. Anyone on the team can open a browser and test the app on a real simulator, without Xcode, without device setup, without accounts on external services. tapflow Core is the layer everything shares:…
- Deletes or force-pushes
- Commits and pushes
Trending
More talk lately.about the whole repository
Who's talking about jo-duchan/tapflow
- A Hacker News story“A 0.5s periodic hitch in my local screen stream turned out to be macOS AWDL”Hacker News · Jun 22, 2026 · 13 points
- A Hacker News story“Show HN: Tapflow – self-hosted iOS/Android simulator streaming for mobile QA”Hacker News · Jun 10, 2026 · 4 points
- @yug1224.com“ブラウザでiOSやAndroidのシミュレータを共有できるセルフホストツールっぽい チームでのモバイルQA効率化に使えそう yug1224 starred jo-duchan/tapflow …”Bluesky · Nov 5, 2026 · 1 likes
- jaywcjlove/awesome-mac“Add tapflow to Developer Utilities”GitHub · Oct 28, 2026
- awhite0030/tapflow“fix(ios-agent): fix audio streamer and polling interval leaks on simulator boot”GitHub · Oct 16, 2026
The biggest 5 of 54 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 54
Talked about on GitHub, Hacker News and Bluesky (54 times).
On GitHub · 50
- Oct 28, 2026Add tapflow to Developer Utilities — jaywcjlove/awesome-mac0 reactions
- Oct 16, 2026fix(ios-agent): fix audio streamer and polling interval leaks on simulator boot — awhite0030/tapflow0 reactions · 3 comments
- Oct 16, 2026fix(ios-agent): fix audio streamer and polling interval leaks on simulator boot — awhite0030/tapflow0 reactions · 3 comments
- Oct 1, 2026fix: explicitly target rotation and reset on boot — awhite0030/tapflow0 reactions · 2 comments
- Oct 1, 2026fix(ios-agent): fix incorrect pixel scale division for newer @3x devices — awhite0030/tapflow0 reactions · 3 comments
45 more on GitHub
- Oct 1, 2026fix(ios-agent): fix incorrect pixel scale division for newer @3x devices — awhite0030/tapflow0 reactions · 3 comments
- Oct 1, 2026Integration — joeblew999/irgo-windows-vm0 reactions · 1 comment
- Sep 24, 2026fix(android-agent): increase adb maxBuffer to 64MB for large outputs — awhite0030/tapflow0 reactions · 3 comments
- Sep 22, 2026fix: resolve migrate data-dir path errors and live relay conflicts — awhite0030/tapflow0 reactions · 3 comments
- Sep 22, 2026fix: resolve migrate data-dir path errors and live relay conflicts — awhite0030/tapflow0 reactions · 3 comments
- Sep 22, 2026fix(dashboard): handle focus when an item leaves a list that stays a list — awhite0030/tapflow0 reactions · 3 comments
- Sep 22, 2026fix(dashboard): handle focus when an item leaves a list that stays a list — awhite0030/tapflow0 reactions · 3 comments
- Sep 21, 2026fix(cli): retry a11y-lens check on ETIMEDOUT to prevent silent skips — awhite0030/tapflow0 reactions · 2 comments
- Sep 20, 2026feat(cli): support TAPFLOW_NONINTERACTIVE env to bypass interactive prompts — awhite0030/tapflow0 reactions · 1 comment
- Sep 19, 2026fix(cli): do not prompt when stdin is redirected — awhite0030/tapflow0 reactions · 2 comments
- Sep 18, 2026fix(cli): treat setup as interactive only when both stdin and stdout are terminals — awhite0030/tapflow0 reactions · 1 comment
- Sep 18, 2026fix(cli): setup prompts correctly handle empty stdin by checking both TTYs — awhite0030/tapflow0 reactions · 1 comment
- Sep 18, 2026fix(cli): treat session as interactive only when both stdin and stdout are terminals — awhite0030/tapflow0 reactions · 1 comment
- Sep 18, 2026fix(cli): treat session as interactive only when both stdout and stdin are TTYs — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): fix silent exit when stdin is empty in setup — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): prevent silent exit on empty stdin during setup — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): treat a session as interactive only when both stdin and stdout are terminals — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): use both stdout and stdin for interactive checks in setup — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): verify both stdin and stdout are interactive for setup — awhite0030/tapflow0 reactions · 1 comment
- Sep 17, 2026fix(cli): check both stdout and stdin for isTTY in setup — awhite0030/tapflow0 reactions · 1 comment
- Sep 16, 2026fix(cli): treat a session as interactive only when stdout and stdin are both terminals — awhite0030/tapflow0 reactions · 1 comment
- Sep 16, 2026fix(cli): ensure tapflow setup correctly detects interactive mode — awhite0030/tapflow0 reactions · 1 comment
- Sep 13, 2026fix(cli): start tunnel before relay to capture auto-detected publicUrl — awhite0030/tapflow0 reactions · 1 comment
- Sep 13, 2026fix(dashboard): centralize device lifecycle status announcement — awhite0030/tapflow0 reactions · 2 comments
- Sep 12, 2026fix(android-agent): throw explicitly when emulator vanishes before video starts — awhite0030/tapflow0 reactions · 3 comments
- Sep 12, 2026fix(android-agent): fix orphan video session on abandoned boot — awhite0030/tapflow0 reactions · 2 comments
- Sep 12, 2026fix: answer a relay-lost boot at the bump, while the socket is still open — awhite0030/tapflow0 reactions · 3 comments
- Sep 11, 2026fix(mcp-server): wrap network fetch errors as RelayHttpError with status 0 — awhite0030/tapflow0 reactions · 2 comments
- Sep 2, 2026fix(flow-runner): let the engine reboot on a mid-flow session rebound — awhite0030/tapflow0 reactions · 3 comments
- Sep 2, 2026fix(mcp-server): handle transient query errors and abort signal in run_flow — awhite0030/tapflow0 reactions · 3 comments
- Sep 2, 2026fix: handle transient query errors and abort signal in run_flow — awhite0030/tapflow0 reactions · 3 comments
- Sep 2, 2026fix(relay): answer malformed device:shutdown rather than dropping it silently — awhite0030/tapflow0 reactions · 3 comments
- Sep 2, 2026fix(relay): answer malformed device:shutdown rather than dropping it silently — awhite0030/tapflow0 reactions · 3 comments
- Sep 1, 2026docs(ios-agent): document why the socket observation failed in #538 — awhite0030/tapflow0 reactions · 3 comments
- Sep 1, 2026fix(flow-runner): route environmental step failures to exit 2 — awhite0030/tapflow0 reactions · 3 comments
- Sep 1, 2026fix(android-agent): log unexpected scrcpy server exits — awhite0030/tapflow0 reactions · 3 comments
- Sep 1, 2026fix(android-agent): log unexpected scrcpy server exits — awhite0030/tapflow0 reactions · 3 comments
- Sep 1, 2026docs(relay): document Docker deployment and Compose setup — awhite0030/tapflow0 reactions · 1 comment
- Aug 30, 2026docs(relay): document Docker deployment and Compose setup — awhite0030/tapflow0 reactions · 3 comments
- Aug 30, 2026feat(relay): add Route53 DNS provider — awhite0030/tapflow0 reactions · 3 comments
- Aug 30, 2026feat: Add Route53 DNS provider — awhite0030/tapflow0 reactions · 2 comments
- Aug 30, 2026chore(relay): update lockfile for dashboard workspace dependency — awhite0030/tapflow0 reactions · 3 comments
- Aug 30, 2026chore(build): use topological graph for parallel builds — awhite0030/tapflow0 reactions · 3 comments
- Aug 29, 2026perf(relay): caching + compression headers for static assets (serveStatic) — awhite0030/tapflow0 reactions · 3 comments
- Aug 3, 2026Add tapflow to Carbon Efficiency — github/GreenSoftwareDirectory0 reactions
On Hacker News · 2
- Jun 22, 2026A 0.5s periodic hitch in my local screen stream turned out to be macOS AWDL13 points · 9 comments
- Jun 10, 2026Show HN: Tapflow – self-hosted iOS/Android simulator streaming for mobile QA4 points
On Bluesky · 2
- Nov 5, 2026ブラウザでiOSやAndroidのシミュレータを共有できるセルフホストツールっぽい チームでのモバイルQA効率化に使えそう yug1224 starred jo-duchan/tapflow https://github.com/jo-duchan/tapflow — @yug1224.com1 likes and repost
- Jun 10, 2026Show HN: Tapflow – self-hosted iOS/Android simulator streaming for mobile QA I'd love feedback from anyone who's fought the… — @wasm.activitypub.awakari.com.ap.brid.gy0 likes and reposts · 1 reply
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository6 other files it gives its agents.
- docs/AGENTS.mdAGENTS.md
- playground/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- playground/CLAUDE.mdCLAUDE.md
- .work/CLAUDE.mdCLAUDE.md
The file
We split this AGENTS.md into the rules it gives an agent
12 rules. About 0 are common, 8 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 20
Remove friction. Anyone on the team can open a browser and test the app on a real simulator, without Xcode, without device setup, without …
- Very specific: line 31
Don't design for a single entry point.
- Distinctive: line 47
Evidence-based: verify a root cause with code, logs, or tests before fixing — no guess-driven changes.
- Distinctive: line 53
Only create commits or PRs when the user explicitly requests it.
- Very specific: line 54
Do not merge PRs: Always leave merging to the user — even with --admin.
- Distinctive: line 61
Avoid breaking changes: If unavoidable, report to the user and get approval before proceeding.
- Distinctive: line 96
When starting a new task that requires code changes (not when continuing work on an existing branch):
- Very specific: line 236
When changing an interface, update agent-core first, then align implementations.
- Very specific: line 265
Every package whose build tsconfig excludes src/__tests__ has src/__tests__/tsconfig.json, wired into its typecheck script.
- Distinctive: line 296
After running tests (especially repeated or looped runs), always check for zombie vitest processes and kill them:
- Distinctive: line 342
Do not write code that sends app data or streams to external services.
- Distinctive: line 343
Do not proactively add features not on the roadmap.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
tapflow — AGENTS.md (Common Rules)
Package-specific rules are referenced via INDEX.md.
WHAT
tapflow is an open-source self-hosted library that lets the entire team — PO, PM, designers, backend engineers, and QA — test iOS/Android apps directly from a browser. It uses the Mac you already own — no external cloud dependency.
Core value
Remove friction. Anyone on the team can open a browser and test the app on a real simulator, without Xcode, without device setup, without accounts on external services.
Three ways to use tapflow
tapflow Core is the layer everything shares: devices, sessions, UI trees, builds, run records, and resource management. It is a concept, not a package. A team can start from any of the three:
- Manual QA: CI uploads a build → the team tests in the browser. The most mature path.
- AI agents via MCP (experimental): an LLM agent drives a device through
@tapflowio/mcp-server. - Automation (experimental): the flow runner replays YAML flows deterministically, mainly in CI.
Don't design for a single entry point. Every change keeps the hard line in VISION.md: replay calls no model, tapflow saves agent or model output as a flow only after a deterministic replay passes, and Manual QA and the flow runner never depend on an agent or any model.
WHY
- Appetize / BrowserStack are expensive and send app data outside your network.
- Reuses infrastructure (Mac) the team already owns.
- Fully open-source and customizable.
For the product direction and philosophy behind these — start anywhere, replay without a model, Flow Capture as the moat — see VISION.md.
Core Principles
- Evidence-based: verify a root cause with code, logs, or tests before fixing — no guess-driven changes.
- Minimal changes: stay within the requested scope; follow the file's existing conventions. Scope is decided by what a reviewer has to hold in their head, not by which file a line sits in.
- Verifiable goal: know how success will be measured before starting (a reproducing test, same tests passing after a refactor, etc.).
- Stop before risky actions — get user confirmation before any hard-to-reverse operation (
git push --force,git reset --hard, sending messages to external systems, DB drops, etc.). Specifically:- Only create commits or PRs when the user explicitly requests it.
- Do not merge PRs. Always leave merging to the user — even with
--admin. Create the PR and stop. Enforced by the PreToolUse hook.claude/hooks/pr-merge-guard.sh, which blocksgh pr mergeandgh pr reviewin command position. It does not cover thegh apiand git plumbing equivalents: same threat model, a cooperative agent rather than an adversary. - A push to
docs-productionis a production deploy of tapflow.dev. Confirm before every one. The only manual push is a docs hotfix: a commit already onmain, cherry-picked, with its diff checked for pages about unreleased features — procedure in CONTRIBUTING, "Docs follow releases". - Avoid breaking changes. If unavoidable, report to the user and get approval before proceeding. Breaking change scope: public API / interface signature changes, DB schema changes, WebSocket message protocol changes, CLI command / flag changes.
HOW
Language & Stack
- TypeScript throughout. No
any. Node.js ≥ 22. Everything else is in each package'spackage.json.
Branches, Commits & Releases
→ CONTRIBUTING.md
Write GitHub PR and issue titles/bodies in English, and write new code comments in English too. (Conversation and docs follow the existing KO/EN rules.) Code comments default to English so contributors of any language can read and extend them — existing Korean comments stay until the line they sit on is changed.
The PR and issue half is enforced by .claude/hooks/gh-language-gate.sh, which reads the title, the
body, the contents of a --body-file, and the heredoc behind --body-file - — so the form
CONTRIBUTING recommends is covered rather than only the text typed inline. A line counts as Korean
only when its Hangul outnumbers its Latin letters, so an English sentence naming a Korean UI label
passes; #660 shipped one. gh pr comment is deliberately out of scope, because a review comment is a
conversation and the rule is about titles and bodies.
Docs prose is checked before the session ends. Editing any Markdown under docs/ — nested too,
so docs/ko/operate/agents.md counts — and finishing without running
/ai-tells detect is blocked by .claude/hooks/docs-aitells-gate.sh, with
docs-aitells-reminder.sh nudging at the moment of the edit. The check is a lint, not a
laundry: it flags AI-writing tells in prose a human wrote, and rewrite stays manual and
docs-only. Stopping a second time passes, so the block is a prompt rather than a wall.
A skipped accessibility check is followed up before the session ends. The a11y-lens
pre-commit job never blocks on its own infrastructure, so a timeout passes with the same ✔️ as a
clean run (#827). a11y-lens records the files it could not review, and
.claude/hooks/a11y-lens-pending-gate.sh blocks finishing while any are recorded, until
pnpm exec a11y-lens check --pending has run in the session after the latest one was recorded.
Stopping a second time passes.
When starting a new task that requires code changes (not when continuing work on an existing branch):
git checkout main && git pull origin main— start from the latest main.git checkout -b <branch-name>— work on a new branch, never directly on main.
The PR body follows the repo's template, and --body bypasses it
Read .github/pull_request_template.md before opening one. It exists, and 212 of the last 220 PRs did
not follow it because nobody went looking — not because anything stopped them. That is the whole cause, and
it is worth stating plainly, because the mechanism below reads like an excuse and is not one.
The mechanism explains only why nothing said so: the template is applied when a PR is opened in the browser,
and gh pr create --body replaces it wholesale. A PR opened from a terminal carries whatever the author
typed, and no gate looks at it. So write the body into a file that starts from the template, and pass
--body-file.
The body is a pointer, not a copy. Depth already lives in three places that outlast it — the reasoning
beside the code, the release note in the changeset, and the findings in .work/reviews/, which the template
has a slot for. A body that reproduces all three is noise to the reviewer who has the diff open, and it
tells a first-time contributor that this is the bar for opening a PR. Keep ## Summary to a few sentences
and link the rest.
Workflow (Plan → Work → Review → Compound)
Work logs go in .work/. Conventions: .work/CLAUDE.md.
- Plan — define requirements + test cases first (
type: plan). - Work — write tests first, implement until they pass.
- Review — edge cases + real data validation → adversarial review (below) → PR (
type: review). - Compound — extract repeating patterns into test + code + prompt bundles (
type: compound).
Before writing a static check, or a test that asserts something does not happen, read contributing/test-and-guard-coverage.md. A test asserting absence passes when nothing happens — that is its definition — so it cannot tell a working drop from a bug that produces the same silence; and a check whose header cites a lesson usually reads as having applied it. Four measured shapes, each of which shipped a hole behind a fully green suite.
Adversarial Review (required before every code-change PR)
The authoring session inherits its own assumptions, so before creating a PR the diff must be refuted by an independent context that has NOT seen the working conversation. Docs-only PRs may skip the review itself, but still write the record (with the skip reason) — the gate always requires it.
- A blocked hook stops the whole
Bashcommand, not the part it objected to. Agh pr createwritten in the same command as the heredoc that built its--body-fileleaves no body file behind when the gate refuses: the file write never ran either. Write the file in one command and publish in the next. Same forgh issue createand the comment gates. - The gate resolves your work tree from the session's cwd, which this harness resets to the project root after every command — so a PR cannot be created from a worktree even though the hook supports one. The way through is to bring the branch to the main checkout: stash what is there, remove the worktree, check the branch out, create the PR, then restore.
- Record: findings + dispositions (fixed, or skipped with a reason) go in
.work/reviews/<branch>.md(slashes →__), including the full 40-character HEAD hash (git rev-parse HEAD— an abbreviated hash will not pass the gate). Mention the review in the PR body. - Enforcement: the PreToolUse hook
.claude/hooks/adversarial-review-gate.shblocks PR creation unless that record exists and references the current HEAD — any commit after the review invalidates the record until it is refreshed against the new diff. - A change spanning two or more packages, or both platforms, needs a second, earlier review — of the design, before the code. One adversarial pass over the plan, in addition to the pre-PR one.
Read contributing/adversarial-review.md before launching a reviewer. It covers how to design the channels, how to run the cross-cutting design pass, why a reviewer's "checked and cleared" list expires as soon as you fix the findings, why the reason written beside the code is part of the fix rather than documentation of it, and how to keep the cost in minutes rather than hours — the same review can take 4 minutes or 106 depending only on what its prompt makes it execute.
An adjacent defect is fixed here unless it needs its own decision
A good review produces findings next to the one you came for. Deferring each of them is the default
that feels safe and is not: one four-issue session deferred six, and three of those were a single
line each — a format? that should have been required, a package missing from changeset's
ignore, one sentence in an AGENTS.md. Each cost a branch, a review, a PR and a CI run to land
later. The overhead was an order of magnitude larger than the fix.
The line is what a reviewer has to hold in their head, not which file a line sits in:
| Fix it in this PR | Split it out |
| Under ~10 lines and judged by the lens already running | It needs a design decision |
| The other half of the same defect in another package, when the running lens can judge both sides | It needs a different lens (perf, security, a11y) |
| Prose the change just made false | It is hard to reverse, or breaking |
Two habits keep this honest:
- Ask the reviewer to classify. A findings list with no now/later column leaves the split to whoever is holding the diff, which is the person least able to see the cost of deferring.
- What is split out becomes an issue in the same session, not a line in
.work/reviews/. That directory is gitignored and nobody reads it looking for work. Six deferrals lived only in one conversation until someone asked what was left. The reviewer prompt asks for the issue title and body alongside the reason, so writing "later" costs the same as filing it.
Splitting is still right when it is right — #508's relay type drift genuinely belongs elsewhere.
The rule is that it must be a decision with a reason, and the reason may not be "it was in a
different file".
A reviewer's later is an input, not a verdict — and deferring has a budget
The rule above was written for a session choosing its own deferrals. Once the findings arrive from
review channels, the choosing is quietly delegated: a reviewer writes later, and it becomes an
issue because that is what the column said.
Measured on one day of work on #607: nine issues, from three PRs. That is not bad luck, it is
arithmetic — two or three channels per PR, a cap of four or five findings each, and a later rate of
about a third. Two of the nine were closed within the hour as things that should never have been
filed, and one of them (#673, a missing timeout) was a few lines in a file the running lens was
already reading, which the section above says is fixed in place.
Three things keep it honest, and none of them is remembering harder:
- Re-grade every
lateryourself. Severity was already being re-graded; disposition was not. Hold each one against the same two questions: under ~10 lines, and judgable by the lens already running? Then it is fixed here, whatever the column says. - Give the reviewer a
laterbudget — at most two per channel, each justified against that rule. A cap on findings with no cap on deferrals makes deferring free, and free is what it was. - Every split-out issue names its parent, on a line of its own:
Parent: #607. Prose like "raised by the review of #647" is not it — nothing can build a checklist from a mention, which is how the nine above became unreachable from the issue they all came from..claude/hooks/issue-parent-gate.shblocks an issue that carries neither aParent:line nor an explicit<!-- standalone: reason -->.
Re-grading is a step that leaves no trace, and steps like that get skipped — one session later,
three issues filed and all three closed again (#754, #755, #756). Not one needed to exist. Nothing was
missing: each reviewer had supplied its reasoning, and each was re-graded correctly in well under a
minute once somebody asked. The bullet above was already in this file and was read; the step just
never happened, the way any step with no artifact does not happen. In the same session mutation
testing was run every single time, because run-tests.sh --mutate prints a line per mutation and
fails loudly.
A candidate answer is not more emphasis on that bullet — writing a lesson down reads as having applied it (test-and-guard-coverage.md rule 1). It is to ask the reviewer for facts instead of a verdict: a reaching path (a concrete consumer or sequence, or "none found"), the fix in lines, and which lens it needs. No reaching path is not an issue, it is a comment beside the code. The three fields are defined in adversarial-review.md.
Nothing above changes yet. The now/later column and the budget stay exactly as the three
bullets describe them, and a reviewer prompt should still ask for them — that shape has nine measured
issues behind it and this one has three, from one author in one area. It is written down because the
count is worth knowing, not because it has earned a replacement. What decides it is the next few
reviews: fewer issues that close within the hour, or the same rate with a longer prompt.
And the parent keeps a checklist. Asked whether #607 was finished, nobody could answer — the feature's remaining surface existed only as unlinked rows in a tracker sorted by date.
Design Principles (SOLID — priority subset)
- OCP: New platforms and features are added without modifying existing code — platforms register via
AgentRegistry.register()only; relay and dashboard code stay unchanged. - ISP:
DeviceAgentonly contains methods every platform can implement. Platform-specific behavior goes in separate interfaces. - DIP: Dependencies via constructor injection. Depend on interfaces, not implementations.
Code Rules
- Comments only when the WHY is non-obvious. Write new comments in English; leave existing Korean comments unless you're already editing that line.
- When changing an interface, update
agent-corefirst, then align implementations.
A package whose tests import a sibling extends vitest.shared.ts
Cross-package imports resolve through exports to dist/, so without it a test in one package
exercises whatever was last built of another. #459 shipped a regression behind a green 1889-test
run for exactly that reason: ios-agent stands up a real RelayServer, and the relay it stood up
was the previous one. It surfaced only when the pre-commit tsc -b refreshed the build.
ssr.resolve, not resolve — vitest runs in node and takes the SSR resolution path. Measured:
resolve.conditions, NODE_OPTIONS=--conditions=source and server.deps.inline all still loaded
dist. And source is prepended to vite's defaults rather than replacing them; a replacement
list applies to every dependency, and dropping node from it sent jsdom to the wrong entry of
decimal.js.
Adding a package that imports a sibling in its tests means adding the config too.
scripts/__tests__/testsReadSource.test.mjs finds those packages by inspection and fails if one is
missing it — and separately plants a marker in a built artifact to prove the resolution actually
lands on source, because a config can be present and not work.
Not solved by pointing the manifests at source with publishConfig, which needs no per-tool
config at all and was tried first. pnpm deploy does not apply publishConfig
(pnpm#6693, open), so the Docker image would ship
node_modules full of .ts and die on boot — and packages/cli/bin/tapflow.js is plain node too.
Covering every consumer uniformly means covering the ones that cannot read TypeScript. Opting in
per tool is the point, not the cost.
Tests are type-checked and linted, and the tsconfig lives in the test folder
Every package whose build tsconfig excludes src/__tests__ has src/__tests__/tsconfig.json,
wired into its typecheck script. Tests still must not reach dist, so they are not added to the
build's include — the same shape as protocol/tsconfig.assertions.json. dashboard is the
exception and needs no second file: its tsconfig already includes src, so its tests were type-checked
all along and only the lint half was off.
scripts/__tests__/testTreeChecked.test.mjs holds that by inspection — a new package that copies the
exclude and forgets the tsconfig would otherwise put its whole test tree outside every project with
pnpm typecheck still green, which is the failure #422 exists to end.
The file name and location are load-bearing. typescript-eslint's projectService resolves a
file's project the way tsserver does, by walking up for a tsconfig.json, so a tsconfig.test.json
at the package root is invisible to it and every rule fails as was not found by the project service
rather than reporting. One file serves both gates only in this position.
moduleResolution: bundler, not the build's Node16: vitest resolves through vite and does not
require the .js suffix. Under Node16 a further 166 errors were nothing but that suffix missing, on
top of the 247 real ones — checking tests under a resolution they never run with would mean rewriting
every import to satisfy a compiler no test obeys.
The two exclusions used to compound: nothing typed the test tree and nothing linted it, so a double
could drift from the interface it doubled with both gates green. What that hid, measured when the
gates went on: AgentRegistry.test.ts declared implements DeviceAgent while missing two members,
EmulatorVideo.test.ts was short three on RawEmulatorController, five duplicate object keys sat in
SimctlWrapper.test.ts, a clearAppData call passed one argument to a two-argument method, and
test-utils' waitForType constraint was one no named message could satisfy — which all 49 call
sites naming a type violated.
Test Hygiene
Tests run through pnpm --filter <pkg> test; use pnpm test:scripts for the root scripts suite. Never use npx vitest — not even from inside the package directory, and not for a single file. npm rewrites the root package.json on its way through and collapses pnpm.overrides to pnpm: {}, leaving pnpm-lock.yaml rewritten beside it, and it reports none of that. Reviewing #474 cost exactly this: one npx vitest on one test file, and the entire override block was gone with only git status to say so. git checkout HEAD -- package.json pnpm-lock.yaml puts both back — check git diff on them first if you were editing either on purpose, since that discards everything uncommitted in both.
After running tests (especially repeated or looped runs), always check for zombie vitest processes and kill them:
ps aux | grep vitest | grep -v grep
pkill -f "vitest"Zombie worker processes accumulate silently from pnpm test loops and consume memory. Kill them before starting new test runs.
Dev Server Hygiene
Same rule, different processes. Anything you start with pnpm dev you stop before the session ends:
pnpm dev:down # stops relay / agents / vite for THIS checkoutpnpm dev refuses to start when :4000 or :3001 is already held, and names the pid — because the failure it produces otherwise mentions neither. A relay left running once survived a day and cost a debugging session: it failed with EADDRINUSE, concurrently SIGTERMed the dashboard and both agents, and the visible symptom was four processes dying for no stated reason.
concurrently -k cleans up on a normal exit, not when the terminal goes away or the machine sleeps.
Changesets
A PR that changes published source needs a changeset. The CI changeset job fails without one, and it is a required status check on the protect-main ruleset, so that failure blocks the merge. The same job also demands an entry in the root CHANGELOG.md under ## [Unreleased], in one of the sections CONTRIBUTING lists — the per-package changelogs are generated and cannot be forgotten, the root one is hand-written and is what a self-hoster reads to decide whether to upgrade. A changeset with no changelog entry fails the job, which reads as "no changeset" and sends you looking in the wrong place; #733 lost a CI cycle to exactly that. Opt out only for a change nobody can observe, with <!-- changelog: internal — reason --> in the changeset. Note that the job is skipped for bot PRs, and a skipped required check counts as passing — the gate is deliberately not applied to them. Opt out only by writing the reason in the PR body, on its own line:
<!-- no-changeset: comment-only follow-up to #123 -->A dashboard change names @tapflowio/relay, never @tapflowio/dashboard. The dashboard is ignored in .changeset/config.json because it is private — but it is built into the relay's public/ and ships inside that package, so that is where its release note belongs. Naming both in one changeset is rejected outright by changeset version, and nothing catches it until release day: the CI gate only checks that a changeset exists.
pnpm changeset:check runs the same check locally, against committed work. A changeset written for an EARLIER PR must say so — Backfills: #413 on its own line — or the audit keeps reporting that merge for the rest of the cycle. That gate cannot see anything already on main, so /release audits the merges too (pnpm changeset:audit) — four merged PRs once got as far as release preparation with no changelog entry between them, and only the audit would have caught it.
A security bump is pnpm update first, and an override only if that fails
Reach for pnpm.overrides last: try pnpm update <pkg> first, which usually takes the patch with no
permanent entry to maintain, because the problem is normally a stale lockfile rather than a
forbidding range. The block is empty, and that is the finished state rather than a gap: it held
fourteen entries on 2026-08-06 and eight on 2026-09-03, and on both dates every one of them was
inert. They were retired once the audit could say so — deleting them and resolving cold moved no
resolved version, and every package they named was already at or above its advisory floor.
pnpm overrides:audit judges whatever is there — still needed, correct, reaching production — so run
it before adding the first one back, and when one is added, plan to remove it. Derive the
override key from the GHSA advisory, never from the Dependabot alert, and scope both the key and
its replacement to one major line.
Read contributing/security-bumps.md before adding an entry. It has the why behind each of those — how to page the full advisory set, and the two PRs (#469, #471) that shipped a key leaving the current major unguarded.
HOW NOT
- Do not write code that sends app data or streams to external services.
- Do not proactively add features not on the roadmap.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

