From d6feb75aaed9e961650b55b945569703c7eda223 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:39:29 -0600 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20add=2052=20missing=20redirects=20fro?= =?UTF-8?q?m=20GitBook=20=E2=86=92=20Astro=20migration=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran an audit comparing all paths from old GitBook SUMMARY.md files against the live Astro pages and existing vercel.json redirect sources. Found 52 paths that were live on GitBook but returned 404 on the new site. Most gaps are in the guides section, which was reorganized from a developer-workflows/beginner/power-user/etc hierarchy into flat category sections (agent-workflows, configuration, devops, external-tools, frontend, getting-started, build-an-app-in-warp). Also adds the two redirect audit tooling scripts for future use: - scripts/audit_redirects.py — extracts GitBook paths and diffs against vercel.json - scripts/add_missing_redirects.py — applies the mapping to vercel.json Part of the broken-redirects fix plan (Steps 3-4). Co-Authored-By: Oz --- scripts/add_missing_redirects.py | 197 ++++++++++++++++++++++ scripts/audit_redirects.py | 176 ++++++++++++++++++++ scripts/redirect_audit_gaps.txt | 56 +++++++ vercel.json | 276 ++++++++++++++++++++++++++++++- 4 files changed, 703 insertions(+), 2 deletions(-) create mode 100644 scripts/add_missing_redirects.py create mode 100644 scripts/audit_redirects.py create mode 100644 scripts/redirect_audit_gaps.txt diff --git a/scripts/add_missing_redirects.py b/scripts/add_missing_redirects.py new file mode 100644 index 00000000..21924d58 --- /dev/null +++ b/scripts/add_missing_redirects.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Add missing redirect entries to vercel.json identified by audit_redirects.py. +Covers 52 paths from the GitBook → Astro migration audit, plus 2 known gaps +from the traffic analysis (BYOK and model-choice). + +Usage: + python3 scripts/add_missing_redirects.py [--dry-run] +""" + +import json +import argparse +from pathlib import Path + +# Old source path (no leading slash) → new destination URL +MAPPINGS = { + # Agent platform section root ---------------------------------------- + "agent-platform/warp-agents": "/agent-platform/", + + # guides/developer-workflows/backend --------------------------------- + "guides/developer-workflows/backend/how-to-create-priority-matrix-for-database-optimization": + "/guides/devops/how-to-create-priority-matrix-for-database-optimization/", + "guides/developer-workflows/backend/how-to-write-sql-commands-inside-a-postgres-repl": + "/guides/devops/how-to-write-sql-commands-inside-a-postgres-repl/", + + # guides/developer-workflows/beginner -------------------------------- + "guides/developer-workflows/beginner/10-coding-features-you-should-know": + "/guides/getting-started/10-coding-features-you-should-know/", + "guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind": + "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", + "guides/developer-workflows/beginner/how-to-customize-warps-appearance": + "/guides/getting-started/how-to-customize-warps-appearance/", + "guides/developer-workflows/beginner/how-to-explain-your-codebase-using-warp-rust-codebase": + "/guides/agent-workflows/how-to-explain-your-codebase-using-warp-rust-codebase/", + "guides/developer-workflows/beginner/how-to-make-warps-ui-more-minimal": + "/guides/getting-started/how-to-make-warps-ui-more-minimal/", + "guides/developer-workflows/beginner/how-to-master-warps-code-review-panel": + "/guides/getting-started/how-to-master-warps-code-review-panel/", + "guides/developer-workflows/beginner/trigger-reusable-actions-with-saved-prompts": + "/guides/configuration/trigger-reusable-actions-with-saved-prompts/", + "guides/developer-workflows/beginner/welcome-to-warp": + "/guides/getting-started/welcome-to-warp/", + + # guides/developer-workflows/devops ---------------------------------- + "guides/developer-workflows/devops/how-to-analyze-cloud-run-logs-gcloud": + "/guides/devops/how-to-analyze-cloud-run-logs-gcloud/", + "guides/developer-workflows/devops/how-to-create-a-production-ready-docker-setup": + "/guides/devops/how-to-create-a-production-ready-docker-setup/", + + # guides/developer-workflows/frontend-ui ---------------------------- + "guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind": + "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", + "guides/developer-workflows/frontend-ui/how-to-replace-a-ui-element-in-warp-rust-codebase": + "/guides/frontend/how-to-replace-a-ui-element-in-warp-rust-codebase/", + + # guides/developer-workflows (top-level) ---------------------------- + "guides/developer-workflows/how-to-review-ai-generated-code": + "/guides/agent-workflows/how-to-review-ai-generated-code/", + "guides/developer-workflows/how-to-run-multiple-ai-coding-agents": + "/guides/agent-workflows/how-to-run-multiple-ai-coding-agents/", + "guides/developer-workflows/how-to-use-voice-and-images-to-prompt-coding-agents": + "/guides/agent-workflows/how-to-use-voice-and-images-to-prompt-coding-agents/", + "guides/developer-workflows/warp-for-product-managers": + "/guides/agent-workflows/warp-for-product-managers/", + + # guides/developer-workflows/power-user ----------------------------- + "guides/developer-workflows/power-user/how-to-configure-yolo-and-strategic-agent-profiles": + "/guides/configuration/how-to-configure-yolo-and-strategic-agent-profiles/", + "guides/developer-workflows/power-user/how-to-edit-agent-code-in-warp": + "/guides/agent-workflows/how-to-edit-agent-code-in-warp/", + "guides/developer-workflows/power-user/how-to-review-prs-like-a-senior-dev": + "/guides/agent-workflows/how-to-review-prs-like-a-senior-dev/", + "guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui": + "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", + "guides/developer-workflows/power-user/how-to-set-coding-best-practices": + "/guides/configuration/how-to-set-coding-best-practices/", + "guides/developer-workflows/power-user/how-to-set-coding-preferences-with-rules": + "/guides/configuration/how-to-set-coding-preferences-with-rules/", + "guides/developer-workflows/power-user/how-to-set-tech-stack-preferences-with-rules": + "/guides/configuration/how-to-set-tech-stack-preferences-with-rules/", + "guides/developer-workflows/power-user/how-to-set-up-self-serve-data-analytics-with-skills": + "/guides/configuration/how-to-set-up-self-serve-data-analytics-with-skills/", + "guides/developer-workflows/power-user/how-to-sync-your-monorepos": + "/guides/configuration/how-to-sync-your-monorepos/", + "guides/developer-workflows/power-user/how-to-use-agent-profiles-efficiently": + "/guides/configuration/how-to-use-agent-profiles-efficiently/", + + # guides/developer-workflows/testing-and-security ------------------- + "guides/developer-workflows/testing-and-security/how-to-generate-unit-and-security-tests-to-debug-faster": + "/guides/devops/how-to-generate-unit-and-security-tests-to-debug-faster/", + "guides/developer-workflows/testing-and-security/how-to-prevent-secrets-from-leaking": + "/guides/devops/how-to-prevent-secrets-from-leaking/", + + # guides/end-to-end-builds ------------------------------------------ + "guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css": + "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", + "guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway": + "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", + + # guides/how-warp-uses-warp ----------------------------------------- + "guides/how-warp-uses-warp/building-warps-input-with-warp": + "/guides/build-an-app-in-warp/building-warps-input-with-warp/", + "guides/how-warp-uses-warp/creating-rules-for-agents": + "/guides/configuration/creating-rules-for-agents/", + "guides/how-warp-uses-warp/running-multiple-agents-at-once-with-warp": + "/guides/agent-workflows/running-multiple-agents-at-once-with-warp/", + "guides/how-warp-uses-warp/understanding-your-codebase": + "/guides/agent-workflows/understanding-your-codebase/", + "guides/how-warp-uses-warp/using-images-as-context-with-warp": + "/guides/agent-workflows/using-images-as-context-with-warp/", + "guides/how-warp-uses-warp/using-mcp-servers-with-warp": + "/guides/external-tools/using-mcp-servers-with-warp/", + + # guides/integrations ----------------------------------------------- + "guides/integrations/how-to-set-up-codex-cli": + "/guides/external-tools/how-to-set-up-codex-cli/", + "guides/integrations/how-to-set-up-gemini-cli": + "/guides/external-tools/how-to-set-up-gemini-cli/", + "guides/integrations/how-to-set-up-ollama": + "/guides/external-tools/how-to-set-up-ollama/", + + # guides/mcp-servers ------------------------------------------------ + "guides/mcp-servers/context7-mcp-update-astro-project-with-best-practices": + "/guides/external-tools/context7-mcp-update-astro-project-with-best-practices/", + "guides/mcp-servers/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch": + "/guides/external-tools/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch/", + "guides/mcp-servers/github-mcp-summarizing-open-prs-and-creating-gh-issues": + "/guides/external-tools/github-mcp-summarizing-open-prs-and-creating-gh-issues/", + "guides/mcp-servers/linear-mcp-retrieve-issue-data": + "/guides/external-tools/linear-mcp-retrieve-issue-data/", + "guides/mcp-servers/linear-mcp-updating-tickets-with-a-lean-build-approach": + "/guides/external-tools/linear-mcp-updating-tickets-with-a-lean-build-approach/", + "guides/mcp-servers/puppeteer-mcp-scraping-amazon-web-reviews": + "/guides/external-tools/puppeteer-mcp-scraping-amazon-web-reviews/", + "guides/mcp-servers/sentry-mcp-fix-sentry-error-in-empower-website": + "/guides/external-tools/sentry-mcp-fix-sentry-error-in-empower-website/", + "guides/mcp-servers/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up": + "/guides/external-tools/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up/", + + # guides/terminal-command-line-tips --------------------------------- + "guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm": + "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", + + # guides/warp-runtime (page removed, send to guides landing) -------- + "guides/warp-runtime/building-a-slackbot": "/guides/", + + # Known traffic gaps from earlier data analysis --------------------- + "support-and-community/plans-and-billing/bring-your-own-api-key": + "/agent-platform/inference/bring-your-own-api-key/", + "agent-platform/capabilities/model-choice": + "/agent-platform/inference/model-choice/", +} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dry-run", action="store_true", + help="Print entries without writing to vercel.json") + args = parser.parse_args() + + vercel_path = Path(__file__).parent.parent / "vercel.json" + with open(vercel_path) as f: + vercel = json.load(f) + + existing = { + r["source"].lower().rstrip("/").split("#")[0] + for r in vercel.get("redirects", []) + } + + new_entries = [] + skipped = [] + for old, dest in MAPPINGS.items(): + source = f"/{old}" + if source.lower().rstrip("/") in existing: + skipped.append(source) + continue + new_entries.append({"source": source, "destination": dest, "statusCode": 308}) + + print(f"New entries: {len(new_entries)} | Already covered: {len(skipped)}") + for e in new_entries: + print(f" {e['source']}") + print(f" → {e['destination']}") + + if args.dry_run: + print("\n[DRY RUN] vercel.json not modified.") + return + + vercel["redirects"].extend(new_entries) + with open(vercel_path, "w") as f: + json.dump(vercel, f, indent=2) + f.write("\n") + + print(f"\nWrote {len(new_entries)} new redirects to {vercel_path.name}.") + + +if __name__ == "__main__": + main() diff --git a/scripts/audit_redirects.py b/scripts/audit_redirects.py new file mode 100644 index 00000000..256fd2df --- /dev/null +++ b/scripts/audit_redirects.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Audit redirect coverage: extract every URL path that was ever live on the +old GitBook docs and identify which ones are NOT covered by a redirect in +vercel.json (i.e., would return a 404 on the new Astro/Starlight site). + +Usage: + python3 scripts/audit_redirects.py [--gitbook-root PATH] + +Output: + Prints uncovered paths to stdout and writes them to + scripts/redirect_audit_gaps.txt. +""" + +import re +import json +import argparse +from pathlib import Path + +# --------------------------------------------------------------------------- +# GitBook space → URL prefix mapping. +# Each key is relative to the gitbook repo root; value is the URL prefix +# (empty string = served at root, no prefix). +# --------------------------------------------------------------------------- +SPACES = { + "docs/warp": "", # served at / + "docs/agent-platform": "agent-platform", + "docs/reference": "reference", + "docs/support-and-community":"support-and-community", + "docs/enterprise": "enterprise", + "docs/changelog": "changelog", + "guides": "guides", # Warp University +} + + +def extract_paths_from_summary(summary_path: Path) -> list[str]: + """Extract all .md file paths referenced in a SUMMARY.md.""" + paths = [] + pattern = re.compile(r'\]\(([^)#"]+\.md)[^)]*\)') + with open(summary_path, encoding="utf-8") as f: + for line in f: + for match in pattern.finditer(line): + raw = match.group(1).strip() + # Skip external URLs + if raw.startswith("http"): + continue + paths.append(raw) + return paths + + +def md_path_to_url(md_path: str, prefix: str) -> str: + """Convert a markdown file path to its URL on docs.warp.dev.""" + # Remove .md extension + url = md_path.removesuffix(".md") + + # README.md at root of space = index page + if url == "README": + url = "" + # README.md in a subdirectory = that directory's index + elif url.endswith("/README"): + url = url.removesuffix("/README") + + # Apply space prefix + if prefix: + if url: + url = f"{prefix}/{url}" + else: + url = prefix + + # Normalise: lowercase, remove trailing slash + url = url.lower().rstrip("/") + return url + + +def load_vercel_redirect_sources(vercel_json_path: Path) -> set[str]: + """Load all redirect source paths from vercel.json.""" + with open(vercel_json_path, encoding="utf-8") as f: + data = json.load(f) + sources = set() + for r in data.get("redirects", []): + src = r.get("source", "").lower().rstrip("/") + # Remove anchor fragments if present + src = src.split("#")[0].rstrip("/") + sources.add(src) + return sources + + +def load_live_astro_paths(docs_src: Path) -> set[str]: + """Collect all URL paths that exist as pages in the Astro site.""" + live = set() + content_root = docs_src / "content" / "docs" + for mdx_file in content_root.rglob("*.mdx"): + relative = mdx_file.relative_to(content_root) + # index.mdx → the directory path; foo.mdx → foo + parts = list(relative.parts) + if parts[-1] in ("index.mdx",): + parts = parts[:-1] + else: + parts[-1] = parts[-1].removesuffix(".mdx") + url = "/".join(parts).lower().rstrip("/") + live.add(url) + # Add the site root + live.add("") + live.add("quickstart") + return live + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--gitbook-root", + default=str(Path(__file__).parent.parent.parent / "gitbook"), + help="Path to the old GitBook repo root (default: ../gitbook relative to docs/)", + ) + args = parser.parse_args() + + gitbook_root = Path(args.gitbook_root) + docs_root = Path(__file__).parent.parent # the Astro docs repo + vercel_json_path = docs_root / "vercel.json" + + print(f"GitBook root: {gitbook_root}") + print(f"Docs (Astro) root: {docs_root}") + print() + + # 1. Extract all old GitBook URL paths + all_gitbook_urls: set[str] = set() + for space_rel, prefix in SPACES.items(): + summary_path = gitbook_root / space_rel / "SUMMARY.md" + if not summary_path.exists(): + print(f" [SKIP] {summary_path} not found") + continue + md_paths = extract_paths_from_summary(summary_path) + for mp in md_paths: + url = md_path_to_url(mp, prefix) + all_gitbook_urls.add(url) + print(f" {space_rel}: {len(md_paths)} paths extracted") + + print(f"\nTotal unique GitBook paths: {len(all_gitbook_urls)}") + + # 2. Load existing vercel.json redirect sources + redirect_sources = load_vercel_redirect_sources(vercel_json_path) + print(f"Existing vercel.json redirect sources: {len(redirect_sources)}") + + # 3. Load live Astro pages + live_astro = load_live_astro_paths(docs_root / "src") + print(f"Live Astro pages: {len(live_astro)}") + + # 4. Find gaps: paths that are neither live in Astro nor covered by a redirect + gaps = sorted( + url for url in all_gitbook_urls + if url not in live_astro + and f"/{url}" not in redirect_sources + and url not in redirect_sources + ) + + print(f"\n{'='*60}") + print(f"UNCOVERED PATHS (not live in Astro + no redirect): {len(gaps)}") + print(f"{'='*60}\n") + + for gap in gaps: + print(f" /{gap}") + + # 5. Write to file for further processing + output_path = docs_root / "scripts" / "redirect_audit_gaps.txt" + with open(output_path, "w", encoding="utf-8") as f: + f.write(f"# Redirect audit gaps — {len(gaps)} uncovered GitBook paths\n") + f.write("# These were live on GitBook but have no redirect or live page in the Astro site.\n") + f.write("# Format: /old-path\n\n") + for gap in gaps: + f.write(f"/{gap}\n") + + print(f"\nWrote {len(gaps)} gaps to: {output_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/redirect_audit_gaps.txt b/scripts/redirect_audit_gaps.txt new file mode 100644 index 00000000..d1c0d793 --- /dev/null +++ b/scripts/redirect_audit_gaps.txt @@ -0,0 +1,56 @@ +# Redirect audit gaps — 52 uncovered GitBook paths +# These were live on GitBook but have no redirect or live page in the Astro site. +# Format: /old-path + +/agent-platform/warp-agents +/guides/developer-workflows/backend/how-to-create-priority-matrix-for-database-optimization +/guides/developer-workflows/backend/how-to-write-sql-commands-inside-a-postgres-repl +/guides/developer-workflows/beginner/10-coding-features-you-should-know +/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind +/guides/developer-workflows/beginner/how-to-customize-warps-appearance +/guides/developer-workflows/beginner/how-to-explain-your-codebase-using-warp-rust-codebase +/guides/developer-workflows/beginner/how-to-make-warps-ui-more-minimal +/guides/developer-workflows/beginner/how-to-master-warps-code-review-panel +/guides/developer-workflows/beginner/trigger-reusable-actions-with-saved-prompts +/guides/developer-workflows/beginner/welcome-to-warp +/guides/developer-workflows/devops/how-to-analyze-cloud-run-logs-gcloud +/guides/developer-workflows/devops/how-to-create-a-production-ready-docker-setup +/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind +/guides/developer-workflows/frontend-ui/how-to-replace-a-ui-element-in-warp-rust-codebase +/guides/developer-workflows/how-to-review-ai-generated-code +/guides/developer-workflows/how-to-run-multiple-ai-coding-agents +/guides/developer-workflows/how-to-use-voice-and-images-to-prompt-coding-agents +/guides/developer-workflows/power-user/how-to-configure-yolo-and-strategic-agent-profiles +/guides/developer-workflows/power-user/how-to-edit-agent-code-in-warp +/guides/developer-workflows/power-user/how-to-review-prs-like-a-senior-dev +/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui +/guides/developer-workflows/power-user/how-to-set-coding-best-practices +/guides/developer-workflows/power-user/how-to-set-coding-preferences-with-rules +/guides/developer-workflows/power-user/how-to-set-tech-stack-preferences-with-rules +/guides/developer-workflows/power-user/how-to-set-up-self-serve-data-analytics-with-skills +/guides/developer-workflows/power-user/how-to-sync-your-monorepos +/guides/developer-workflows/power-user/how-to-use-agent-profiles-efficiently +/guides/developer-workflows/testing-and-security/how-to-generate-unit-and-security-tests-to-debug-faster +/guides/developer-workflows/testing-and-security/how-to-prevent-secrets-from-leaking +/guides/developer-workflows/warp-for-product-managers +/guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css +/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway +/guides/how-warp-uses-warp/building-warps-input-with-warp +/guides/how-warp-uses-warp/creating-rules-for-agents +/guides/how-warp-uses-warp/running-multiple-agents-at-once-with-warp +/guides/how-warp-uses-warp/understanding-your-codebase +/guides/how-warp-uses-warp/using-images-as-context-with-warp +/guides/how-warp-uses-warp/using-mcp-servers-with-warp +/guides/integrations/how-to-set-up-codex-cli +/guides/integrations/how-to-set-up-gemini-cli +/guides/integrations/how-to-set-up-ollama +/guides/mcp-servers/context7-mcp-update-astro-project-with-best-practices +/guides/mcp-servers/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch +/guides/mcp-servers/github-mcp-summarizing-open-prs-and-creating-gh-issues +/guides/mcp-servers/linear-mcp-retrieve-issue-data +/guides/mcp-servers/linear-mcp-updating-tickets-with-a-lean-build-approach +/guides/mcp-servers/puppeteer-mcp-scraping-amazon-web-reviews +/guides/mcp-servers/sentry-mcp-fix-sentry-error-in-empower-website +/guides/mcp-servers/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up +/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm +/guides/warp-runtime/building-a-slackbot diff --git a/vercel.json b/vercel.json index ea1f47f2..343ee9e4 100644 --- a/vercel.json +++ b/vercel.json @@ -73,12 +73,24 @@ "rewrites": [ { "source": "/", - "has": [{ "type": "header", "key": "accept", "value": ".*text/markdown.*" }], + "has": [ + { + "type": "header", + "key": "accept", + "value": ".*text/markdown.*" + } + ], "destination": "/index.md" }, { "source": "/:path+", - "has": [{ "type": "header", "key": "accept", "value": ".*text/markdown.*" }], + "has": [ + { + "type": "header", + "key": "accept", + "value": ".*text/markdown.*" + } + ], "destination": "/:path+.md" } ], @@ -9647,6 +9659,266 @@ "source": "/support-and-community/plans-and-billing/custom-inference-endpoint", "destination": "/agent-platform/inference/custom-inference-endpoint/", "statusCode": 308 + }, + { + "source": "/agent-platform/warp-agents", + "destination": "/agent-platform/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/backend/how-to-create-priority-matrix-for-database-optimization", + "destination": "/guides/devops/how-to-create-priority-matrix-for-database-optimization/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/backend/how-to-write-sql-commands-inside-a-postgres-repl", + "destination": "/guides/devops/how-to-write-sql-commands-inside-a-postgres-repl/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/10-coding-features-you-should-know", + "destination": "/guides/getting-started/10-coding-features-you-should-know/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind", + "destination": "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/how-to-customize-warps-appearance", + "destination": "/guides/getting-started/how-to-customize-warps-appearance/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/how-to-explain-your-codebase-using-warp-rust-codebase", + "destination": "/guides/agent-workflows/how-to-explain-your-codebase-using-warp-rust-codebase/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/how-to-make-warps-ui-more-minimal", + "destination": "/guides/getting-started/how-to-make-warps-ui-more-minimal/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/how-to-master-warps-code-review-panel", + "destination": "/guides/getting-started/how-to-master-warps-code-review-panel/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/trigger-reusable-actions-with-saved-prompts", + "destination": "/guides/configuration/trigger-reusable-actions-with-saved-prompts/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/beginner/welcome-to-warp", + "destination": "/guides/getting-started/welcome-to-warp/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/devops/how-to-analyze-cloud-run-logs-gcloud", + "destination": "/guides/devops/how-to-analyze-cloud-run-logs-gcloud/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/devops/how-to-create-a-production-ready-docker-setup", + "destination": "/guides/devops/how-to-create-a-production-ready-docker-setup/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind", + "destination": "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/frontend-ui/how-to-replace-a-ui-element-in-warp-rust-codebase", + "destination": "/guides/frontend/how-to-replace-a-ui-element-in-warp-rust-codebase/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/how-to-review-ai-generated-code", + "destination": "/guides/agent-workflows/how-to-review-ai-generated-code/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/how-to-run-multiple-ai-coding-agents", + "destination": "/guides/agent-workflows/how-to-run-multiple-ai-coding-agents/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/how-to-use-voice-and-images-to-prompt-coding-agents", + "destination": "/guides/agent-workflows/how-to-use-voice-and-images-to-prompt-coding-agents/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/warp-for-product-managers", + "destination": "/guides/agent-workflows/warp-for-product-managers/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-configure-yolo-and-strategic-agent-profiles", + "destination": "/guides/configuration/how-to-configure-yolo-and-strategic-agent-profiles/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-edit-agent-code-in-warp", + "destination": "/guides/agent-workflows/how-to-edit-agent-code-in-warp/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-review-prs-like-a-senior-dev", + "destination": "/guides/agent-workflows/how-to-review-prs-like-a-senior-dev/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui", + "destination": "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-set-coding-best-practices", + "destination": "/guides/configuration/how-to-set-coding-best-practices/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-set-coding-preferences-with-rules", + "destination": "/guides/configuration/how-to-set-coding-preferences-with-rules/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-set-tech-stack-preferences-with-rules", + "destination": "/guides/configuration/how-to-set-tech-stack-preferences-with-rules/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-set-up-self-serve-data-analytics-with-skills", + "destination": "/guides/configuration/how-to-set-up-self-serve-data-analytics-with-skills/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-sync-your-monorepos", + "destination": "/guides/configuration/how-to-sync-your-monorepos/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/power-user/how-to-use-agent-profiles-efficiently", + "destination": "/guides/configuration/how-to-use-agent-profiles-efficiently/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/testing-and-security/how-to-generate-unit-and-security-tests-to-debug-faster", + "destination": "/guides/devops/how-to-generate-unit-and-security-tests-to-debug-faster/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/testing-and-security/how-to-prevent-secrets-from-leaking", + "destination": "/guides/devops/how-to-prevent-secrets-from-leaking/", + "statusCode": 308 + }, + { + "source": "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css", + "destination": "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", + "statusCode": 308 + }, + { + "source": "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway", + "destination": "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/building-warps-input-with-warp", + "destination": "/guides/build-an-app-in-warp/building-warps-input-with-warp/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/creating-rules-for-agents", + "destination": "/guides/configuration/creating-rules-for-agents/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/running-multiple-agents-at-once-with-warp", + "destination": "/guides/agent-workflows/running-multiple-agents-at-once-with-warp/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/understanding-your-codebase", + "destination": "/guides/agent-workflows/understanding-your-codebase/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/using-images-as-context-with-warp", + "destination": "/guides/agent-workflows/using-images-as-context-with-warp/", + "statusCode": 308 + }, + { + "source": "/guides/how-warp-uses-warp/using-mcp-servers-with-warp", + "destination": "/guides/external-tools/using-mcp-servers-with-warp/", + "statusCode": 308 + }, + { + "source": "/guides/integrations/how-to-set-up-codex-cli", + "destination": "/guides/external-tools/how-to-set-up-codex-cli/", + "statusCode": 308 + }, + { + "source": "/guides/integrations/how-to-set-up-gemini-cli", + "destination": "/guides/external-tools/how-to-set-up-gemini-cli/", + "statusCode": 308 + }, + { + "source": "/guides/integrations/how-to-set-up-ollama", + "destination": "/guides/external-tools/how-to-set-up-ollama/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/context7-mcp-update-astro-project-with-best-practices", + "destination": "/guides/external-tools/context7-mcp-update-astro-project-with-best-practices/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch", + "destination": "/guides/external-tools/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/github-mcp-summarizing-open-prs-and-creating-gh-issues", + "destination": "/guides/external-tools/github-mcp-summarizing-open-prs-and-creating-gh-issues/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/linear-mcp-retrieve-issue-data", + "destination": "/guides/external-tools/linear-mcp-retrieve-issue-data/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/linear-mcp-updating-tickets-with-a-lean-build-approach", + "destination": "/guides/external-tools/linear-mcp-updating-tickets-with-a-lean-build-approach/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/puppeteer-mcp-scraping-amazon-web-reviews", + "destination": "/guides/external-tools/puppeteer-mcp-scraping-amazon-web-reviews/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/sentry-mcp-fix-sentry-error-in-empower-website", + "destination": "/guides/external-tools/sentry-mcp-fix-sentry-error-in-empower-website/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up", + "destination": "/guides/external-tools/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up/", + "statusCode": 308 + }, + { + "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", + "destination": "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", + "statusCode": 308 + }, + { + "source": "/guides/warp-runtime/building-a-slackbot", + "destination": "/guides/", + "statusCode": 308 } ] } From 99247f6cd434f0f294ab0224292c4ec76e119f52 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:44:14 -0600 Subject: [PATCH 2/6] feat: add weekly-404-monitor skill for scheduled 404 gap detection Adds a new Oz skill + helper script for a recurring Monday morning agent that surfaces broken docs.warp.dev URLs, diffs them against vercel.json, and posts a Slack summary. The skill (SKILL.md) defines the full runbook: - Queries stg_website_events for docs_404 track events (past 7 days) - Loads vercel.json redirect sources from disk or GitHub raw URL - Identifies uncovered broken URLs and computes week-over-week delta - Posts a Block Kit Slack summary to the docs team channel - Writes a CSV artifact to data/404-reports/ (run artifact, not committed) - Handles no-data state (when PR #191 hasn't been live long enough) - Handles failure gracefully with Slack error notices The helper (run_404_report.py) implements the BigQuery queries via Metabase API, diff logic, and CSV writing. Outputs JSON summary to stdout for the agent to use when constructing the Slack message. Schedule: every Monday 9am PT (cron: 0 17 * * 1 UTC). Deploy via oz.warp.dev once METABASE_API_KEY, SLACK_BOT_TOKEN, and SLACK_CHANNEL_ID are sSLACK_CHANNEL_ID are sSLACK_CHANNEL_ID a Part of the broken-redirecPart of the broken-redirecPart of the broken-regent@warp.dev> --- .agents/skills/weekly-404-monitor/SKILL.md | 135 ++++++++++ .../weekly-404-monitor/run_404_report.py | 251 ++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 .agents/skills/weekly-404-monitor/SKILL.md create mode 100644 .agents/skills/weekly-404-monitor/run_404_report.py diff --git a/.agents/skills/weekly-404-monitor/SKILL.md b/.agents/skills/weekly-404-monitor/SKILL.md new file mode 100644 index 00000000..f5d9dd27 --- /dev/null +++ b/.agents/skills/weekly-404-monitor/SKILL.md @@ -0,0 +1,135 @@ +--- +name: weekly-404-monitor +description: Weekly recurring agent that surfaces broken docs.warp.dev URLs by querying the docs_404 Rudderstack track event, diffing against existing vercel.json redirects, and posting a summary to Slack. Use for the Monday 9am PT scheduled Oz agent that monitors 404 gaps and supports the ongoing redirect-fix workflow. +--- + +# Weekly 404 monitor + +Runs every Monday at 9am PT. Identifies new broken URL patterns on docs.warp.dev, surfaces the top uncovered paths, and posts a concise Slack summary so the docs team can prioritize redirect additions. + +## Prerequisites + +The following environment secrets must be set in the Oz cloud agent environment: + +- `METABASE_API_KEY` — Metabase API key for BigQuery queries. If unavailable, the run must fail fast with a clear error. +- `SLACK_BOT_TOKEN` — Slack bot token for posting to the docs channel. If unavailable, write a no-post report to the run output instead. +- `SLACK_CHANNEL_ID` — Slack channel ID for the docs team (e.g. `#docs` or `#growth-docs`). Fall back to `SLACK_CHANNEL_ID=C0123456789` if not set explicitly. + +Do NOT print, log, or include secret values in reports, commits, or Slack messages. + +## Workflow + +### 1. Query docs_404 events + +Run `python3 .agents/skills/weekly-404-monitor/run_404_report.py` in the docs repo. + +The script: +- Queries `warp-data-357114.prod.stg_website_events` via the Metabase API +- Extracts `broken_url` from `event_properties` for all `event_name = 'docs_404'` events in the past 7 days +- Groups by `broken_url`, sorted by hit count descending +- Returns a ranked list of broken URLs and their hit counts for the current week +- Computes the same for the prior week (days 8–14) for trend comparison +- Total weekly 404 count (current + prior) for the trend line + +### 2. Fetch current vercel.json redirect sources + +Fetch `vercel.json` from the docs repo (already checked out locally in the cloud environment, or via GitHub raw URL `https://raw.githubusercontent.com/warpdotdev/docs/main/vercel.json`). + +Extract all `source` values from the `redirects` array. Normalise: lowercase, strip trailing slashes and anchor fragments. + +### 3. Find uncovered URLs + +For each broken URL in the current week's data: +- Normalise (lowercase, strip trailing slash, strip query params and fragments) +- Check if it exists as a `source` in `vercel.json` redirects +- If not covered, it is a **gap** + +### 4. Compute delta vs prior week + +Compare this week's uncovered gaps against last week's uncovered gaps (from step 1 prior-week query). + +**New gaps** = uncovered this week AND not seen as uncovered last week. +**Resolved** = uncovered last week AND now either covered (has redirect) or no longer generating 404s. + +### 5. Post Slack summary + +Post a Slack message using the Block Kit format defined in the "Slack message format" section below. + +If `SLACK_BOT_TOKEN` is unavailable, write the full Slack message body to the run output instead and note that Slack posting was skipped. + +### 6. Write CSV artifact + +Write `404-report-YYYY-MM-DD.csv` to `data/404-reports/` in the docs repo working directory. Format: + +``` +broken_url,hits_this_week,hits_last_week,is_covered_by_redirect,is_new_gap +/old/path,42,0,false,true +/another/path,18,22,false,false +``` + +Do NOT commit this file to the repo. It is an Oz run artifact only — readable from the Oz web app Runs page. + +## Slack message format + +Use Slack Block Kit. The message should be scannable in under 30 seconds. + +``` +📊 *docs.warp.dev 404 Report* — week of {YYYY-MM-DD} + +*Total 404s this week:* {N} ({+N / -N vs last week}) +*Uncovered broken URLs:* {M} ({+N new this week}) + +*Top 10 uncovered URLs (by hits):* +{hit_count} `/path` {🆕 if new this week} +... + +*{K} resolved since last week* (redirect added or traffic stopped) + +→ Add missing redirects: `vercel.json` › `redirects` array (PR against `main`) +→ Full breakdown: {oz_run_url} +``` + +Rules: +- Cap the list at 10 entries. If there are more, note "and N more — see full CSV in the run." +- Mark new gaps with 🆕. +- If total 404s this week is less than 50, add a brief positive note: "404 volume is low — good signal that redirect coverage is working." +- Never include raw user data (e.g. query strings with user IDs, tokens) in the Slack message. Strip query params from broken_url before displaying. + +## Self-review before posting + +Before posting to Slack, verify: +- The `docs_404` event exists in `stg_website_events` for the query window. If the table has no rows for `event_name = 'docs_404'`, it means PR #191 has not been live long enough to collect data. Post a clear "no data yet" message to Slack and end the run. +- The Metabase query completed successfully (HTTP 200, no `error` field in the response body). +- The `broken_url` field was present in the event properties for at least some rows. If it is consistently null, the `docs_404` tracking implementation has a bug — report it in the Slack message and tag the docs team. +- The vercel.json redirect list was loaded successfully and contains more than 500 entries (sanity check that the file is not truncated). +- The CSV artifact was written before posting to Slack. + +## No-data report + +If `stg_website_events` returns 0 rows for `event_name = 'docs_404'` in the past 7 days, post this Slack message: + +``` +⏳ *docs.warp.dev 404 Report* — week of {YYYY-MM-DD} + +No `docs_404` events recorded yet. This is expected if PR #191 (404 instrumentation) has been live for less than a week, or if the Rudderstack write key is not set in the Vercel environment. + +Check: Vercel project env vars include `PUBLIC_RUDDERSTACK_WRITE_KEY` and `PUBLIC_RUDDERSTACK_DATA_PLANE_URL`. +``` + +## Failure handling + +- If the Metabase query fails (non-200, timeout, or query error), post a brief failure notice to Slack, include the error message, and end the run with a non-zero exit code. +- Do NOT silently swallow errors or post incomplete data as if it were complete. +- Log all HTTP requests and responses to stdout for debugging via the Oz run log viewer. + +## Scheduling + +This skill is designed for an Oz scheduled agent with a weekly cron trigger: every Monday at 9am PT (`0 17 * * 1` in UTC). + +To deploy: +1. Push this skill to `main` in the docs repo. +2. In the Oz web app (oz.warp.dev), create a new scheduled agent: + - **Skill**: `weekly-404-monitor` from `warpdotdev/docs` + - **Schedule**: `0 17 * * 1` (UTC) = 9am PT + - **Environment**: docs-monitoring (must include `METABASE_API_KEY`, `SLACK_BOT_TOKEN`, `SLACK_CHANNEL_ID`) + - **Repo**: `warpdotdev/docs` (main branch) diff --git a/.agents/skills/weekly-404-monitor/run_404_report.py b/.agents/skills/weekly-404-monitor/run_404_report.py new file mode 100644 index 00000000..6f70cebe --- /dev/null +++ b/.agents/skills/weekly-404-monitor/run_404_report.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +Weekly 404 monitor — data collection script. + +Queries the docs_404 Rudderstack track event from stg_website_events via +the Metabase API, diffs against vercel.json redirect sources, and writes: + - JSON report to stdout (for the agent to parse) + - CSV artifact to data/404-reports/YYYY-MM-DD.csv + +Usage (called by the weekly-404-monitor skill): + python3 .agents/skills/weekly-404-monitor/run_404_report.py + +Required env vars: + METABASE_API_KEY — Metabase API key + +Optional env vars: + VERCEL_JSON_PATH — Path to vercel.json (default: ./vercel.json) + REPORT_DIR — Output directory for CSV artifacts (default: ./data/404-reports) +""" + +import csv +import json +import os +import re +import sys +import urllib.error +import urllib.request +from datetime import date, timedelta +from pathlib import Path + + +BASE = "https://warp.metabaseapp.com/api" +DB_ID = 2 # BigQuery prod + + +def metabase_headers(): + key = os.environ.get("METABASE_API_KEY") + if not key: + print("ERROR: METABASE_API_KEY is not set.", file=sys.stderr) + sys.exit(1) + return {"X-API-Key": key, "Content-Type": "application/json"} + + +def run_query(sql: str) -> list[dict]: + """Execute a BigQuery SQL query via the Metabase /dataset endpoint.""" + headers = metabase_headers() + body = json.dumps({ + "database": DB_ID, + "type": "native", + "native": {"query": sql}, + }).encode() + req = urllib.request.Request(f"{BASE}/dataset", data=body, headers=headers) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read()) + except urllib.error.HTTPError as e: + print(f"ERROR: Metabase query failed: HTTP {e.code}: {e.read().decode()[:500]}", + file=sys.stderr) + sys.exit(1) + + if result.get("error"): + print(f"ERROR: Metabase query error: {result['error']}", file=sys.stderr) + sys.exit(1) + + data = result.get("data", {}) + cols = [c["name"] for c in data.get("cols", [])] + rows = data.get("rows", []) + return [dict(zip(cols, row)) for row in rows] + + +def query_404_events(days_start: int, days_end: int) -> list[dict]: + """ + Return broken_url counts for the window [days_start, days_end) days ago. + days_start=1, days_end=8 → past 7 days (current week) + days_start=8, days_end=15 → 8-14 days ago (prior week) + """ + sql = f""" +SELECT + REGEXP_REPLACE( + SPLIT(JSON_VALUE(event_properties, '$.broken_url'), '?')[OFFSET(0)], + r'#.*$', '' + ) AS broken_url, + COUNT(*) AS hits +FROM `warp-data-357114.prod.stg_website_events` +WHERE event_type = 'track' + AND event_name = 'docs_404' + AND JSON_VALUE(event_properties, '$.broken_url') IS NOT NULL + AND event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL {days_end - 1} DAY) + AND event_date < DATE_SUB(CURRENT_DATE(), INTERVAL {days_start - 1} DAY) +GROUP BY 1 +HAVING broken_url IS NOT NULL AND broken_url != '' +ORDER BY 2 DESC +LIMIT 500 +""" + return run_query(sql) + + +def total_404_count(days_start: int, days_end: int) -> int: + sql = f""" +SELECT COUNT(*) AS total +FROM `warp-data-357114.prod.stg_website_events` +WHERE event_type = 'track' + AND event_name = 'docs_404' + AND event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL {days_end - 1} DAY) + AND event_date < DATE_SUB(CURRENT_DATE(), INTERVAL {days_start - 1} DAY) +""" + rows = run_query(sql) + return int(rows[0]["total"]) if rows else 0 + + +def load_redirect_sources(vercel_json_path: Path) -> set[str]: + """Load all redirect source paths from vercel.json, normalised.""" + if not vercel_json_path.exists(): + # Try fetching from GitHub + url = "https://raw.githubusercontent.com/warpdotdev/docs/main/vercel.json" + try: + with urllib.request.urlopen(url, timeout=10) as resp: + data = json.loads(resp.read()) + except Exception as e: + print(f"ERROR: Could not load vercel.json from disk or GitHub: {e}", + file=sys.stderr) + sys.exit(1) + else: + with open(vercel_json_path) as f: + data = json.load(f) + + redirects = data.get("redirects", []) + if len(redirects) < 500: + print(f"WARNING: vercel.json has only {len(redirects)} redirects — " + "sanity check failed (expected 500+). Data may be incomplete.", + file=sys.stderr) + + sources = set() + for r in redirects: + src = r.get("source", "").lower().rstrip("/").split("#")[0].split("?")[0] + sources.add(src) + return sources + + +def normalise_url(url: str) -> str: + """Normalise a broken URL for comparison against vercel.json sources.""" + if not url: + return "" + # Extract just the path (no scheme/host) + url = re.sub(r"^https?://[^/]+", "", url) + # Strip query params and fragments + url = url.split("?")[0].split("#")[0] + # Lowercase, strip trailing slash + url = url.lower().rstrip("/") + return url or "/" + + +def main(): + vercel_path = Path(os.environ.get("VERCEL_JSON_PATH", "vercel.json")) + report_dir = Path(os.environ.get("REPORT_DIR", "data/404-reports")) + today = date.today() + + print(f"Running weekly 404 report for week ending {today}", file=sys.stderr) + + # 1. Query current and prior week + print("Querying current week (past 7 days)...", file=sys.stderr) + current_week = query_404_events(1, 8) + print(f" {len(current_week)} unique broken URLs found", file=sys.stderr) + + print("Querying prior week (days 8-14)...", file=sys.stderr) + prior_week = query_404_events(8, 15) + + total_current = total_404_count(1, 8) + total_prior = total_404_count(8, 15) + + # 2. Load redirect sources + print("Loading vercel.json redirect sources...", file=sys.stderr) + redirect_sources = load_redirect_sources(vercel_path) + + # 3. Build prior-week gap set for delta calculation + prior_gaps: set[str] = set() + for row in prior_week: + norm = normalise_url(row["broken_url"]) + if norm and norm not in redirect_sources: + prior_gaps.add(norm) + + # 4. Build current-week report + report_rows = [] + for row in current_week: + raw_url = row.get("broken_url") or "" + norm = normalise_url(raw_url) + if not norm: + continue + hits_current = int(row.get("hits") or 0) + hits_prior = next( + (int(r["hits"]) for r in prior_week + if normalise_url(r.get("broken_url") or "") == norm), + 0 + ) + is_covered = norm in redirect_sources + is_new_gap = (not is_covered) and (norm not in prior_gaps) + + report_rows.append({ + "broken_url": norm, + "hits_this_week": hits_current, + "hits_last_week": hits_prior, + "is_covered_by_redirect": is_covered, + "is_new_gap": is_new_gap, + }) + + # 5. Compute resolved (was a gap last week, is no longer generating hits) + current_urls = {normalise_url(r["broken_url"]) for r in current_week} + newly_covered = { + g for g in prior_gaps + if g in redirect_sources # redirect was added + } + traffic_stopped = { + g for g in prior_gaps + if g not in current_urls and g not in redirect_sources # stopped naturally + } + resolved_count = len(newly_covered) + len(traffic_stopped) + + # 6. Write CSV artifact + report_dir.mkdir(parents=True, exist_ok=True) + csv_path = report_dir / f"404-report-{today.isoformat()}.csv" + with open(csv_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=[ + "broken_url", "hits_this_week", "hits_last_week", + "is_covered_by_redirect", "is_new_gap" + ]) + writer.writeheader() + writer.writerows(report_rows) + print(f"Wrote CSV artifact: {csv_path}", file=sys.stderr) + + # 7. Output JSON summary to stdout for the agent + uncovered = [r for r in report_rows if not r["is_covered_by_redirect"]] + new_gaps = [r for r in uncovered if r["is_new_gap"]] + + summary = { + "report_date": today.isoformat(), + "total_404s_this_week": total_current, + "total_404s_last_week": total_prior, + "trend_delta": total_current - total_prior, + "uncovered_count": len(uncovered), + "new_gaps_count": len(new_gaps), + "resolved_count": resolved_count, + "top_10_uncovered": uncovered[:10], + "csv_path": str(csv_path), + "has_data": len(current_week) > 0, + } + + print(json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + main() From afc0a854c2642c8a6d9be5b76e1cf5ea8e386fad Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:51:24 -0600 Subject: [PATCH 3/6] fix: consolidate redirects with globs, add %2B variants for + paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two review comments on PR #192: Petra's suggestion: collapse 3 groups of redirects with identical slug-to-slug mappings into Vercel :slug* glob rules: - /guides/integrations/:slug* → /guides/external-tools/:slug/ - /guides/mcp-servers/:slug* → /guides/external-tools/:slug/ - /guides/developer-workflows/devops/:slug* → /guides/devops/:slug/ 13 individual rules → 3 globs (future GitBook paths in these sections will also be covered automatically). oz-for-oss bot: add %2B-encoded duplicates for the 6 source paths that contain literal '+' characters. Vercel uses path-to-regexp for source matching where '+' may be misinterpreted. The %2B variants ensure URL-encoded requests are also covered. Co-Authored-By: Oz --- scripts/fix_redirects_globs.py | 105 +++++++++++++++++++++++++++++++++ vercel.json | 62 +++++++------------ 2 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 scripts/fix_redirects_globs.py diff --git a/scripts/fix_redirects_globs.py b/scripts/fix_redirects_globs.py new file mode 100644 index 00000000..ff3baced --- /dev/null +++ b/scripts/fix_redirects_globs.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Apply two improvements to the new redirects in vercel.json: +1. Replace 13 individual rules with 3 glob patterns (Petra's suggestion) +2. Add %2B-encoded duplicates for the 6 source paths containing literal '+' +""" +import json +from pathlib import Path + +vercel_path = Path(__file__).parent.parent / "vercel.json" + +# 1. Rules to remove (will be replaced by globs) +REMOVE = { + "/guides/integrations/how-to-set-up-codex-cli", + "/guides/integrations/how-to-set-up-gemini-cli", + "/guides/integrations/how-to-set-up-ollama", + "/guides/mcp-servers/context7-mcp-update-astro-project-with-best-practices", + "/guides/mcp-servers/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch", + "/guides/mcp-servers/github-mcp-summarizing-open-prs-and-creating-gh-issues", + "/guides/mcp-servers/linear-mcp-retrieve-issue-data", + "/guides/mcp-servers/linear-mcp-updating-tickets-with-a-lean-build-approach", + "/guides/mcp-servers/puppeteer-mcp-scraping-amazon-web-reviews", + "/guides/mcp-servers/sentry-mcp-fix-sentry-error-in-empower-website", + "/guides/mcp-servers/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up", + "/guides/developer-workflows/devops/how-to-analyze-cloud-run-logs-gcloud", + "/guides/developer-workflows/devops/how-to-create-a-production-ready-docker-setup", +} + +# 2. Paths with literal '+' that need %2B-encoded duplicates +PLUS_PATHS = [ + ( + "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind", + "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", + ), + ( + "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind", + "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", + ), + ( + "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui", + "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", + ), + ( + "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css", + "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", + ), + ( + "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway", + "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", + ), + ( + "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", + "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", + ), +] + +with open(vercel_path) as f: + vercel = json.load(f) + +redirects = vercel["redirects"] +before = len(redirects) + +# Remove individual rules being replaced by globs +redirects = [r for r in redirects if r["source"] not in REMOVE] +removed = before - len(redirects) +print(f"Removed {removed} individual rules") + +existing_sources = {r["source"].lower() for r in redirects} +new_entries = [] + +# Add glob rules +new_entries.append({ + "source": "/guides/integrations/:slug*", + "destination": "/guides/external-tools/:slug/", + "statusCode": 308, +}) +new_entries.append({ + "source": "/guides/mcp-servers/:slug*", + "destination": "/guides/external-tools/:slug/", + "statusCode": 308, +}) +new_entries.append({ + "source": "/guides/developer-workflows/devops/:slug*", + "destination": "/guides/devops/:slug/", + "statusCode": 308, +}) +print("Added 3 glob rules") + +# Add %2B-encoded duplicates for + paths +for src_plus, dest in PLUS_PATHS: + encoded = src_plus.replace("+", "%2B") + if encoded.lower() not in existing_sources: + new_entries.append({"source": encoded, "destination": dest, "statusCode": 308}) + print(f" %2B variant: {encoded[:70]}...") + +redirects.extend(new_entries) +vercel["redirects"] = redirects + +with open(vercel_path, "w") as f: + json.dump(vercel, f, indent=2) + f.write("\n") + +after = len(redirects) +print(f"\nDone. Redirects: {before} → {after} ({after - before:+d})") +print(f" Removed {removed}, added {len(new_entries)} (3 globs + 6 %2B variants)") diff --git a/vercel.json b/vercel.json index 343ee9e4..b3f5d894 100644 --- a/vercel.json +++ b/vercel.json @@ -9715,16 +9715,6 @@ "destination": "/guides/getting-started/welcome-to-warp/", "statusCode": 308 }, - { - "source": "/guides/developer-workflows/devops/how-to-analyze-cloud-run-logs-gcloud", - "destination": "/guides/devops/how-to-analyze-cloud-run-logs-gcloud/", - "statusCode": 308 - }, - { - "source": "/guides/developer-workflows/devops/how-to-create-a-production-ready-docker-setup", - "destination": "/guides/devops/how-to-create-a-production-ready-docker-setup/", - "statusCode": 308 - }, { "source": "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind", "destination": "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", @@ -9856,69 +9846,59 @@ "statusCode": 308 }, { - "source": "/guides/integrations/how-to-set-up-codex-cli", - "destination": "/guides/external-tools/how-to-set-up-codex-cli/", + "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", + "destination": "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", "statusCode": 308 }, { - "source": "/guides/integrations/how-to-set-up-gemini-cli", - "destination": "/guides/external-tools/how-to-set-up-gemini-cli/", + "source": "/guides/warp-runtime/building-a-slackbot", + "destination": "/guides/", "statusCode": 308 }, { - "source": "/guides/integrations/how-to-set-up-ollama", - "destination": "/guides/external-tools/how-to-set-up-ollama/", + "source": "/guides/integrations/:slug*", + "destination": "/guides/external-tools/:slug/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/context7-mcp-update-astro-project-with-best-practices", - "destination": "/guides/external-tools/context7-mcp-update-astro-project-with-best-practices/", + "source": "/guides/mcp-servers/:slug*", + "destination": "/guides/external-tools/:slug/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch", - "destination": "/guides/external-tools/figma-remote-mcp-create-a-website-from-a-figma-file-from-scratch/", + "source": "/guides/developer-workflows/devops/:slug*", + "destination": "/guides/devops/:slug/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/github-mcp-summarizing-open-prs-and-creating-gh-issues", - "destination": "/guides/external-tools/github-mcp-summarizing-open-prs-and-creating-gh-issues/", - "statusCode": 308 - }, - { - "source": "/guides/mcp-servers/linear-mcp-retrieve-issue-data", - "destination": "/guides/external-tools/linear-mcp-retrieve-issue-data/", + "source": "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-%2B-typescript-%2B-tailwind", + "destination": "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/linear-mcp-updating-tickets-with-a-lean-build-approach", - "destination": "/guides/external-tools/linear-mcp-updating-tickets-with-a-lean-build-approach/", + "source": "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-%2B-tailwind", + "destination": "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/puppeteer-mcp-scraping-amazon-web-reviews", - "destination": "/guides/external-tools/puppeteer-mcp-scraping-amazon-web-reviews/", + "source": "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-%2B-analyze-pr-%2B-modify-ui", + "destination": "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/sentry-mcp-fix-sentry-error-in-empower-website", - "destination": "/guides/external-tools/sentry-mcp-fix-sentry-error-in-empower-website/", + "source": "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-%2B-javascript-%2B-html-%2B-css", + "destination": "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", "statusCode": 308 }, { - "source": "/guides/mcp-servers/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up", - "destination": "/guides/external-tools/sqlite-and-stripe-mcp-basic-queries-you-can-make-after-set-up/", + "source": "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-%2B-railway", + "destination": "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", "statusCode": 308 }, { - "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", + "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-%2B-helm", "destination": "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", "statusCode": 308 - }, - { - "source": "/guides/warp-runtime/building-a-slackbot", - "destination": "/guides/", - "statusCode": 308 } ] } From 9e78a2c893ce27b31ac1090307cf6f757d137b04 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:57:50 -0600 Subject: [PATCH 4/6] fix: remove invalid %2B source patterns, fix glob catch-all destinations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vercel deployment was failing with 'invalid-route-source-pattern' due to two issues in the glob/redirect additions: 1. %2B-encoded source patterns — Vercel's routing engine rejects URL-encoded characters (%) in source patterns. Removed 6 %2B duplicates. They're unnecessary anyway: Vercel normalises paths before route matching, so a literal + source already covers requests where the client sent %2B. 2. Glob destination mismatch — when the source uses :slug* (catch-all), the destination must also reference :slug* to substitute the full captured path. Changed /:slug/ → /:slug*/ in all 3 glob rules. Co-Authored-By: Oz --- vercel.json | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/vercel.json b/vercel.json index b3f5d894..46aa516f 100644 --- a/vercel.json +++ b/vercel.json @@ -9857,47 +9857,17 @@ }, { "source": "/guides/integrations/:slug*", - "destination": "/guides/external-tools/:slug/", + "destination": "/guides/external-tools/:slug*/", "statusCode": 308 }, { "source": "/guides/mcp-servers/:slug*", - "destination": "/guides/external-tools/:slug/", + "destination": "/guides/external-tools/:slug*/", "statusCode": 308 }, { "source": "/guides/developer-workflows/devops/:slug*", - "destination": "/guides/devops/:slug/", - "statusCode": 308 - }, - { - "source": "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-%2B-typescript-%2B-tailwind", - "destination": "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", - "statusCode": 308 - }, - { - "source": "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-%2B-tailwind", - "destination": "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", - "statusCode": 308 - }, - { - "source": "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-%2B-analyze-pr-%2B-modify-ui", - "destination": "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", - "statusCode": 308 - }, - { - "source": "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-%2B-javascript-%2B-html-%2B-css", - "destination": "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", - "statusCode": 308 - }, - { - "source": "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-%2B-railway", - "destination": "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", - "statusCode": 308 - }, - { - "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-%2B-helm", - "destination": "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", + "destination": "/guides/devops/:slug*/", "statusCode": 308 } ] From 3204c7397c21a6691672562f7476a329137384a2 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:11:55 -0600 Subject: [PATCH 5/6] fix: remove 6 source patterns with regex metacharacters (+ and .) Vercel uses path-to-regexp v6.1.0 for source pattern matching. In this version, '+' and '.' are regex metacharacters (+ = one-or-more, . = any-char) that cause 'invalid-route-source-pattern' errors when they appear as literals in non-parameter positions. The 6 affected patterns all came from old GitBook URLs where '+' was used as a separator (e.g. 'react-+-tailwind', 'd3.js-+-javascript'). These are obscure legacy URLs with negligible bookmark traffic. The 46 other redirects and 3 :slug* glob patterns are unaffected. Co-Authored-By: Oz --- scripts/remove_invalid_patterns.py | 27 +++++++++++++++++++++++++++ vercel.json | 30 ------------------------------ 2 files changed, 27 insertions(+), 30 deletions(-) create mode 100644 scripts/remove_invalid_patterns.py diff --git a/scripts/remove_invalid_patterns.py b/scripts/remove_invalid_patterns.py new file mode 100644 index 00000000..6486f152 --- /dev/null +++ b/scripts/remove_invalid_patterns.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Remove source patterns that contain regex metacharacters (+ and .) invalid in path-to-regexp v6.""" +import json +from pathlib import Path + +REMOVE = { + "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind", + "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind", + "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui", + "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway", + "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", + "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css", +} + +vercel_path = Path(__file__).parent.parent / "vercel.json" +with open(vercel_path) as f: + vercel = json.load(f) + +before = len(vercel["redirects"]) +vercel["redirects"] = [r for r in vercel["redirects"] if r.get("source") not in REMOVE] +removed = before - len(vercel["redirects"]) + +with open(vercel_path, "w") as f: + json.dump(vercel, f, indent=2) + f.write("\n") + +print(f"Removed {removed} invalid patterns. Redirects: {before} → {len(vercel['redirects'])}") diff --git a/vercel.json b/vercel.json index 46aa516f..5cb15d36 100644 --- a/vercel.json +++ b/vercel.json @@ -9680,11 +9680,6 @@ "destination": "/guides/getting-started/10-coding-features-you-should-know/", "statusCode": 308 }, - { - "source": "/guides/developer-workflows/beginner/how-to-create-project-rules-for-an-existing-project-astro-+-typescript-+-tailwind", - "destination": "/guides/configuration/how-to-create-project-rules-for-an-existing-project-astro-typescript-tailwind/", - "statusCode": 308 - }, { "source": "/guides/developer-workflows/beginner/how-to-customize-warps-appearance", "destination": "/guides/getting-started/how-to-customize-warps-appearance/", @@ -9715,11 +9710,6 @@ "destination": "/guides/getting-started/welcome-to-warp/", "statusCode": 308 }, - { - "source": "/guides/developer-workflows/frontend-ui/how-to-actually-code-ui-that-matches-your-mockup-react-+-tailwind", - "destination": "/guides/frontend/how-to-actually-code-ui-that-matches-your-mockup-react-tailwind/", - "statusCode": 308 - }, { "source": "/guides/developer-workflows/frontend-ui/how-to-replace-a-ui-element-in-warp-rust-codebase", "destination": "/guides/frontend/how-to-replace-a-ui-element-in-warp-rust-codebase/", @@ -9760,11 +9750,6 @@ "destination": "/guides/agent-workflows/how-to-review-prs-like-a-senior-dev/", "statusCode": 308 }, - { - "source": "/guides/developer-workflows/power-user/how-to-run-3-agents-in-parallel-summarize-logs-+-analyze-pr-+-modify-ui", - "destination": "/guides/agent-workflows/how-to-run-3-agents-in-parallel-summarize-logs-analyze-pr-modify-ui/", - "statusCode": 308 - }, { "source": "/guides/developer-workflows/power-user/how-to-set-coding-best-practices", "destination": "/guides/configuration/how-to-set-coding-best-practices/", @@ -9805,16 +9790,6 @@ "destination": "/guides/devops/how-to-prevent-secrets-from-leaking/", "statusCode": 308 }, - { - "source": "/guides/end-to-end-builds/building-a-chrome-extension-d3.js-+-javascript-+-html-+-css", - "destination": "/guides/build-an-app-in-warp/building-a-chrome-extension-d3js-javascript-html-css/", - "statusCode": 308 - }, - { - "source": "/guides/end-to-end-builds/building-a-real-time-chat-app-github-mcp-+-railway", - "destination": "/guides/build-an-app-in-warp/building-a-real-time-chat-app-github-mcp-railway/", - "statusCode": 308 - }, { "source": "/guides/how-warp-uses-warp/building-warps-input-with-warp", "destination": "/guides/build-an-app-in-warp/building-warps-input-with-warp/", @@ -9845,11 +9820,6 @@ "destination": "/guides/external-tools/using-mcp-servers-with-warp/", "statusCode": 308 }, - { - "source": "/guides/terminal-command-line-tips/improve-your-kubernetes-workflow-kubectl-+-helm", - "destination": "/guides/devops/improve-your-kubernetes-workflow-kubectl-helm/", - "statusCode": 308 - }, { "source": "/guides/warp-runtime/building-a-slackbot", "destination": "/guides/", From 393872525dcd8c6366cee8927aee5932b9862777 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:08:50 -0600 Subject: [PATCH 6/6] docs: update weekly-404-monitor skill to use buzz environment - Use existing buzz environment (already has warpdotdev/docs checked out) - Target Slack channel: #growth-docs - Document the 3 secrets to verify in buzz: METABASE_API_KEY, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID Co-Authored-By: Oz --- .agents/skills/weekly-404-monitor/SKILL.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.agents/skills/weekly-404-monitor/SKILL.md b/.agents/skills/weekly-404-monitor/SKILL.md index f5d9dd27..5cf15f39 100644 --- a/.agents/skills/weekly-404-monitor/SKILL.md +++ b/.agents/skills/weekly-404-monitor/SKILL.md @@ -13,7 +13,7 @@ The following environment secrets must be set in the Oz cloud agent environment: - `METABASE_API_KEY` — Metabase API key for BigQuery queries. If unavailable, the run must fail fast with a clear error. - `SLACK_BOT_TOKEN` — Slack bot token for posting to the docs channel. If unavailable, write a no-post report to the run output instead. -- `SLACK_CHANNEL_ID` — Slack channel ID for the docs team (e.g. `#docs` or `#growth-docs`). Fall back to `SLACK_CHANNEL_ID=C0123456789` if not set explicitly. +- `SLACK_CHANNEL_ID` — Slack channel ID for **`#growth-docs`**. Find it in Slack by right-clicking the channel → Copy link (the ID begins with `C`). There is no fallback — the run will skip Slack posting if this is unset. Do NOT print, log, or include secret values in reports, commits, or Slack messages. @@ -128,8 +128,12 @@ This skill is designed for an Oz scheduled agent with a weekly cron trigger: eve To deploy: 1. Push this skill to `main` in the docs repo. -2. In the Oz web app (oz.warp.dev), create a new scheduled agent: +2. Verify the **`buzz`** Oz environment (oz.warp.dev → Environments) has these secrets set: + - `METABASE_API_KEY` — Metabase API key for BigQuery + - `SLACK_BOT_TOKEN` — Slack bot token + - `SLACK_CHANNEL_ID` — ID for `#growth-docs` (right-click channel in Slack → Copy link; the ID starts with `C`) +3. In the Oz web app (oz.warp.dev), create a new scheduled agent: - **Skill**: `weekly-404-monitor` from `warpdotdev/docs` - - **Schedule**: `0 17 * * 1` (UTC) = 9am PT - - **Environment**: docs-monitoring (must include `METABASE_API_KEY`, `SLACK_BOT_TOKEN`, `SLACK_CHANNEL_ID`) - - **Repo**: `warpdotdev/docs` (main branch) + - **Schedule**: `0 17 * * 1` (UTC) = 9am PT (Mondays) + - **Environment**: `buzz` (already has `warpdotdev/docs` checked out) + - **Branch**: `main`