diff --git a/.agents/skills/weekly-404-monitor/SKILL.md b/.agents/skills/weekly-404-monitor/SKILL.md new file mode 100644 index 00000000..5cf15f39 --- /dev/null +++ b/.agents/skills/weekly-404-monitor/SKILL.md @@ -0,0 +1,139 @@ +--- +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 **`#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. + +## 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. 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 (Mondays) + - **Environment**: `buzz` (already has `warpdotdev/docs` checked out) + - **Branch**: `main` 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() 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/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/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/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 ea1f47f2..5cb15d36 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,186 @@ "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-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/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-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/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/warp-runtime/building-a-slackbot", + "destination": "/guides/", + "statusCode": 308 + }, + { + "source": "/guides/integrations/:slug*", + "destination": "/guides/external-tools/:slug*/", + "statusCode": 308 + }, + { + "source": "/guides/mcp-servers/:slug*", + "destination": "/guides/external-tools/:slug*/", + "statusCode": 308 + }, + { + "source": "/guides/developer-workflows/devops/:slug*", + "destination": "/guides/devops/:slug*/", + "statusCode": 308 } ] }