Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .agents/skills/validate_ui_refs/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: validate_ui_refs
description: Scan Warp Astro Starlight documentation for UI menu paths and Command Palette command names, then validate them against the warp-internal codebase for accuracy. Catch and surface outdated steps automatically.
description: Scan Warp Astro Starlight documentation for UI menu paths and Command Palette command names, then validate them against the public warp client codebase for accuracy. Catch and surface outdated steps automatically.
---

# Validate UI References

This skill scans Warp's Astro Starlight documentation for references to UI paths (e.g. `Settings > AI > Active AI`) and Command Palette command names (e.g. "Open Theme Picker"), then validates them against a snapshot of known-valid paths extracted from the `warp-internal` codebase.
This skill scans Warp's Astro Starlight documentation for references to UI paths (e.g. `Settings > AI > Active AI`) and Command Palette command names (e.g. "Open Theme Picker"), then validates them against a snapshot of known-valid paths extracted from the public warp client repo ([warpdotdev/warp](https://github.com/warpdotdev/warp)).

## Running the Check

Expand All @@ -25,10 +25,10 @@ python3 .agents/skills/validate_ui_refs/validate_ui_refs.py --all
- `--create-pr`: Create a branch and PR with auto-fixes (requires `gh` CLI)
- `--slack-notify`: Post results to `#growth-docs` Slack channel when unfixed issues remain (requires `SLACK_BOT_TOKEN` env var; channel is hardcoded in the script)
- `--slack-channel ID`: Override the default Slack channel (`C09BVK0PL3Y`)
- `--self-test`: Run internal sanity checks against the current snapshot and exit (no `warp-internal` needed)
- `--self-test`: Run internal sanity checks against the current snapshot and exit (no warp checkout needed)
- `--include-changelog`: Include `changelog/` in the scan (excluded by default since it's a historical record)
- `--refresh-valid-paths`: Re-extract valid paths from `warp-internal` and update `valid_paths.json`
- `--warp-internal-path PATH`: Path to the `warp-internal` repo (default: `../warp-internal` relative to docs root, or `WARP_INTERNAL_PATH` env var)
- `--refresh-valid-paths`: Re-extract valid paths from the warp client repo and update `valid_paths.json`
- `--warp PATH`: Path to the public warp client repo (auto-detected as a sibling of the docs repo named `warp`, with `warp-internal` as a fallback, or the `WARP_REPO_PATH` env var). `--warp-internal-path` is a deprecated alias, and `WARP_INTERNAL_PATH` is still honored as a deprecated env var fallback.
- `--output FILE`: Save results to a JSON file

### Quick path-only check:
Expand Down Expand Up @@ -72,10 +72,10 @@ Files scanned: 174

## Refreshing Valid Paths

The `valid_paths.json` file is a static snapshot of valid UI paths. To update it from the latest `warp-internal` source:
The `valid_paths.json` file is a static snapshot of valid UI paths. To update it from the latest warp client source:

```bash
python3 .agents/skills/validate_ui_refs/validate_ui_refs.py --refresh-valid-paths --warp-internal-path /path/to/warp-internal
python3 .agents/skills/validate_ui_refs/validate_ui_refs.py --refresh-valid-paths --warp /path/to/warp
```

This parses:
Expand Down Expand Up @@ -157,7 +157,7 @@ python3 .agents/skills/validate_ui_refs/validate_ui_refs.py --all --slack-notify

1. A push to `master` in `warpdotdev/warp` that touches `app/src/settings_view/**` sends a `repository_dispatch` event (`settings-ui-changed`) to `warpdotdev/docs`.
2. The `refresh-ui-paths` GHA workflow fires and dispatches an Oz cloud agent to the Docs Agent environment (`K5KStCm5aYvhfBJb8cHol6`).
3. The cloud agent runs `--refresh-valid-paths` using the `warp-internal` checkout available in that environment.
3. The cloud agent runs `--refresh-valid-paths` using the `warpdotdev/warp` checkout available in that environment.
4. If the snapshot changed, the agent runs `--all --fix --create-pr --slack-notify` to validate, auto-fix, open a PR, and post to `#growth-docs` if issues remain.
5. If the snapshot is unchanged, the agent exits with no-op.

Expand All @@ -181,12 +181,12 @@ To trigger the workflow manually (e.g., if the PAT expired or a migration was mi
```bash
python3 .agents/skills/validate_ui_refs/validate_ui_refs.py \
--refresh-valid-paths \
--warp-internal-path /path/to/warp-internal
--warp /path/to/warp
```

## Dependencies

- Python 3.7+
- `requests` (for Slack notifications): `pip3 install requests`
- `gh` CLI (for PR creation)
- Access to `warp-internal` repo (only for `--refresh-valid-paths`)
- A checkout of the public warp client repo ([warpdotdev/warp](https://github.com/warpdotdev/warp)) (only for `--refresh-valid-paths`)
128 changes: 99 additions & 29 deletions .agents/skills/validate_ui_refs/validate_ui_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

Scans markdown files for references to Warp UI paths (Settings > ..., File > ..., etc.)
and Command Palette command names, then validates them against a snapshot of known-valid
paths extracted from the warp-internal codebase.
paths extracted from the public warp client repo (warpdotdev/warp).

Usage:
python3 validate_ui_refs.py --all
python3 validate_ui_refs.py --check-paths
python3 validate_ui_refs.py --check-commands
python3 validate_ui_refs.py --all --fix --create-pr --slack-notify
python3 validate_ui_refs.py --refresh-valid-paths --warp-internal-path /path/to/warp-internal
python3 validate_ui_refs.py --refresh-valid-paths --warp /path/to/warp
"""
from __future__ import annotations

Expand All @@ -35,6 +35,10 @@
DEFAULT_DOCS_DIR = SCRIPT_DIR.parents[2] / "src" / "content" / "docs"
DEFAULT_SLACK_CHANNEL = "C09BVK0PL3Y" # #growth-docs

# Sibling directory names tried when auto-detecting the warp client checkout.
# Prefer the public warpdotdev/warp repo; `warp-internal` is a legacy fallback.
WARP_REPO_SIBLING_NAMES = ("warp", "warp-internal")

# Known Warp UI roots — paths starting with these are Warp UI paths
WARP_UI_ROOTS = {"Settings", "File", "View", "Warp", "Warp Drive", "Personal"}

Expand Down Expand Up @@ -1358,11 +1362,38 @@ def notify_slack(


# ---------------------------------------------------------------------------
# Refresh valid_paths.json from warp-internal
# Refresh valid_paths.json from the warp client repo
# ---------------------------------------------------------------------------

def refresh_valid_paths(warp_internal_path: Path, output_path: Path) -> None:
"""Re-extract valid paths from warp-internal Rust sources and save to JSON.
def resolve_warp_repo(explicit_path: Optional[str]) -> Path:
"""Resolve the warp client repo checkout used for snapshot extraction.

Resolution order:
1. An explicit `--warp PATH` (or the deprecated `--warp-internal-path`).
2. The `WARP_REPO_PATH` env var, or the deprecated `WARP_INTERNAL_PATH`.
3. A sibling of the docs repo named `warp` (the public warpdotdev/warp
checkout), falling back to a legacy `warp-internal` sibling.

When nothing is found, returns the preferred sibling path so the caller
can report a useful "not found" error.
"""
if explicit_path:
return Path(explicit_path)

env_path = os.environ.get("WARP_REPO_PATH") or os.environ.get("WARP_INTERNAL_PATH")
if env_path:
return Path(env_path)

siblings_root = SCRIPT_DIR.parents[2].parent
for name in WARP_REPO_SIBLING_NAMES:
candidate = siblings_root / name
if candidate.exists():
return candidate
return siblings_root / WARP_REPO_SIBLING_NAMES[0]


def refresh_valid_paths(warp_repo_path: Path, output_path: Path) -> None:
"""Re-extract valid paths from the warp client repo's Rust sources and save to JSON.

Preserves hand-maintained lists (macos_menu_bar, warp_drive, umbrellas,
deprecated_sections, top_level_sidebar) from the existing snapshot.
Expand All @@ -1378,10 +1409,10 @@ def refresh_valid_paths(warp_internal_path: Path, output_path: Path) -> None:
other's sub_sections. Any sub_sections value curated in the existing
snapshot is treated as authoritative and is not overwritten.
"""
print(f"Refreshing valid_paths.json from {warp_internal_path}...")
print(f"Refreshing valid_paths.json from {warp_repo_path}...")

settings_sections = _extract_settings_sections(warp_internal_path)
command_palette = _extract_command_palette_commands(warp_internal_path)
settings_sections = _extract_settings_sections(warp_repo_path)
command_palette = _extract_command_palette_commands(warp_repo_path)

# Load existing for menu bar, warp drive, umbrellas, deprecated_sections,
# and top_level_sidebar (all manually maintained lists).
Expand All @@ -1394,7 +1425,7 @@ def refresh_valid_paths(warp_internal_path: Path, output_path: Path) -> None:
# Best-effort: pull umbrellas from `SettingsUmbrella::new(...)` calls in mod.rs
# and merge into the existing snapshot (existing entries win on conflict).
try:
extracted_umbrellas = _extract_umbrellas(warp_internal_path)
extracted_umbrellas = _extract_umbrellas(warp_repo_path)
except Exception as e: # pragma: no cover - defensive, parser errors
print(f" Warning: umbrella extraction failed: {e}", file=sys.stderr)
extracted_umbrellas = {}
Expand Down Expand Up @@ -1445,14 +1476,14 @@ def refresh_valid_paths(warp_internal_path: Path, output_path: Path) -> None:
)


def _extract_umbrellas(warp_internal: Path) -> Dict[str, Any]:
def _extract_umbrellas(warp_repo: Path) -> Dict[str, Any]:
"""Parse SettingsUmbrella::new("Label", vec![...]) calls from mod.rs.

Maps each umbrella label to its ordered list of subpage **display names**
(resolved via the `Display for SettingsSection` impl). Returns a dict
shaped like the `umbrellas` field in valid_paths.json.
"""
mod_rs = warp_internal / "app" / "src" / "settings_view" / "mod.rs"
mod_rs = warp_repo / "app" / "src" / "settings_view" / "mod.rs"
umbrellas: Dict[str, Any] = {}
try:
mod_text = mod_rs.read_text(encoding="utf-8")
Expand Down Expand Up @@ -1512,9 +1543,9 @@ def _display(variant: str) -> str:
return umbrellas


def _extract_settings_sections(warp_internal: Path) -> Dict[str, Any]:
def _extract_settings_sections(warp_repo: Path) -> Dict[str, Any]:
"""Parse SettingsSection enum and sub-sections from Rust source files."""
mod_rs = warp_internal / "app" / "src" / "settings_view" / "mod.rs"
mod_rs = warp_repo / "app" / "src" / "settings_view" / "mod.rs"
sections = {}

# Parse Display impl for section display names
Expand Down Expand Up @@ -1590,7 +1621,7 @@ def _extract_settings_sections(warp_internal: Path) -> Dict[str, Any]:
"Privacy": "privacy_page.rs",
}

settings_dir = warp_internal / "app" / "src" / "settings_view"
settings_dir = warp_repo / "app" / "src" / "settings_view"

for variant, display_name in display_map.items():
source_file = page_files.get(variant, "mod.rs")
Expand Down Expand Up @@ -1622,14 +1653,14 @@ def _extract_settings_sections(warp_internal: Path) -> Dict[str, Any]:
return sections


def _extract_command_palette_commands(warp_internal: Path) -> List[Dict[str, str]]:
def _extract_command_palette_commands(warp_repo: Path) -> List[Dict[str, str]]:
"""Parse EditableBinding registrations to extract command palette commands."""
commands = []
seen_descriptions = set()

source_files = [
warp_internal / "app" / "src" / "terminal" / "view" / "init.rs",
warp_internal / "app" / "src" / "workspace" / "mod.rs",
warp_repo / "app" / "src" / "terminal" / "view" / "init.rs",
warp_repo / "app" / "src" / "workspace" / "mod.rs",
]

for source_file in source_files:
Expand Down Expand Up @@ -1832,8 +1863,10 @@ def _run_self_test(valid_paths_path: Path) -> int:
2. `_is_external_path()` no longer suppresses `Settings > MCP Servers` in a
sentence containing GitHub / Linear mentions (previous bug).
3. `refresh_valid_paths()` preserves umbrellas + deprecated_sections when
run against a synthetic warp-internal with the new enum, and populates
run against a synthetic warp checkout with the new enum, and populates
the new subpage entries.
4. `resolve_warp_repo()` honors the explicit path, the `WARP_REPO_PATH` env
var, and the deprecated `WARP_INTERNAL_PATH` fallback in that order.
"""
import textwrap

Expand Down Expand Up @@ -1872,16 +1905,16 @@ def _run_self_test(valid_paths_path: Path) -> int:

# --- 3. refresh_valid_paths preservation + extraction
with tempfile.TemporaryDirectory() as td:
wi_root = Path(td) / "warp-internal"
mod_rs = wi_root / "app" / "src" / "settings_view" / "mod.rs"
warp_root = Path(td) / "warp"
mod_rs = warp_root / "app" / "src" / "settings_view" / "mod.rs"
mod_rs.parent.mkdir(parents=True)
mod_rs.write_text(textwrap.dedent(_SYNTHETIC_MOD_RS))

# Copy the committed snapshot to a tmp path so we don't clobber it.
snap_path = Path(td) / "valid_paths.json"
snap_path.write_text(valid_paths_path.read_text())

refresh_valid_paths(wi_root, snap_path)
refresh_valid_paths(warp_root, snap_path)

refreshed = load_valid_paths(snap_path)

Expand All @@ -1893,7 +1926,7 @@ def _run_self_test(valid_paths_path: Path) -> int:
failures.append("refresh lost the Agents umbrella")

# The extractor should have picked up the synthetic umbrellas too.
extracted = _extract_umbrellas(wi_root)
extracted = _extract_umbrellas(warp_root)
for expected in ("Agents", "Code", "Cloud platform"):
if expected not in extracted:
failures.append(
Expand All @@ -1917,6 +1950,32 @@ def _run_self_test(valid_paths_path: Path) -> int:
f"settings_sections missing subpage `{expected_subpage}` after refresh"
)

# --- 4. resolve_warp_repo precedence (explicit > WARP_REPO_PATH >
# deprecated WARP_INTERNAL_PATH > sibling auto-detect)
saved_env = {
key: os.environ.get(key) for key in ("WARP_REPO_PATH", "WARP_INTERNAL_PATH")
}
try:
os.environ["WARP_REPO_PATH"] = "/tmp/from-warp-repo-path"
os.environ["WARP_INTERNAL_PATH"] = "/tmp/from-warp-internal-path"

if resolve_warp_repo("/tmp/explicit") != Path("/tmp/explicit"):
failures.append("resolve_warp_repo() ignored the explicit --warp path")
if resolve_warp_repo(None) != Path("/tmp/from-warp-repo-path"):
failures.append("resolve_warp_repo() did not prefer WARP_REPO_PATH")

del os.environ["WARP_REPO_PATH"]
if resolve_warp_repo(None) != Path("/tmp/from-warp-internal-path"):
failures.append(
"resolve_warp_repo() dropped the deprecated WARP_INTERNAL_PATH fallback"
)
finally:
for key, value in saved_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value

if failures:
print("SELF-TEST FAILED:")
for f in failures:
Expand Down Expand Up @@ -1944,11 +2003,18 @@ def main() -> int:
parser.add_argument("--slack-notify", action="store_true", help="Post results to Slack")
parser.add_argument("--slack-channel", default=DEFAULT_SLACK_CHANNEL, help="Slack channel ID")
parser.add_argument("--include-changelog", action="store_true", help="Include changelog/ in scan")
parser.add_argument("--refresh-valid-paths", action="store_true", help="Re-extract from warp-internal")
parser.add_argument("--refresh-valid-paths", action="store_true", help="Re-extract from the warp client repo")
parser.add_argument(
"--warp",
dest="warp_repo_path",
help="Path to the public warp client repo (auto-detected as a sibling "
"of the docs repo named 'warp', with 'warp-internal' as fallback; "
"also reads the WARP_REPO_PATH env var)",
)
parser.add_argument(
"--warp-internal-path",
default=os.environ.get("WARP_INTERNAL_PATH", str(SCRIPT_DIR.parents[2].parent / "warp-internal")),
help="Path to warp-internal repo",
dest="warp_repo_path",
help="Deprecated alias for --warp",
)
parser.add_argument("--valid-paths", default=str(DEFAULT_VALID_PATHS_FILE), help="Path to valid_paths.json")
parser.add_argument("--docs-dir", default=str(DEFAULT_DOCS_DIR), help="Path to docs directory")
Expand All @@ -1966,14 +2032,18 @@ def main() -> int:
args.all = True

valid_paths_file = Path(args.valid_paths)
warp_internal = Path(args.warp_internal_path)
warp_repo = resolve_warp_repo(args.warp_repo_path)

# Refresh valid paths if requested
if args.refresh_valid_paths:
if not warp_internal.exists():
print(f"Error: warp-internal not found at {warp_internal}", file=sys.stderr)
if not warp_repo.exists():
print(
f"Error: warp client repo not found at {warp_repo}. Pass --warp PATH "
"or set WARP_REPO_PATH.",
file=sys.stderr,
)
return 1
refresh_valid_paths(warp_internal, valid_paths_file)
refresh_valid_paths(warp_repo, valid_paths_file)
if not args.all and not args.check_paths and not args.check_commands:
return 0

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
python3 .agents/skills/missing_docs/scripts/test_audit_docs.py

# Validate the validate_ui_refs snapshot and script invariants. Uses
# a synthetic warp-internal fixture internally — no checkout required.
# a synthetic warp client fixture internally — no checkout required.
- name: Self-test validate_ui_refs skill
run: python3 .agents/skills/validate_ui_refs/validate_ui_refs.py --self-test

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/refresh-ui-paths.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Refresh UI paths snapshot

# Triggered automatically when warp-internal's Settings UI files change
# Triggered automatically when the warp client's Settings UI files change
# (via repository_dispatch from warpdotdev/warp) or manually via
# workflow_dispatch as a fallback.
on:
Expand All @@ -9,7 +9,7 @@ on:
- settings-ui-changed
workflow_dispatch:

# Only WARP_API_KEY is needed here — warp-internal access and Slack
# Only WARP_API_KEY is needed here — warpdotdev/warp access and Slack
# notifications are handled by the Oz cloud agent environment.
permissions:
contents: read
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
Then run:
python3 .agents/skills/validate_ui_refs/validate_ui_refs.py \\
--refresh-valid-paths \\
--warp-internal-path /workspace/warp
--warp /workspace/warp
2. Compare valid_paths.json before and after (strip generated_at from both,
then diff). If unchanged, exit — no PR needed.
3. If the snapshot changed, run:
Expand Down
Loading