Authored by T1 (research) for the autonomous convergence loop. Maps each of the 49 confirmed findings in the v0.2.0 post-ship adversarial dossier to its file:line target, its Evidence reproduction command, the cluster it belongs to, and the code-vs-OPS split. The coordinator dispatches each cluster (C1..C16) as its own remediation task using this plan.
Sources. Recommendations are paraphrased from the raw dossier
(.frontguard-audit/adversarial-v020-postship.md, gitignored — it quotes the
live session secret and a forged-cookie example verbatim). The
public/redacted dossier on main (docs/adversarial-v020-postship.md) is the
model for safe quotation. The finding-ID → cluster mapping is pinned in
docs/fix-progress.md (the coordinator ledger). Policies are in
docs/DECISIONS.md.
Secret-handling rule (applies to every cluster). This plan references
findings by ID and public file:line only. No literal DEV_SESSION_SECRET
value, no forged-cookie string, no API key, token, or .env content appears
anywhere in this document or in any commit/PR produced from it. Workers run the
DECISIONS.md secret-scan grep on the staged diff before every commit and every
push; any hit on an added line (diff lines beginning +, excluding +++
headers) blocks the commit.
49 findings. Floor + ceiling. Out of scope: 15 coverage gaps
(gap-1…gap-15 — "we did not look there," not findings) and 112 refuted
candidate findings (did not survive two-lens — code-reality + customer-impact —
verification). The remediation closes exactly the 49; it neither drops a
confirmed finding nor promotes a refuted candidate or a coverage gap.
Severity split: 22 P0 / 15 P1 / 12 P2. Cluster split: 4 foundation (C1, C2, C3, C7) and 12 independent (C4–C6, C8–C16).
| Cluster | Findings | n | Foundation? |
|---|---|---|---|
| C1 ts-config-loader | install-1, sb-2 | 2 | ✅ |
| C2 bin/package-name | docs-1, ci-3, install-7, docs-10 | 4 | ✅ |
| C3 hosts/DNS code-side | claim-4, dist-3, docs-2, install-6, claim-6, install-9 | 6 | ✅ |
| C7 cloud-api data model | cloud-1, cloud-9, mcp-1, mcp-2, mcp-7, mcp-9 | 6 | ✅ |
| C4 docker-render | install-4, docker-1, docker-3, docs-3 | 4 | — |
| C5 session-secret | sec-1, cloud-4 | 2 | — |
| C6 ssrf-guard | sec-2, int-7 | 2 | — |
| C8 github-action-ref | int-3, docs-5, docs-6 | 3 | — |
| C9 slack-result-shape | int-1 | 1 | — |
| C10 storybook | sb-1, sb-3 | 2 | — |
| C11 supply-chain | supply-2, supply-6, install-13 | 3 | — |
| C12 mcp-correctness | mcp-3, mcp-6, mcp-8, mcp-10 | 4 | — |
| C13 init-gitignore | install-2 | 1 | — |
| C14 marketing-claims | claim-5, claim-7, claim-9, dist-11 | 4 | — |
| C15 docs-hygiene-residual | docs-4, docs-7, docs-8, docs-9 | 4 | — |
| C16 validation-methodology | val-5 | 1 | — |
| Total | 49 |
Closure vocabulary (matches docs/fix-progress.md legend): CLOSED —
evidence no longer reproduces from code alone. CODE_CLOSED (OPS: <action>) —
the code-side mitigation ships and the evidence no longer reproduces against the
shipped artifact, but full real-world closure additionally requires an
out-of-band OPS action the loop must not execute (DNS attach, image publish, npm
shim/republish, marketplace listing, secret rotation, deploy, tag push). Every
finding below states which terminal status it can reach from code alone.
Reproduction-command note: several dossier repros invoke the
frontguardbin vianpx frontguard …. That exact form is itself broken (docs-1: the npm package is@frontguard/cli, the bin isfrontguard). To reproduce a downstream finding you must use the working invocationnpx -y -p @frontguard/cli frontguard <cmd>(or run the local buildnode packages/cli/dist/cli/index.js <cmd>). Where a repro is shown with the barenpx frontguardform it reproduces two findings at once (the 404 of docs-1 plus the downstream behavior); the-pform isolates the downstream one.
Findings: install-1 (P0), sb-2 (P0).
Why foundation: both findings have one root cause — the CLI config loader
cannot read a TypeScript config. Fixing the loader (and/or the init default
format) unblocks the documented Quick Start commands #2/#3 that almost every
other manual repro in this plan depends on.
Shared-file collision: packages/cli/src/cli/init.ts is also edited by
C13 (install-2 changes the .gitignore entry list at init.ts:247; install-1
changes the default format at init.ts:158). Not in the directive's
collision list — flagged here. Serialise C1 and C13 on init.ts, or have
one worktree own both edits. C1 also touches integrations/docs only via the
doc caveat in sb-2's recommendation (storybook.mdx fixture command) — see the
storybook.mdx collision under C10/C15.
install-1 — frontguard init writes a .ts config that doctor/loadConfig cannot read
- Recommendation (paraphrased): either default
initto--format=jsso the generated config loads under a plain Nodeimport(), or register the bundledtsxESM loader (register()fromtsx/esm/api) at CLI startup so.tsconfigs actually load. Whichever path is taken,doctorandloadConfigmust agree. Thetsxdependency currently sits independenciesunused. - Evidence repro: in a fresh dir —
npm install @frontguard/cli(0.2.0) →npx -y -p @frontguard/cli frontguard init(writesfrontguard.config.ts) →npx -y -p @frontguard/cli frontguard doctor→ prints❌ Configuration: config file is invalid: Unknown file extension ".ts". - file:line targets:
packages/cli/src/cli/init.ts:158(defaultsformatto'ts');packages/cli/src/core/config.ts:337-341(bareawait import(fileUrl), no TS hook); published bindist/cli/index.js(plain#!/usr/bin/env node, no--import tsx);tsx@^4.21.0inpackages/cli/package.jsondeps. - Code vs OPS: CODE only → CLOSED. No OPS.
sb-2 — documented fixture command frontguard run --config ./frontguard.config.ts fails
- Recommendation (paraphrased): same root cause as install-1 — either bundle/
register a
.tsloader, or rewrite the fixture + storybook doc to use a.mjs/.js/JSON config with an accompanying.d.ts. Stop shipping.tsexamples until the loader is wired. The fix to install-1 resolves sb-2's code path; the doc/fixture edit closes the documented-command side. - Evidence repro:
node packages/cli/dist/cli/index.js run --config packages/cli/__fixtures__/storybook/frontguard.config.ts --update-baselines→Unknown file extension ".ts". (Switching to an equivalent JSON config succeeds.) - file:line targets:
packages/cli/src/core/config.ts:337-341;apps/docs/content/docs/integrations/storybook.mdx:495(the documented.tscommand);packages/cli/__fixtures__/storybook/README + config. - Code vs OPS: CODE only → CLOSED. No OPS. (storybook.mdx edit coordinates with C10/C15 — see collision note.)
Findings: docs-1 (P0), ci-3 (P1), install-7 (P1), docs-10 (P2).
Why foundation: the canonical decision "how does a user invoke the CLI"
(publish a frontguard shim package vs. teach npx -p @frontguard/cli frontguard … everywhere) sets the wording of every getting-started snippet and
every repro command in this plan. It must be frozen before the doc-heavy
clusters (C3/C8/C14/C15) rewrite their command examples.
Shared-file collisions (docs set — directive: C2, C3, C8, C14, C15): C2
owns the getting-started surface — apps/docs/content/docs/index.mdx,
quick-start.mdx, installation.mdx — plus packages/cli/README.md (the
npm-bundled README) and scripts/build-daytona-snapshot.ts. It does not
touch the root README.md (that's C3/C14) or the integration mdx pages (that's
C3/C8). Partition by file → C2 can run alongside C3.
docs-1 — every npx frontguard … snippet invokes a non-existent npm package
- Recommendation (paraphrased): either (a) reserve and publish a thin
frontguardpackage that re-exports@frontguard/clisonpx frontguard …resolves, or (b) rewrite every getting-started snippet tonpx -p @frontguard/cli frontguard <cmd>and add an explicitnpm i -D @frontguard/clistep before anynpx. Option (b) is the code-side mitigation that ships now; option (a) is the lower-doc-churn path but requires an OPS publish. - Evidence repro:
npm view frontguard→404 Not Found;npx -y @frontguard/cli→npm error could not determine executable to run(package name ≠ bin name); the working form isnpx -y -p @frontguard/cli frontguard --version. - file:line targets:
apps/docs/content/docs/index.mdx:28-37;apps/docs/content/docs/quick-start.mdx:21,66,95,124;apps/docs/content/docs/installation.mdx:46. - Code vs OPS: CODE → CLOSED via option (b) (rewrite snippets to the
-pform). If option (a) is preferred, CODE_CLOSED (OPS: publishfrontguardnpm shim package).
ci-3 — Daytona snapshot script installs frontguard@latest (404 package)
- Recommendation (paraphrased): change the install line to
npm install -g @frontguard/cli@<VERSION>, readingVERSIONfrom the repo rather thanlatest. Add a CI/release.shpost-publish lane that runs the Daytona script with--dry-runso this typo surfaces. The script's own comments already say "NOTfrontguard@latest". - Evidence repro:
npm view frontguard@latest→E404 Not Found … /frontguard - Not found; inspectscripts/build-daytona-snapshot.ts:260(npm install -g frontguard@latest), contradicted by the comment at:13and:139. - file:line targets:
scripts/build-daytona-snapshot.ts:260(fix);:13,:139(contradicting comments). - Code vs OPS: CODE → CLOSED (the line fix). Note: actually building and publishing the Daytona snapshot is a separate OPS action (see OPS list / C7 verification), but ci-3 itself is closed by the source fix.
install-7 — npm-bundled README is stale (v0.1.x: "Phase 2", 27 files, 142KB, 395 tests, 3 plugins)
- Recommendation (paraphrased): make
packages/cli/README.mdgenerated from or kept in sync with the repo-root README, or at minimum refresh the bundled README for 0.2.0. Add aprepublishOnlyguard that fails if the bundled README's version-string drifts frompackage.json. - Evidence repro: compare
node_modules/@frontguard/cli/README.md(in a freshnpm install @frontguard/cli) lines 6/12/23/41-49 against the repo-rootREADME.mdlines 7/13/51/201-211 — the bundled file is the older v0.1.x README. - file:line targets:
packages/cli/README.md(whole file); addprepublishOnlytopackages/cli/package.json. - Code vs OPS: CODE_CLOSED (OPS: re-publish
@frontguard/clito npm). The source/guard fix ships now; the npm landing page only refreshes on the next publish.
docs-10 — installation.mdx claims frontguard --version prints "frontguard"; it prints the SemVer
- Recommendation (paraphrased): replace the expected-output block with
0.2.0(or0.2.0with a note that the exact version tracks what's installed). Also correct the surrounding invocation to the-p @frontguard/cliform per docs-1. - Evidence repro:
npx -y -p @frontguard/cli frontguard --version→0.2.0(docs say expect the literal wordfrontguard). - file:line targets:
apps/docs/content/docs/installation.mdx:130,132-134. - Code vs OPS: CODE only → CLOSED.
Findings: claim-4 (P0), dist-3 (P0), docs-2 (P0), install-6 (P1),
claim-6 (P1), install-9 (P2).
Why foundation: every finding here is a dead-host symptom
(app./api./github-app./telemetry.frontguard.dev are NXDOMAIN;
frontguard.dev/docs/* 404s). The canonical decision — make the API URL a
required input with a self-host fallback and strip dead defaults, vs. wait for
DNS — sets the code-side wording across the landing page, the root README, the
integration docs, and the CLI telemetry default. Frozen first so dependents
inherit it.
Shared-file collisions:
README.mdlines ~74 and ~101-104 — C3 (install-6/claim-6 doc links at:74/:104) overlaps C14 (claim-7 at:102, claim-9 at:101) in the comparison-table region. Directive-listed (docs set). Serialise C3↔C14 onREADME.md.- Integration mdx (
netlify/mcp/slack/vercel/cloud-api) — docs-2 edits theFRONTGUARD_API_URLdefaults;slack.mdx/vercel.mdx/mcp.mdxare also touched by C8 (docs-6) and C12/C7 (mcp.mdx). Partition by line/section. apps/landing/**— claim-4 editspricing.tsx; C14 (claim-5/dist-11) also editsapps/landing/**. Different files (pricing.tsxroute data vsindex.html), low risk.packages/clitelemetry (install-9) — isolated.
claim-4 — pricing "Start 14-day trial" CTA points at app.frontguard.dev (NXDOMAIN)
- Recommendation (paraphrased): either attach DNS + deploy the dashboard before the Pro tier is visible, or point the CTA at a real waitlist (Tally/mailto) and badge the Pro tier "coming soon". Code-side mitigation is the CTA repoint + badge; the live signup needs DNS+deploy.
- Evidence repro:
curl -v https://app.frontguard.dev/signup --max-time 8→Could not resolve host: app.frontguard.dev(same forapi.frontguard.dev). - file:line targets:
apps/landing/src/routes/pricing.tsx:65(CTA href);apps/landing/src/test/pricing.test.tsx:44(asserts the broken URL — update the assertion). - Code vs OPS: CODE_CLOSED (OPS: attach
app.frontguard.devDNS + deploy dashboard). Repointing the CTA to a live waitlist closes it outright; the full $29 trial flow needs the OPS deploy.
dist-3 — api.frontguard.dev NXDOMAIN; every shipped integration defaults to the dead host
- Recommendation (paraphrased): either point
api.frontguard.devDNS at the Worker, or makeFRONTGUARD_API_URLa required input with a self-host fallback and remove the false default values from the integrations. Add a CI guard inscripts/release.shthat resolves every referenced subdomain before allowing publish. - Evidence repro:
host api.frontguard.dev→NXDOMAIN;host github-app.frontguard.dev→NXDOMAIN;host app.frontguard.dev→NXDOMAIN. - file:line targets: integration defaults —
integrations/github-app/manifest.yml:11(hook_attributes.url); Netlify plugin manifest/README defaultapiUrl; MCP server defaultFRONTGUARD_API_URL; CLI/integration forwarders. - Code vs OPS: CODE_CLOSED (OPS: attach
api.frontguard.dev+github-app.frontguard.devDNS + deploy Worker). Removing the dead defaults / requiring the URL is the code mitigation.
docs-2 — api.frontguard.dev hard-coded throughout docs but does not resolve
- Recommendation (paraphrased): provision DNS, or rewrite every integration
page so
FRONTGUARD_API_URLis a required input with a self-host fallback and the false default values are removed. - Evidence repro:
nslookup api.frontguard.dev→NXDOMAIN. - file:line targets:
apps/docs/content/docs/integrations/netlify.mdx:62,88;mcp.mdx:290;slack.mdx:201;vercel.mdx:67;cloud-api.mdx:40,76. - Code vs OPS: CODE_CLOSED (OPS: attach
api.frontguard.devDNS). Doc rewrite ships now.
install-6 — README documentation links 404 (frontguard.dev/docs/* vs docs.frontguard.dev)
- Recommendation (paraphrased): rewrite the README doc links to the
docs.frontguard.devsubdomain, or add a301redirect forfrontguard.dev/docs/*→docs.frontguard.dev/docs/*on the marketing site. - Evidence repro:
curl -s -o /dev/null -w '%{http_code}' 'https://frontguard.dev/docs/guides/migrate-from-backstopjs'→404; same path underdocs.frontguard.dev/docs/…→200. - file:line targets:
README.md:104(five links),README.md:74. - Code vs OPS: CODE → CLOSED (rewrite to
docs.frontguard.dev). A redirect rule is an alternative OPS path; the README rewrite closes it without OPS. - Coordination: C15/docs-8 may move
frontguard-vs-percy.mdxandfrontguard-vs-chromatic.mdxintocomparisons/, changing their URLs — the README links rewritten here must target the final paths. Sequence C15 before the README rewrite, or agree the destination paths up front.
claim-6 — README comparison-table footnote: five migration/comparison links 404
- Recommendation (paraphrased): configure
frontguard.dev/docs/*to rewrite/proxy todocs.frontguard.dev/docs/*, or editREADME.md:104and:74to use thedocs.frontguard.devhost. Note one of the five (frontguard-vs-argos) also 404s on the docs subdomain (content not yet deployed) — that page's deploy is OPS. - Evidence repro: identical to install-6
(
curl … 'https://frontguard.dev/docs/guides/migrate-from-backstopjs'→404;docs.frontguard.dev/docs/…→200). - file:line targets:
README.md:104,README.md:74(same region as install-6 — fix once, closes both). - Code vs OPS: CODE → CLOSED for the link rewrite; OPS: deploy the
frontguard-vs-argospage content for the one path that 404s even on the docs subdomain.
install-9 — telemetry endpoint telemetry.frontguard.dev NXDOMAIN; undisclosed default-on telemetry
- Recommendation (paraphrased): stand up the telemetry endpoint, or disable
telemetry by default, or remove the telemetry code; and add a one-line
disclosure to the README and to
frontguard init's next-steps output naming--no-telemetry/FRONTGUARD_TELEMETRY=0.showFirstRunNoticeexists but useslogger.debug(invisible) and is never called. - Evidence repro:
nslookup telemetry.frontguard.dev→NXDOMAIN. - file:line targets: CLI telemetry default endpoint +
SEND_TIMEOUT_MS(packages/cli/src/.../telemetry*, surfaced indist/cli/index.jsasDEFAULT_ENDPOINT/SEND_TIMEOUT_MS=1500);README.md(no telemetry section);initnext-steps output; the unusedshowFirstRunNotice. - Code vs OPS: CODE → CLOSED if the chosen path is disable-by-default + disclosure. CODE_CLOSED (OPS: stand up telemetry endpoint) only if the team chooses to keep telemetry on and host it.
Findings: cloud-1 (P0), cloud-9 (P1), mcp-1 (P1), mcp-2 (P1), mcp-7 (P1), mcp-9 (P2). Why foundation + internal serialisation: these six findings all reshape the cloud-api run/result data model and its persistence, then the MCP layer reads the reshaped data. cloud-1 lands first — it adds the R2→sandbox baseline restore and, with it, the run/result shape that actually carries regression status out of the sandbox. The remaining five depend on that foundation and on each other through shared files, so they serialise behind cloud-1: cloud-1 → {cloud-9, mcp-1, mcp-2, mcp-7, mcp-9}. Shared-file collisions (directive):
packages/cloud-api/src/**— collides with C5 and C6. C7 touchesdaytona-runner.ts,processor.ts,types.ts,db/schema.ts,db/d1-store.ts,report-html.ts,otel/index.ts, andindex.ts(listing region:377-381); C5 touchesauth/session.ts+index.tsboot; C6 touchesindex.ts:54. Serialise all cloud-api edits (see §3).packages/mcp/**— collides with C12. C7's mcp findings touchclient/cloud.ts,tools/list-regressions.ts(mcp-1 filter:70-77),tools/recent-runs.ts(:71-78),tools/get-suggested-fix.ts; C12 touchesindex.ts,tools/accept-baseline.ts,tools/list-regressions.ts(:64),tools/recent-runs.ts(:62).list-regressions.tsandrecent-runs.tsare touched by both — serialise C7-mcp → C12.- Internal to C7:
processor.ts:53-60is touched by cloud-1, mcp-2, and mcp-9 — they must serialise within the cluster (the order below respects this).
cloud-1 — Worker can never detect regressions (Daytona sandbox ephemeral; prior baselines never restored)
- Recommendation (paraphrased): before invoking
frontguard runin the sandbox, the runner must (1) list R2 objects under the user's baseline prefix (<userId>/<projectId-or-run-key>/baseline/*), (2) download them into the sandbox baseline directory, and (3) only then exec. Without this the cloud product is a screenshot service, not a regression detector. - Evidence repro: code inspection —
grep -n "uploadFile\|baseline" packages/cloud-api/src/daytona-runner.tsshows the only upload isfrontguard.config.json; no R2-restore path exists. Behavioral repro requires a live/emulated sandbox (see §5 / risk register stub). - file:line targets:
packages/cloud-api/src/daytona-runner.ts:64-77(ephemeral create),:102-148(only uploads config at:135);packages/cloud-api/src/processor.ts:84-93(no-Daytona branch returnsnew_baseline);packages/cloud-api/src/storage/screenshots.ts:49-59(R2 keys to read back). - Code vs OPS: CODE → CLOSED (the restore path is pure code). Full behavioral verification depends on a reachable Daytona sandbox → see the risk register's stub plan; the marketed live cloud run additionally needs the Worker deployed + Daytona snapshot published (OPS), but cloud-1 the defect is closed by the restore code.
cloud-9 — report HTML footer still hardcoded "Frontguard v0.1.0"
- Recommendation (paraphrased): wire
PACKAGE_VERSIONthrough toreport-html.tsandotel/index.tsso all version strings trackpackage.json. Add a test that greps\d+\.\d+\.\d+acrosssrc/and fails if any literal differs frompkg.version. - Evidence repro:
grep -n "0.1.0" packages/cloud-api/src/report-html.ts→ hit at:201;grep -n "0.2.0" packages/cloud-api/src/otel/index.ts→ hit at:122(a second hard-coded literal). - file:line targets:
packages/cloud-api/src/report-html.ts:201;packages/cloud-api/src/otel/index.ts:122;packages/cloud-api/src/index.ts:28-32(the comment citing the prior drift). - Code vs OPS: CODE only → CLOSED. (Independent of cloud-1's data model — serialised only because it shares the cloud-api worktree.)
mcp-1 — D1 store drops run.github; list_regressions(pr_id) never matches in production
- Recommendation (paraphrased): add
github_owner/github_repo/github_pr_number/github_commit_shacolumns to the runs table (or fold them into the existing config JSON blob), persist them increateRun(), reconstruct them inrowToRun(), and add a D1 integration test that round-trips a run with GitHub linkage. The same gap affectsrecent-runs. - Evidence repro:
grep -rnE 'github' packages/cloud-api/src/db/d1-store.ts→ zero matches (the in-memory store keeps it via shallow copy, so tests pass while D1 drops it). - file:line targets:
packages/cloud-api/src/db/schema.ts:8(no github_* columns);packages/cloud-api/src/db/d1-store.ts:64-74(runConfig omits github),:77-97(rowToRun never sets.github);packages/mcp/src/tools/list-regressions.ts:70-77(filter);packages/mcp/src/tools/recent-runs.ts:71-78. - Code vs OPS: CODE → CLOSED. No OPS. (Depends on cloud-1's persisted-run shape.)
mcp-2 — get_suggested_fix is structurally null; cloud-api never emits suggestedFix
- Recommendation (paraphrased): plumb
suggestedFixfrom the CLI report throughprocessor.tsinto the cloud-apiRun.results, add it to the wire schema and D1 storage (results JSON blob), and add an e2e test where an AI-flagged regression survives a round-trip throughGET /v1/runs/:id. The alternative is to pull thesuggestedFixclaim from the MCP docs and tool description. - Evidence repro:
grep -rnE 'suggestedFix' packages/cloud-api/src→ zero matches (the CLI produces it atpackages/cli/src/diff/ai-vision.ts:531, but it never crosses the cloud-api boundary). - file:line targets:
packages/cloud-api/src/types.ts:30-37(RunResult has no field);packages/cloud-api/src/processor.ts:53-60(object-literal whitelist strips it);packages/cloud-api/src/daytona-runner.ts:27-42; MCP doc claim atapps/docs/content/docs/integrations/mcp.mdx:217-228. - Code vs OPS: CODE → CLOSED. No OPS. (Shares
processor.ts:53-60with cloud-1 and mcp-9 → serialise.) mcp.mdx edit coordinates with C3/C12.
mcp-7 — listRuns is per-user, not per-team; agent keys see only the key owner's runs
- Recommendation (paraphrased): make listing team-aware in the cloud-api —
return runs from any team the user is a member of, or accept
?teamId=likePOST /v1/runalready does — and add a team-scope smoke test inpackages/mcp/testthat exercises the cross-user-key case. - Evidence repro: code inspection —
POST /v1/runenforces team membership butGET /v1/runsfilters strictly byuser_id(grep -n "user_id" packages/cloud-api/src/db/d1-store.ts→:186-191). - file:line targets:
packages/cloud-api/src/index.ts:377-381;packages/cloud-api/src/db/d1-store.ts:186-191;packages/mcp/src/client/cloud.ts:106-108. - Code vs OPS: CODE → CLOSED. No OPS. (Touches
index.tslisting region — serialise vs C5/C6 cloud-api edits.)
mcp-9 — diff_id loses the browser dimension; multi-browser runs collapse
- Recommendation (paraphrased): encode browser into the diff id as
${runId}:${route}:${viewport}:${browser}, add abrowserfield toCloudRunResult, updateparseDiffIdto handle four segments (carefully — routes can contain colons), and add a test for a two-browser sample run. Requires the browser field to surviveprocessor.ts(which currently strips it). - Evidence repro: code inspection —
diffIdForis route+viewport only (packages/mcp/src/client/cloud.ts:151-153); the sandbox result includesbrowser(daytona-runner.ts:28-37) but the MCPCloudRunResultinterface (cloud.ts:21-31) has no browser field. - file:line targets:
packages/mcp/src/client/cloud.ts:151-153,:21-31;packages/mcp/src/tools/get-suggested-fix.ts:51-53;packages/cloud-api/src/daytona-runner.ts:28-37;packages/cloud-api/src/processor.ts:53-60(must stop stripping browser). - Code vs OPS: CODE → CLOSED. No OPS. (Shares
processor.ts:53-60with cloud-1/mcp-2 → land after them.)
Findings: install-4 (P0), docker-1 (P0), docker-3 (P0), docs-3 (P0).
Shared-file collision: apps/docs/content/docs/cross-os-rendering.mdx is
edited by docs-3 (lines 69/93/100/228) and by C15/docs-9 (line 273
relabel). Not in the directive's collision list — flagged here. Also
sandbox.mdx (docs-3) overlaps C15/docs-8's sidebar work (meta.json, different
file — low risk). Serialise C4↔C15 on cross-os-rendering.mdx. The code surface
(docker.ts, Dockerfile, release.yml) is C4-exclusive.
install-4 / docker-1 — frontguard/render:latest is not published; --docker fails out of the box
(install-4 and docker-1 are the same defect from the install-path and
docker-render lenses; fix once.)
- Recommendation (paraphrased): either add a Docker buildx + multi-arch push
step to
release.ymlthat publishesfrontguard/render:vX.Y.Zand:latest(and smoke-pulls before claiming the feature ships), or flip--dockerto a build-locally-from-source path and add a preflightdocker manifest inspectthat, on miss, surfaces an actionable "build it yourself frompackages/cli/docker/Dockerfile" error instead of a rawpull access denied. - Evidence repro:
curl -s -o /dev/null -w '%{http_code}' https://hub.docker.com/v2/repositories/frontguard/render/→404;curl -s https://hub.docker.com/v2/repositories/frontguard/→{"count":0,…}. - file:line targets:
packages/cli/src/render/docker.ts:42(DEFAULT_IMAGE);packages/cli/src/cli/index.ts:265(advertises--docker);packages/cli/src/render/docker.ts:144-186(runDocker, add preflight);.github/workflows/release.yml(no buildx/push today). - Code vs OPS: CODE_CLOSED (OPS: publish
frontguard/renderimage to Docker Hub/GHCR). The preflight + build-locally fallback + actionable error is the code mitigation that removes the raw-failure evidence now.
docker-3 — no --platform pin; arm64 vs amd64 images produce different bytes
- Recommendation (paraphrased): pin
FROM --platform=linux/amd64 …in the Dockerfile so the same machine code rasterizes everywhere; inject--platform linux/amd64into thedocker runargv; document the Apple-Silicon emulation perf cost; publish multi-arch tags only if byte-equivalence is proven across them. - Evidence repro:
grep -n 'platform' packages/cli/docker/Dockerfile packages/cli/docker/docker-compose.yml packages/cli/src/render/docker.ts→ no matches. - file:line targets:
packages/cli/docker/Dockerfile:37(base image);packages/cli/src/render/docker.ts:144-186(buildDockerArgs). - Code vs OPS: CODE → CLOSED (the Dockerfile pin + argv injection is pure code). Multi-arch publication, if pursued, is OPS — but the byte-divergence defect is closed by pinning to a single platform.
docs-3 — cross-os-rendering.mdx tells users to pull an unpublished image
- Recommendation (paraphrased): publish the image as part of the release
pipeline, or gate
--dockerbehind an explicitfrontguard build-imagestep in the doc and drop the "just--dockerand it works" framing. - Evidence repro:
curl -s https://hub.docker.com/v2/repositories/frontguard/render/→{"message":"object not found"}. - file:line targets:
apps/docs/content/docs/cross-os-rendering.mdx:69,93,100,228;apps/docs/content/docs/sandbox.mdx:41-45(inherits the same image). - Code vs OPS: CODE_CLOSED (OPS: publish image) — doc gating ships now.
(Coordinate
cross-os-rendering.mdxwith C15/docs-9.)
Findings: sec-1 (P0), cloud-4 (P0). (Same root defect from the
security-auth and cloud-api lenses; fix once.)
Shared-file collision: packages/cloud-api/src/index.ts (boot-time check)
collides with C6 (:54) and C7 (:377-381); auth/session.ts is C5-exclusive.
Serialise cloud-api index.ts edits (see §3).
SECRET-HANDLING (critical): never write the literal DEV_SESSION_SECRET
value, never reproduce or quote a forged cookie. Reference by ID and public
file:line only.
sec-1 / cloud-4 — dashboard session secret has a hardcoded production fallback shipped in published code
- Recommendation (paraphrased): fail closed — when
isProduction(env)is true andDASHBOARD_SESSION_SECRETis unset, refuse to mint any session cookie (return 503 / throw at startup on first request). Remove theDEV_SESSION_SECRETconstant from shipped source, or gate it strictly behind a non-production runtime check (and rename it to make misuse obvious). AddDASHBOARD_SESSION_SECRETtowrangler.toml's documented-secrets list and todocs/launch-readiness.md. Rotation of the secret and revocation of any sessions signed with the old value is OPS (per DECISIONS.md), not code. - Evidence repro (SAFE — no forging):
grep -n "DEV_SESSION_SECRET" packages/cloud-api/src/auth/session.ts(shows the exported constant + the|| DEV_SESSION_SECRETfallback at:32-34);grep -n "DASHBOARD_SESSION_SECRET" packages/cloud-api/wrangler.toml(shows it is absent from the documented secrets at:39-49). The dossier's cookie-forge step is deliberately omitted here per the secret-handling rule. - file:line targets:
packages/cloud-api/src/auth/session.ts:29(constant),:32-34(silent fallback);packages/cloud-api/wrangler.toml:39-49(documented secrets, missing the session secret);packages/cloud-api/src/index.ts(add the boot-timeisProductionguard);packages/cloud-api/dist/index.js:1498(where the constant currently ships);docs/launch-readiness.md(add the required secret). - Code vs OPS: CODE_CLOSED (OPS: rotate
DASHBOARD_SESSION_SECRET/ remove the public constant's value from the deployed env + revoke in-flight sessions signed with the old key + set the secret in prod). The fail-closed guard + constant removal ships now and removes the forge-from-source vector; rotation closes the residual real-world exposure.
Findings: sec-2 (P1), int-7 (P1).
Shared-file collisions: packages/cloud-api/src/index.ts:54 collides with
C5 (boot) and C7 (:377-381) — serialise. integrations/slack-app/src/runs.ts
collides with C9 (int-1 summarizeRun) — directive-listed; serialise
C6↔C9 on runs.ts.
sec-2 — POST /v1/run accepts any http/https URL; no SSRF guard on the cloud-api
- Recommendation (paraphrased): port the
isPrivateOrLoopbackHosthelper from the Vercel integration into a shared module and run it ondata.urlin/v1/run; reject plainhttp://(https-only) in production; resolve the hostname at fetch time and re-check (DNS-rebinding guard). - Evidence repro (SAFE local check): with the bundled zod —
z.string().url().safeParse('http://127.0.0.1/')→{ success: true }(proving the schema admits loopback);grep -rnE 'isPrivateOrLoopbackHost|169\.254|SSRF' packages/cloud-api/src→ no matches. - file:line targets:
packages/cloud-api/src/index.ts:54(thez.string().url()validator); source of the helper to port —integrations/vercel/src/webhook.ts:127-154;integrations/slack-app/src/runs.ts:17(acceptshttp:). - Code vs OPS: CODE → CLOSED. No OPS.
int-7 — Slack /frontguard status <url> has no SSRF guard; pivots through the cloud-api
- Recommendation (paraphrased): import the Vercel
isPrivateOrLoopbackHosthelper into the Slack app and reject before submitting, or — preferable — move SSRF guarding into the cloud-api so every entry point inherits it (the sandbox is the actual fetcher). Fixing sec-2 at the cloud-api layer largely subsumes this. - Evidence repro: code inspection —
integrations/slack-app/src/runs.ts:17-24andintegrations/slack-app/src/events.ts:89check only the URL scheme, never a private/loopback host. - file:line targets:
integrations/slack-app/src/runs.ts:17-24;integrations/slack-app/src/events.ts:82-101(esp.:89);packages/cloud-api/src/index.ts:53. - Code vs OPS: CODE → CLOSED. No OPS.
Findings: int-3 (P0), docs-5 (P0), docs-6 (P0).
Shared-file collisions (docs set): apps/docs/content/docs/integrations/slack.mdx
and vercel.mdx are touched by docs-6 and also by C3/docs-2 (api-url) and
C15/docs-9 (vercel.mdx link). distribution.mdx is C8-exclusive. Partition by
section. The code surface (integrations/github-app/**, repo-root action.yml)
is C8-exclusive.
int-3 — GitHub App bootstrap PR pins to ravidsrk/frontguard@v1 (nonexistent ref; no repo-root action.yml)
- Recommendation (paraphrased): add an
action.ymlshim at the repo root that re-usespackages/cli/action.yml(GitHub resolves<owner>/<repo>against the root only), changeACTION_REFto a working pinned ref (av0/v0.2.0major tag), and add a CI smoke test that runs the action against the local repo so the drift cannot ship again. - Evidence repro:
git tag --list→ onlyv0.1.0,v0.2.0(nov1);find . -name action.yml -not -path '*/node_modules/*'→ onlypackages/cli/action.yml. - file:line targets:
integrations/github-app/src/github-api.ts:362(ACTION_REF),:373-391(DEFAULT_WORKFLOW_YML);integrations/github-app/README.md:19; new repo-rootaction.yml. - Code vs OPS: CODE_CLOSED (OPS: push the
v0git tag pointing at a stable commit). The rootaction.ymlshim + correctedACTION_REFship now; the tag must be pushed out-of-band.
docs-5 — GH Actions/App docs reference @main and @v1, but action.yml isn't at root and no v1 tag exists
- Recommendation (paraphrased): pick one canonical reference — add the root
action.ymlshim, tag av0major tag pinned tov0.2.0, rewrite every doc touses: ravidsrk/frontguard@v0, and delete the phantomfrontguard/frontguard-action@v0reference. - Evidence repro:
find . -maxdepth 2 -name action.yml→ onlypackages/cli/action.yml;git ls-remote --tags https://github.com/ravidsrk/frontguard→ onlyv0.1.0,v0.2.0;curl -sI https://github.com/frontguard/frontguard-action→HTTP/2 404. - file:line targets:
apps/docs/content/docs/ci-cd/github-actions.mdx:31,82,123,134,157;apps/docs/content/docs/integrations/github.mdx:106;apps/docs/content/docs/distribution.mdx:45,53. - Code vs OPS: CODE_CLOSED (OPS: push
v0tag). Doc rewrite + rootaction.ymlship now.
docs-6 — marketplace listings claimed live but 404 (GitHub Marketplace/App, Slack directory, Vercel)
- Recommendation (paraphrased): add a
<Callout type="warn">banner at the top of every integration page stating the marketplace listing is in review and the manifest can be self-hosted today; propagatedistribution.mdx's honest "Coming soon" tone; remove or 404-proof every dead link; and either implement or delete the Vercel post-installfrontguard.dev/api/installendpoint. - Evidence repro:
curl -sI https://github.com/marketplace/frontguard→404;curl -sI https://github.com/apps/frontguard→404;curl -sI https://frontguard.dev/api/install→404. - file:line targets:
apps/docs/content/docs/integrations/github.mdx;apps/docs/content/docs/distribution.mdx;apps/docs/content/docs/integrations/slack.mdx;apps/docs/content/docs/integrations/vercel.mdx(+ the/api/installbackend if implemented). - Code vs OPS: CODE_CLOSED (OPS: submit/publish the GitHub/Slack/Vercel
marketplace listings; implement the
/api/installbackend if the listing is pursued). The honest callouts + dead-link removal ship now.
Findings: int-1 (P0).
Shared-file collision: integrations/slack-app/src/runs.ts is touched by C6
(int-7 SSRF). Directive-listed. Serialise C9↔C6 on runs.ts.
int-1 — Slack app reads a fabricated run-result shape; every message reports "No visual regressions"
- Recommendation (paraphrased): replace
summarizeRun()with the same status-string logic the Netlify integration uses (status === 'regression' | 'changed' | 'failed', etc.) and add a fixture-driven test parallel to Netlify'sFIXTURE_FAILING, so the test exercises the real cloud-api wire shape rather than a hand-fed one. - Evidence repro: code inspection —
integrations/slack-app/src/runs.ts:45declaresresultsasArray<{ regression?; warning? }>, but the real wire shape isstatus:'passed'|'regression'|…(packages/cloud-api/src/types.ts:30-37);summarizeRun()at:128-144counts fields that never exist on the payload, so it always reports zero regressions. The existing unit test (test/runs.test.ts:178-202) hand-feeds the wrong shape. - file:line targets:
integrations/slack-app/src/runs.ts:45,:128-144;integrations/slack-app/test/runs.test.ts:178-202; reference correct logic atintegrations/netlify/lib/core.js:215-227and testintegrations/netlify/test/core.test.js:177-200; wire shapepackages/cloud-api/src/types.ts:30-37,processor.ts:53-60. - Code vs OPS: CODE → CLOSED. No OPS.
Findings: sb-1 (P0), sb-3 (P0).
Shared-file collision: apps/docs/content/docs/integrations/storybook.mdx
is touched by the doc-retraction parts of sb-1/sb-3 and by C15/docs-4 (strip
nonexistent flags) and by C1/sb-2 (fixture .ts command). Not in the
directive's collision list — flagged here. Serialise the storybook.mdx
edits across C10, C15, and C1. The code surface
(discovery/storybook.ts, render/playwright.ts) is C10-exclusive.
sb-1 — real Storybook 8 strips parameters from /index.json; per-story overrides silently no-op
- Recommendation (paraphrased): extract per-story parameters from a source
that actually has them — either navigate to each
iframe.html?id=…&viewMode=storyand readwindow.__STORYBOOK_PREVIEW__.storyStore.fromId(id).parameters?.frontguard, or static-parse the.storiesfiles via@storybook/csf-tools. If neither is wired before shipping, retract the docs claim thatparameters.frontguardworks today. - Evidence repro: in
packages/cli/__fixtures__/storybook—npm install && npm run storybook, thencurl -s localhost:6006/index.json -o /tmp/sb-index.jsonandpython3 -c "import json; d=json.load(open('/tmp/sb-index.json')); print(list(next(iter(d['entries'].values())).keys()))"→ keys aretype/id/name/title/importPath/componentPath/tags(noparameters). - file:line targets:
packages/cli/src/discovery/storybook.ts:350-354(entry.parameters?.frontguard);packages/cli/test/discovery/storybook.test.ts:107-147(hand-fabricated index); doc claim inapps/docs/content/docs/integrations/storybook.mdx. - Code vs OPS: CODE → CLOSED. No OPS.
sb-3 — STORYBOOK_READY_SCRIPT ready-wait throws on every story; play()-await is silently best-effort
- Recommendation (paraphrased): invoke the ready-script as a function —
wrap it as an immediately-invoked arrow in the evaluate string
(
(STORYBOOK_READY_SCRIPT)(sbTimeout)) or pass a real function, becausepage.evaluate(string, arg)does not invoke the function on the Playwright side, so the result isundefinedandresult.reasonthrows. Add an integration test that boots the fixture's Storybook and asserts no ready-wait warnings appear and that a play()-driven story matches a known-good baseline. Until fixed, drop the "first-class play()-aware" claim. - Evidence repro: a live run against the fixture's seven stories emits
⚠ Storybook ready-wait failed for /iframe.html?id=…on every story (visible infrontguard runoutput / logs against the fixture Storybook). - file:line targets:
packages/cli/src/render/playwright.ts:269(thepage.evaluate(STORYBOOK_READY_SCRIPT, sbTimeout)call),:275(result.reason),:281-285(the swallowing catch); doc claim atapps/docs/content/docs/integrations/storybook.mdx:13-15,264-296. - Code vs OPS: CODE → CLOSED. No OPS.
Findings: supply-2 (P1), supply-6 (P1), install-13 (P2).
Shared-file collision: packages/cli/package.json is edited by supply-2 and
install-13 — internal to C11. package-lock.json regenerates here (large diff,
isolated to this cluster). No cross-cluster collision (C7 doesn't change
package manifests).
supply-2 — two critical CVEs in the runtime dep tree (protobufjs, shell-quote) via @daytonaio/sdk
- Recommendation (paraphrased): bump
@daytonaio/sdkto the audit's listed fix (^0.187.0) in bothpackages/cli/package.jsonandpackages/cloud-api/package.json, regeneratepackage-lock.json, runnpm audituntil critical/high is 0, then cut and re-publish 0.2.1. Add a CI gate that runsnpm audit --audit-level=highand fails the build. - Evidence repro:
npm audit(summary shows the critical/high counts);npm ls shell-quoteandnpm ls protobufjsshow the prod (dev=false) subtree under@daytonaio/sdk. - file:line targets:
packages/cli/package.jsonandpackages/cloud-api/package.json(@daytonaio/sdkversion);package-lock.json(regenerate);.github/workflows/ci.yml(add audit gate). - Code vs OPS: CODE_CLOSED (OPS: cut + publish
@frontguard/cli@0.2.1to npm). The dep bump + lock regen + CI gate close the audit locally; consumers only get the fix on the next publish.
supply-6 — no CI gate on npm audit; no dependabot/renovate automation
- Recommendation (paraphrased): add
.github/dependabot.yml(npm + github-actions ecosystems, weekly), add a CI job that runsnpm audit --audit-level=high --omit=devand fails the build, and add a separate scheduled weekly audit that posts the diff into an issue. - Evidence repro:
grep -RE "audit|trivy|snyk" .github/→ no matches;ls .github/dependabot.yml renovate.json .renovaterc* 2>/dev/null→ not found. - file:line targets:
.github/workflows/ci.yml; new.github/dependabot.yml; optional new scheduled workflow. - Code vs OPS: CODE → CLOSED. No OPS.
install-13 — clean install ships 28 npm vulnerabilities (13 high) via @daytonaio/sdk's OTel exporters; sdk deprecated
- Recommendation (paraphrased): move
@daytonaio/sdkto anoptionalDependency/peerDependencyMetaentry so users who never use Daytona don't pull the chain; migrate to the renamed@daytona/sdk; track the upstream OTel CVEs and pin past them or ship apackage-lock.jsonoverride. - Evidence repro:
npm install @frontguard/cliin a fresh dir prints the deprecation warning and the vulnerability count;npm audit --jsonenumerates the high CVEs in@opentelemetry/exporter-*andprotobufjs. - file:line targets:
packages/cli/package.json(dependencies→optionalDependencies/peerDependencyMeta);package-lock.json. - Code vs OPS: CODE_CLOSED (OPS: re-publish to npm). Source change ships now; consumers see the smaller tree only after publish.
Findings: mcp-3 (P1), mcp-6 (P1), mcp-8 (P2), mcp-10 (P2).
Shared-file collisions: packages/mcp/** collides with C7 — both edit
tools/list-regressions.ts (C7/mcp-1 at :70-77, C12/mcp-8 at :64) and
tools/recent-runs.ts (C7/mcp-1 at :71-78, C12/mcp-8 at :62).
Directive-listed. Serialise C12 after C7's mcp work.
apps/docs/content/docs/integrations/mcp.mdx is touched by mcp-6/mcp-10 and
by C7/mcp-2 (doc claim) and C3/docs-2 (api-url at :290) — serialise the
mcp.mdx edits. accept-baseline.ts reaches into
packages/cloud-api/src/index.ts:405-414 (collision with C5/C6/C7 on
index.ts).
mcp-3 — invokedDirectly path-equality check fails via /tmp, firmlinks, symlinks (silent no-op on every npx)
- Recommendation (paraphrased): replace the path-equality guard with a robust
idiom — drop the conditional entirely (the file only runs as a bin) and gate
the test-side import via a named
main()export, or compare realpaths (realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])). - Evidence repro:
cd /tmp/mcp-test && cat init.jsonl | npx -y @frontguard/mcp@0.2.0→ exit 0 with zero bytes of stdout/stderr (no tools registered). Prefer reproducing against the local build rather than the published 0.2.0:node packages/mcp/dist/index.js < init.jsonlfrom a/tmpcwd. - file:line targets:
packages/mcp/src/index.ts:152-159. - Code vs OPS: CODE → CLOSED. No OPS. (mcp-10 depends on this fix.)
mcp-6 — accept_baseline silently approves an entire run when the agent thinks it accepted one diff
- Recommendation (paraphrased): add per-diff approval to the cloud-api
(
POST /v1/baselines/:runId/approvewith a{diffIds:[…]}body) and have the MCP tool take and forward an array; or rename the tool toaccept_run_baselinesand rewrite the docs so the agent performs an explicit "I reviewed all N regressions" check before calling it. As shipped, an LLM following the docs can promote broken UI to canonical. - Evidence repro: code inspection —
accept-baseline.ts:29-37parsesdiff_idonly to extract therunIdand discards the per-diff selector;index.ts:405-414setsbaselinesApproved=trueon the whole run; the docs prompt atmcp.mdx:353-360encourages the per-diff phrasing. - file:line targets:
packages/mcp/src/tools/accept-baseline.ts:29-37;packages/cloud-api/src/index.ts:405-414;apps/docs/content/docs/integrations/mcp.mdx:353-360. - Code vs OPS: CODE → CLOSED. No OPS. (Touches cloud-api
index.ts→ serialise with C5/C6/C7.)
mcp-8 — treating status='new' as a regression mislabels first-time baselines
- Recommendation (paraphrased): drop
'new'fromREGRESSION_STATUSESin both tools (or rename the setBASELINE_ISSUE_STATUSESand document the semantic), and add a regression test with a sample run whose results includestatus='new'. The codebase already knows the right answer — the cloud-api emitsnew_baseline, which is correctly excluded. - Evidence repro: code inspection —
grep -n "'new'" packages/mcp/src/tools/list-regressions.ts packages/mcp/src/tools/recent-runs.tsshows'new'inside the regression set (list-regressions.ts:64,recent-runs.ts:62); the CLI setsstatus:'new'for first-time baselines (packages/cli/src/diff/pixel.ts:207). - file:line targets:
packages/mcp/src/tools/list-regressions.ts:64;packages/mcp/src/tools/recent-runs.ts:62. - Code vs OPS: CODE → CLOSED. No OPS. (Shares the two files with C7/mcp-1 → serialise.)
mcp-10 — documented "tools/list empty → Node version" troubleshooting is wrong (real cause is mcp-3)
- Recommendation (paraphrased): after fixing mcp-3, add a real launch
diagnostic — have the binary write a one-line
frontguard-mcp v<version> starting on stdioto stderr on launch — and update the troubleshooting table to add "no output at all → likely a path-resolution bug; file an issue withwhich npxand your cwd." - Evidence repro:
apps/docs/content/docs/integrations/mcp.mdx:381-391states the empty-tools case is "almost always a Node version issue"; reproduced on Node 22 (see mcp-3 repro) — the real cause is the silent-exit path bug. - file:line targets:
apps/docs/content/docs/integrations/mcp.mdx:381-391; the MCP binary launch path (packages/mcp/src/index.ts, add the stderr line). - Code vs OPS: CODE → CLOSED. No OPS. Depends on mcp-3 (land mcp-3 first within C12).
Findings: install-2 (P0).
Shared-file collision: packages/cli/src/cli/init.ts — C13 edits the
.gitignore entry list at :247; C1/install-1 edits the default format at
:158. Not in the directive's collision list — flagged here. Serialise
C13↔C1 on init.ts (or fold both into one worktree).
install-2 — first-run baseline init explodes when node_modules is in the worktree (init's .gitignore omits node_modules/)
- Recommendation (paraphrased): add
node_modules/to the entriesinitappends to.gitignore; more robustly, haveGitOrphanStoragedetect a worktree containing apackage.jsonand refuse to check out unless the working tree is clean of ignorable dirs, and emit a real error message rather than "All screenshots will be treated as new" followed by an internal "not initialized" panic. Add amaxBufferoverride to thegitspawn. - Evidence repro: in a fresh dir —
git init && npm install && <frontguard init> && git commit -am init, then serve a local site (npx serveon:8765) and run<frontguard run>→ fails withBaseline storage init failed: git rm failed: spawnSync git ENOBUFSandComparison failed: GitOrphanStorage not initialized.(use the-p @frontguard/cliinvocation form for thefrontguardcommands). - file:line targets:
packages/cli/src/cli/init.ts:247(theentriesToAddlist); theGitOrphanStoragecheckout /execFileSyncsite (packages/cli/src/.../git-orphan.ts:88-92, nomaxBuffer). - Code vs OPS: CODE → CLOSED. No OPS.
Findings: claim-5 (P0), claim-7 (P2), claim-9 (P2), dist-11 (P2).
Shared-file collisions: README.md comparison-table region (claim-7 :102,
claim-9 :101) overlaps C3 (install-6/claim-6 at :74/:104).
Directive-listed (docs set). Serialise C14↔C3 on README.md.
apps/landing/** (claim-5 pricing.tsx, dist-11 index.html) overlaps C3
(claim-4 pricing.tsx) — pricing.tsx is touched by both claim-4 (C3) and
claim-5 (C14); serialise on pricing.tsx.
claim-5 — Pro tier advertises "Production monitoring scheduler" but the plan flag is false (Business-only); runtime returns 402
- Recommendation (paraphrased): decide intent — either flip
pro.limits.productionMonitoringtotrue(and let the cron route allow the Pro plan), or remove the feature from the Pro tier'sfeaturesarray and from the comparisonMATRIXrow. Add a snapshot/integration test asserting every feature claim on the pricing page maps back to ahasFeature()returningtruefor that plan id. - Evidence repro: code inspection —
apps/landing/src/routes/pricing.tsx:71lists the feature;packages/cloud-api/src/billing/plans.ts:59haspro.limits.productionMonitoring: falsewhile:73has businesstrue;packages/cloud-api/src/routes/monitors.ts:68returns HTTP 402 to Pro. - file:line targets:
apps/landing/src/routes/pricing.tsx:71,:115(MATRIX);packages/cloud-api/src/billing/plans.ts:48-61,:63-76,:107-110;packages/cloud-api/src/routes/monitors.ts:68. - Code vs OPS: CODE → CLOSED. No OPS.
claim-7 — README "BackstopJS — 6yr (quiet)" cell is fabricated
- Recommendation (paraphrased): change the cell to a claim defensible from the
project's own cited source (e.g.
❌ (low activity)or❌ (last release ~2024)). Add a test asserting every quantitative claim in the README comparison table maps to a citation indocs/research.md. - Evidence repro:
npm view backstopjs time→ last releasev6.3.25~2024 (not six years);docs/research.md:230-238does not say "6 years". - file:line targets:
README.md:102; referencedocs/research.md:230-238. - Code vs OPS: CODE → CLOSED. No OPS.
claim-9 — README "Chromatic Pro entry per-snapshot" is misleading (Starter is flat $179/mo)
- Recommendation (paraphrased): replace "per-snapshot" with "$179/mo" in the
README table to match the project's own research.md and the live
/comparisonspage. - Evidence repro:
docs/research.md:93-98documents Chromatic Starter at$179/mo(35,000 snapshots);apps/landing/src/routes/comparisons/data.ts:79already says$179/mo. - file:line targets:
README.md:101; referencedocs/research.md:93-98,apps/landing/src/routes/comparisons/data.ts:79. - Code vs OPS: CODE → CLOSED. No OPS.
dist-11 — live site Schema.org aggregateRating 4.8/36 on a 0-star repo
- Recommendation (paraphrased): strip the
AggregateRatingblock. The fix is already inapps/landing/index.htmlon main — but the live site still serves the old build, so the remaining work is deployment (or a Cloudflare HTML Rewriter hot-fix that strips the block immediately). - Evidence repro:
curl -s https://frontguard.dev/ | grep -i aggregateRating(the live HTML still includes it);curl -s https://api.github.com/repos/ravidsrk/frontguard | python3 -c "import json,sys;print(json.load(sys.stdin)['stargazers_count'])"→0. - file:line targets:
apps/landing/index.html(verify the source no longer contains the block — already removed on main). - Code vs OPS: CODE_CLOSED (OPS: deploy/redeploy the landing site so the live HTML matches main). Source is clean; the live misrepresentation persists until the deploy.
Findings: docs-4 (P0), docs-7 (P2), docs-8 (P1), docs-9 (P2). Shared-file collisions (heavy — partition carefully):
storybook.mdx(docs-4 flag strip) ↔ C10 (sb-1/sb-3 doc retraction) ↔ C1 (sb-2 fixture command).cross-os-rendering.mdx(docs-9:273) ↔ C4 (docs-3:69/93/100/228).vercel.mdx(docs-9:281) ↔ C3 (docs-2:67) ↔ C8 (docs-6 install URL).- moving
frontguard-vs-percy.mdx/frontguard-vs-chromatic.mdxintocomparisons/(docs-8) changes URLs that C3 (install-6/claim-6 README links) point to — coordinate destinations. Sequence C15 last among the doc clusters (after C2/C3/C4/C8/C10 settle their shared mdx), or partition each file by line and serialise.
docs-4 — Storybook integration doc invokes CLI flags that don't exist (--baseline-strategy, --ai)
- Recommendation (paraphrased): strip
--baseline-strategyand--aifrom the Storybook CI recipe (AI is enabled by config + env keys; baseline strategy is config-only today) and fix the "Next steps" claim that lists them as documented flags. Either implement the flags or fix the docs — fixing the docs is recommended sincefrontguard.config.tsalready covers both. - Evidence repro:
node packages/cli/dist/cli/index.js run --baseline-strategy git --ai→error: unknown option '--baseline-strategy';grep -n "option('--" packages/cli/src/cli/index.tslists the real flags (:256-263). - file:line targets:
apps/docs/content/docs/integrations/storybook.mdx:356,505; actual flags atpackages/cli/src/cli/index.ts:256-263. - Code vs OPS: CODE → CLOSED. No OPS.
docs-7 — self-host.mdx claims /health returns 0.1.0; ghcr image isn't published
- Recommendation (paraphrased): update the
/healthsample output to0.2.0(or a<your release>placeholder). Either publish a publicghcr.io/ravidsrk/frontguard-cloud-apiimage and verify anonymous pulls, or drop the GHCR pattern and documentdocker compose buildfrom source. - Evidence repro:
curl -sI https://ghcr.io/v2/ravidsrk/frontguard-cloud-api/manifests/latest→HTTP/2 401;/healthactually returnsPACKAGE_VERSION(packages/cloud-api/src/index.ts:132). - file:line targets:
apps/docs/content/docs/self-host.mdx:117(version sample),:357,:406(ghcr deploy lines). - Code vs OPS: CODE_CLOSED (OPS: publish public
ghcr.iocloud-api image) if the GHCR pattern is kept; CODE → CLOSED if the doc switches to build-from-source (the version-string fix is pure doc either way).
docs-8 — sandbox / cross-os / distribution / results pages orphaned from the sidebar
- Recommendation (paraphrased): add
sandbox,cross-os-rendering, anddistributionto the top-levelmeta.json(e.g. under a new Deployment/Advanced group), and movefrontguard-vs-percy.mdx/frontguard-vs-chromatic.mdxintocomparisons/and list all three incomparisons/meta.json. - Evidence repro:
grep -n "sandbox\|cross-os\|distribution" apps/docs/content/docs/meta.json→ no entries;cat apps/docs/content/docs/comparisons/meta.json→"pages": ["frontguard-vs-argos"]only. - file:line targets:
apps/docs/content/docs/meta.json:3-22;apps/docs/content/docs/comparisons/meta.json:3;apps/docs/content/docs/guides/meta.json:20-22. - Code vs OPS: CODE → CLOSED. No OPS. (Coordinate moved-file URLs with C3 README links.)
docs-9 — internal links to nonexistent docs + a frontguard approve command that doesn't exist
- Recommendation (paraphrased): replace
/docs/guides/scheduled-monitorswith/docs/guides/production-monitoring; relabel the cross-os link to the Playwright-plugin-setup page; replace the inventedfrontguard approvewithfrontguard update-baselines(oraccept-fix <id>) and rephrase the surrounding sentence. - Evidence repro:
ls apps/docs/content/docs/guides/scheduled-monitors* 2>/dev/null→ no match;grep -rn "frontguard approve" apps/docs/content/docs→ hits in comparison/ migration pages;grep -n ".command('" packages/cli/src/cli/index.ts→ real commands (run,init,update-baselines,doctor,monitor,accept-fix,reject-fix,export-patterns,plugin …) — noapprove. - file:line targets:
apps/docs/content/docs/integrations/vercel.mdx:281;apps/docs/content/docs/cross-os-rendering.mdx:273;apps/docs/content/docs/guides/frontguard-vs-chromatic.mdx:98(+ the otherfrontguard approveoccurrences across the comparison/migration files). - Code vs OPS: CODE → CLOSED. No OPS.
Findings: val-5 (P2).
Shared-file collision: none — validation/** and the validation results doc
are C16-exclusive. packages/cli/src/diff/pixel.ts is referenced read-only
(the byte-identical fast path is correct behavior; the issue is the harness
methodology, not the fast path).
val-5 — two-pass recheck against unchanged code is not an independent measurement (pixelmatch short-circuits on byte-identical PNGs)
- Recommendation (paraphrased): make the recheck pass an actual independent
measurement — boot a fresh dev-server process (fresh CSS/JS hashes, font
sub-pixel decisions), or render against a separate clone, or seed the recheck
with a no-op
git commit --allow-emptyplus a forced server restart — then re-run the harness and update the methodology write-up. The current setup shares dev-server PID, work dir, SHA, and baseline between passes, so it measures Chromium encoder determinism, not false-positive rate. - Evidence repro: code inspection —
packages/cli/src/diff/pixel.ts:51-63takes aBuffer.compare(current, baseline) === 0fast path;validation/results/tailwind-dashboard.json:40-99shows every diff entry atdiffPercentage: 0(consistent with the fast path firing on every entry). - file:line targets:
validation/**(the recheck harness);packages/cli/src/diff/pixel.ts:51-63(the fast path it exploits);validation/results/results-v0.2.md:46-49(methodology claim to revise). - Code vs OPS: CODE → CLOSED (harness change + re-run + corrected methodology write-up). No OPS — but a local validation re-run is required; if the re-run isn't feasible in the loop, the minimum closure is the corrected methodology disclosure plus the harness change that forces an independent second pass.
The convergence loop should bias toward the four foundation clusters, but they are not all freely parallel — the doc-heavy foundations (C2, C3) and the cloud-api foundation (C7) collide with later clusters on shared files. The order below maximises parallelism subject to the collision map.
Run these as early as possible; the rest of the pipeline inherits their decisions.
- C7 (cloud-api data model) — longest pole; start immediately in an isolated
worktree scoped to
packages/cloud-api/**+packages/mcp/**. Internally serialised:cloud-1first (it lands the R2→sandbox restore and the run/result shape), then in order:cloud-9(report-html/otel version — independent but shares the worktree) →mcp-1(db schema + d1-store) →mcp-2(processorsuggestedFix) →mcp-9(processorbrowser— sharesprocessor.ts:53-60with mcp-2, so after it) →mcp-7(index.ts listing). This serial chain exists because all six touch the same handful of cloud-api files; do not fan them out. - C1 (ts-config-loader) — isolated to
packages/cli/src/core/config.ts+init.ts+ the storybook fixture. Landconfig.ts(the loader) first; it unblocks the Quick-Start repros (#2/#3) that many other clusters' manual verifications rely on. Ownsinit.tsahead of C13 (or hand C13'sinit.ts:247edit to the same worktree). - C2 (bin/package-name) ∥ C3 (hosts/DNS code-side) — these two
foundations can run in parallel iff partitioned by file:
- C2 owns
apps/docs/.../index.mdx|quick-start.mdx|installation.mdx,packages/cli/README.md,scripts/build-daytona-snapshot.ts. - C3 owns
apps/landing/**, rootREADME.md(doc-links region:74/:104), the integration*.mdxapi-url defaults, integration manifests, and CLI telemetry. They do not share a file under this partition. Freeze the bin-invocation decision (C2) and the api-url decision (C3) before any other cluster rewrites command/URL examples.
- C2 owns
- C13 (init-gitignore) — after C1 releases
init.ts(:247vs:158). - C5 (session-secret) then C6 (ssrf-guard) — both edit
packages/cloud-api/src/index.ts; run them after C7 finishes the cloud-api worktree, and serialise C5→C6 (or partition by region: C5 = boot guard +auth/session.ts; C6 =index.ts:54validator). C6 also editsintegrations/slack-app/src/runs.ts. - C12 (mcp-correctness) — after C7's mcp work (shares
list-regressions.ts/recent-runs.ts/mcp.mdx). Internally land mcp-3 before mcp-10 (mcp-10's diagnostic depends on the mcp-3 fix). mcp-6 also editscloud-api/src/index.ts:405-414→ coordinate with the C5/C6/C7 cloud-api serialisation. - C9 (slack-result-shape) — shares
integrations/slack-app/src/runs.tswith C6/int-7; serialise C9↔C6 (either order; one worktree at a time onruns.ts).
These have no code-file collision with each other and can fan out, but the doc-touching ones must respect the shared-mdx serialisation:
- C4 (docker-render) — code surface (
docker.ts,Dockerfile,release.yml) is exclusive and can run anytime; its doc edit (cross-os-rendering.mdx,sandbox.mdx) serialises with C15/docs-9. - C10 (storybook) — code surface (
discovery/storybook.ts,render/playwright.ts) exclusive; itsstorybook.mdxretraction serialises with C15/docs-4 and C1/sb-2. - C11 (supply-chain) — fully isolated (package manifests + lock + CI). Safe to run fully parallel.
- C16 (validation-methodology) — fully isolated. Safe to run fully parallel.
- C8 (github-action-ref), C14 (marketing-claims), C15
(docs-hygiene-residual) — the docs cluster. Serialise/partition on the
shared docs set (
README.md,slack.mdx,vercel.mdx,mcp.mdx,cross-os-rendering.mdx,storybook.mdx). Recommended sub-order: C8 → C14 → C15 last (C15 has the most cross-cluster mdx overlap), after C2/C3 have frozen the getting-started + api-url wording.
| Shared file | Clusters | In directive? | Resolution |
|---|---|---|---|
packages/cli/src/cli/init.ts |
C1, C13 | No (new) | C1 owns first, then C13 |
packages/cloud-api/src/index.ts |
C5, C6, C7, C12(mcp-6) | Partial (C5/C6/C7) | Serialise; C7 → C5 → C6; mcp-6 coordinates |
packages/cloud-api/src/processor.ts:53-60 |
C7 (cloud-1, mcp-2, mcp-9) | Internal | Serialise within C7 |
packages/mcp/tools/list-regressions.ts, recent-runs.ts |
C7(mcp-1), C12(mcp-8) | Yes | C7-mcp → C12 |
integrations/slack-app/src/runs.ts |
C6, C9 | Yes | Serialise C6↔C9 |
README.md (:74,:101-104) |
C3, C14 | Yes (docs set) | Serialise C3↔C14 |
apps/landing/.../pricing.tsx |
C3(claim-4), C14(claim-5) | No (new) | Serialise |
mcp.mdx |
C3(docs-2), C7(mcp-2), C12(mcp-6/10) | Partial | Serialise mdx edits |
vercel.mdx |
C3(docs-2), C8(docs-6), C15(docs-9) | Partial (docs set) | Partition by section |
slack.mdx |
C3(docs-2), C8(docs-6) | Partial (docs set) | Partition by section |
cross-os-rendering.mdx |
C4(docs-3), C15(docs-9) | No (new) | Serialise C4↔C15 |
storybook.mdx |
C1(sb-2), C10(sb-1/3), C15(docs-4) | No (new) | Serialise |
| moved comparison pages | C15(docs-8) → C3(README links) | No (new) | Agree dest paths first |
Every OPS action surfaced across the 49 findings. The convergence loop does
not execute any of these (per DECISIONS.md). T_FINAL turns this into
docs/ops-actions.md. Each finding gated by an OPS action can still reach
CODE_CLOSED once its code-side mitigation ships.
- Attach DNS for the
frontguard.devsubdomains (pointapp.,api.,github-app., and — if telemetry is kept —telemetry.at Cloudflare/the Worker) and deploy the cloud-api Worker + dashboard. Unblocks: claim-4, dist-3, docs-2, install-6†, claim-6†, install-9†. († these also reach full CLOSED via the code-side rewrite without DNS; DNS is the alternative path.) - Publish the Docker renderer image (
frontguard/render:latest+ version tags, multi-arch only if byte-equivalence is proven) to Docker Hub/GHCR, and add the buildx+push step to the release pipeline. Unblocks: install-4, docker-1, docker-3 (multi-arch only), docs-3. - Publish a
frontguardnpm shim package (re-exports@frontguard/cli) — only if option (a) is chosen for docs-1; the-p @frontguard/clidoc rewrite closes docs-1 without it. Unblocks: docs-1 (option a). - Re-publish the npm packages (
@frontguard/clietc.) to ship the refreshed bundled README and the dependency-bumped0.2.1. Unblocks: install-7, supply-2, install-13. - Push the
v0(and/orv1) git tag pointing at a stable commit, paired with the repo-rootaction.ymlshim, for the GitHub Action reference. Unblocks: int-3, docs-5. - Submit/publish the marketplace listings (GitHub Marketplace, GitHub App
github.com/apps/frontguard, Slack directory, Vercel integration) and implement the Vercel/api/installbackend endpoint. Unblocks: docs-6. - Rotate the dashboard session secret / set
DASHBOARD_SESSION_SECRETin production and revoke any sessions signed with the old public constant. Unblocks: sec-1, cloud-4 (full real-world closure beyond the fail-closed code guard). - Publish a public
ghcr.io/ravidsrk/frontguard-cloud-apiimage (verify anonymous pulls) — only if the GHCR self-host pattern is kept. Unblocks: docs-7. - Deploy/redeploy the landing site so the live HTML matches main (strips the
stale
aggregateRatingblock; also makes the C3 landing CTA fix live). Unblocks: dist-11 (and the live side of claim-4). - Build, publish, and wire the Daytona snapshot into CI so cloud-side regression/ fix-verification actually runs end-to-end. Unblocks: the live verification of cloud-1 (C7); related to ci-3's snapshot path (ci-3's source fix closes independently).
OPS action count: 10.
Per-finding closure test = "the Evidence no longer reproduces." Each cluster worker re-runs the exact Evidence command(s) from §2 after its fix and confirms the failing signal is gone. Rules:
- Read-only network probes (
curl,nslookup,host,npm view,git ls-remote) are safe to re-run as-is; for DNS/registry/marketplace findings whose true fix is OPS, the code-side verification is instead "the dead default/link is no longer present in source" (grep the file) — not "the host now resolves." - Destructive or external-publishing evidence is never run against live
services. Specifically:
- npm publish (supply-2, install-7, install-13, docs-1 option a): verify on
a local clean install —
npm packthe package into a tmp dir, install from the tarball, and re-run the repro (npm audit --audit-level=high --omit=devreturns 0; the bundled README shows 0.2.0;frontguardresolves via the packed shim). Nevernpm publish. - docker push (install-4/docker-1/docs-3): verify by building the image
locally (
docker build -f packages/cli/docker/Dockerfile) and confirming the preflight/build-locally path produces an actionable message — never push to a registry. - git tag push (int-3/docs-5): verify the repo-root
action.ymlresolves a localact/workflow smoke run; do not create/push public tags. - deploys (dist-11, claim-4 live side, DNS): verify the source is clean
(grep shows the
aggregateRatingblock / dead CTA removed); do not deploy.
- npm publish (supply-2, install-7, install-13, docs-1 option a): verify on
a local clean install —
- Secret finding (sec-1/cloud-4): verification is code-only and
forge-free. Confirm (a)
grepshows no plaintextDEV_SESSION_SECRETvalue remains insrc/(and ideallydist/), and (b) in a non-production local build, with the DB binding present andDASHBOARD_SESSION_SECRETunset, session minting refuses (503/throw). Never mint or replay a forged cookie, and never write the literal secret into a test fixture or assertion. - Behavioral cloud findings (cloud-1 and dependents): verify against a
local emulation (
wrangler dev/ miniflare with a local D1 + R2 fixture), notapi.frontguard.dev. The closure test for cloud-1: submit a run, change the baseline, submit again, and confirm the second run reports a regression rather thannew_baseline. If a Daytona sandbox is unreachable, use the stubbed runner (see §6) and assert the R2-restore code path executes (the restore list/download is invoked before exec). - Tests as durable verification: where the recommendation prescribes a test
(the pricing-feature/
hasFeaturesnapshot for claim-5; the D1 github round-trip for mcp-1; the e2esuggestedFixround-trip for mcp-2; the Netlify-parallel fixture for int-1; thestatus='new'regression test for mcp-8; the ready-wait-warning assertion for sb-3; the README-claim→research.md citation test for claim-7), the new test failing onmainand passing on the branch is the verification artifact — it prevents regression of the finding. - Per-finding "no longer reproduces" is recorded in
docs/fix-progress.md's EVID column by the convergence loop (T2 owns that ledger). T1 only specifies the test; it does not edit the ledger.
R1 — C7/cloud-1 baseline-restore depends on the Daytona sandbox API; CI may not
reach Daytona. The restore path (list R2 → download to sandbox → exec) is hard
to exercise end-to-end without a live Daytona sandbox, and api.frontguard.dev
is NXDOMAIN so there is no deployed instance to test against.
Fallback / stub plan: split the verification — (a) unit-test the R2-restore
logic against a local R2/D1 fixture (miniflare) with the Daytona client
mocked (assert it lists the baseline prefix and downloads each object before
exec); (b) gate the actual Daytona end-to-end behind an opt-in env flag
(DAYTONA_API_KEY present) that is skipped in CI with a logged "skipped: no
Daytona" line. Do not let "Daytona unreachable" block marking cloud-1
CODE_CLOSED — the defect (missing restore code) is closed by the code + the
mocked-restore test; live behavior is gated on OPS #10.
R2 — shared-file collisions cause merge churn / clobbered edits. The
collision table in §3 lists four collisions not in the original directive
(init.ts C1↔C13; pricing.tsx C3↔C14; cross-os-rendering.mdx C4↔C15;
storybook.mdx C1↔C10↔C15), plus the per-mdx overlaps inside the docs set.
Mitigation: the loop must serialise (one open PR at a time) on each shared file
per the partition table; never dispatch two clusters that touch the same file
concurrently. Worktree isolation prevents local clobbering but not merge
conflicts — the serialisation is what avoids the conflict.
R3 — over-claiming closure on OPS-gated findings. 9 of 49 findings cannot
reach real-world CLOSED from code alone (the OPS list). Mitigation: mark them
CODE_CLOSED (OPS: <action>) explicitly in the ledger; the PR description must
state the residual OPS dependency so a reviewer doesn't read CODE_CLOSED as
fully resolved.
R4 — the npx frontguard repro form masks/compounds findings. Many dossier
repros use the broken bin form, which 404s before reaching the downstream
behavior. Mitigation: §2's repro note mandates the -p @frontguard/cli form
(or local dist) for downstream verification; a worker that runs the bare form
and sees a 404 must not conclude "still broken" for the downstream finding.
R5 — secret leakage during C5 work or in any commit. The raw dossier and the
live source contain the literal secret + a forged cookie. Mitigation: the
DECISIONS.md secret-scan grep runs on the staged diff before every commit and
push (filtering removal lines); C5 verification is forge-free and code-only; the
raw dossier (.frontguard-audit/) is gitignored and must never be staged. A
commit that removes the constant will show the literal on a - line — that is
expected and is filtered by the removal-line exclusion, but the worker must
confirm the value does not also appear on any + line.
R6 — doc moves break inbound links. C15/docs-8 relocates the Percy/Chromatic
comparison pages; C3's README links and any internal cross-links point at the old
paths. Mitigation: agree the destination paths before either cluster edits, and
re-grep for the old paths after the move (grep -rn "guides/frontguard-vs-percy" README.md apps/docs should return only intended references).
R7 — validation re-run (C16) may not be reproducible in the loop. val-5's
true fix requires re-running the validation harness with an independent second
pass, which needs the sample repos to boot. Mitigation: if the harness re-run
isn't feasible, the minimum closure is the harness code change (force a fresh
dev-server/clone on the recheck pass) plus a corrected methodology disclosure in
results-v0.2.md; flag the un-re-run state in the ledger note rather than
fabricating a new FP number.
End of fix-plan. Next: T2 freezes scope into docs/fix-progress.md; the
convergence loop dispatches C1..C16 per §3.