From 9f6128b7e9f1c98e43f8a3503965b39c695f9a9f Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Thu, 11 Jun 2026 05:47:30 +0000 Subject: [PATCH 01/15] Overhaul missing_docs skill: fix audit blind spots, add change detection - audit_docs.py: fail loud (exit 2 + audits_skipped) when repos are missing; fix repo auto-detection to siblings of the docs repo root - GA detection via the app/src/features.rs cargo-feature bridge plus RELEASE_FLAGS/PREVIEW_FLAGS/DOGFOOD_FLAGS instead of snake_case guessing - New audits: public API routes (router/handlers/public_api gin groups vs OpenAPI spec), CLI subcommands (clap enum tree, hidden skipped), slash commands (static registry), surface-map hygiene (dead entries) - Staleness: strip code spans, word-boundary matching, skip historical changelog pages (73 -> 29 findings; 'oz agent' CLI noise eliminated) - Change detection: references/surface_snapshot.json + --diff mode reporting added/removed/promoted surfaces and changelog items since last run; --update-snapshot regenerates the baseline - Seed feature_surface_map.md: map handoff/orchestration/queueing/BYOK/ billing/etc. flags, prune 45+ dead entries, add slash-command section and API internal sentinels - SKILL.md: document the exit-code contract, diff workflow, drift-watch mode for the recurring agent (with copyable scheduled-agent prompt), and make surface-map + snapshot updates explicit drafting steps Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 170 +- .../references/feature_surface_map.md | 180 ++- .../references/surface_snapshot.json | 670 ++++++++ .../skills/missing_docs/scripts/audit_docs.py | 1416 +++++++++++++---- 4 files changed, 2057 insertions(+), 379 deletions(-) create mode 100644 .agents/skills/missing_docs/references/surface_snapshot.json diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 5d38bf5ab..fb6246aa3 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -4,19 +4,33 @@ description: >- Find and fill documentation gaps in Warp's Astro Starlight docs by auditing coverage against code surfaces in warp-internal and warp-server, then drafting missing pages. Use when asked to find missing docs, audit documentation coverage, - identify undocumented features, draft docs for new features, or do a docs - coverage check. Runs a Python audit script to identify gaps, then researches + identify undocumented features, draft docs for new features, detect doc-impacting + code changes since the last audit, or do a docs coverage check. Runs a Python + audit script (coverage + snapshot-based change detection), then researches source code and writes first-pass doc pages. Can run audit-only, draft-only, - or end-to-end. + drift-watch (recurring agent), or end-to-end. --- # Missing Docs -Find documentation gaps and draft missing pages in one workflow. +Find documentation gaps, detect doc-impacting code changes, and draft missing pages. -## Two-phase workflow +## Requirements -### Phase 1: Audit +The audit compares docs against code, so both source repos must be available: +- `warp-internal` and `warp-server`, auto-detected as siblings of the docs repo + root (e.g. `/workspace/docs` next to `/workspace/warp-internal` and + `/workspace/warp-server`), or passed explicitly via `--warp-internal PATH` / + `--warp-server PATH`. + +The script FAILS LOUD when a repo is missing: it exits with code 2 and lists the +skipped audits in the report's `audits_skipped` field. Never treat an exit-2 run +as a clean audit — fix the repo paths and re-run. Exit 0 means all requested +audits ran (findings may still exist). + +## Workflows + +### Phase 1: Audit (coverage) Run the audit script to identify gaps: @@ -25,36 +39,87 @@ python3 .agents/skills/missing_docs/scripts/audit_docs.py ``` Options: -- `--category features|cli|api|staleness` — run a single audit category +- `--category features|cli|api|slash|staleness|map` — run a single audit category - `--severity high|medium|low` — filter by minimum severity - `--weak-coverage` — also flag GA features whose mapped doc exists but doesn't mention feature keywords (low-severity, noisy) - `--output report.json` — save JSON report to file -- `--warp-internal PATH` — explicit path to warp-internal (auto-detected from sibling dirs) -- `--warp-server PATH` — explicit path to warp-server (auto-detected from sibling dirs) +- `--warp-internal PATH` / `--warp-server PATH` — explicit repo paths +- `--diff` — change detection against the committed snapshot (see Phase 2) +- `--update-snapshot` — regenerate `references/surface_snapshot.json` (full runs only) The script resolves doc paths from the docs repo root and accepts `.md` and `.mdx` interchangeably (and `README.md` ↔ `index.mdx`), so surface-map entries can use the canonical filename even when the on-disk extension differs. -The script performs 4 audits: -1. **Feature flag coverage** — compares GA flags in `crates/warp_features/src/lib.rs` + `app/Cargo.toml` against the surface map; default mode trusts a mapped entry as verified, `--weak-coverage` additionally checks the target page mentions feature keywords -2. **CLI command coverage** — compares `warp_cli/src/lib.rs` subcommands against `src/content/docs/reference/cli/` -3. **API endpoint coverage** — compares `router/router.go` routes against `src/content/docs/reference/api-and-sdk/` and the OpenAPI spec -4. **Docs staleness** — checks for outdated terminology in existing docs +The script performs 6 coverage audits: +1. **Feature flag coverage** — classifies every `FeatureFlag` by rollout status using + the cargo-feature→flag bridge in warp-internal `app/src/features.rs` plus + `RELEASE_FLAGS`/`PREVIEW_FLAGS`/`DOGFOOD_FLAGS` in `crates/warp_features/src/lib.rs`. + GA flags must be mapped in the surface map or covered in docs; Preview flags produce + low-severity "docs needed soon" findings; dogfood/other flags are tracked by the + snapshot only. +2. **CLI command coverage** — parses the full `oz` command tree (top-level commands and + subcommands, skipping `hide = true`) from `crates/warp_cli/src/` and checks the CLI + reference docs. +3. **API endpoint coverage** — extracts public routes from warp-server + `router/handlers/public_api/*.go` (nested gin groups resolved) and checks them + against `developers/agent-api-openapi.yaml` and the API reference docs. For spec + drift, run the docs `sync-openapi-spec` skill (or warp-server's + `update-open-api-spec`) instead of hand-editing the YAML. +4. **Slash command coverage** — parses the static registry in warp-internal + `app/src/search/slash_command_menu/static_commands/` and checks each `/command` + is mentioned in docs. +5. **Docs staleness** — flags renamed/removed-feature terminology in prose (code + spans stripped; historical changelog pages excluded). Broader terminology and + style enforcement is owned by the `style_lint` skill — delegate pure wording + issues there. +6. **Surface map hygiene** — flags map entries whose flag/command no longer exists in + code, and mapped doc targets that no longer exist. Verify the doc page is still + accurate, then prune or update the entry. Present the report to the user, grouped by category and sorted by severity. -### Phase 2: Draft +### Phase 2: Change detection (diff mode) + +The snapshot at `references/surface_snapshot.json` records all extracted surfaces +(flags + rollout status, CLI commands, API routes, slash commands) plus the last-seen +docs-changelog version. It makes change detection possible: a feature flag that is +deleted after stabilizing (per warp-internal's remove-feature-flag policy) would +otherwise vanish from the audit's universe silently. + +```bash +python3 .agents/skills/missing_docs/scripts/audit_docs.py --diff +``` + +Diff mode reports, since the snapshot was last updated: +- **Added / removed / promoted surfaces** — e.g. a new GA flag (high), a flag promoted + dogfood→ga (high), a removed flag ("feature stabilized or killed — verify docs and + map entry"), new CLI/API/slash surfaces. +- **Changelog items to verify** — "New features" and "Improvements" bullets from + `src/content/docs/changelog/.mdx` entries newer than the snapshot's last-seen + version. This is the best signal for launches no static code parse can see + (server-side features, Oz web app, experiment rollouts). A changelog mention is NOT + documentation — verify each item has real doc coverage. + +After triaging and addressing diff findings, refresh the snapshot and commit it with +your PR so the next run diffs against the new baseline: -For each gap the user wants to address (prioritize high → medium → low): +```bash +python3 .agents/skills/missing_docs/scripts/audit_docs.py --update-snapshot +``` + +### Phase 3: Draft + +For each gap to address (prioritize high → medium → low): 1. Read `references/feature_surface_map.md` to determine the target doc section 2. Read `AGENTS.md` in the docs repo root for the complete style guide 3. Read 2-3 strong examples in the target section to match formatting patterns 4. Research the relevant source code: - **Feature gaps** → read the implementation in warp-internal `app/src/`, check UI code, settings, user-facing strings - - **CLI gaps** → read command definition in `warp_cli/src/`, extract flags, arguments, help text - - **API gaps** → read handler in warp-server `router/handlers/`, route definition, request/response types + - **CLI gaps** → read command definition in `crates/warp_cli/src/`, extract flags, arguments, help text + - **API gaps** → read handler in warp-server `router/handlers/public_api/`, route definition, request/response types; prefer fixing the OpenAPI spec via the `sync-openapi-spec` skill + - **Slash command gaps** → read the registry entry and gating flags in `app/src/search/slash_command_menu/` 5. Draft the doc following style guide conventions: - YAML frontmatter with description - **All headings (H1–H4) must use sentence case** — capitalize only the first word and proper feature names (e.g., "Agent Mode", "Warp Drive"). ✅ `## How it works` ❌ `## How It Works` @@ -63,16 +128,63 @@ For each gap the user wants to address (prioritize high → medium → low): - Correct terminology (Agent, Agent Mode, Warp Drive, Oz, etc.) - Bold + dash format for list items: `* **Term** - Description` 6. Create the markdown file at the suggested path -7. Remind user to add new pages to the relevant `astro.config.mjs (sidebar config)` - -To find warp-internal and warp-server, search for sibling directories of the docs repo. If not found, ask the user. +7. Add new pages to the sidebar config in `astro.config.mjs` +8. **Update `references/feature_surface_map.md` in the same PR**: add a + `Flag -> src/content/docs/...` mapping for every feature you documented (or add the + flag to the ignore list with a comment if you confirmed it is internal-only). This + step is NOT optional — unmapped features become repeat findings, and an unmaintained + map is how gaps get lost. +9. Run `--update-snapshot` and commit the refreshed snapshot with the same PR. + +### Drift-watch mode (recurring scheduled agent) + +This is the end-to-end workflow for the scheduled cloud agent that keeps docs in sync +with the product. Each run: + +1. **Audit**: run both modes and save reports. Pass explicit repo paths; verify + exit code 0 — if the script exits 2, STOP and report the environment problem + instead of concluding "no gaps": + ```bash + python3 .agents/skills/missing_docs/scripts/audit_docs.py \ + --warp-internal ../warp-internal --warp-server ../warp-server \ + --diff --output /tmp/docs_audit.json + ``` +2. **Triage**: work through `surface_changes` and `changelog_review` first (what + changed since last run), then standing coverage findings (high → medium → low). + For each item decide: draft/update a doc page, update the OpenAPI spec via + `sync-openapi-spec`, add a surface-map entry (documented elsewhere), or add an + ignore/`internal` entry with a comment (internal-only). +3. **Draft**: follow Phase 3 for every item that needs docs. +4. **Update references**: apply surface-map edits, then regenerate the snapshot: + ```bash + python3 .agents/skills/missing_docs/scripts/audit_docs.py --update-snapshot + ``` +5. **Validate**: `npm run build` if doc pages changed; re-run the audit and confirm + the addressed findings are gone. +6. **Open a PR** with the doc pages + map + snapshot changes together, using the + `create_pr` skill. Summarize remaining (deferred) findings in the PR body so + nothing is silently dropped. + +Recommended scheduled-agent prompt (copy when setting up the agent): + +> Run the missing_docs skill in drift-watch mode. Use the audit script with explicit +> --warp-internal and --warp-server paths and --diff. If the script exits non-zero with +> skipped audits, report the environment problem and stop. Otherwise triage all +> surface_changes and changelog_review findings plus high/medium coverage findings: +> draft or update doc pages, update the surface map (mapping or ignore entry with a +> comment) for every triaged flag, and use the sync-openapi-spec skill for API spec +> gaps. Regenerate the surface snapshot with --update-snapshot. Open a single PR with +> the doc pages, feature_surface_map.md, and surface_snapshot.json changes, and list +> any findings you deferred in the PR body. ### Invocation modes -The user can trigger either phase or both: +The user can trigger any subset: - **"Run a docs audit"** or **"Check docs coverage"** → Phase 1 only -- **"Draft docs for [specific gap]"** → Phase 2 only (skip audit) -- **"Find and fix missing docs"** → Both phases end-to-end +- **"What changed since the last audit?"** → Phase 1 + 2 (`--diff`) +- **"Draft docs for [specific gap]"** → Phase 3 only (skip audit) +- **"Find and fix missing docs"** → Phases 1–3 end-to-end +- **Scheduled/recurring run** → Drift-watch mode ### Drafting standards @@ -84,6 +196,12 @@ The user can trigger either phase or both: ## References -- `references/feature_surface_map.md` — curated mapping of flags/commands to doc pages, also lists internal-only flags to ignore -- `references/stale_terms.md` — deprecated/outdated terms to flag during staleness audits, sourced from AGENTS.md terminology standards +- `references/feature_surface_map.md` — curated mapping of flags/commands/routes/slash + commands to doc pages, ignore list for internal flags, and the `internal` sentinel + for surfaces that intentionally have no public docs. Update it with every docs PR + that ships a feature. +- `references/surface_snapshot.json` — generated snapshot of all code surfaces used by + `--diff`. Regenerate with `--update-snapshot`; never hand-edit. +- `references/stale_terms.md` — renamed/removed-feature terms to flag during staleness + audits. Pure terminology/style policing belongs to the `style_lint` skill. - `AGENTS.md` (docs repo root) — full documentation style guide diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index 8d149d767..e0aa3ba19 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -1,14 +1,23 @@ # Feature Surface Map -Curated mapping of feature flags, CLI commands, and code modules to their expected documentation pages. -The audit script reads this file to reduce false positives — entries here are verified rather than flagged. +Curated mapping of feature flags, CLI commands, API endpoints, and slash commands +to their expected documentation pages. +The audit script reads this file to reduce false positives — entries here are +verified rather than flagged. -Format: `CodeIdentifier -> docs/path/to/page.md` (one per line within each section). +Format: `CodeIdentifier -> src/content/docs/path/to/page.md` (one per line within each section). Lines starting with `#` are comments. Blank lines are ignored. +The sentinel target `internal` marks surfaces that intentionally have no public docs. -# Maintenance: when a new GA feature flag ships, add a mapping here. -# Run `python3 .agents/skills/missing_docs/scripts/audit_docs.py` to find unmapped flags. -# This audit is also run as a recurring scheduled cloud agent to catch drift. +# Maintenance policy: +# - When a feature ships (GA or Preview), add a mapping here in the same PR that +# adds/updates its doc page. +# - When a flag/command/route is removed from code, the audit's map-hygiene check +# flags the dead entry — verify the doc page is still accurate, then prune it. +# - Run `python3 .agents/skills/missing_docs/scripts/audit_docs.py` to find unmapped +# surfaces, and `--update-snapshot` to refresh references/surface_snapshot.json. +# - This audit also runs as a recurring scheduled cloud agent to catch drift +# (see the drift-watch workflow in SKILL.md). ## Feature flags -> doc pages @@ -20,7 +29,6 @@ AgentModeWorkflows -> src/content/docs/knowledge-and-collaboration/warp-drive/wo AgentOnboarding -> src/content/docs/agent-platform/getting-started/agents-in-warp.md AIRules -> src/content/docs/agent-platform/capabilities/rules.mdx AIResumeButton -> src/content/docs/agent-platform/local-agents/interacting-with-agents/terminal-and-agent-modes.mdx -CodeReviewView -> src/content/docs/code/code-review.md InlineCodeReview -> src/content/docs/agent-platform/local-agents/interactive-code-review.mdx FileTree -> src/content/docs/code/code-editor/file-tree.md CodeFindReplace -> src/content/docs/code/code-editor/find-and-replace.md @@ -32,57 +40,33 @@ SelectionAsContext -> src/content/docs/agent-platform/local-agents/agent-context DiffSetAsContext -> src/content/docs/agent-platform/local-agents/agent-context/selection-as-context.mdx WebSearchUI -> src/content/docs/agent-platform/capabilities/web-search.mdx WebFetchUI -> src/content/docs/agent-platform/capabilities/web-search.mdx -CodebaseContext -> src/content/docs/agent-platform/capabilities/codebase-context.mdx CrossRepoContext -> src/content/docs/agent-platform/capabilities/codebase-context.mdx FullSourceCodeEmbedding -> src/content/docs/agent-platform/capabilities/codebase-context.mdx SearchCodebaseUI -> src/content/docs/agent-platform/capabilities/codebase-context.mdx +RemoteCodebaseIndexing -> src/content/docs/agent-platform/capabilities/codebase-context.mdx CloudEnvironments -> src/content/docs/agent-platform/cloud-agents/environments.md CloudMode -> src/content/docs/agent-platform/cloud-agents/overview.md AmbientAgentsCommandLine -> src/content/docs/agent-platform/cloud-agents/overview.md ScheduledAmbientAgents -> src/content/docs/agent-platform/cloud-agents/triggers/scheduled-agents.md WarpManagedSecrets -> src/content/docs/agent-platform/cloud-agents/secrets.md IntegrationCommand -> src/content/docs/reference/cli/integration-setup.md -ConversationManagement -> src/content/docs/agent-platform/local-agents/cloud-conversations.mdx -ForkConversationFromBlock -> src/content/docs/agent-platform/local-agents/interacting-with-agents/conversation-forking.mdx -Voice -> src/content/docs/agent-platform/local-agents/interacting-with-agents/voice.mdx -WarpDrive -> src/content/docs/knowledge-and-collaboration/warp-drive/index.mdx -EnvVars -> src/content/docs/knowledge-and-collaboration/warp-drive/environment-variables.md CommandPaletteFileSearch -> src/content/docs/terminal/command-palette.md -Themes -> src/content/docs/terminal/appearance/themes.md Ligatures -> src/content/docs/terminal/appearance/text-fonts-cursor.md UIZoom -> src/content/docs/terminal/appearance/size-opacity-blurring.md -SSH -> src/content/docs/terminal/warpify/ssh.md -SplitPanes -> src/content/docs/terminal/windows/split-panes.md -Tabs -> src/content/docs/terminal/windows/tabs.md -GlobalHotkey -> src/content/docs/terminal/windows/global-hotkey.md -LaunchConfigurations -> src/content/docs/terminal/sessions/launch-configurations.md -SessionRestoration -> src/content/docs/terminal/sessions/session-restoration.md -BlockBasics -> src/content/docs/terminal/blocks/block-basics.md -Autosuggestions -> src/content/docs/terminal/command-completions/autosuggestions.md -Completions -> src/content/docs/terminal/command-completions/completions.md -CommandHistory -> src/content/docs/terminal/entry/command-history.md -CommandCorrections -> src/content/docs/terminal/entry/command-corrections.md UsageBasedPricing -> src/content/docs/support-and-community/plans-and-billing/credits.md APIKeyAuthentication -> src/content/docs/reference/cli/api-keys.md APIKeyManagement -> src/content/docs/reference/cli/api-keys.md -SecretRedaction -> src/content/docs/support-and-community/privacy-and-security/secret-redaction.md CreatingSharedSessions -> src/content/docs/knowledge-and-collaboration/session-sharing/index.mdx AgentSharedSessions -> src/content/docs/agent-platform/local-agents/session-sharing.mdx ProfilesDesignRevamp -> src/content/docs/agent-platform/capabilities/agent-profiles-permissions.mdx MultiProfile -> src/content/docs/agent-platform/capabilities/agent-profiles-permissions.mdx InlineProfileSelector -> src/content/docs/agent-platform/capabilities/agent-profiles-permissions.mdx -ModelChoice -> src/content/docs/agent-platform/capabilities/model-choice.mdx -Skills -> src/content/docs/agent-platform/capabilities/skills.mdx ListSkills -> src/content/docs/agent-platform/capabilities/skills.mdx BundledSkills -> src/content/docs/agent-platform/capabilities/skills.mdx -Planning -> src/content/docs/agent-platform/capabilities/planning.mdx SyncAmbientPlans -> src/content/docs/agent-platform/capabilities/planning.mdx -TaskLists -> src/content/docs/agent-platform/capabilities/task-lists.mdx -SlashCommands -> src/content/docs/agent-platform/capabilities/slash-commands.mdx SuggestedRules -> src/content/docs/agent-platform/capabilities/rules.mdx RectSelection -> src/content/docs/terminal/more-features/text-selection.md ContextWindowUsageV2 -> src/content/docs/agent-platform/local-agents/interacting-with-agents/index.mdx -ConfigurableBlockLimits -> src/content/docs/terminal/blocks/block-basics.md CommandCorrectionKey -> src/content/docs/terminal/entry/command-corrections.md ClassicCompletions -> src/content/docs/terminal/command-completions/completions.md DynamicWorkflowEnums -> src/content/docs/knowledge-and-collaboration/warp-drive/workflows.md @@ -104,11 +88,13 @@ RevertToCheckpoints -> src/content/docs/agent-platform/capabilities/slash-comman RewindSlashCommand -> src/content/docs/agent-platform/capabilities/slash-commands.mdx ForkFromCommand -> src/content/docs/agent-platform/capabilities/slash-commands.mdx SummarizationConversationCommand -> src/content/docs/agent-platform/capabilities/slash-commands.mdx +CreateEnvironmentSlashCommand -> src/content/docs/agent-platform/capabilities/slash-commands.mdx CodeReviewFind -> src/content/docs/code/code-review.md CodeReviewSaveChanges -> src/content/docs/code/code-review.md DiscardPerFileAndAllChanges -> src/content/docs/code/code-review.md AutoOpenCodeReviewPane -> src/content/docs/code/code-review.md GitOperationsInCodeReview -> src/content/docs/code/code-review.md +RemoteCodeReview -> src/content/docs/code/code-review.md AgentView -> src/content/docs/agent-platform/local-agents/interacting-with-agents/terminal-and-agent-modes.mdx AgentViewBlockContext -> src/content/docs/agent-platform/local-agents/agent-context/blocks-as-context.mdx CloudConversations -> src/content/docs/agent-platform/local-agents/cloud-conversations.mdx @@ -134,6 +120,32 @@ KittyKeyboardProtocol -> src/content/docs/terminal/more-features/full-screen-app InlineRepoMenu -> src/content/docs/agent-platform/capabilities/codebase-context.mdx InlineHistoryMenu -> src/content/docs/agent-platform/local-agents/interacting-with-agents/terminal-and-agent-modes.mdx SkillArguments -> src/content/docs/agent-platform/capabilities/skills.mdx +ConfigurableToolbar -> src/content/docs/terminal/windows/configurable-toolbar.mdx +SettingsFile -> src/content/docs/terminal/settings/index.mdx +Changelog -> src/content/docs/changelog/index.mdx +Autoupdate -> src/content/docs/support-and-community/troubleshooting-and-support/updating-warp.mdx + +# Handoff (local <-> cloud, cloud <-> cloud) and snapshots +OzHandoff -> src/content/docs/agent-platform/cloud-agents/handoff/index.mdx +HandoffLocalCloud -> src/content/docs/agent-platform/cloud-agents/handoff/local-to-cloud.mdx +HandoffCloudCloud -> src/content/docs/agent-platform/cloud-agents/handoff/cloud-to-cloud.mdx + +# Orchestration / multi-agent runs +RunAgentsTool -> src/content/docs/agent-platform/cloud-agents/orchestration/multi-agent-runs.mdx + +# Prompt queueing +QueueSlashCommand -> src/content/docs/agent-platform/local-agents/interacting-with-agents/prompt-queueing.mdx +QueuedPromptsV2 -> src/content/docs/agent-platform/local-agents/interacting-with-agents/prompt-queueing.mdx + +# Reusable agents (named agents + agent-scoped API keys) +NamedAgents -> src/content/docs/agent-platform/cloud-agents/agents.mdx + +# Inference: BYOK and custom endpoints +SoloUserByok -> src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx +CustomInferenceEndpoints -> src/content/docs/agent-platform/inference/custom-inference-endpoint.mdx + +# Billing & Usage settings page (redesigned) +BillingAndUsagePageV2 -> src/content/docs/support-and-community/plans-and-billing/index.mdx ## CLI commands -> doc pages @@ -152,40 +164,68 @@ oz secret -> src/content/docs/reference/cli/index.mdx oz provider -> src/content/docs/reference/cli/index.mdx oz federate -> src/content/docs/reference/cli/federate.mdx oz artifact -> src/content/docs/reference/cli/artifacts.mdx +oz api-key -> src/content/docs/reference/cli/api-keys.mdx # Internal/hidden command — not a user-facing surface, so no public docs. oz harness-support -> internal ## API endpoints -> doc pages -# Public API endpoints +# Paths are relative to /api/v1 and use OpenAPI-style {param} segments. +# Public API endpoints documented via the OpenAPI spec (developers/agent-api-openapi.yaml). POST /agent/run -> src/content/docs/reference/api-and-sdk/index.mdx GET /agent/runs -> src/content/docs/reference/api-and-sdk/index.mdx GET /agent/runs/{runId} -> src/content/docs/reference/api-and-sdk/index.mdx -# Internal/infrastructure endpoints (not part of public API, no docs needed) -GET /block/embed/:id -> internal -GET /block/:id -> internal -GET /referral/:id -> internal -GET /client_version -> internal -GET /client_version/daily -> internal -POST /receive_nps_response -> internal -POST /receive_pmf_response -> internal -GET /current_time -> internal -POST /graphql/v2 -> internal -GET /graphql/v2 -> internal -GET /graphiql -> internal -GET /graphiql/v2 -> internal -GET /download -> internal -GET /download/brew -> internal -GET /download/windows -> internal -GET /download/cli -> internal +# OAuth device-flow / OIDC plumbing used by `oz login` — not a public REST surface. +GET /oauth/authorize -> internal +POST /oauth/device/auth -> internal +POST /oauth/session -> internal +POST /oauth/token -> internal +GET /oauth/jwks.json -> internal +GET /.well-known/openid-configuration -> internal + +# Anonymous-viewer redirect probes (documented exceptions to auth, not API surfaces). +GET /agent/sessions/{session_uuid}/redirect -> internal +GET /agent/conversations/{conversation_id}/redirect -> internal + +# Legacy aliases of /agent/runs kept for compatibility. +GET /agent/tasks -> internal +GET /agent/tasks/{id} -> internal +POST /agent/tasks/{id}/cancel -> internal + +# Handoff/worker attachment plumbing (driven by clients and workers, not end users). +POST /agent/runs/{runId}/attachments/prepare -> internal +POST /agent/runs/{runId}/attachments/download -> internal +GET /agent/runs/{runId}/handoff/attachments -> internal +POST /agent/handoff/upload-snapshot -> internal +PATCH /agent/runs/{runId}/event-sequence -> internal +POST /agent/runs/{runId}/client-events -> internal +GET /agent/conversations/{conversation_id}/block-snapshot -> internal + +# Support endpoints for third-party harnesses (hidden `oz harness-support` CLI). +POST /harness-support/external-conversation -> internal +POST /harness-support/block-snapshot -> internal +POST /harness-support/transcript -> internal +GET /harness-support/transcript -> internal +POST /harness-support/resolve-prompt -> internal +POST /harness-support/report-artifact -> internal +POST /harness-support/notify-user -> internal +POST /harness-support/finish-task -> internal +POST /harness-support/report-shutdown -> internal +POST /harness-support/upload-snapshot -> internal + +## Slash commands -> doc pages + +# Most documented commands are matched automatically against the +# slash-commands page content; add entries here only for exceptions. +# Gated by the dogfood-only LocalDockerSandbox flag — not user-facing yet. +/docker-sandbox -> internal ## Flags to ignore (internal-only, not user-facing) # These flags are internal implementation details and don't need documentation CocoaSentry CrashReporting -CrashRecoveryForceX11 DebugMode LogExpensiveFramesInSentry WithSandboxTelemetry @@ -199,19 +239,14 @@ RecordPtyThroughput FetchGenericStringObjects IntegratedGPU LazySceneBuilding -RemoveAltScreenPadding MaximizeFlatStorage SharedBlockTitleGeneration RetryTruncatedCodeResponses ReloadStaleConversationFiles -NLDClassifierModelEnabled -ChangedLinesOnlyApplyDiffResult SendTelemetryToFile -SendEvalMetadata FileGlobV2Warnings ExpandEditToPane MCPGroupedServerContext -MultiAgentParallelToolCalls AgentDecidesCommandExecution AgentModePrimaryXML AgentModePrePlanXML @@ -221,12 +256,9 @@ GlobalAIAnalyticsCollection FastForwardAutoexecuteButton LinkedCodeBlocks V4AFileDiffs -NewWarpingAnimation -NewDiffModel SummarizationViaMessageReplacement SummarizationCancellationConfirmation TabCloseButtonOnLeft -LessHorizontalTerminalPadding RemoveAutosuggestionDuringTabCompletions ResizeFix ForceClassicCompletions @@ -237,21 +269,24 @@ MinimalistUI AvatarInTabBar SessionSharingAcls ImeMarkedText -ConvertLegacyMcps NewTabStyling AmbientAgentsRTC -OzBranding OzLaunchModal # One-time launch modal announcing Warp going open-source. # The announcement itself is covered in the 2026 changelog ("Warp is now open source.") # and the modal has no recurring user-facing surface that warrants its own doc page. OpenWarpLaunchModal +# One-time launch modal announcing multi-agent orchestration; the feature itself +# is documented via RunAgentsTool -> orchestration/multi-agent-runs.mdx. +OrchestrationLaunchModal GetStartedTab CreateProjectFlow CodeLaunchModal ValidateAutosuggestions ClearAutosuggestionOnEscape OzPlatformSkills +# Rendering detail for markdown tables in notebooks/AI output; no dedicated doc surface. +MarkdownTables # UI implementation details (not user-facing features) FallbackModelLoadOutputMessaging @@ -273,26 +308,27 @@ HOAOnboardingFlow AgentViewConversationListView BuildPlanAutoReloadBannerToggle BuildPlanAutoReloadPostPurchaseModal -UpgradeToProModal -UpgradeToProModalPromo FreeUserNoAi -SoloUserByok ForceLogin SimulateGithubUnauthed ConversationApi McpDebuggingIds ContextLineReviewComments RichTextMultiselect -ActiveConversationRequiresInteraction +# Redux iterations of the cloud mode setup/input UI; the cloud agents feature +# itself is documented via CloudMode -> cloud-agents/overview.md. +CloudModeSetupV2 +CloudModeInputV2 +# Internal GitHub credential refresh during task runs (changelog-only behavior fix). +GitCredentialRefresh +# Internal SSE streaming infrastructure for orchestration viewers/owners. +OrchestrationViewerStreamer +OwnerOrchestrationAncestorStreamer # Non-GA flags in dogfood/preview only -Orchestration -OrchestrationV2 -OrchestrationEventPush LSPAsATool SshRemoteServer EmbeddedCodeReviewComments -AgentManagementDetailsView InteractiveConversationManagementView MarkdownImages MarkdownMermaid @@ -301,11 +337,9 @@ OzIdentityFederation AgentHarness DirectoryTabColors ArtifactCommand -AgentViewBlockContext CloudModeImageContext CloudModeHostSelector AmbientAgentsImageUpload -NldImprovements CodebaseIndexSpeedbump CodebaseIndexPersistence SharedSessionWriteToLongRunningCommands @@ -317,8 +351,6 @@ CodeModeChip UndoClosedPanes RevertDiffHunk ViewingSharedSessions -SettingsImport -BlockToolbeltSaveAsWorkflow ShellSelector FullScreenZenMode WorkflowAliases @@ -339,7 +371,5 @@ PredictAMQueries UseTantivySearch CommandCorrectionsHistoryRule SuggestedAgentModeWorkflows -ConversationArtifacts -ConversationApi PRCommentsSkill FigmaDetection diff --git a/.agents/skills/missing_docs/references/surface_snapshot.json b/.agents/skills/missing_docs/references/surface_snapshot.json new file mode 100644 index 000000000..1dcccdc43 --- /dev/null +++ b/.agents/skills/missing_docs/references/surface_snapshot.json @@ -0,0 +1,670 @@ +{ + "schema_version": 1, + "flags": { + "AIBlockOverflowMenu": "other", + "AIContextMenuCode": "ga", + "AIContextMenuCommands": "other", + "AIContextMenuEnabled": "ga", + "AIGeneratedOnboardingSuggestions": "other", + "AIMemories": "other", + "AIResumeButton": "ga", + "AIRules": "ga", + "APIKeyAuthentication": "ga", + "APIKeyManagement": "ga", + "ActiveConversationRequiresInteraction": "ga", + "AgentDecidesCommandExecution": "ga", + "AgentHarness": "ga", + "AgentManagementDetailsView": "ga", + "AgentManagementView": "ga", + "AgentMode": "ga", + "AgentModeAnalytics": "dogfood", + "AgentModeComputerUse": "ga", + "AgentModePrePlanXML": "ga", + "AgentModePrimaryXML": "ga", + "AgentModeWorkflows": "ga", + "AgentOnboarding": "ga", + "AgentPredict": "other", + "AgentSharedSessions": "ga", + "AgentTips": "ga", + "AgentToolbarEditor": "ga", + "AgentView": "ga", + "AgentViewBlockContext": "ga", + "AgentViewConversationListView": "ga", + "AgentViewPromptChip": "other", + "AlacrittySettingsImport": "other", + "AllowIgnoringInputSuggestions": "ga", + "AllowOpeningFileLinksUsingEditorEnv": "ga", + "AmbientAgentsCommandLine": "ga", + "AmbientAgentsImageUpload": "ga", + "AmbientAgentsRTC": "ga", + "ArtifactCommand": "ga", + "AskUserQuestion": "ga", + "AsyncFind": "dogfood", + "AtMenuOutsideOfAIMode": "ga", + "AutoOpenCodeReviewPane": "ga", + "Autoupdate": "ga", + "AutoupdateUIRevamp": "ga", + "AvatarInTabBar": "ga", + "BillingAndUsagePageV2": "ga", + "BlocklistMarkdownImages": "ga", + "BlocklistMarkdownTableRendering": "ga", + "BuildPlanAutoReloadBannerToggle": "other", + "BuildPlanAutoReloadPostPurchaseModal": "other", + "BundledSkills": "ga", + "CLIAgentRichInput": "ga", + "Changelog": "ga", + "ClassicCompletions": "ga", + "ClearAutosuggestionOnEscape": "ga", + "CloudConversations": "ga", + "CloudEnvironments": "ga", + "CloudMode": "ga", + "CloudModeFromLocalSession": "ga", + "CloudModeHostSelector": "other", + "CloudModeImageContext": "ga", + "CloudModeInputV2": "ga", + "CloudModeSetupV2": "ga", + "CloudObjects": "other", + "CocoaSentry": "other", + "CodeFindReplace": "ga", + "CodeLaunchModal": "ga", + "CodeModeChip": "other", + "CodeReviewFind": "ga", + "CodeReviewSaveChanges": "ga", + "CodeReviewScrollPreservation": "dogfood", + "CodebaseIndexPersistence": "dogfood", + "CodebaseIndexSpeedbump": "dogfood", + "CodexNotifications": "ga", + "CodexPlugin": "dogfood", + "CommandCorrectionKey": "ga", + "CommandCorrectionsHistoryRule": "other", + "CommandPaletteFileSearch": "ga", + "ConfigurableToolbar": "ga", + "ContextChips": "other", + "ContextLineReviewComments": "dogfood", + "ContextWindowUsageV2": "ga", + "ConversationApi": "ga", + "ConversationArtifacts": "ga", + "ConversationsAsContext": "ga", + "CrashReporting": "ga", + "CreateEnvironmentSlashCommand": "ga", + "CreateProjectFlow": "ga", + "CreatingSharedSessions": "dogfood", + "CrossRepoContext": "dogfood", + "CustomInferenceEndpoints": "ga", + "CustomInferenceEndpointsEnterprise": "other", + "CycleNextCommandSuggestion": "other", + "DebugMode": "other", + "DefaultAdeberryTheme": "other", + "DefaultWaterfallMode": "ga", + "DiffSetAsContext": "ga", + "DirectoryTabColors": "ga", + "DiscardPerFileAndAllChanges": "ga", + "DragTabsToWindows": "preview", + "DriveObjectsAsContext": "ga", + "DynamicWorkflowEnums": "ga", + "EditableMarkdownMermaid": "dogfood", + "EmbeddedCodeReviewComments": "other", + "ExpandEditToPane": "ga", + "FallbackModelLoadOutputMessaging": "ga", + "FastForwardAutoexecuteButton": "ga", + "FetchChannelVersionsFromWarpServer": "other", + "FetchGenericStringObjects": "other", + "FigmaDetection": "ga", + "FileAndDiffSetComments": "dogfood", + "FileBasedMcp": "ga", + "FileGlobV2Warnings": "dogfood", + "FileRetrievalTools": "ga", + "FileTree": "ga", + "ForceClassicCompletions": "ga", + "ForceLogin": "other", + "ForkFromCommand": "ga", + "FreeUserNoAi": "other", + "FullScreenZenMode": "ga", + "FullSourceCodeEmbedding": "dogfood", + "GPTConfigurableContextWindow": "dogfood", + "GeminiNotifications": "dogfood", + "GetStartedTab": "ga", + "GitCredentialRefresh": "ga", + "GitOperationsInCodeReview": "ga", + "GithubPrPromptChip": "ga", + "GlobalAIAnalyticsBanner": "other", + "GlobalAIAnalyticsCollection": "ga", + "GlobalSearch": "ga", + "GrepTool": "ga", + "GroupedTabs": "preview", + "HOANotifications": "ga", + "HOAOnboardingFlow": "ga", + "HOARemoteControl": "ga", + "HandoffCloudCloud": "ga", + "HandoffLocalCloud": "ga", + "HarnessSessionHeader": "other", + "HoaCodeReview": "ga", + "ITermImages": "other", + "ImageAsContext": "ga", + "ImeMarkedText": "ga", + "InBandGeneratorsForSSH": "other", + "IncrementalAutoReload": "ga", + "InlineCodeReview": "ga", + "InlineHistoryMenu": "ga", + "InlineMenuHeaders": "ga", + "InlineProfileSelector": "ga", + "InlineRepoMenu": "ga", + "IntegratedGPU": "other", + "IntegrationCommand": "ga", + "InteractiveConversationManagementView": "ga", + "KittyImages": "ga", + "KittyKeyboardProtocol": "ga", + "KnowledgeSidebar": "other", + "LSPAsATool": "other", + "LazySceneBuilding": "dogfood", + "Ligatures": "ga", + "LinkedCodeBlocks": "ga", + "ListSkills": "ga", + "LocalClaudeCodexChildHarnesses": "other", + "LocalComputerUse": "dogfood", + "LocalDockerSandbox": "dogfood", + "LogExpensiveFramesInSentry": "dogfood", + "MCPGroupedServerContext": "ga", + "MSYS2Shells": "dogfood", + "MarkdownImages": "dogfood", + "MarkdownMermaid": "ga", + "MarkdownTables": "ga", + "MaximizeFlatStorage": "other", + "McpDebuggingIds": "other", + "McpOauth": "ga", + "McpServer": "ga", + "MinimalistUI": "ga", + "MultiProfile": "ga", + "MultiWorkspace": "dogfood", + "NamedAgents": "ga", + "NativeShellCompletions": "other", + "NewTabStyling": "ga", + "OpenCodeNotifications": "ga", + "OpenWarpLaunchModal": "ga", + "OpenWarpNewSettingsModes": "ga", + "OrchestrationLaunchModal": "ga", + "OrchestrationViewerStreamer": "ga", + "OwnerOrchestrationAncestorStreamer": "ga", + "OzChangelogUpdates": "ga", + "OzHandoff": "ga", + "OzIdentityFederation": "ga", + "OzLaunchModal": "ga", + "OzPlatformSkills": "ga", + "PRCommentsSkill": "ga", + "PRCommentsSlashCommand": "ga", + "PRCommentsV2": "ga", + "PartialNextCommandSuggestions": "other", + "PendingUserQueryIndicator": "ga", + "PinnedTabs": "other", + "PluggableNotifications": "ga", + "PredictAMQueries": "other", + "ProfilesDesignRevamp": "ga", + "Projects": "dogfood", + "PromptSuggestionsViaMAA": "other", + "ProviderCommand": "dogfood", + "QueueSlashCommand": "ga", + "QueuedPromptsV2": "ga", + "ReadImageFiles": "ga", + "RecordAppActiveEvents": "other", + "RecordPtyThroughput": "other", + "RectSelection": "ga", + "ReloadStaleConversationFiles": "ga", + "RememberFastForwardState": "dogfood", + "RemoteCodeReview": "ga", + "RemoteCodebaseIndexing": "ga", + "RemoveAutosuggestionDuringTabCompletions": "dogfood", + "ResizeFix": "dogfood", + "RestorePromptOnInlineModelSelectorSearch": "dogfood", + "RetryTruncatedCodeResponses": "dogfood", + "RevertDiffHunk": "ga", + "RevertToCheckpoints": "ga", + "RewindSlashCommand": "ga", + "RichTextMultiselect": "ga", + "RunAgentsTool": "ga", + "RunGeneratorsWithCmdExe": "dogfood", + "RuntimeFeatureFlags": "other", + "SSHTmuxWrapper": "dogfood", + "ScheduledAmbientAgents": "ga", + "SearchCodebaseUI": "ga", + "SelectablePrompt": "other", + "SelectionAsContext": "ga", + "SendTelemetryToFile": "other", + "SequentialStorage": "other", + "SessionSharingAcls": "ga", + "SettingsFile": "ga", + "SharedBlockTitleGeneration": "ga", + "SharedSessionWriteToLongRunningCommands": "ga", + "SharedWithMe": "ga", + "ShellSelector": "ga", + "SimulateGithubUnauthed": "other", + "SkillArguments": "ga", + "SkipFirebaseAnonymousUser": "ga", + "SoloUserByok": "ga", + "SshDragAndDrop": "dogfood", + "SshRemoteServer": "ga", + "SuggestedAgentModeWorkflows": "other", + "SuggestedRules": "ga", + "SummarizationCancellationConfirmation": "ga", + "SummarizationConversationCommand": "ga", + "SummarizationViaMessageReplacement": "dogfood", + "SuperGrok": "dogfood", + "SyncAmbientPlans": "ga", + "TabCloseButtonOnLeft": "ga", + "TabConfigs": "ga", + "TabbedEditorView": "ga", + "TeamApiKeys": "ga", + "ThinStrokes": "other", + "ToggleBootstrapBlock": "dogfood", + "TransferControlTool": "ga", + "TrimTrailingBlankLines": "ga", + "UIZoom": "ga", + "UndoClosedPanes": "ga", + "UsageBasedPricing": "ga", + "UseTantivySearch": "other", + "V4AFileDiffs": "ga", + "ValidateAutosuggestions": "ga", + "VerticalTabs": "ga", + "VerticalTabsSummaryMode": "ga", + "ViewingSharedSessions": "ga", + "VimCodeEditor": "ga", + "WarpControlCli": "other", + "WarpManagedSecrets": "ga", + "WarpPacks": "ga", + "WarpifyFooter": "ga", + "WebFetchUI": "ga", + "WebSearchUI": "ga", + "WelcomeBlock": "other", + "WelcomeTab": "other", + "WelcomeTips": "other", + "WithSandboxTelemetry": "other", + "WorkflowAliases": "ga" + }, + "cli_commands": [ + { + "command": "oz agent", + "hidden": false + }, + { + "command": "oz agent create", + "hidden": false + }, + { + "command": "oz agent delete", + "hidden": false + }, + { + "command": "oz agent get", + "hidden": false + }, + { + "command": "oz agent list", + "hidden": false + }, + { + "command": "oz agent profile", + "hidden": false + }, + { + "command": "oz agent run", + "hidden": false + }, + { + "command": "oz agent run-cloud", + "hidden": false + }, + { + "command": "oz agent skills", + "hidden": false + }, + { + "command": "oz agent update", + "hidden": false + }, + { + "command": "oz api-key", + "hidden": false + }, + { + "command": "oz api-key create", + "hidden": false + }, + { + "command": "oz api-key expire", + "hidden": false + }, + { + "command": "oz api-key list", + "hidden": false + }, + { + "command": "oz artifact", + "hidden": false + }, + { + "command": "oz artifact download", + "hidden": false + }, + { + "command": "oz artifact get", + "hidden": false + }, + { + "command": "oz artifact upload", + "hidden": true + }, + { + "command": "oz environment", + "hidden": false + }, + { + "command": "oz environment create", + "hidden": false + }, + { + "command": "oz environment delete", + "hidden": false + }, + { + "command": "oz environment get", + "hidden": false + }, + { + "command": "oz environment image", + "hidden": false + }, + { + "command": "oz environment list", + "hidden": false + }, + { + "command": "oz environment update", + "hidden": false + }, + { + "command": "oz federate", + "hidden": false + }, + { + "command": "oz federate issue-gcp-token", + "hidden": true + }, + { + "command": "oz federate issue-token", + "hidden": false + }, + { + "command": "oz harness-support", + "hidden": true + }, + { + "command": "oz harness-support finish-task", + "hidden": true + }, + { + "command": "oz harness-support notify-user", + "hidden": true + }, + { + "command": "oz harness-support ping", + "hidden": true + }, + { + "command": "oz harness-support report-artifact", + "hidden": true + }, + { + "command": "oz harness-support report-shutdown", + "hidden": true + }, + { + "command": "oz integration", + "hidden": false + }, + { + "command": "oz integration create", + "hidden": false + }, + { + "command": "oz integration list", + "hidden": false + }, + { + "command": "oz integration update", + "hidden": false + }, + { + "command": "oz login", + "hidden": false + }, + { + "command": "oz logout", + "hidden": false + }, + { + "command": "oz mcp", + "hidden": false + }, + { + "command": "oz mcp list", + "hidden": false + }, + { + "command": "oz model", + "hidden": false + }, + { + "command": "oz model list", + "hidden": false + }, + { + "command": "oz provider", + "hidden": false + }, + { + "command": "oz provider list", + "hidden": false + }, + { + "command": "oz provider setup", + "hidden": false + }, + { + "command": "oz run", + "hidden": false + }, + { + "command": "oz run conversation", + "hidden": false + }, + { + "command": "oz run get", + "hidden": false + }, + { + "command": "oz run list", + "hidden": false + }, + { + "command": "oz run message", + "hidden": false + }, + { + "command": "oz schedule", + "hidden": false + }, + { + "command": "oz schedule create", + "hidden": false + }, + { + "command": "oz schedule delete", + "hidden": false + }, + { + "command": "oz schedule get", + "hidden": false + }, + { + "command": "oz schedule list", + "hidden": false + }, + { + "command": "oz schedule pause", + "hidden": false + }, + { + "command": "oz schedule unpause", + "hidden": false + }, + { + "command": "oz schedule update", + "hidden": false + }, + { + "command": "oz secret", + "hidden": false + }, + { + "command": "oz secret create", + "hidden": false + }, + { + "command": "oz secret delete", + "hidden": false + }, + { + "command": "oz secret list", + "hidden": false + }, + { + "command": "oz secret update", + "hidden": false + }, + { + "command": "oz whoami", + "hidden": false + } + ], + "api_routes": [ + "DELETE /api/v1/agent/identities/{uid}", + "DELETE /api/v1/agent/schedules/{id}", + "DELETE /api/v1/memory_stores/{uid}", + "DELETE /api/v1/memory_stores/{uid}/memories/{memoryUid}", + "GET /.well-known/openid-configuration", + "GET /api/v1/agent", + "GET /api/v1/agent/artifacts/{uid}", + "GET /api/v1/agent/connected-self-hosted-workers", + "GET /api/v1/agent/conversations/{conversation_id}/block-snapshot", + "GET /api/v1/agent/conversations/{conversation_id}/redirect", + "GET /api/v1/agent/environments", + "GET /api/v1/agent/events", + "GET /api/v1/agent/events/stream", + "GET /api/v1/agent/identities", + "GET /api/v1/agent/identities/{uid}", + "GET /api/v1/agent/messages/{run_id}", + "GET /api/v1/agent/models", + "GET /api/v1/agent/runs", + "GET /api/v1/agent/runs/{runId}", + "GET /api/v1/agent/runs/{runId}/handoff/attachments", + "GET /api/v1/agent/runs/{runId}/timeline", + "GET /api/v1/agent/runs/{runId}/transcript", + "GET /api/v1/agent/schedules", + "GET /api/v1/agent/schedules/{id}", + "GET /api/v1/agent/sessions/{session_uuid}/redirect", + "GET /api/v1/agent/tasks", + "GET /api/v1/agent/tasks/{id}", + "GET /api/v1/harness-support/transcript", + "GET /api/v1/memory_stores", + "GET /api/v1/memory_stores/{uid}", + "GET /api/v1/memory_stores/{uid}/agents", + "GET /api/v1/memory_stores/{uid}/memories", + "GET /api/v1/memory_stores/{uid}/memories/{memoryUid}/versions", + "GET /oauth/authorize", + "GET /oauth/jwks.json", + "PATCH /api/v1/agent/runs/{runId}/event-sequence", + "POST /api/v1/agent/events/{run_id}", + "POST /api/v1/agent/handoff/upload-snapshot", + "POST /api/v1/agent/identities", + "POST /api/v1/agent/messages", + "POST /api/v1/agent/messages/{id}/delivered", + "POST /api/v1/agent/messages/{id}/read", + "POST /api/v1/agent/run", + "POST /api/v1/agent/runs", + "POST /api/v1/agent/runs/{runId}/attachments/download", + "POST /api/v1/agent/runs/{runId}/attachments/prepare", + "POST /api/v1/agent/runs/{runId}/cancel", + "POST /api/v1/agent/runs/{runId}/client-events", + "POST /api/v1/agent/runs/{runId}/followups", + "POST /api/v1/agent/schedules", + "POST /api/v1/agent/schedules/{id}/pause", + "POST /api/v1/agent/schedules/{id}/resume", + "POST /api/v1/agent/tasks/{id}/cancel", + "POST /api/v1/harness-support/block-snapshot", + "POST /api/v1/harness-support/external-conversation", + "POST /api/v1/harness-support/finish-task", + "POST /api/v1/harness-support/notify-user", + "POST /api/v1/harness-support/report-artifact", + "POST /api/v1/harness-support/report-shutdown", + "POST /api/v1/harness-support/resolve-prompt", + "POST /api/v1/harness-support/transcript", + "POST /api/v1/harness-support/upload-snapshot", + "POST /api/v1/memory_stores", + "POST /api/v1/memory_stores/{uid}/memories", + "POST /oauth/device/auth", + "POST /oauth/session", + "POST /oauth/token", + "PUT /api/v1/agent/identities/{uid}", + "PUT /api/v1/agent/schedules/{id}", + "PUT /api/v1/memory_stores/{uid}", + "PUT /api/v1/memory_stores/{uid}/memories/{memoryUid}" + ], + "slash_commands": [ + "/add-mcp", + "/add-prompt", + "/add-rule", + "/agent", + "/changelog", + "/cloud-agent", + "/compact", + "/compact-and", + "/continue-locally", + "/conversations", + "/cost", + "/create-environment", + "/create-new-project", + "/docker-sandbox", + "/environment", + "/export-to-clipboard", + "/export-to-file", + "/feedback", + "/fork", + "/fork-and-compact", + "/fork-from", + "/handoff", + "/harness", + "/host", + "/index", + "/init", + "/model", + "/new", + "/open-code-review", + "/open-file", + "/open-mcp-servers", + "/open-project-rules", + "/open-repo", + "/open-rules", + "/open-settings-file", + "/open-skill", + "/pr-comments", + "/profile", + "/prompts", + "/queue", + "/remote-control", + "/rename-tab", + "/rewind", + "/set-tab-color", + "/skills", + "/usage" + ], + "changelog_last_version": "2026.06.03" +} diff --git a/.agents/skills/missing_docs/scripts/audit_docs.py b/.agents/skills/missing_docs/scripts/audit_docs.py index 020d88d67..b4b21f164 100755 --- a/.agents/skills/missing_docs/scripts/audit_docs.py +++ b/.agents/skills/missing_docs/scripts/audit_docs.py @@ -3,13 +3,21 @@ Missing Docs Audit Script for Warp Astro Starlight Documentation Compares documentation coverage against code surfaces in warp-internal and -warp-server to identify gaps. Produces a structured JSON report. +warp-server to identify gaps, and (in --diff mode) detects surface changes +since the last committed snapshot. Produces a structured JSON report. Usage: python3 .agents/skills/missing_docs/scripts/audit_docs.py python3 .agents/skills/missing_docs/scripts/audit_docs.py --category features python3 .agents/skills/missing_docs/scripts/audit_docs.py --output report.json - python3 .agents/skills/missing_docs/scripts/audit_docs.py --weak-coverage + python3 .agents/skills/missing_docs/scripts/audit_docs.py --diff + python3 .agents/skills/missing_docs/scripts/audit_docs.py --update-snapshot + +Exit codes: + 0 — all requested audits ran (findings may still exist; check the report) + 1 — fatal setup error (docs directory not found, bad arguments) + 2 — one or more audits were SKIPPED (missing repo paths). Never treat a + run that exits 2 as a clean audit. """ import argparse @@ -33,6 +41,9 @@ SKILL_DIR = SCRIPT_DIR.parent SURFACE_MAP_PATH = SKILL_DIR / "references" / "feature_surface_map.md" STALE_TERMS_PATH = SKILL_DIR / "references" / "stale_terms.md" +DEFAULT_SNAPSHOT_PATH = SKILL_DIR / "references" / "surface_snapshot.json" + +SNAPSHOT_SCHEMA_VERSION = 1 # --------------------------------------------------------------------------- # Surface map parser @@ -44,6 +55,7 @@ def parse_surface_map(path: Path) -> dict: "feature_to_doc": {}, "cli_to_doc": {}, "api_to_doc": {}, + "slash_to_doc": {}, "ignore_flags": set(), } if not path.exists(): @@ -59,6 +71,8 @@ def parse_surface_map(path: Path) -> dict: current_section = "cli" elif line.startswith("## API endpoints"): current_section = "api" + elif line.startswith("## Slash commands"): + current_section = "slash" elif line.startswith("## Flags to ignore"): current_section = "ignore" continue @@ -77,6 +91,8 @@ def parse_surface_map(path: Path) -> dict: result["cli_to_doc"][key] = doc_path elif current_section == "api": result["api_to_doc"][key] = doc_path + elif current_section == "slash": + result["slash_to_doc"][key] = doc_path return result @@ -100,8 +116,11 @@ def parse_stale_terms(path: Path) -> list[tuple[str, str]]: # Helpers # --------------------------------------------------------------------------- -def find_repo(name: str, explicit_path: str | None, docs_root: Path) -> Path | None: - """Find a sibling repo by name, or use the explicit path.""" +def find_repo(name: str, explicit_path: str | None, repo_root: Path) -> Path | None: + """Find a source repo by explicit path or as a sibling of the docs repo root. + + e.g. docs at /workspace/docs -> look for /workspace/. + """ if explicit_path: p = Path(explicit_path).resolve() if p.exists(): @@ -109,8 +128,7 @@ def find_repo(name: str, explicit_path: str | None, docs_root: Path) -> Path | N print(f"Warning: explicit path {explicit_path} does not exist", file=sys.stderr) return None - # Try sibling directory (docs is at .../code/docs, look for .../code/) - sibling = docs_root.parent / name + sibling = repo_root.parent / name if sibling.exists(): return sibling return None @@ -128,7 +146,7 @@ def find_markdown_files(docs_root: Path) -> list[Path]: def read_all_docs_text(docs_root: Path) -> dict[str, str]: - """Read all doc files into a dict of {relative_path: content}.""" + """Read all doc files into a dict of {relative_path: content} (lowercased).""" result = {} for f in find_markdown_files(docs_root): try: @@ -139,6 +157,23 @@ def read_all_docs_text(docs_root: Path) -> dict[str, str]: return result +_FENCED_CODE_RE = re.compile(r"```.*?```", re.DOTALL) +_INLINE_CODE_RE = re.compile(r"`[^`\n]*`") +_HTML_CODE_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) + + +def strip_code_spans(text: str) -> str: + """Remove fenced code blocks, inline code spans, and elements. + + Used by the staleness audit so CLI examples (e.g. `oz agent run`) don't + trigger terminology findings meant for prose. + """ + text = _FENCED_CODE_RE.sub(" ", text) + text = _HTML_CODE_RE.sub(" ", text) + text = _INLINE_CODE_RE.sub(" ", text) + return text + + def resolve_doc_path(doc_path: str, repo_root: Path) -> Path | None: """Return the first existing variant of a mapped doc path. @@ -197,28 +232,33 @@ def search_docs_for_terms(docs_text: dict[str, str], terms: list[str]) -> list[s break return matches + +def kebab_case(name: str) -> str: + """PascalCase -> kebab-case: RunCloud -> run-cloud, MCP -> mcp.""" + return re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", name).lower() + # --------------------------------------------------------------------------- -# Audit 1: Feature flag coverage +# Extraction: feature flags (warp-internal) # --------------------------------------------------------------------------- -def parse_feature_flags(warp_internal: Path) -> list[str]: - """Parse FeatureFlag enum variants from features.rs.""" - # Try known locations in order +def _features_lib_rs(warp_internal: Path) -> Path | None: candidates = [ warp_internal / "crates" / "warp_features" / "src" / "lib.rs", warp_internal / "crates" / "warp_core" / "src" / "features.rs", warp_internal / "app" / "src" / "features.rs", warp_internal / "warp_core" / "src" / "features.rs", ] - features_rs = next((c for c in candidates if c.exists()), None) + return next((c for c in candidates if c.exists()), None) + + +def parse_feature_flags(warp_internal: Path) -> list[str]: + """Parse FeatureFlag enum variants from the features lib.""" + features_rs = _features_lib_rs(warp_internal) if features_rs is None: - print(f"Warning: features.rs not found. Tried: {[str(c) for c in candidates]}", - file=sys.stderr) + print("Warning: FeatureFlag enum source not found in warp-internal", file=sys.stderr) return [] content = features_rs.read_text() - # Match enum variants (lines like " AgentMode," or " AgentMode {") - # inside the FeatureFlag enum in_enum = False flags = [] for line in content.splitlines(): @@ -229,16 +269,64 @@ def parse_feature_flags(warp_internal: Path) -> list[str]: if in_enum: if stripped == "}": break - # Skip comments, attributes, empty lines - if stripped.startswith("//") or stripped.startswith("#[") or stripped.startswith("///") or not stripped: + if stripped.startswith("//") or stripped.startswith("#[") or not stripped: continue - # Extract variant name match = re.match(r"^([A-Z]\w+)", stripped) if match: flags.append(match.group(1)) return flags +def parse_flag_list_const(warp_internal: Path, const_name: str) -> set[str]: + """Parse a `pub const : &[FeatureFlag] = &[...]` block into flag names.""" + features_rs = _features_lib_rs(warp_internal) + if features_rs is None: + return set() + content = features_rs.read_text() + match = re.search( + rf"const\s+{const_name}\s*:\s*&\[FeatureFlag\]\s*=\s*&\[(.*?)\];", + content, + re.DOTALL, + ) + if not match: + return set() + return set(re.findall(r"FeatureFlag::(\w+)", match.group(1))) + + +def parse_features_bridge(warp_internal: Path) -> dict[str, dict]: + """Parse the cargo-feature -> FeatureFlag bridge from app/src/features.rs. + + The authoritative mapping is the `enabled_features()` extend block: + + #[cfg(feature = "am_workflows")] + FeatureFlag::AgentModeWorkflows, + + Names frequently differ from a naive snake_case conversion, so this + bridge (not string transformation) decides which cargo feature gates a + flag. Entries gated on `debug_assertions` are never GA. + + Returns {flag_name: {"cargo_feature": str, "debug_only": bool}}. + """ + bridge_rs = warp_internal / "app" / "src" / "features.rs" + if not bridge_rs.exists(): + print(f"Warning: {bridge_rs} not found; GA detection will be incomplete", + file=sys.stderr) + return {} + + content = bridge_rs.read_text() + bridge: dict[str, dict] = {} + for match in re.finditer( + r"#\[cfg\(([^]]*?feature\s*=\s*\"(\w+)\"[^]]*?)\)\]\s*FeatureFlag::(\w+)", + content, + ): + cfg_expr, cargo_feature, flag = match.group(1), match.group(2), match.group(3) + bridge[flag] = { + "cargo_feature": cargo_feature, + "debug_only": "debug_assertions" in cfg_expr, + } + return bridge + + def parse_default_features(warp_internal: Path) -> set[str]: """Parse the default feature list from app/Cargo.toml.""" candidates = [ @@ -252,53 +340,428 @@ def parse_default_features(warp_internal: Path) -> set[str]: return set() content = cargo_toml.read_text() - # Find the default = [...] block match = re.search(r'default\s*=\s*\[(.*?)\]', content, re.DOTALL) if not match: return set() features_block = match.group(1) - # Extract quoted feature names return set(re.findall(r'"(\w+)"', features_block)) -def snake_to_pascal(snake: str) -> str: - """Convert snake_case to PascalCase: agent_mode -> AgentMode.""" - return "".join(word.capitalize() for word in snake.split("_")) +def compute_flag_statuses(warp_internal: Path) -> dict[str, str]: + """Classify every FeatureFlag by rollout status. + - "ga": gating cargo feature is in app/Cargo.toml default features, or the + flag is in RELEASE_FLAGS (enabled for all release builds). + - "preview": in PREVIEW_FLAGS (Preview builds; launching soon). + - "dogfood": in DOGFOOD_FLAGS (dev team only). + - "other": none of the above (runtime/experiment-gated or unused). These + may still be enabled via server-side experiments; the docs changelog + cross-check covers those launches. + """ + flags = parse_feature_flags(warp_internal) + bridge = parse_features_bridge(warp_internal) + default_features = parse_default_features(warp_internal) + release_flags = parse_flag_list_const(warp_internal, "RELEASE_FLAGS") + preview_flags = parse_flag_list_const(warp_internal, "PREVIEW_FLAGS") + dogfood_flags = parse_flag_list_const(warp_internal, "DOGFOOD_FLAGS") + + statuses: dict[str, str] = {} + for flag in flags: + info = bridge.get(flag) + is_ga = flag in release_flags + if info and not info["debug_only"] and info["cargo_feature"] in default_features: + is_ga = True + if is_ga: + statuses[flag] = "ga" + elif flag in preview_flags: + statuses[flag] = "preview" + elif flag in dogfood_flags: + statuses[flag] = "dogfood" + else: + statuses[flag] = "other" + return statuses + +# --------------------------------------------------------------------------- +# Extraction: CLI command tree (warp-internal) +# --------------------------------------------------------------------------- + +def _extract_enum_block(content: str, enum_name: str) -> str | None: + """Return the body of `pub enum { ... }` using brace matching.""" + match = re.search(rf"pub enum {enum_name}\s*\{{", content) + if not match: + return None + start = match.end() + depth = 1 + i = start + while i < len(content) and depth > 0: + if content[i] == "{": + depth += 1 + elif content[i] == "}": + depth -= 1 + i += 1 + return content[start:i - 1] + + +def _parse_enum_variants(enum_body: str) -> list[dict]: + """Parse top-level variants of a clap enum body. + + Returns [{"name", "hidden", "subcommand", "referenced_type"}]. + Tracks brace/paren depth so struct-variant fields aren't mistaken for + variants, and reads `hide = true` / `#[command(subcommand)]` from the + attributes stacked above each variant. + """ + variants = [] + depth = 0 + pending_attrs: list[str] = [] + for raw_line in enum_body.splitlines(): + line = raw_line.strip() + if depth == 0: + if line.startswith("#["): + pending_attrs.append(line) + elif line.startswith("///") or line.startswith("//") or not line: + pass + else: + match = re.match(r"^([A-Z]\w*)\s*(\(|\{|,|$)", line) + if match: + name = match.group(1) + attrs = " ".join(pending_attrs) + ref_match = re.search(r"\(\s*(?:crate::)?([\w:]+)\s*\)", line) + variants.append({ + "name": name, + "hidden": "hide = true" in attrs, + "subcommand": "subcommand" in attrs, + "referenced_type": ref_match.group(1) if ref_match else None, + }) + pending_attrs = [] + depth += raw_line.count("{") - raw_line.count("}") + depth += raw_line.count("(") - raw_line.count(")") + depth = max(depth, 0) + return variants + + +def _resolve_subcommand_enum(module_content: str, referenced_type: str | None) -> str | None: + """Find the enum body holding a variant's subcommands within a module file. + + Handles both direct enum references (AgentCommand) and the struct-wrapper + pattern (ScheduleCommand struct containing `Option`). + """ + if referenced_type: + type_name = referenced_type.split("::")[-1] + body = _extract_enum_block(module_content, type_name) + if body is not None: + return body + # Struct wrapper: look for any Subcommand-derived enum in the module. + for match in re.finditer(r"#\[derive\([^)]*Subcommand[^)]*\)\]", module_content): + tail = module_content[match.end():] + enum_match = re.search(r"pub enum (\w+)", tail[:300]) + if enum_match: + return _extract_enum_block(module_content, enum_match.group(1)) + return None + + +def parse_cli_commands(warp_internal: Path) -> list[dict]: + """Parse the full `oz` CLI command tree (top-level + one level of subcommands). + + Returns [{"command": "oz agent", "hidden": bool, "source_file": str, + "subcommands": [{"command": "oz agent run", "hidden": bool}]}] + """ + src_candidates = [ + warp_internal / "crates" / "warp_cli" / "src", + warp_internal / "warp_cli" / "src", + ] + src_dir = next((c for c in src_candidates if c.exists()), None) + if src_dir is None: + print("Warning: warp_cli/src not found in warp-internal", file=sys.stderr) + return [] + + lib_rs = src_dir / "lib.rs" + if not lib_rs.exists(): + print(f"Warning: {lib_rs} not found", file=sys.stderr) + return [] + + content = lib_rs.read_text() + enum_body = _extract_enum_block(content, "CliCommand") + if enum_body is None: + print("Warning: CliCommand enum not found in warp_cli/src/lib.rs", file=sys.stderr) + return [] + + commands = [] + for variant in _parse_enum_variants(enum_body): + cmd_name = kebab_case(variant["name"]) + entry = { + "command": f"oz {cmd_name}", + "hidden": variant["hidden"], + "source_file": None, + "subcommands": [], + } + ref = variant["referenced_type"] + if ref and "::" in ref: + module = ref.split("::")[0] + module_file = src_dir / f"{module}.rs" + if module_file.exists(): + entry["source_file"] = f"warp_cli/src/{module}.rs" + module_content = module_file.read_text() + sub_body = _resolve_subcommand_enum(module_content, ref) + if sub_body is not None: + for sub in _parse_enum_variants(sub_body): + entry["subcommands"].append({ + "command": f"oz {cmd_name} {kebab_case(sub['name'])}", + "hidden": sub["hidden"] or variant["hidden"], + }) + commands.append(entry) + return commands + +# --------------------------------------------------------------------------- +# Extraction: public API routes (warp-server) +# --------------------------------------------------------------------------- + +_GO_FUNC_RE = re.compile(r"^func (\w+)\(([^)]*)\)", re.MULTILINE) +_GO_GROUP_ASSIGN_RE = re.compile(r"(\w+)\s*:?=\s*(\w+)\.Group\(\s*\"([^\"]*)\"") +_GO_ROUTE_RE = re.compile(r"(\w+)\.(GET|POST|PUT|DELETE|PATCH)\(\s*\"([^\"]*)\"") +_GO_REGISTER_CALL_RE = re.compile( + r"(Register\w+)\(\s*(\w+)(?:\.Group\(\s*\"([^\"]*)\"\s*\))?\s*," +) + + +def _parse_go_functions(content: str) -> dict[str, dict]: + """Split a Go file into {func_name: {"params": str, "body": str}}.""" + functions = {} + matches = list(_GO_FUNC_RE.finditer(content)) + for i, match in enumerate(matches): + start = match.end() + end = matches[i + 1].start() if i + 1 < len(matches) else len(content) + functions[match.group(1)] = { + "params": match.group(2), + "body": content[start:end], + } + return functions + + +def parse_public_api_routes(warp_server: Path) -> list[dict]: + """Extract public API routes from router/handlers/public_api/*.go. + + Routes are registered via nested gin groups, e.g.: + + group := router.Group("/api/v1") (public_api.go) + RegisterAgentMessagingRoutes(group.Group("/agent"), ...) + messages := group.Group("/messages") (agent_messaging.go) + messages.POST("", SendMessageHandler(...)) -> POST /api/v1/agent/messages + + This walks group-variable assignments per registration function and + resolves caller-passed prefixes via Register* call sites, starting from + RegisterPublicAPIRoutes. Gin `:param` segments are normalized to + OpenAPI-style `{param}`. + """ + api_dir = warp_server / "router" / "handlers" / "public_api" + if not api_dir.exists(): + print(f"Warning: {api_dir} not found", file=sys.stderr) + return [] + + functions: dict[str, dict] = {} + for go_file in sorted(api_dir.glob("*.go")): + if go_file.name.endswith("_test.go"): + continue + for name, fn in _parse_go_functions(go_file.read_text()).items(): + fn["file"] = f"router/handlers/public_api/{go_file.name}" + functions[name] = fn + + def analyze(fn: dict) -> dict: + """Resolve a function body to routes/calls relative to its params.""" + params = fn["params"] + group_param = None + router_param = None + for param_match in re.finditer(r"(\w+)(?:\s*,\s*\w+)*\s+\*gin\.(RouterGroup|Engine)", params): + if param_match.group(2) == "RouterGroup" and group_param is None: + group_param = param_match.group(1) + elif param_match.group(2) == "Engine" and router_param is None: + router_param = param_match.group(1) + + # var name -> (base, prefix); base is "PARAM" (caller group) or + # "ROUTER" (engine root) + var_bases: dict[str, tuple] = {} + if group_param: + var_bases[group_param] = ("PARAM", "") + if router_param: + var_bases[router_param] = ("ROUTER", "") + + routes = [] + calls = [] + events = [] + for assign in _GO_GROUP_ASSIGN_RE.finditer(fn["body"]): + events.append(("assign", assign.start(), assign.groups())) + for route in _GO_ROUTE_RE.finditer(fn["body"]): + events.append(("route", route.start(), route.groups())) + for call in _GO_REGISTER_CALL_RE.finditer(fn["body"]): + events.append(("call", call.start(), call.groups())) + events.sort(key=lambda e: e[1]) + + for kind, _pos, groups in events: + if kind == "assign": + target, parent, prefix = groups + base = var_bases.get(parent) + if base is not None: + var_bases[target] = (base[0], base[1] + prefix) + elif kind == "route": + var, method, path = groups + base = var_bases.get(var) + if base is not None: + routes.append((base[0], method, base[1] + path)) + else: # call + callee, arg_var, arg_prefix = groups + base = var_bases.get(arg_var) + if base is not None: + calls.append((callee, base[0], base[1] + (arg_prefix or ""))) + return {"routes": routes, "calls": calls, "file": fn["file"]} + + analyzed = {name: analyze(fn) for name, fn in functions.items()} + + # Resolve absolute route prefixes by walking the call graph from + # RegisterPublicAPIRoutes. ROUTER-based paths are absolute already. + resolved: list[dict] = [] + visited: set[tuple] = set() + emitted_fns: set[str] = set() + + def emit(fn_name: str, param_prefix: str): + info = analyzed.get(fn_name) + if info is None: + return + key = (fn_name, param_prefix) + if key in visited: + return + visited.add(key) + emitted_fns.add(fn_name) + for base, method, path in info["routes"]: + full = (param_prefix + path) if base == "PARAM" else path + resolved.append({"method": method, "path": full, "file": info["file"]}) + for callee, base, prefix in info["calls"]: + callee_prefix = (param_prefix + prefix) if base == "PARAM" else prefix + emit(callee, callee_prefix) + + if "RegisterPublicAPIRoutes" in analyzed: + emit("RegisterPublicAPIRoutes", "") + # Any registration function not reachable from the entry point is assumed + # to hang off the /api/v1 group (conservative default so routes are never + # silently dropped). + for fn_name in sorted(analyzed): + if fn_name.startswith("Register") and fn_name not in emitted_fns: + emit(fn_name, "/api/v1") + + routes = [] + seen = set() + for route in resolved: + path = re.sub(r":(\w+)", r"{\1}", route["path"]) + path = re.sub(r"/{2,}", "/", path) or "/" + key = (route["method"], path) + if key in seen: + continue + seen.add(key) + routes.append({ + "method": route["method"], + "path": path, + "route": f"{route['method']} {path}", + "file": route["file"], + }) + routes.sort(key=lambda r: (r["path"], r["method"])) + return routes + +# --------------------------------------------------------------------------- +# Extraction: slash commands (warp-internal) +# --------------------------------------------------------------------------- + +def parse_slash_commands(warp_internal: Path) -> list[str]: + """Parse static slash command names from the registry.""" + registry_dir = ( + warp_internal / "app" / "src" / "search" / "slash_command_menu" / "static_commands" + ) + if not registry_dir.exists(): + print(f"Warning: {registry_dir} not found", file=sys.stderr) + return [] + + names: set[str] = set() + for rs_file in sorted(registry_dir.glob("*.rs")): + if rs_file.name.endswith("_tests.rs"): + continue + for match in re.finditer(r'name:\s*"(/[a-z0-9][a-z0-9-]*)"', rs_file.read_text()): + names.add(match.group(1)) + return sorted(names) + +# --------------------------------------------------------------------------- +# Extraction: docs changelog entries +# --------------------------------------------------------------------------- + +_CHANGELOG_HEADER_RE = re.compile(r"^### (\d{4}\.\d{2}\.\d{2})", re.MULTILINE) +_CHANGELOG_TRACKED_SECTIONS = ("new features", "improvements") + + +def parse_changelog_entries(repo_root: Path) -> list[dict]: + """Parse release entries from src/content/docs/changelog/.mdx. + + Returns [{"version": "2026.06.03", "file": str, "items": + [{"category": "new features", "text": str}]}] sorted newest first. + Only "New features" and "Improvements" bullets are tracked — those are the + sections that may represent undocumented feature launches. + """ + changelog_dir = repo_root / "src" / "content" / "docs" / "changelog" + if not changelog_dir.exists(): + return [] + + entries = [] + for mdx in sorted(changelog_dir.glob("*.mdx")): + if not re.fullmatch(r"\d{4}", mdx.stem): + continue + content = mdx.read_text(encoding="utf-8") + headers = list(_CHANGELOG_HEADER_RE.finditer(content)) + for i, header in enumerate(headers): + end = headers[i + 1].start() if i + 1 < len(headers) else len(content) + body = content[header.end():end] + items = [] + current_section = None + for line in body.splitlines(): + stripped = line.strip() + section_match = re.match(r"^\*\*(.+?)\*\*$", stripped) + if section_match: + current_section = section_match.group(1).strip().lower() + continue + if current_section in _CHANGELOG_TRACKED_SECTIONS and stripped.startswith("* "): + items.append({ + "category": current_section, + "text": stripped[2:].strip(), + }) + entries.append({ + "version": header.group(1), + "file": str(mdx.relative_to(repo_root)), + "items": items, + }) + entries.sort(key=lambda e: e["version"], reverse=True) + return entries + +# --------------------------------------------------------------------------- +# Audit 1: Feature flag coverage +# --------------------------------------------------------------------------- def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str], + flag_statuses: dict[str, str] | None = None, weak_coverage: bool = False) -> list[dict]: """Audit feature flag coverage in docs. - By default, a flag is treated as covered if its mapped doc exists (the - surface map maintainer has verified the mapping). When ``weak_coverage`` - is True, also verify that the target doc actually mentions feature - keywords — useful for catching pages that have been renamed or trimmed - so the flag is no longer documented in prose. + GA flags must be mapped in the surface map (with an existing target page) + or appear in docs prose. Preview flags produce low-severity "docs needed + soon" findings when uncovered. Dogfood/other flags are skipped (tracked by + the snapshot diff instead). """ - flags = parse_feature_flags(warp_internal) - default_features = parse_default_features(warp_internal) + if flag_statuses is None: + flag_statuses = compute_flag_statuses(warp_internal) ignore_flags = surface_map.get("ignore_flags", set()) feature_to_doc = surface_map.get("feature_to_doc", {}) - - # Mapped paths in feature_surface_map.md are repo-root relative - # (e.g., "src/content/docs/..."), so resolve against the repo root. repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent findings = [] - for flag in flags: - # Skip ignored flags + for flag, status in flag_statuses.items(): if flag in ignore_flags: continue - - # Determine if GA (in default features) - snake = re.sub(r"([a-z])([A-Z])", r"\1_\2", flag).lower() - is_ga = snake in default_features - - # Skip non-GA features (they're behind flags and may not need docs yet) - if not is_ga: + if status not in ("ga", "preview"): continue # Check if mapped in surface map @@ -311,9 +774,7 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, # maintainer has confirmed the page covers this flag. continue # Optional weak-coverage check: verify the target page - # actually mentions feature keywords. Skip the lowercase - # concatenated / snake_case variants since they rarely - # match human-written prose. + # actually mentions feature keywords. try: doc_content = resolved.read_text(encoding="utf-8").lower() except Exception: @@ -323,6 +784,7 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, if check_terms and not any(t in doc_content for t in check_terms): findings.append({ "flag": flag, + "status": status, "search_terms": terms, "severity": "low", "suggested_doc_path": doc_path, @@ -338,9 +800,22 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, terms = camel_to_search_terms(flag) matches = search_docs_for_terms(docs_text, terms) if matches: + findings.append({ + "flag": flag, + "status": status, + "search_terms": terms, + "severity": "low", + "suggested_doc_path": doc_path, + "matched_docs": matches[:3], + "reason": ( + f"Mapped doc {doc_path} does not exist but docs mention the " + "feature — update the surface map entry to the new location" + ), + }) continue findings.append({ "flag": flag, + "status": status, "search_terms": terms, "severity": "high", "suggested_doc_path": doc_path, @@ -351,14 +826,36 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, # Not in surface map — search docs for mentions terms = camel_to_search_terms(flag) matches = search_docs_for_terms(docs_text, terms) - if not matches: + if matches: + # Mentioned somewhere but unmapped. The fuzzy match may be a false + # negative (generic fragments like "slash command" match anything), + # so surface it as low-severity map work instead of passing silently. findings.append({ "flag": flag, + "status": status, "search_terms": terms, - "severity": "medium", + "severity": "low", "suggested_doc_path": None, - "reason": "GA feature with no doc mentions found", + "matched_docs": matches[:3], + "reason": ( + f"{'GA' if status == 'ga' else 'Preview'} flag is unmapped; docs " + "may mention it (fuzzy match) — verify coverage and add a surface " + "map entry (or move to the ignore list)" + ), }) + continue + findings.append({ + "flag": flag, + "status": status, + "search_terms": terms, + "severity": "medium" if status == "ga" else "low", + "suggested_doc_path": None, + "reason": ( + "GA feature with no doc mentions found" + if status == "ga" + else "Preview feature with no doc mentions found — docs needed soon" + ), + }) return findings @@ -366,72 +863,9 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, # Audit 2: CLI command coverage # --------------------------------------------------------------------------- -def parse_cli_commands(warp_internal: Path) -> list[dict]: - """Parse CLI subcommands from warp_cli/src/lib.rs.""" - candidates = [ - warp_internal / "crates" / "warp_cli" / "src" / "lib.rs", - warp_internal / "warp_cli" / "src" / "lib.rs", - ] - lib_rs = next((c for c in candidates if c.exists()), None) - if lib_rs is None: - print(f"Warning: warp_cli/src/lib.rs not found. Tried: {[str(c) for c in candidates]}", - file=sys.stderr) - return [] - - content = lib_rs.read_text() - commands = [] - - # Find CliCommand enum variants - in_enum = False - for line in content.splitlines(): - stripped = line.strip() - if "enum CliCommand" in stripped: - in_enum = True - continue - if in_enum: - if stripped == "}": - break - # Match lines like "Agent(crate::agent::AgentCommand)," - match = re.match(r"^([A-Z]\w+)(?:\(|,|\s*$)", stripped) - if match: - name = match.group(1) - # Convert PascalCase to lowercase command name - cmd_name = re.sub(r"([a-z])([A-Z])", r"\1-\2", name).lower() - # Find source file - source_match = re.search(rf"crate::(\w+)::", stripped) - source_file = f"warp_cli/src/{source_match.group(1)}.rs" if source_match else None - commands.append({ - "name": name, - "command": f"oz {cmd_name}", - "source_file": source_file, - }) - - return commands - - -def parse_subcommands_from_file(warp_internal: Path, filename: str) -> list[str]: - """Parse subcommand names from a CLI command file (e.g., agent.rs).""" - candidates = [ - warp_internal / "crates" / "warp_cli" / "src" / filename, - warp_internal / "warp_cli" / "src" / filename, - ] - filepath = next((c for c in candidates if c.exists()), None) - if filepath is None: - return [] - - content = filepath.read_text() - subcommands = [] - - # Find enum variants that represent subcommands - for match in re.finditer(r"///\s*(.+?)\n\s*(?:#\[.*?\]\s*\n\s*)*([A-Z]\w+)", content): - subcommands.append(match.group(2)) - - return subcommands - - def audit_cli(warp_internal: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str]) -> list[dict]: - """Audit CLI command coverage in docs.""" + """Audit CLI command and subcommand coverage in docs.""" commands = parse_cli_commands(warp_internal) cli_to_doc = surface_map.get("cli_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -446,35 +880,47 @@ def audit_cli(warp_internal: Path, docs_root: Path, surface_map: dict, except Exception: pass - findings = [] - for cmd in commands: - cmd_str = cmd["command"] - - # Check surface map + def is_covered(cmd_str: str, search_phrase: str) -> bool: if cmd_str in cli_to_doc: doc_path = cli_to_doc[cmd_str] # `internal` is a sentinel for hidden/internal commands that # intentionally have no public docs (matches API audit semantics). if doc_path == "internal": - continue + return True if resolve_doc_path(doc_path, repo_root) is not None: - continue # Mapped and exists + return True + return any(search_phrase in content for content in cli_docs_text.values()) - # Search CLI docs for the command name - cmd_name = cmd_str.split()[-1] # e.g., "agent" from "oz agent" - found = False - for content in cli_docs_text.values(): - if cmd_name in content: - found = True - break - - if not found: + findings = [] + for cmd in commands: + if cmd["hidden"]: + continue + cmd_str = cmd["command"] + # e.g. "agent" from "oz agent" + top_phrase = cmd_str.split(" ", 1)[1] + if not is_covered(cmd_str, top_phrase): findings.append({ "command": cmd_str, "source_file": cmd.get("source_file"), "severity": "high", "reason": f"CLI command '{cmd_str}' not mentioned in CLI reference docs", }) + continue # Subcommand findings would be redundant noise. + for sub in cmd["subcommands"]: + if sub["hidden"]: + continue + sub_str = sub["command"] + sub_phrase = sub_str.split(" ", 1)[1] # e.g. "agent run-cloud" + if not is_covered(sub_str, sub_phrase): + findings.append({ + "command": sub_str, + "source_file": cmd.get("source_file"), + "severity": "medium", + "reason": ( + f"CLI subcommand '{sub_str}' not mentioned in CLI " + "reference docs" + ), + }) return findings @@ -482,40 +928,16 @@ def audit_cli(warp_internal: Path, docs_root: Path, surface_map: dict, # Audit 3: API endpoint coverage # --------------------------------------------------------------------------- -def parse_api_routes(warp_server: Path) -> list[dict]: - """Parse API route definitions from router.go.""" - router_go = warp_server / "router" / "router.go" - if not router_go.exists(): - return [] - - content = router_go.read_text() - routes = [] - - # Match patterns like: - # r.GET("/api/v1/agent/runs", ...) - # r.POST("/api/v1/agent/run", ...) - for match in re.finditer( - r'\.\s*(GET|POST|PUT|DELETE|PATCH)\s*\(\s*"(/[^"]+)"', - content, - ): - method = match.group(1) - path = match.group(2) - # Skip internal/debug endpoints - if "/internal/" in path or "/debug/" in path: - continue - routes.append({ - "method": method, - "path": path, - "route": f"{method} {path}", - }) - - return routes - - def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str]) -> list[dict]: - """Audit API endpoint coverage in docs.""" - routes = parse_api_routes(warp_server) + """Audit public API endpoint coverage in the OpenAPI spec and API docs. + + The public docs API reference (docs.warp.dev/api) renders + developers/agent-api-openapi.yaml, so a route missing from the spec is a + docs gap. Use the warp-server `update-open-api-spec` skill / docs + `sync-openapi-spec` skill to fix spec drift rather than hand-editing. + """ + routes = parse_public_api_routes(warp_server) api_to_doc = surface_map.get("api_to_doc", {}) # Read API docs @@ -546,62 +968,109 @@ def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, for route in routes: route_str = route["route"] - # Check surface map - if route_str in api_to_doc: + # Surface-map entries use the path relative to /api/v1 (e.g. + # "POST /agent/run"); also accept the full path for compatibility. + rel_path = route["path"] + if rel_path.startswith("/api/v1"): + rel_path = rel_path[len("/api/v1"):] or "/" + rel_route_str = f"{route['method']} {rel_path}" + if route_str in api_to_doc or rel_route_str in api_to_doc: continue - # Search API docs and OpenAPI spec for the path - path_lower = route["path"].lower() + # Search the OpenAPI spec and API docs for the path found = False - for content in api_docs_text.values(): - if path_lower in content: + for candidate in {route["path"].lower(), rel_path.lower()}: + if candidate in openapi_text: + found = True + break + if any(candidate in content for content in api_docs_text.values()): found = True break - if not found and path_lower in openapi_text: - found = True if not found: - # Determine handler file - handler_file = None - handlers_dir = warp_server / "router" / "handlers" - if handlers_dir.exists(): - # Try to match based on path segments - path_parts = [p for p in route["path"].split("/") if p and p != "api" and p != "v1"] - if path_parts: - for f in handlers_dir.iterdir(): - if f.suffix == ".go" and path_parts[0] in f.name: - handler_file = f"router/handlers/{f.name}" - break - findings.append({ - "route": route_str, - "handler_file": handler_file, + "route": rel_route_str, + "handler_file": route.get("file"), "severity": "medium", - "reason": f"API endpoint '{route_str}' not documented in API reference", + "reason": ( + f"Public API endpoint '{rel_route_str}' is missing from the " + "OpenAPI spec (developers/agent-api-openapi.yaml) — run the " + "sync-openapi-spec skill, or map it as internal in the " + "surface map" + ), }) return findings # --------------------------------------------------------------------------- -# Audit 4: Docs staleness +# Audit 4: Slash command coverage +# --------------------------------------------------------------------------- + +def _slash_mention_re(name: str) -> re.Pattern: + # Boundary-aware: "/new" must not match "issues/new"; require the char + # before "/" to be a non-word char and the name to end at a word boundary. + return re.compile(r"(? list[dict]: + """Audit static slash command coverage in docs.""" + names = parse_slash_commands(warp_internal) + slash_to_doc = surface_map.get("slash_to_doc", {}) + repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent + + findings = [] + for name in names: + if name in slash_to_doc: + doc_path = slash_to_doc[name] + if doc_path == "internal": + continue + if resolve_doc_path(doc_path, repo_root) is not None: + continue + pattern = _slash_mention_re(name) + if any(pattern.search(content) for content in docs_text.values()): + continue + findings.append({ + "command": name, + "severity": "medium", + "reason": ( + f"Slash command '{name}' is not mentioned in any docs page — " + "document it (slash-commands page) or map it as internal" + ), + }) + return findings + +# --------------------------------------------------------------------------- +# Audit 5: Docs staleness # --------------------------------------------------------------------------- def audit_staleness(warp_internal: Path, docs_root: Path, docs_text: dict[str, str], stale_terms_path: Path = STALE_TERMS_PATH) -> list[dict]: - """Check for docs referencing features that no longer exist in code.""" - # Get current feature flags for comparison - current_flags = set(parse_feature_flags(warp_internal)) - current_flags_lower = {f.lower() for f in current_flags} + """Check existing docs for stale terminology. - # Load stale terms from external reference file + Code spans are stripped first (CLI examples like `oz agent run` are + legitimate command syntax, not terminology) and terms match on word + boundaries only. Broader terminology enforcement is owned by the + style_lint skill; this audit only flags terms tied to renamed/removed + features. + """ stale_terms = parse_stale_terms(stale_terms_path) + term_patterns = [ + (term, reason, re.compile(r"\b" + re.escape(term) + r"\b")) + for term, reason in stale_terms + ] findings = [] for doc_path, content in docs_text.items(): + # Historical changelog entries are records of what shipped at the + # time — old feature names there are correct, not stale. + if "/changelog/" in doc_path or doc_path.startswith("changelog/"): + continue + prose = strip_code_spans(content) stale_found = [] - for term, reason in stale_terms: - if term in content: + for term, reason, pattern in term_patterns: + if pattern.search(prose): stale_found.append({"term": term, "reason": reason}) if stale_found: @@ -614,28 +1083,324 @@ def audit_staleness(warp_internal: Path, docs_root: Path, return findings +# --------------------------------------------------------------------------- +# Audit 6: Surface map hygiene +# --------------------------------------------------------------------------- + +def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], + cli_commands: list[dict], api_routes: list[dict], + slash_commands: list[str], docs_root: Path) -> list[dict]: + """Flag surface-map entries that reference code surfaces that no longer exist. + + Dead entries usually mean a feature was renamed or removed — verify the + target doc page is still accurate, then prune or update the entry. + """ + findings = [] + repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent + known_flags = set(flag_statuses) + + for flag in sorted(surface_map.get("feature_to_doc", {})): + if flag not in known_flags: + findings.append({ + "entry": flag, + "section": "Feature flags", + "severity": "low", + "reason": ( + f"Map entry '{flag}' does not match any FeatureFlag in code " + "(flag removed or renamed) — verify the doc page is still " + "accurate, then prune or update the entry" + ), + }) + for flag in sorted(surface_map.get("ignore_flags", set())): + if flag not in known_flags: + findings.append({ + "entry": flag, + "section": "Flags to ignore", + "severity": "low", + "reason": ( + f"Ignore-list entry '{flag}' does not match any FeatureFlag " + "in code — prune it" + ), + }) + + known_cli = set() + for cmd in cli_commands: + known_cli.add(cmd["command"]) + for sub in cmd["subcommands"]: + known_cli.add(sub["command"]) + for cmd in sorted(surface_map.get("cli_to_doc", {})): + if cmd not in known_cli: + findings.append({ + "entry": cmd, + "section": "CLI commands", + "severity": "low", + "reason": ( + f"Map entry '{cmd}' does not match any CLI command in code — " + "verify and prune or update" + ), + }) + + known_slash = set(slash_commands) + for name in sorted(surface_map.get("slash_to_doc", {})): + if name not in known_slash: + findings.append({ + "entry": name, + "section": "Slash commands", + "severity": "low", + "reason": ( + f"Map entry '{name}' does not match any static slash command " + "in code — verify and prune or update" + ), + }) + + # Mapped doc targets that no longer exist (any section). + for section, mapping in ( + ("Feature flags", surface_map.get("feature_to_doc", {})), + ("CLI commands", surface_map.get("cli_to_doc", {})), + ("API endpoints", surface_map.get("api_to_doc", {})), + ("Slash commands", surface_map.get("slash_to_doc", {})), + ): + for key, doc_path in sorted(mapping.items()): + if doc_path == "internal": + continue + if resolve_doc_path(doc_path, repo_root) is None: + findings.append({ + "entry": key, + "section": section, + "severity": "medium", + "reason": ( + f"Mapped doc {doc_path} does not exist — the page was " + "moved or deleted; update the map (and redirects)" + ), + }) + + return findings + +# --------------------------------------------------------------------------- +# Snapshot + change detection +# --------------------------------------------------------------------------- + +def build_snapshot(flag_statuses: dict[str, str], cli_commands: list[dict], + api_routes: list[dict], slash_commands: list[str], + changelog_entries: list[dict]) -> dict: + """Assemble the surface snapshot (deterministic ordering for clean diffs).""" + cli_flat = [] + for cmd in cli_commands: + cli_flat.append({"command": cmd["command"], "hidden": cmd["hidden"]}) + for sub in cmd["subcommands"]: + cli_flat.append({"command": sub["command"], "hidden": sub["hidden"]}) + cli_flat.sort(key=lambda c: c["command"]) + + return { + "schema_version": SNAPSHOT_SCHEMA_VERSION, + "flags": dict(sorted(flag_statuses.items())), + "cli_commands": cli_flat, + "api_routes": sorted(r["route"] for r in api_routes), + "slash_commands": sorted(slash_commands), + "changelog_last_version": ( + changelog_entries[0]["version"] if changelog_entries else None + ), + } + + +def load_snapshot(path: Path) -> dict | None: + if not path.exists(): + return None + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: + print(f"Warning: failed to parse snapshot {path}: {exc}", file=sys.stderr) + return None + + +def diff_snapshots(old: dict, new: dict) -> list[dict]: + """Compare two snapshots and report added/removed/promoted surfaces.""" + findings = [] + + old_flags = old.get("flags", {}) + new_flags = new.get("flags", {}) + for flag in sorted(set(new_flags) - set(old_flags)): + status = new_flags[flag] + severity = {"ga": "high", "preview": "medium"}.get(status, "low") + findings.append({ + "change": "flag_added", + "surface": flag, + "detail": f"status: {status}", + "severity": severity, + "reason": ( + f"New feature flag '{flag}' ({status}) — " + + ("needs docs and a surface map entry" + if status in ("ga", "preview") + else "track it; no docs needed until promotion") + ), + }) + for flag in sorted(set(old_flags) - set(new_flags)): + findings.append({ + "change": "flag_removed", + "surface": flag, + "detail": f"was: {old_flags[flag]}", + "severity": "medium", + "reason": ( + f"Feature flag '{flag}' was removed — the feature either " + "stabilized (flag cleanup) or was killed. Verify docs cover the " + "final behavior and prune/keep the surface map entry accordingly" + ), + }) + for flag in sorted(set(old_flags) & set(new_flags)): + if old_flags[flag] == new_flags[flag]: + continue + promoted_to_user_facing = new_flags[flag] in ("ga", "preview") + findings.append({ + "change": "flag_status_changed", + "surface": flag, + "detail": f"{old_flags[flag]} -> {new_flags[flag]}", + "severity": "high" if new_flags[flag] == "ga" else ( + "medium" if promoted_to_user_facing else "low"), + "reason": ( + f"Feature flag '{flag}' moved {old_flags[flag]} -> {new_flags[flag]}" + + (" — verify docs exist and the surface map is updated" + if promoted_to_user_facing else "") + ), + }) + + old_cli = {c["command"] for c in old.get("cli_commands", []) if not c.get("hidden")} + new_cli = {c["command"] for c in new.get("cli_commands", []) if not c.get("hidden")} + for cmd in sorted(new_cli - old_cli): + findings.append({ + "change": "cli_added", + "surface": cmd, + "severity": "medium", + "reason": f"New CLI command '{cmd}' — document it in the CLI reference", + }) + for cmd in sorted(old_cli - new_cli): + findings.append({ + "change": "cli_removed", + "surface": cmd, + "severity": "medium", + "reason": ( + f"CLI command '{cmd}' was removed or hidden — update the CLI " + "reference docs and surface map" + ), + }) + + old_api = set(old.get("api_routes", [])) + new_api = set(new.get("api_routes", [])) + for route in sorted(new_api - old_api): + findings.append({ + "change": "api_added", + "surface": route, + "severity": "medium", + "reason": ( + f"New public API route '{route}' — add it to the OpenAPI spec " + "(sync-openapi-spec skill) or map it as internal" + ), + }) + for route in sorted(old_api - new_api): + findings.append({ + "change": "api_removed", + "surface": route, + "severity": "medium", + "reason": ( + f"Public API route '{route}' was removed — verify the OpenAPI " + "spec and API docs no longer document it" + ), + }) + + old_slash = set(old.get("slash_commands", [])) + new_slash = set(new.get("slash_commands", [])) + for name in sorted(new_slash - old_slash): + findings.append({ + "change": "slash_added", + "surface": name, + "severity": "medium", + "reason": ( + f"New slash command '{name}' — add it to the slash-commands docs " + "page or map it as internal" + ), + }) + for name in sorted(old_slash - new_slash): + findings.append({ + "change": "slash_removed", + "surface": name, + "severity": "medium", + "reason": ( + f"Slash command '{name}' was removed — update the slash-commands " + "docs page and surface map" + ), + }) + + return findings + + +def changelog_review_findings(changelog_entries: list[dict], + last_seen_version: str | None) -> list[dict]: + """Emit verification findings for changelog entries newer than the snapshot. + + The weekly human-curated changelog is the best signal for launches that no + static code parse can see (server-side features, Oz web app, experiment + rollouts). Each bullet should be verified for real docs coverage — a + changelog mention alone is not documentation. + """ + findings = [] + for entry in changelog_entries: + if last_seen_version and entry["version"] <= last_seen_version: + continue + for item in entry["items"]: + findings.append({ + "version": entry["version"], + "category": item["category"], + "text": item["text"], + "severity": "low", + "reason": ( + "New changelog item since last audit — verify the feature " + "has real docs coverage (not just the changelog mention)" + ), + }) + return findings + # --------------------------------------------------------------------------- # Report generation # --------------------------------------------------------------------------- -def generate_report(features: list, cli: list, api: list, staleness: list) -> dict: +REPORT_CATEGORIES = [ + ("undocumented_features", "UNDOCUMENTED FEATURES", + lambda i: i.get("flag", "")), + ("undocumented_cli_commands", "UNDOCUMENTED CLI COMMANDS", + lambda i: i.get("command", "")), + ("undocumented_api_endpoints", "UNDOCUMENTED API ENDPOINTS", + lambda i: i.get("route", "")), + ("undocumented_slash_commands", "UNDOCUMENTED SLASH COMMANDS", + lambda i: i.get("command", "")), + ("surface_changes", "SURFACE CHANGES SINCE SNAPSHOT", + lambda i: f"{i.get('change', '')}: {i.get('surface', '')}"), + ("changelog_review", "CHANGELOG ITEMS TO VERIFY", + lambda i: f"{i.get('version', '')} [{i.get('category', '')}] {i.get('text', '')[:100]}"), + ("map_hygiene", "SURFACE MAP HYGIENE", + lambda i: f"{i.get('section', '')}: {i.get('entry', '')}"), + ("potentially_stale_docs", "POTENTIALLY STALE DOCS", + lambda i: i.get("doc_path", "")), +] + + +def generate_report(findings_by_category: dict[str, list], audits_run: list[str], + audits_skipped: list[dict], mode: str) -> dict: """Assemble the full audit report.""" - total = len(features) + len(cli) + len(api) + len(staleness) - return { + total = sum(len(v) for v in findings_by_category.values()) + report = { "summary": { + "mode": mode, "total_gaps": total, + "audits_run": audits_run, + "audits_skipped": audits_skipped, "by_category": { - "undocumented_features": len(features), - "undocumented_cli_commands": len(cli), - "undocumented_api_endpoints": len(api), - "potentially_stale_docs": len(staleness), + key: len(findings_by_category.get(key, [])) + for key, _, _ in REPORT_CATEGORIES }, }, - "undocumented_features": features, - "undocumented_cli_commands": cli, - "undocumented_api_endpoints": api, - "potentially_stale_docs": staleness, } + for key, _, _ in REPORT_CATEGORIES: + report[key] = findings_by_category.get(key, []) + return report def print_report(report: dict) -> None: @@ -644,63 +1409,48 @@ def print_report(report: dict) -> None: print("=" * 60) print("MISSING DOCS AUDIT REPORT") print("=" * 60) + print(f"Mode: {summary['mode']}") + print(f"Audits run: {', '.join(summary['audits_run']) or 'none'}") + if summary["audits_skipped"]: + print("!! AUDITS SKIPPED (results are incomplete):") + for skipped in summary["audits_skipped"]: + print(f" - {skipped['audit']}: {skipped['reason']}") print(f"Total gaps found: {summary['total_gaps']}") for category, count in summary["by_category"].items(): - print(f" {category}: {count}") + if count: + print(f" {category}: {count}") print() severity_order = {"high": 0, "medium": 1, "low": 2} - if report["undocumented_features"]: + for key, title, describe in REPORT_CATEGORIES: + items = report.get(key, []) + if not items: + continue print("-" * 60) - print(f"UNDOCUMENTED FEATURES ({len(report['undocumented_features'])})") + print(f"{title} ({len(items)})") print("-" * 60) - items = sorted(report["undocumented_features"], - key=lambda x: severity_order.get(x.get("severity", "low"), 3)) - for item in items: + for item in sorted(items, key=lambda x: severity_order.get(x.get("severity", "low"), 3)): sev = item.get("severity", "?").upper() - print(f"\n [{sev}] {item['flag']}") - print(f" Reason: {item['reason']}") + print(f"\n [{sev}] {describe(item)}") + if item.get("reason"): + print(f" Reason: {item['reason']}") if item.get("suggested_doc_path"): print(f" Suggested: {item['suggested_doc_path']}") - print(f" Search terms: {', '.join(item['search_terms'][:3])}") - - if report["undocumented_cli_commands"]: - print() - print("-" * 60) - print(f"UNDOCUMENTED CLI COMMANDS ({len(report['undocumented_cli_commands'])})") - print("-" * 60) - for item in report["undocumented_cli_commands"]: - sev = item.get("severity", "?").upper() - print(f"\n [{sev}] {item['command']}") - print(f" Reason: {item['reason']}") + if item.get("matched_docs"): + print(f" Mentioned in: {', '.join(item['matched_docs'])}") + if item.get("search_terms"): + print(f" Search terms: {', '.join(item['search_terms'][:3])}") if item.get("source_file"): print(f" Source: {item['source_file']}") - - if report["undocumented_api_endpoints"]: - print() - print("-" * 60) - print(f"UNDOCUMENTED API ENDPOINTS ({len(report['undocumented_api_endpoints'])})") - print("-" * 60) - for item in report["undocumented_api_endpoints"]: - sev = item.get("severity", "?").upper() - print(f"\n [{sev}] {item['route']}") - print(f" Reason: {item['reason']}") if item.get("handler_file"): print(f" Handler: {item['handler_file']}") - - if report["potentially_stale_docs"]: - print() - print("-" * 60) - print(f"POTENTIALLY STALE DOCS ({len(report['potentially_stale_docs'])})") - print("-" * 60) - for item in report["potentially_stale_docs"]: - sev = item.get("severity", "?").upper() - print(f"\n [{sev}] {item['doc_path']}") + if item.get("detail"): + print(f" Detail: {item['detail']}") for t in item.get("stale_terms", []): print(f" - \"{t['term']}\": {t['reason']}") + print() - print() print("=" * 60) # --------------------------------------------------------------------------- @@ -713,11 +1463,11 @@ def main(): ) parser.add_argument( "--warp-internal", - help="Path to warp-internal repo (auto-detected if not provided)", + help="Path to warp-internal repo (auto-detected as a sibling of the docs repo)", ) parser.add_argument( "--warp-server", - help="Path to warp-server repo (auto-detected if not provided)", + help="Path to warp-server repo (auto-detected as a sibling of the docs repo)", ) parser.add_argument( "--output", "-o", @@ -725,7 +1475,7 @@ def main(): ) parser.add_argument( "--category", - choices=["features", "cli", "api", "staleness"], + choices=["features", "cli", "api", "slash", "staleness", "map"], help="Run only a specific audit category", ) parser.add_argument( @@ -739,9 +1489,31 @@ def main(): help="Also flag features whose mapped doc exists but doesn't mention " "feature keywords (noisy; produces low-severity findings)", ) + parser.add_argument( + "--diff", + action="store_true", + help="Compare current surfaces against the committed snapshot and report " + "added/removed/promoted surfaces plus new changelog items", + ) + parser.add_argument( + "--update-snapshot", + action="store_true", + help="Regenerate the surface snapshot from current code (commit it with " + "the docs PR). Requires a full run (no --category)", + ) + parser.add_argument( + "--snapshot", + default=str(DEFAULT_SNAPSHOT_PATH), + help=f"Path to the surface snapshot (default: {DEFAULT_SNAPSHOT_PATH})", + ) args = parser.parse_args() - # Find repos + if args.update_snapshot and args.category: + print("Error: --update-snapshot requires a full run (drop --category)", + file=sys.stderr) + sys.exit(1) + + # Find repos. # SKILL_DIR is at /.agents/skills/missing_docs (or legacy /.warp/skills/...) repo_root = SKILL_DIR.parent.parent.parent # Astro Starlight docs live at src/content/docs @@ -757,8 +1529,8 @@ def main(): # repo_root carries the developers/ openapi spec etc. DOCS_REPO_ROOT[0] = repo_root - warp_internal = find_repo("warp-internal", args.warp_internal, docs_root) - warp_server = find_repo("warp-server", args.warp_server, docs_root) + warp_internal = find_repo("warp-internal", args.warp_internal, repo_root) + warp_server = find_repo("warp-server", args.warp_server, repo_root) # Parse surface map surface_map = parse_surface_map(SURFACE_MAP_PATH) @@ -768,57 +1540,137 @@ def main(): docs_text = read_all_docs_text(docs_root) print(f" Found {len(docs_text)} markdown files", file=sys.stderr) - # Run audits - features_findings = [] - cli_findings = [] - api_findings = [] - staleness_findings = [] + findings: dict[str, list] = {} + audits_run: list[str] = [] + audits_skipped: list[dict] = [] + + needs_internal = args.category in (None, "features", "cli", "slash", "staleness", "map") \ + or args.diff or args.update_snapshot + needs_server = args.category in (None, "api", "map") \ + or args.diff or args.update_snapshot - if warp_internal: + flag_statuses: dict[str, str] = {} + cli_commands: list[dict] = [] + slash_commands: list[str] = [] + api_routes: list[dict] = [] + + if warp_internal and needs_internal: print(f"Using warp-internal: {warp_internal}", file=sys.stderr) + flag_statuses = compute_flag_statuses(warp_internal) + cli_commands = parse_cli_commands(warp_internal) + slash_commands = parse_slash_commands(warp_internal) + if args.category in (None, "features"): print("Running feature flag coverage audit...", file=sys.stderr) - features_findings = audit_features( + findings["undocumented_features"] = audit_features( warp_internal, docs_root, surface_map, docs_text, - weak_coverage=args.weak_coverage, + flag_statuses=flag_statuses, weak_coverage=args.weak_coverage, ) + audits_run.append("features") if args.category in (None, "cli"): print("Running CLI command coverage audit...", file=sys.stderr) - cli_findings = audit_cli(warp_internal, docs_root, surface_map, docs_text) + findings["undocumented_cli_commands"] = audit_cli( + warp_internal, docs_root, surface_map, docs_text) + audits_run.append("cli") + + if args.category in (None, "slash"): + print("Running slash command coverage audit...", file=sys.stderr) + findings["undocumented_slash_commands"] = audit_slash_commands( + warp_internal, docs_root, surface_map, docs_text) + audits_run.append("slash") if args.category in (None, "staleness"): print("Running docs staleness audit...", file=sys.stderr) - staleness_findings = audit_staleness(warp_internal, docs_root, docs_text) - else: - print("Warning: warp-internal not found, skipping feature/CLI/staleness audits", - file=sys.stderr) + findings["potentially_stale_docs"] = audit_staleness( + warp_internal, docs_root, docs_text) + audits_run.append("staleness") + elif needs_internal: + for audit in ("features", "cli", "slash", "staleness"): + if args.category in (None, audit): + audits_skipped.append({ + "audit": audit, + "reason": "warp-internal repo not found (pass --warp-internal)", + }) - if warp_server: + if warp_server and needs_server: print(f"Using warp-server: {warp_server}", file=sys.stderr) + api_routes = parse_public_api_routes(warp_server) if args.category in (None, "api"): print("Running API endpoint coverage audit...", file=sys.stderr) - api_findings = audit_api(warp_server, docs_root, surface_map, docs_text) - else: - print("Warning: warp-server not found, skipping API audit", file=sys.stderr) + findings["undocumented_api_endpoints"] = audit_api( + warp_server, docs_root, surface_map, docs_text) + audits_run.append("api") + elif needs_server: + if args.category in (None, "api"): + audits_skipped.append({ + "audit": "api", + "reason": "warp-server repo not found (pass --warp-server)", + }) + + if args.category in (None, "map"): + if warp_internal and warp_server: + print("Running surface map hygiene audit...", file=sys.stderr) + findings["map_hygiene"] = audit_map_hygiene( + surface_map, flag_statuses, cli_commands, api_routes, + slash_commands, docs_root) + audits_run.append("map") + else: + audits_skipped.append({ + "audit": "map", + "reason": "requires both warp-internal and warp-server", + }) + + # Change detection (diff + snapshot update) + changelog_entries = parse_changelog_entries(repo_root) + snapshot_path = Path(args.snapshot) + if args.diff or args.update_snapshot: + if warp_internal and warp_server: + current_snapshot = build_snapshot( + flag_statuses, cli_commands, api_routes, slash_commands, + changelog_entries) + if args.diff: + previous = load_snapshot(snapshot_path) + if previous is None: + audits_skipped.append({ + "audit": "diff", + "reason": ( + f"snapshot {snapshot_path} not found or unreadable — " + "run --update-snapshot first" + ), + }) + else: + print("Running surface change detection (diff)...", file=sys.stderr) + findings["surface_changes"] = diff_snapshots(previous, current_snapshot) + findings["changelog_review"] = changelog_review_findings( + changelog_entries, previous.get("changelog_last_version")) + audits_run.append("diff") + if args.update_snapshot: + snapshot_path.parent.mkdir(parents=True, exist_ok=True) + snapshot_path.write_text( + json.dumps(current_snapshot, indent=2, sort_keys=False) + "\n", + encoding="utf-8", + ) + print(f"Snapshot updated: {snapshot_path}", file=sys.stderr) + else: + audits_skipped.append({ + "audit": "diff" if args.diff else "update-snapshot", + "reason": "requires both warp-internal and warp-server", + }) # Filter by severity if args.severity: severity_order = {"high": 0, "medium": 1, "low": 2} min_severity = severity_order[args.severity] - features_findings = [f for f in features_findings - if severity_order.get(f.get("severity"), 3) <= min_severity] - cli_findings = [f for f in cli_findings - if severity_order.get(f.get("severity"), 3) <= min_severity] - api_findings = [f for f in api_findings - if severity_order.get(f.get("severity"), 3) <= min_severity] - staleness_findings = [f for f in staleness_findings - if severity_order.get(f.get("severity"), 3) <= min_severity] - - # Generate report - report = generate_report(features_findings, cli_findings, api_findings, staleness_findings) - - # Output + for key in list(findings): + findings[key] = [ + f for f in findings[key] + if severity_order.get(f.get("severity"), 3) <= min_severity + ] + + mode = "diff" if args.diff else "audit" + report = generate_report(findings, audits_run, audits_skipped, mode) + print_report(report) if args.output: @@ -826,6 +1678,14 @@ def main(): output_path.write_text(json.dumps(report, indent=2)) print(f"\nJSON report saved to {output_path}", file=sys.stderr) + if audits_skipped: + print( + "Error: one or more audits were skipped — this run is INCOMPLETE " + "and must not be treated as a clean audit.", + file=sys.stderr, + ) + sys.exit(2) + if __name__ == "__main__": main() From 9e35135694d9ff1752ec1c9f23f120c6937d7cfd Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Thu, 11 Jun 2026 06:22:58 +0000 Subject: [PATCH 02/15] Expand missing_docs audit: settings, web app, tools, skills, structure, stale refs - Settings audit: parse the define_setting! toml_path registry (~200 settings, flag-status aware, object-typed settings handled) and check coverage in the all-settings reference; reverse check catches documented settings that were renamed/removed in code (e.g. agents.oz.* -> agents.warp_agent.*) - Stale doc references: validate documented keybinding actions (scope:action) still exist anywhere in warp-internal source - Docs structure audit: flag pages missing from src/sidebar.ts (with an allowlist section in the surface map) - CLI: recursive subcommand parsing (oz run message send, oz environment image list, ...) plus per-module --flag tracking in the snapshot - API: positional RouterGroup argument resolution at Register* call sites (fixes oauth route prefixes) and param-name-insensitive OpenAPI matching - Snapshot v2: settings, web app routes (AgentsApp.tsx), server-side agent tools (ToolName consts + Create*NativeTool registrations), bundled + channel-gated skills, CLI flags; graceful one-time note when diffing against a v1 snapshot - Changelog cross-check now also tracks 'Oz updates' bullets - Extraction sanity guards: implausibly low parse counts (broken parser after a code-layout change) skip dependent audits and exit 2 instead of silently under-reporting; map hygiene and reverse checks gated on healthy extraction - Feature flag enum parsing is brace-safe (survives future struct variants) - SKILL.md documents the 9 coverage audits, snapshot-only surfaces, and adjacent-skill ownership (validate_ui_refs, sync-error-docs, style_lint, weekly-404-monitor) Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 106 +- .../references/feature_surface_map.md | 17 +- .../references/surface_snapshot.json | 482 +++++- .../skills/missing_docs/scripts/audit_docs.py | 1298 ++++++++++++++--- 4 files changed, 1673 insertions(+), 230 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index fb6246aa3..3fe1106a2 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -23,10 +23,12 @@ The audit compares docs against code, so both source repos must be available: `/workspace/warp-server`), or passed explicitly via `--warp-internal PATH` / `--warp-server PATH`. -The script FAILS LOUD when a repo is missing: it exits with code 2 and lists the -skipped audits in the report's `audits_skipped` field. Never treat an exit-2 run -as a clean audit — fix the repo paths and re-run. Exit 0 means all requested -audits ran (findings may still exist). +The script FAILS LOUD when a repo is missing OR when an extraction sanity guard +trips (a parser returning implausibly few surfaces means the source layout +changed and the parser needs fixing): it exits with code 2 and lists the skipped +audits in the report's `audits_skipped` field (`extraction:*` entries identify +broken parsers). Never treat an exit-2 run as a clean audit — fix the problem +and re-run. Exit 0 means all requested audits ran (findings may still exist). ## Workflows @@ -39,7 +41,7 @@ python3 .agents/skills/missing_docs/scripts/audit_docs.py ``` Options: -- `--category features|cli|api|slash|staleness|map` — run a single audit category +- `--category features|cli|api|slash|settings|structure|staleness|map` — run a single audit category - `--severity high|medium|low` — filter by minimum severity - `--weak-coverage` — also flag GA features whose mapped doc exists but doesn't mention feature keywords (low-severity, noisy) - `--output report.json` — save JSON report to file @@ -51,41 +53,71 @@ The script resolves doc paths from the docs repo root and accepts `.md` and `.md interchangeably (and `README.md` ↔ `index.mdx`), so surface-map entries can use the canonical filename even when the on-disk extension differs. -The script performs 6 coverage audits: +The script performs these coverage audits: 1. **Feature flag coverage** — classifies every `FeatureFlag` by rollout status using the cargo-feature→flag bridge in warp-internal `app/src/features.rs` plus `RELEASE_FLAGS`/`PREVIEW_FLAGS`/`DOGFOOD_FLAGS` in `crates/warp_features/src/lib.rs`. GA flags must be mapped in the surface map or covered in docs; Preview flags produce low-severity "docs needed soon" findings; dogfood/other flags are tracked by the snapshot only. -2. **CLI command coverage** — parses the full `oz` command tree (top-level commands and - subcommands, skipping `hide = true`) from `crates/warp_cli/src/` and checks the CLI - reference docs. +2. **CLI command coverage** — parses the full `oz` command tree from + `crates/warp_cli/src/` (recursive subcommands like `oz run message send`, skipping + `hide = true`) and checks the CLI reference docs. Per-module `--long` flags are + additionally tracked in the snapshot for change detection. 3. **API endpoint coverage** — extracts public routes from warp-server - `router/handlers/public_api/*.go` (nested gin groups resolved) and checks them - against `developers/agent-api-openapi.yaml` and the API reference docs. For spec - drift, run the docs `sync-openapi-spec` skill (or warp-server's - `update-open-api-spec`) instead of hand-editing the YAML. + `router/handlers/public_api/*.go` (nested gin groups resolved, caller-passed group + prefixes matched positionally) and checks them against + `developers/agent-api-openapi.yaml` (param-name-insensitive: `{runId}` matches + `{run_id}`) and the API reference docs. For spec drift, run the docs + `sync-openapi-spec` skill (or warp-server's `update-open-api-spec`) instead of + hand-editing the YAML. 4. **Slash command coverage** — parses the static registry in warp-internal `app/src/search/slash_command_menu/static_commands/` and checks each `/command` is mentioned in docs. -5. **Docs staleness** — flags renamed/removed-feature terminology in prose (code +5. **Settings coverage** — parses every `toml_path: "section.key"` setting + registration in warp-internal (the same registry the JSON-schema generator uses) + and checks the all-settings reference page documents it. Private and + dogfood/other-flagged settings are exempt; object-typed settings documented as + their own `[section]` count as covered. +6. **Docs staleness** — flags renamed/removed-feature terminology in prose (code spans stripped; historical changelog pages excluded). Broader terminology and style enforcement is owned by the `style_lint` skill — delegate pure wording issues there. -6. **Surface map hygiene** — flags map entries whose flag/command no longer exists in - code, and mapped doc targets that no longer exist. Verify the doc page is still - accurate, then prune or update the entry. +7. **Stale doc references** — reverse checks: settings keys documented in + all-settings.mdx that no longer exist in code (catches renames like + `agents.oz.*` → `agents.warp_agent.*`), and keybinding actions (`scope:action`) + on the keyboard-shortcuts page that no longer exist anywhere in warp-internal. +8. **Docs structure** — pages on disk that are missing from `src/sidebar.ts` + (built but unreachable through navigation). Intentionally unlisted pages go in + the surface map's "Unlisted docs pages" section. +9. **Surface map hygiene** — flags map entries whose flag/command/route/setting no + longer exists in code, and mapped doc targets that no longer exist. Verify the + doc page is still accurate, then prune or update the entry. + +Snapshot-only surfaces (no standing coverage audit, but added/removed/changed items +are reported by `--diff`): Oz web app routes (`AgentsApp.tsx`), server-side agent +tools (multi_agent tool registries), bundled + channel-gated skills +(`resources/bundled/skills`, `resources/channel-gated-skills`), and per-module CLI +flags. Present the report to the user, grouped by category and sorted by severity. +Adjacent checks owned by other skills (do not duplicate them here): +- UI menu paths and Command Palette names → `validate_ui_refs` +- Platform error-code pages → `sync-error-docs` +- Broken links and 404s/redirects → `check_for_broken_links` / `weekly-404-monitor` +- Terminology/style sweeps → `style_lint` + ### Phase 2: Change detection (diff mode) The snapshot at `references/surface_snapshot.json` records all extracted surfaces -(flags + rollout status, CLI commands, API routes, slash commands) plus the last-seen -docs-changelog version. It makes change detection possible: a feature flag that is -deleted after stabilizing (per warp-internal's remove-feature-flag policy) would -otherwise vanish from the audit's universe silently. +(flags + rollout status, CLI commands and per-module flags, API routes, slash +commands, settings + status, Oz web app routes, server-side agent tools, bundled +skills) plus the last-seen docs-changelog version. It makes change detection +possible: a feature flag that is deleted after stabilizing (per warp-internal's +remove-feature-flag policy) would otherwise vanish from the audit's universe +silently. When a new surface type is introduced, diffing against an older snapshot +emits a one-time "surface type newly tracked" note instead of false positives. ```bash python3 .agents/skills/missing_docs/scripts/audit_docs.py --diff @@ -94,12 +126,15 @@ python3 .agents/skills/missing_docs/scripts/audit_docs.py --diff Diff mode reports, since the snapshot was last updated: - **Added / removed / promoted surfaces** — e.g. a new GA flag (high), a flag promoted dogfood→ga (high), a removed flag ("feature stabilized or killed — verify docs and - map entry"), new CLI/API/slash surfaces. -- **Changelog items to verify** — "New features" and "Improvements" bullets from - `src/content/docs/changelog/.mdx` entries newer than the snapshot's last-seen - version. This is the best signal for launches no static code parse can see - (server-side features, Oz web app, experiment rollouts). A changelog mention is NOT - documentation — verify each item has real doc coverage. + map entry"), new/removed CLI commands and `--flags`, API routes, slash commands, + settings (with status promotions), Oz web app routes, server-side agent tools, and + bundled skills. +- **Changelog items to verify** — "New features", "Improvements", and "Oz updates" + bullets from `src/content/docs/changelog/.mdx` entries newer than the + snapshot's last-seen version. This is the best signal for launches no static code + parse can see (server-side features, Oz web app, experiment rollouts). A changelog + mention is NOT documentation — verify each item has real doc coverage. ("Bug fixes" + bullets are deliberately untracked to keep weekly triage volume manageable.) After triaging and addressing diff findings, refresh the snapshot and commit it with your PR so the next run diffs against the new baseline: @@ -150,10 +185,12 @@ with the product. Each run: --diff --output /tmp/docs_audit.json ``` 2. **Triage**: work through `surface_changes` and `changelog_review` first (what - changed since last run), then standing coverage findings (high → medium → low). - For each item decide: draft/update a doc page, update the OpenAPI spec via - `sync-openapi-spec`, add a surface-map entry (documented elsewhere), or add an - ignore/`internal` entry with a comment (internal-only). + changed since last run), then standing coverage findings (high → medium → low) + across all categories: features, CLI, API, slash commands, settings, stale doc + references, unlisted pages, map hygiene, staleness. For each item decide: + draft/update a doc page, update the OpenAPI spec via `sync-openapi-spec`, add a + surface-map entry (documented elsewhere), or add an ignore/`internal`/allowlist + entry with a comment (internal-only or intentionally unlisted). 3. **Draft**: follow Phase 3 for every item that needs docs. 4. **Update references**: apply surface-map edits, then regenerate the snapshot: ```bash @@ -197,9 +234,10 @@ The user can trigger any subset: ## References - `references/feature_surface_map.md` — curated mapping of flags/commands/routes/slash - commands to doc pages, ignore list for internal flags, and the `internal` sentinel - for surfaces that intentionally have no public docs. Update it with every docs PR - that ships a feature. + commands/settings to doc pages, ignore list for internal flags, allowlist for + intentionally unlisted pages, and the `internal` sentinel for surfaces that + intentionally have no public docs. Update it with every docs PR that ships a + feature. - `references/surface_snapshot.json` — generated snapshot of all code surfaces used by `--diff`. Regenerate with `--update-snapshot`; never hand-edit. - `references/stale_terms.md` — renamed/removed-feature terms to flag during staleness diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index e0aa3ba19..1414fcf94 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -1,7 +1,8 @@ # Feature Surface Map -Curated mapping of feature flags, CLI commands, API endpoints, and slash commands -to their expected documentation pages. +Curated mapping of feature flags, CLI commands, API endpoints, slash commands, +and settings to their expected documentation pages, plus an allowlist for +intentionally unlisted docs pages. The audit script reads this file to reduce false positives — entries here are verified rather than flagged. @@ -221,6 +222,18 @@ POST /harness-support/upload-snapshot -> internal # Gated by the dogfood-only LocalDockerSandbox flag — not user-facing yet. /docker-sandbox -> internal +## Settings -> doc pages + +# Settings are matched automatically against the all-settings reference +# (terminal/settings/all-settings.mdx) by section + key; add entries here only +# for exceptions: settings documented on another page (`section.key -> path`) +# or intentionally undocumented (`section.key -> internal`). + +## Unlisted docs pages to ignore + +# Pages intentionally absent from src/sidebar.ts (one slug per line, e.g. +# `guides/some-page`). Everything else on disk must be reachable via the sidebar. + ## Flags to ignore (internal-only, not user-facing) # These flags are internal implementation details and don't need documentation diff --git a/.agents/skills/missing_docs/references/surface_snapshot.json b/.agents/skills/missing_docs/references/surface_snapshot.json index 1dcccdc43..71d9abb32 100644 --- a/.agents/skills/missing_docs/references/surface_snapshot.json +++ b/.agents/skills/missing_docs/references/surface_snapshot.json @@ -1,5 +1,5 @@ { - "schema_version": 1, + "schema_version": 2, "flags": { "AIBlockOverflowMenu": "other", "AIContextMenuCode": "ga", @@ -304,6 +304,10 @@ "command": "oz agent profile", "hidden": false }, + { + "command": "oz agent profile list", + "hidden": false + }, { "command": "oz agent run", "hidden": false @@ -372,6 +376,10 @@ "command": "oz environment image", "hidden": false }, + { + "command": "oz environment image list", + "hidden": false + }, { "command": "oz environment list", "hidden": false @@ -476,6 +484,10 @@ "command": "oz run conversation", "hidden": false }, + { + "command": "oz run conversation get", + "hidden": false + }, { "command": "oz run get", "hidden": false @@ -488,6 +500,26 @@ "command": "oz run message", "hidden": false }, + { + "command": "oz run message list", + "hidden": false + }, + { + "command": "oz run message mark-delivered", + "hidden": false + }, + { + "command": "oz run message read", + "hidden": false + }, + { + "command": "oz run message send", + "hidden": false + }, + { + "command": "oz run message watch", + "hidden": false + }, { "command": "oz schedule", "hidden": false @@ -545,6 +577,136 @@ "hidden": false } ], + "cli_flags": { + "agent": [ + "--add-secret", + "--add-skill", + "--agent", + "--attach", + "--base-model", + "--computer-use", + "--conversation", + "--cwd", + "--description", + "--environment", + "--host", + "--mcp", + "--mcp-startup-timeout", + "--name", + "--no-computer-use", + "--no-snapshot", + "--open", + "--profile", + "--prompt", + "--remove-all-secrets", + "--remove-all-skills", + "--remove-base-model", + "--remove-description", + "--remove-environment", + "--remove-secret", + "--remove-skill", + "--repo", + "--saved-prompt", + "--secret", + "--skill", + "--snapshot-script-timeout", + "--snapshot-upload-timeout", + "--sort-by", + "--sort-order", + "--strict-mcp-startup" + ], + "api_key": [ + "--agent", + "--expires-at", + "--expires-in", + "--no-expiration", + "--sort-by", + "--sort-order" + ], + "artifact": [ + "--conversation-id", + "--description", + "--out", + "--run-id" + ], + "environment": [ + "--description", + "--docker-image", + "--environment", + "--name", + "--no-environment", + "--remove-description", + "--remove-environment", + "--repo", + "--setup-command" + ], + "federate": [ + "--audience", + "--duration", + "--run-id", + "--subject-template" + ], + "integration": [ + "--host", + "--mcp", + "--prompt", + "--remove-mcp" + ], + "model": [ + "--model" + ], + "schedule": [ + "--cron", + "--host", + "--mcp", + "--name", + "--prompt", + "--remove-mcp", + "--remove-skill", + "--skill" + ], + "secret": [ + "--access-key-id", + "--base-url", + "--bedrock-api-key", + "--description", + "--region", + "--secret-access-key", + "--session-token", + "--type", + "--value", + "--value-file" + ], + "task": [ + "--ancestor-run", + "--artifact-type", + "--body", + "--conversation", + "--created-after", + "--created-before", + "--creator", + "--cursor", + "--environment", + "--execution-location", + "--limit", + "--model", + "--name", + "--query", + "--schedule", + "--sender-run-id", + "--since", + "--since-sequence", + "--skill", + "--sort-by", + "--sort-order", + "--source", + "--state", + "--subject", + "--to", + "--unread", + "--updated-after" + ] + }, "api_routes": [ "DELETE /api/v1/agent/identities/{uid}", "DELETE /api/v1/agent/schedules/{id}", @@ -579,8 +741,8 @@ "GET /api/v1/memory_stores/{uid}/agents", "GET /api/v1/memory_stores/{uid}/memories", "GET /api/v1/memory_stores/{uid}/memories/{memoryUid}/versions", - "GET /oauth/authorize", - "GET /oauth/jwks.json", + "GET /api/v1/oauth/authorize", + "GET /api/v1/oauth/jwks.json", "PATCH /api/v1/agent/runs/{runId}/event-sequence", "POST /api/v1/agent/events/{run_id}", "POST /api/v1/agent/handoff/upload-snapshot", @@ -610,9 +772,9 @@ "POST /api/v1/harness-support/upload-snapshot", "POST /api/v1/memory_stores", "POST /api/v1/memory_stores/{uid}/memories", - "POST /oauth/device/auth", - "POST /oauth/session", - "POST /oauth/token", + "POST /api/v1/oauth/device/auth", + "POST /api/v1/oauth/session", + "POST /api/v1/oauth/token", "PUT /api/v1/agent/identities/{uid}", "PUT /api/v1/agent/schedules/{id}", "PUT /api/v1/memory_stores/{uid}", @@ -666,5 +828,313 @@ "/skills", "/usage" ], + "settings": { + "accessibility.accessibility_verbosity": "always_on", + "account.is_settings_sync_enabled": "always_on", + "agents.cloud_conversation_storage_enabled": "always_on", + "agents.knowledge.rules_enabled": "always_on", + "agents.knowledge.warp_drive_context_enabled": "always_on", + "agents.mcp_servers.file_based_mcp_enabled": "always_on", + "agents.profiles.agent_mode_coding_file_read_allowlist": "always_on", + "agents.profiles.agent_mode_coding_permissions": "always_on", + "agents.profiles.agent_mode_command_execution_allowlist": "always_on", + "agents.profiles.agent_mode_command_execution_denylist": "always_on", + "agents.profiles.agent_mode_execute_readonly_commands": "always_on", + "agents.third_party.auto_dismiss_composer_after_submit": "always_on", + "agents.third_party.auto_open_composer_on_cli_agent_start": "always_on", + "agents.third_party.auto_toggle_composer": "always_on", + "agents.third_party.cli_agent_toolbar_chip_selection_setting": "always_on", + "agents.third_party.cli_agent_toolbar_enabled_commands": "always_on", + "agents.third_party.should_render_cli_agent_toolbar": "always_on", + "agents.third_party.submit_on_ctrl_enter": "always_on", + "agents.voice.voice_input_enabled": "always_on", + "agents.voice.voice_input_toggle_key": "always_on", + "agents.warp_agent.active_ai.agent_mode_query_suggestions_enabled": "always_on", + "agents.warp_agent.active_ai.code_suggestions_enabled": "always_on", + "agents.warp_agent.active_ai.enabled": "always_on", + "agents.warp_agent.active_ai.git_operations_autogen_enabled": "always_on", + "agents.warp_agent.active_ai.intelligent_autosuggestions_enabled": "always_on", + "agents.warp_agent.active_ai.natural_language_autosuggestions_enabled": "other", + "agents.warp_agent.active_ai.rule_suggestions_enabled": "ga", + "agents.warp_agent.active_ai.shared_block_title_generation_enabled": "always_on", + "agents.warp_agent.input.agent_toolbar_chip_selection_setting": "always_on", + "agents.warp_agent.input.ai_auto_detection_enabled": "always_on", + "agents.warp_agent.input.ai_command_denylist": "always_on", + "agents.warp_agent.input.include_agent_commands_in_history": "always_on", + "agents.warp_agent.input.nld_in_terminal_enabled": "always_on", + "agents.warp_agent.input.show_agent_tips": "always_on", + "agents.warp_agent.input.show_model_selectors_in_prompt": "always_on", + "agents.warp_agent.is_any_ai_enabled": "always_on", + "agents.warp_agent.other.agent_attribution_enabled": "always_on", + "agents.warp_agent.other.auto_handoff_on_sleep_enabled": "always_on", + "agents.warp_agent.other.cloud_agent_computer_use_enabled": "always_on", + "agents.warp_agent.other.default_prompt_submission_mode": "ga", + "agents.warp_agent.other.open_conversation_layout_preference": "always_on", + "agents.warp_agent.other.orchestration_message_display_mode": "always_on", + "agents.warp_agent.other.should_force_disable_ampersand_handoff": "always_on", + "agents.warp_agent.other.should_force_disable_cloud_handoff": "always_on", + "agents.warp_agent.other.should_render_use_agent_toolbar_for_user_commands": "always_on", + "agents.warp_agent.other.should_show_oz_updates_in_zero_state": "always_on", + "agents.warp_agent.other.show_agent_notifications": "always_on", + "agents.warp_agent.other.show_conversation_history": "always_on", + "agents.warp_agent.other.thinking_display_mode": "always_on", + "appearance.blocks.should_show_bootstrap_block": "always_on", + "appearance.blocks.should_show_in_band_command_blocks": "always_on", + "appearance.blocks.should_show_ssh_block": "always_on", + "appearance.blocks.show_block_dividers": "always_on", + "appearance.blocks.show_jump_to_bottom_of_block_button": "always_on", + "appearance.cursor.cursor_blink": "always_on", + "appearance.cursor.cursor_display_type": "always_on", + "appearance.full_screen_apps.alt_screen_padding": "always_on", + "appearance.icon.app_icon": "always_on", + "appearance.input.input_mode": "always_on", + "appearance.panes.focus_pane_on_hover": "always_on", + "appearance.panes.should_dim_inactive_panes": "always_on", + "appearance.spacing": "always_on", + "appearance.tabs.directory_tab_colors": "ga", + "appearance.tabs.header_toolbar_chip_selection": "always_on", + "appearance.tabs.preserve_active_tab_color": "always_on", + "appearance.tabs.show_indicators_button": "always_on", + "appearance.tabs.tab_close_button_position": "always_on", + "appearance.tabs.workspace_decoration_visibility": "always_on", + "appearance.text.ai_font_name": "always_on", + "appearance.text.enforce_minimum_contrast": "always_on", + "appearance.text.font_name": "always_on", + "appearance.text.font_size": "always_on", + "appearance.text.font_weight": "always_on", + "appearance.text.ligature_rendering_enabled": "always_on", + "appearance.text.line_height_ratio": "always_on", + "appearance.text.match_ai_font": "always_on", + "appearance.text.match_notebook_to_monospace_font_size": "always_on", + "appearance.text.notebook_font_size": "always_on", + "appearance.text.use_thin_strokes": "always_on", + "appearance.themes.selected_system_themes": "always_on", + "appearance.themes.system_theme": "always_on", + "appearance.themes.theme": "always_on", + "appearance.vertical_tabs.compact_subtitle": "always_on", + "appearance.vertical_tabs.display_granularity": "always_on", + "appearance.vertical_tabs.enabled": "always_on", + "appearance.vertical_tabs.hide_title_bar_search_bar": "always_on", + "appearance.vertical_tabs.primary_info": "always_on", + "appearance.vertical_tabs.show_details_on_hover": "always_on", + "appearance.vertical_tabs.show_diff_stats": "always_on", + "appearance.vertical_tabs.show_panel_in_restored_windows": "always_on", + "appearance.vertical_tabs.show_pr_link": "always_on", + "appearance.vertical_tabs.tab_item_mode": "always_on", + "appearance.vertical_tabs.use_latest_prompt_as_title": "always_on", + "appearance.vertical_tabs.view_mode": "always_on", + "appearance.window.left_panel_visibility_across_tabs": "always_on", + "appearance.window.new_windows_num_columns": "always_on", + "appearance.window.new_windows_num_rows": "always_on", + "appearance.window.open_windows_at_custom_size": "always_on", + "appearance.window.override_blur": "always_on", + "appearance.window.override_blur_texture": "always_on", + "appearance.window.override_opacity": "always_on", + "appearance.window.zoom_level": "always_on", + "cloud_platform.third_party_api_keys.aws_bedrock_auth_refresh_command": "always_on", + "cloud_platform.third_party_api_keys.aws_bedrock_auto_login": "always_on", + "cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled": "always_on", + "cloud_platform.third_party_api_keys.aws_bedrock_profile": "always_on", + "cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok": "always_on", + "code.editor.auto_open_code_review_pane_on_first_agent_change": "always_on", + "code.editor.open_code_panels_file_editor": "always_on", + "code.editor.open_file_editor": "always_on", + "code.editor.open_file_layout": "always_on", + "code.editor.prefer_markdown_viewer": "always_on", + "code.editor.prefer_tabbed_editor_view": "always_on", + "code.editor.show_code_review_button": "always_on", + "code.editor.show_code_review_diff_stats": "always_on", + "code.editor.show_global_search": "always_on", + "code.editor.show_hidden_files": "always_on", + "code.editor.show_project_explorer": "always_on", + "code.editor.use_warp_as_default_editor": "always_on", + "code.indexing.agent_mode_codebase_context": "always_on", + "code.indexing.agent_mode_codebase_context_auto_indexing": "always_on", + "experimental.async_find_enabled": "always_on", + "general.default_session_mode": "always_on", + "general.default_tab_config_path": "always_on", + "general.link_tooltip": "always_on", + "general.login_item": "always_on", + "general.mouse_scroll_multiplier": "always_on", + "general.new_tab_placement": "always_on", + "general.preserve_input_focus_on_block_selection": "always_on", + "general.quit_on_last_window_closed": "always_on", + "general.restore_session": "always_on", + "general.should_confirm_close_session": "always_on", + "general.show_changelog_after_update": "always_on", + "general.show_warning_before_quitting": "always_on", + "general.snackbar_enabled": "always_on", + "general.undo_close.enabled": "always_on", + "general.undo_close.grace_period": "always_on", + "general.user_native_preference": "always_on", + "global_hotkey.dedicated_window.enabled": "always_on", + "global_hotkey.dedicated_window.settings": "always_on", + "global_hotkey.toggle_all_windows.enabled": "always_on", + "global_hotkey.toggle_all_windows.keybinding": "always_on", + "keys.ctrl_tab_behavior_setting": "always_on", + "notifications.preferences": "always_on", + "notifications.toast_duration_secs": "always_on", + "privacy.crash_reporting_enabled": "always_on", + "privacy.custom_secret_regex_list": "always_on", + "privacy.secret_redaction.enabled": "always_on", + "privacy.secret_redaction.hide_secrets_in_block_list": "always_on", + "privacy.secret_redaction.secret_display_mode_setting": "always_on", + "privacy.telemetry_enabled": "always_on", + "session.new_session_shell_override": "always_on", + "session.startup_shell_override": "always_on", + "session.working_directory_config": "private", + "system.force_x11": "always_on", + "system.linux_selection_clipboard": "always_on", + "system.prefer_low_power_gpu": "always_on", + "system.preferred_graphics_backend": "always_on", + "terminal.copy_on_select": "always_on", + "terminal.focus_reporting_enabled": "always_on", + "terminal.input.alias_expansion_enabled": "always_on", + "terminal.input.at_context_menu_in_terminal_mode": "always_on", + "terminal.input.autosuggestions.enabled": "always_on", + "terminal.input.autosuggestions.keybinding_hint": "always_on", + "terminal.input.autosuggestions.show_ignore_button": "always_on", + "terminal.input.classic_completions_mode": "always_on", + "terminal.input.command_corrections": "always_on", + "terminal.input.completions_open_while_typing": "always_on", + "terminal.input.enable_slash_commands_in_terminal": "always_on", + "terminal.input.error_underlining_enabled": "always_on", + "terminal.input.extra_meta_keys": "always_on", + "terminal.input.honor_ps1": "always_on", + "terminal.input.input_box_type_setting": "always_on", + "terminal.input.middle_click_paste_enabled": "always_on", + "terminal.input.outline_codebase_symbols_for_at_context_menu": "always_on", + "terminal.input.show_hint_text": "always_on", + "terminal.input.show_terminal_input_message_bar": "always_on", + "terminal.input.syntax_highlighting": "always_on", + "terminal.maximum_grid_size": "always_on", + "terminal.mouse_reporting_enabled": "always_on", + "terminal.osc52_clipboard_access": "always_on", + "terminal.scroll_reporting_enabled": "always_on", + "terminal.show_terminal_zero_state_block": "always_on", + "terminal.smart_select.enabled": "always_on", + "terminal.smart_select.word_char_allowlist": "always_on", + "terminal.use_audible_bell": "always_on", + "text_editing.autocomplete_symbols": "always_on", + "text_editing.code_editor_line_number_mode": "always_on", + "text_editing.vim_mode_enabled": "always_on", + "text_editing.vim_status_bar": "always_on", + "text_editing.vim_unnamed_system_clipboard": "always_on", + "warp_drive.enabled": "always_on", + "warp_drive.sorting_choice": "always_on", + "warpify.ssh.enable_legacy_ssh_wrapper": "always_on", + "warpify.ssh.enable_ssh_warpification": "always_on", + "warpify.ssh.ssh_extension_install_mode": "always_on", + "warpify.ssh.ssh_hosts_denylist": "always_on", + "warpify.ssh.use_ssh_tmux_wrapper": "always_on", + "warpify.subshells.added_subshell_commands": "always_on", + "warpify.subshells.subshell_commands_denylist": "always_on", + "workflows.show_global_workflows_in_universal_search": "always_on" + }, + "web_routes": [ + ":agentId", + ":envId", + ":runId", + ":scheduleId", + ":secretId", + "agents", + "design", + "environments", + "integrations", + "login", + "login/callback", + "logout", + "memory", + "memory/:storeId", + "runs", + "schedules", + "secrets", + "settings", + "skills", + "welcome/*" + ], + "server_tools": [ + "add_todos", + "address_review_comments", + "answer", + "answer_query", + "apply_patch", + "ask_advice", + "ask_user_question", + "call_mcp_tool", + "chain_of_thought", + "codebase_semantic_search", + "computer_use", + "create_file", + "create_orchestration_config", + "create_plan", + "create_todo_list", + "edit_files", + "edit_plans", + "exa_web_search", + "fetch_web_pages", + "file_glob", + "finish", + "finish_advice", + "finish_computer_use", + "finish_research", + "finish_task", + "finish_warp_documentation_search", + "get_artifacts_for_pull_request_description", + "grep", + "init_project", + "insert_code_review_comments", + "list_messages_from_agents", + "list_relevant_mcp_context", + "mark_todo_as_done", + "notify_user", + "open_code_review", + "read_executed_shell_command_output", + "read_files", + "read_mcp_resource", + "read_messages_from_agents", + "read_output", + "read_plans", + "read_skill", + "read_todos", + "remove_todos", + "report_pr", + "report_screenshot", + "request_computer_use", + "research", + "ripgrep", + "route", + "run_agents", + "run_shell_command", + "search_conversation_history", + "search_warp_documentation", + "search_warp_documentation_index", + "send_message_to_agent", + "skip", + "start_agent", + "suggest_prompt", + "suggest_unit_tests", + "transfer_control_to_user", + "upload_artifact", + "wait_for_events", + "write_block", + "write_line", + "write_raw_bytes" + ], + "bundled_skills": { + "add-mcp-server": "bundled", + "change-keybinding": "bundled", + "claude-api": "bundled", + "create-skill": "bundled", + "create-tab-config": "bundled", + "modify-settings": "bundled", + "oz-platform": "bundled", + "pr-comments": "bundled", + "tab-configs": "bundled", + "test-warp-ui": "dogfood", + "triage-vulnerabilities": "dogfood", + "update-tab-config": "bundled", + "verify-ui-change-in-cloud": "dogfood" + }, "changelog_last_version": "2026.06.03" } diff --git a/.agents/skills/missing_docs/scripts/audit_docs.py b/.agents/skills/missing_docs/scripts/audit_docs.py index b4b21f164..928cd4587 100755 --- a/.agents/skills/missing_docs/scripts/audit_docs.py +++ b/.agents/skills/missing_docs/scripts/audit_docs.py @@ -6,6 +6,19 @@ warp-server to identify gaps, and (in --diff mode) detects surface changes since the last committed snapshot. Produces a structured JSON report. +Audited surfaces: + - Feature flags (rollout status via the app/src/features.rs cargo bridge) + - CLI commands, subcommands (recursive), and per-module long flags + - Public API routes (router/handlers/public_api gin groups vs OpenAPI spec) + - Slash commands (static registry) + - Settings (define_setting! toml_path registry vs all-settings.mdx) + - Oz web app routes (AgentsApp.tsx) [snapshot/diff only] + - Server-side agent tools (multi_agent ToolName constants) [snapshot/diff only] + - Bundled + channel-gated skills [snapshot/diff only] + - Docs structure (pages missing from the sidebar) + - Stale doc references (documented settings/keybinding actions removed from code) + - Docs staleness terminology and surface-map hygiene + Usage: python3 .agents/skills/missing_docs/scripts/audit_docs.py python3 .agents/skills/missing_docs/scripts/audit_docs.py --category features @@ -16,8 +29,10 @@ Exit codes: 0 — all requested audits ran (findings may still exist; check the report) 1 — fatal setup error (docs directory not found, bad arguments) - 2 — one or more audits were SKIPPED (missing repo paths). Never treat a - run that exits 2 as a clean audit. + 2 — one or more audits were SKIPPED (missing repo paths) or an extraction + sanity guard tripped (a parser returned implausibly few surfaces, + meaning the code layout changed). Never treat an exit-2 run as a + clean audit. """ import argparse @@ -33,6 +48,9 @@ SKIP_DIRECTORIES = {"_book", "node_modules", ".git", ".docs"} +# Directories pruned when walking Rust/Go source trees. +SOURCE_SKIP_DIRECTORIES = {"target", "node_modules", ".git", "vendor", "dist", "build"} + # Mutable holder for the docs repo root, set by main() DOCS_REPO_ROOT: list = [None] @@ -43,7 +61,18 @@ STALE_TERMS_PATH = SKILL_DIR / "references" / "stale_terms.md" DEFAULT_SNAPSHOT_PATH = SKILL_DIR / "references" / "surface_snapshot.json" -SNAPSHOT_SCHEMA_VERSION = 1 +SNAPSHOT_SCHEMA_VERSION = 2 + +# Extraction sanity floors: if a parser returns fewer surfaces than this, the +# code layout probably changed and the parser is broken. The audit fails loud +# (exit 2) instead of silently under-reporting. +EXTRACTION_FLOORS = { + "feature flags": 50, + "CLI commands": 5, + "slash commands": 10, + "API routes": 10, + "settings": 100, +} # --------------------------------------------------------------------------- # Surface map parser @@ -56,7 +85,9 @@ def parse_surface_map(path: Path) -> dict: "cli_to_doc": {}, "api_to_doc": {}, "slash_to_doc": {}, + "settings_to_doc": {}, "ignore_flags": set(), + "unlisted_ignore": set(), } if not path.exists(): return result @@ -73,13 +104,20 @@ def parse_surface_map(path: Path) -> dict: current_section = "api" elif line.startswith("## Slash commands"): current_section = "slash" + elif line.startswith("## Settings"): + current_section = "settings" elif line.startswith("## Flags to ignore"): current_section = "ignore" + elif line.startswith("## Unlisted docs pages"): + current_section = "unlisted" continue if current_section == "ignore": result["ignore_flags"].add(line) continue + if current_section == "unlisted": + result["unlisted_ignore"].add(line) + continue if " -> " in line: key, doc_path = line.split(" -> ", 1) @@ -93,6 +131,8 @@ def parse_surface_map(path: Path) -> dict: result["api_to_doc"][key] = doc_path elif current_section == "slash": result["slash_to_doc"][key] = doc_path + elif current_section == "settings": + result["settings_to_doc"][key] = doc_path return result @@ -113,7 +153,7 @@ def parse_stale_terms(path: Path) -> list[tuple[str, str]]: return terms # --------------------------------------------------------------------------- -# Helpers +# Generic helpers # --------------------------------------------------------------------------- def find_repo(name: str, explicit_path: str | None, repo_root: Path) -> Path | None: @@ -145,6 +185,19 @@ def find_markdown_files(docs_root: Path) -> list[Path]: return sorted(files) +def iter_source_files(roots: list[Path], suffix: str): + """Yield source files under the given roots, pruning build directories.""" + for root_dir in roots: + if not root_dir.exists(): + continue + for root, dirs, filenames in os.walk(root_dir): + dirs[:] = [d for d in dirs if d not in SOURCE_SKIP_DIRECTORIES + and not d.startswith(".")] + for f in sorted(filenames): + if f.endswith(suffix): + yield Path(root) / f + + def read_all_docs_text(docs_root: Path) -> dict[str, str]: """Read all doc files into a dict of {relative_path: content} (lowercased).""" result = {} @@ -237,6 +290,113 @@ def kebab_case(name: str) -> str: """PascalCase -> kebab-case: RunCloud -> run-cloud, MCP -> mcp.""" return re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", name).lower() +# --------------------------------------------------------------------------- +# Rust parsing helpers +# --------------------------------------------------------------------------- + +def _extract_enum_block(content: str, enum_name: str) -> str | None: + """Return the body of `[pub] enum { ... }` using brace matching.""" + match = re.search(rf"(?:pub\s+)?enum {enum_name}\s*\{{", content) + if not match: + return None + start = match.end() + depth = 1 + i = start + while i < len(content) and depth > 0: + if content[i] == "{": + depth += 1 + elif content[i] == "}": + depth -= 1 + i += 1 + return content[start:i - 1] + + +def _parse_enum_variants(enum_body: str) -> list[dict]: + """Parse top-level variants of a Rust enum body. + + Returns [{"name", "hidden", "subcommand", "referenced_type"}]. + Tracks brace/paren depth so struct-variant fields aren't mistaken for + variants, and reads `hide = true` / `#[command(subcommand)]` from the + attributes stacked above each variant. + """ + variants = [] + depth = 0 + pending_attrs: list[str] = [] + for raw_line in enum_body.splitlines(): + line = raw_line.strip() + if depth == 0: + if line.startswith("#["): + pending_attrs.append(line) + elif line.startswith("///") or line.startswith("//") or not line: + pass + else: + match = re.match(r"^([A-Z]\w*)\s*(\(|\{|,|$)", line) + if match: + name = match.group(1) + attrs = " ".join(pending_attrs) + ref_match = re.search(r"\(\s*(?:crate::)?([\w:]+)\s*\)", line) + variants.append({ + "name": name, + "hidden": "hide = true" in attrs, + "subcommand": "subcommand" in attrs, + "referenced_type": ref_match.group(1) if ref_match else None, + }) + pending_attrs = [] + depth += raw_line.count("{") - raw_line.count("}") + depth += raw_line.count("(") - raw_line.count(")") + depth = max(depth, 0) + return variants + + +def _enclosing_brace_block(content: str, idx: int) -> str: + """Return the innermost `{...}` block containing the given index. + + Heuristic brace matching (does not understand string literals), good + enough for the macro-invocation blocks it is used on. + """ + depth = 0 + start = None + i = idx + while i >= 0: + c = content[i] + if c == "}": + depth += 1 + elif c == "{": + if depth == 0: + start = i + break + depth -= 1 + i -= 1 + if start is None: + return content[max(0, idx - 500):idx + 500] + depth = 0 + j = start + while j < len(content): + if content[j] == "{": + depth += 1 + elif content[j] == "}": + depth -= 1 + if depth == 0: + return content[start:j + 1] + j += 1 + return content[start:] + + +def _iter_attr_blocks(content: str, names: tuple[str, ...]): + """Yield full `#[name(...)]` attribute blocks, paren-matched.""" + pattern = re.compile(r"#\[(" + "|".join(names) + r")\(") + for match in pattern.finditer(content): + start = match.end() + depth = 1 + i = start + while i < len(content) and depth > 0: + if content[i] == "(": + depth += 1 + elif content[i] == ")": + depth -= 1 + i += 1 + yield content[match.start():i] + # --------------------------------------------------------------------------- # Extraction: feature flags (warp-internal) # --------------------------------------------------------------------------- @@ -252,29 +412,17 @@ def _features_lib_rs(warp_internal: Path) -> Path | None: def parse_feature_flags(warp_internal: Path) -> list[str]: - """Parse FeatureFlag enum variants from the features lib.""" + """Parse FeatureFlag enum variants from the features lib (brace-safe).""" features_rs = _features_lib_rs(warp_internal) if features_rs is None: print("Warning: FeatureFlag enum source not found in warp-internal", file=sys.stderr) return [] - content = features_rs.read_text() - in_enum = False - flags = [] - for line in content.splitlines(): - stripped = line.strip() - if "enum FeatureFlag" in stripped: - in_enum = True - continue - if in_enum: - if stripped == "}": - break - if stripped.startswith("//") or stripped.startswith("#[") or not stripped: - continue - match = re.match(r"^([A-Z]\w+)", stripped) - if match: - flags.append(match.group(1)) - return flags + enum_body = _extract_enum_block(features_rs.read_text(), "FeatureFlag") + if enum_body is None: + print("Warning: FeatureFlag enum not found", file=sys.stderr) + return [] + return [v["name"] for v in _parse_enum_variants(enum_body)] def parse_flag_list_const(warp_internal: Path, const_name: str) -> set[str]: @@ -383,63 +531,9 @@ def compute_flag_statuses(warp_internal: Path) -> dict[str, str]: return statuses # --------------------------------------------------------------------------- -# Extraction: CLI command tree (warp-internal) +# Extraction: CLI command tree + flags (warp-internal) # --------------------------------------------------------------------------- -def _extract_enum_block(content: str, enum_name: str) -> str | None: - """Return the body of `pub enum { ... }` using brace matching.""" - match = re.search(rf"pub enum {enum_name}\s*\{{", content) - if not match: - return None - start = match.end() - depth = 1 - i = start - while i < len(content) and depth > 0: - if content[i] == "{": - depth += 1 - elif content[i] == "}": - depth -= 1 - i += 1 - return content[start:i - 1] - - -def _parse_enum_variants(enum_body: str) -> list[dict]: - """Parse top-level variants of a clap enum body. - - Returns [{"name", "hidden", "subcommand", "referenced_type"}]. - Tracks brace/paren depth so struct-variant fields aren't mistaken for - variants, and reads `hide = true` / `#[command(subcommand)]` from the - attributes stacked above each variant. - """ - variants = [] - depth = 0 - pending_attrs: list[str] = [] - for raw_line in enum_body.splitlines(): - line = raw_line.strip() - if depth == 0: - if line.startswith("#["): - pending_attrs.append(line) - elif line.startswith("///") or line.startswith("//") or not line: - pass - else: - match = re.match(r"^([A-Z]\w*)\s*(\(|\{|,|$)", line) - if match: - name = match.group(1) - attrs = " ".join(pending_attrs) - ref_match = re.search(r"\(\s*(?:crate::)?([\w:]+)\s*\)", line) - variants.append({ - "name": name, - "hidden": "hide = true" in attrs, - "subcommand": "subcommand" in attrs, - "referenced_type": ref_match.group(1) if ref_match else None, - }) - pending_attrs = [] - depth += raw_line.count("{") - raw_line.count("}") - depth += raw_line.count("(") - raw_line.count(")") - depth = max(depth, 0) - return variants - - def _resolve_subcommand_enum(module_content: str, referenced_type: str | None) -> str | None: """Find the enum body holding a variant's subcommands within a module file. @@ -460,17 +554,47 @@ def _resolve_subcommand_enum(module_content: str, referenced_type: str | None) - return None +def _collect_subcommands(src_dir: Path, module_content: str, enum_body: str, + prefix: str, parent_hidden: bool, depth: int) -> list[dict]: + """Recursively collect subcommands (e.g. `oz environment image list`).""" + subs = [] + for sub in _parse_enum_variants(enum_body): + command = f"{prefix} {kebab_case(sub['name'])}" + hidden = sub["hidden"] or parent_hidden + subs.append({"command": command, "hidden": hidden}) + if depth >= 3 or not sub["subcommand"]: + continue + ref = sub["referenced_type"] + target_content = module_content + if ref and "::" in ref: + module = ref.split("::")[0] + module_file = src_dir / f"{module}.rs" + if not module_file.exists(): + continue + target_content = module_file.read_text() + nested_body = _resolve_subcommand_enum(target_content, ref) + if nested_body is not None and nested_body != enum_body: + subs.extend(_collect_subcommands( + src_dir, target_content, nested_body, command, hidden, depth + 1)) + return subs + + +def _cli_src_dir(warp_internal: Path) -> Path | None: + candidates = [ + warp_internal / "crates" / "warp_cli" / "src", + warp_internal / "warp_cli" / "src", + ] + return next((c for c in candidates if c.exists()), None) + + def parse_cli_commands(warp_internal: Path) -> list[dict]: - """Parse the full `oz` CLI command tree (top-level + one level of subcommands). + """Parse the full `oz` CLI command tree (recursive subcommands). Returns [{"command": "oz agent", "hidden": bool, "source_file": str, + "module": str|None, "subcommands": [{"command": "oz agent run", "hidden": bool}]}] """ - src_candidates = [ - warp_internal / "crates" / "warp_cli" / "src", - warp_internal / "warp_cli" / "src", - ] - src_dir = next((c for c in src_candidates if c.exists()), None) + src_dir = _cli_src_dir(warp_internal) if src_dir is None: print("Warning: warp_cli/src not found in warp-internal", file=sys.stderr) return [] @@ -493,6 +617,7 @@ def parse_cli_commands(warp_internal: Path) -> list[dict]: "command": f"oz {cmd_name}", "hidden": variant["hidden"], "source_file": None, + "module": None, "subcommands": [], } ref = variant["referenced_type"] @@ -501,17 +626,48 @@ def parse_cli_commands(warp_internal: Path) -> list[dict]: module_file = src_dir / f"{module}.rs" if module_file.exists(): entry["source_file"] = f"warp_cli/src/{module}.rs" + entry["module"] = module module_content = module_file.read_text() sub_body = _resolve_subcommand_enum(module_content, ref) if sub_body is not None: - for sub in _parse_enum_variants(sub_body): - entry["subcommands"].append({ - "command": f"oz {cmd_name} {kebab_case(sub['name'])}", - "hidden": sub["hidden"] or variant["hidden"], - }) + entry["subcommands"] = _collect_subcommands( + src_dir, module_content, sub_body, + entry["command"], variant["hidden"], depth=1) commands.append(entry) return commands + +def parse_cli_flags(warp_internal: Path, cli_commands: list[dict]) -> dict[str, list[str]]: + """Extract visible `--long` flags per CLI module for change tracking. + + Attribution of flags to specific subcommands would require full clap + resolution; per-module sets are stable and sufficient to detect that a + flag was added or removed (the drift agent then reads the module to see + which command it belongs to). + """ + src_dir = _cli_src_dir(warp_internal) + if src_dir is None: + return {} + + flags_by_module: dict[str, list[str]] = {} + modules = sorted({c["module"] for c in cli_commands if c.get("module") and not c["hidden"]}) + for module in modules: + module_file = src_dir / f"{module}.rs" + if not module_file.exists(): + continue + content = module_file.read_text() + flags: set[str] = set() + for attr in _iter_attr_blocks(content, ("arg", "clap", "command")): + if "hide = true" in attr: + continue + for m in re.finditer(r'long(?:_flag)?\s*=\s*"([a-z0-9][a-z0-9-]*)"', attr): + flags.add(f"--{m.group(1)}") + for m in re.finditer(r'long(?:_flag)?\("([a-z0-9][a-z0-9-]*)"\)', attr): + flags.add(f"--{m.group(1)}") + if flags: + flags_by_module[module] = sorted(flags) + return flags_by_module + # --------------------------------------------------------------------------- # Extraction: public API routes (warp-server) # --------------------------------------------------------------------------- @@ -519,9 +675,6 @@ def parse_cli_commands(warp_internal: Path) -> list[dict]: _GO_FUNC_RE = re.compile(r"^func (\w+)\(([^)]*)\)", re.MULTILINE) _GO_GROUP_ASSIGN_RE = re.compile(r"(\w+)\s*:?=\s*(\w+)\.Group\(\s*\"([^\"]*)\"") _GO_ROUTE_RE = re.compile(r"(\w+)\.(GET|POST|PUT|DELETE|PATCH)\(\s*\"([^\"]*)\"") -_GO_REGISTER_CALL_RE = re.compile( - r"(Register\w+)\(\s*(\w+)(?:\.Group\(\s*\"([^\"]*)\"\s*\))?\s*," -) def _parse_go_functions(content: str) -> dict[str, dict]: @@ -538,6 +691,54 @@ def _parse_go_functions(content: str) -> dict[str, dict]: return functions +def _split_top_level_args(s: str) -> list[str]: + """Split a Go argument list on top-level commas.""" + args = [] + depth = 0 + cur: list[str] = [] + for ch in s: + if ch in "([{": + depth += 1 + elif ch in ")]}": + depth -= 1 + if ch == "," and depth == 0: + args.append("".join(cur).strip()) + cur = [] + else: + cur.append(ch) + tail = "".join(cur).strip() + if tail: + args.append(tail) + return args + + +def _iter_register_calls(body: str): + """Yield (callee, start_pos, args) for Register*(...) calls, paren-matched.""" + for match in re.finditer(r"\b(Register\w+)\(", body): + start = match.end() + depth = 1 + i = start + while i < len(body) and depth > 0: + if body[i] == "(": + depth += 1 + elif body[i] == ")": + depth -= 1 + i += 1 + yield match.group(1), match.start(), _split_top_level_args(body[start:i - 1]) + + +def _go_param_positions(params: str) -> tuple[int | None, int | None]: + """Return (router_group_param_index, engine_param_index) for a Go param list.""" + group_idx = None + engine_idx = None + for idx, param in enumerate(_split_top_level_args(params)): + if "*gin.RouterGroup" in param and group_idx is None: + group_idx = idx + elif "*gin.Engine" in param and engine_idx is None: + engine_idx = idx + return group_idx, engine_idx + + def parse_public_api_routes(warp_server: Path) -> list[dict]: """Extract public API routes from router/handlers/public_api/*.go. @@ -550,8 +751,10 @@ def parse_public_api_routes(warp_server: Path) -> list[dict]: This walks group-variable assignments per registration function and resolves caller-passed prefixes via Register* call sites, starting from - RegisterPublicAPIRoutes. Gin `:param` segments are normalized to - OpenAPI-style `{param}`. + RegisterPublicAPIRoutes. The RouterGroup argument is matched positionally + against the callee's parameter list (so `RegisterOAuthRoutes(router, + group, ...)` resolves `group`, not `router`). Gin `:param` segments are + normalized to OpenAPI-style `{param}`. """ api_dir = warp_server / "router" / "handlers" / "public_api" if not api_dir.exists(): @@ -568,14 +771,16 @@ def parse_public_api_routes(warp_server: Path) -> list[dict]: def analyze(fn: dict) -> dict: """Resolve a function body to routes/calls relative to its params.""" - params = fn["params"] - group_param = None - router_param = None - for param_match in re.finditer(r"(\w+)(?:\s*,\s*\w+)*\s+\*gin\.(RouterGroup|Engine)", params): - if param_match.group(2) == "RouterGroup" and group_param is None: - group_param = param_match.group(1) - elif param_match.group(2) == "Engine" and router_param is None: - router_param = param_match.group(1) + group_idx, engine_idx = _go_param_positions(fn["params"]) + param_names = _split_top_level_args(fn["params"]) + + def param_name(idx): + if idx is None or idx >= len(param_names): + return None + return param_names[idx].split()[0] if param_names[idx].split() else None + + group_param = param_name(group_idx) + router_param = param_name(engine_idx) # var name -> (base, prefix); base is "PARAM" (caller group) or # "ROUTER" (engine root) @@ -585,34 +790,49 @@ def analyze(fn: dict) -> dict: if router_param: var_bases[router_param] = ("ROUTER", "") - routes = [] - calls = [] + def resolve_expr(expr: str): + m = re.fullmatch(r"(\w+)", expr) + if m: + return var_bases.get(m.group(1)) + m = re.fullmatch(r"(\w+)\.Group\(\s*\"([^\"]*)\"\s*\)", expr) + if m: + base = var_bases.get(m.group(1)) + if base is not None: + return (base[0], base[1] + m.group(2)) + return None + events = [] for assign in _GO_GROUP_ASSIGN_RE.finditer(fn["body"]): events.append(("assign", assign.start(), assign.groups())) for route in _GO_ROUTE_RE.finditer(fn["body"]): events.append(("route", route.start(), route.groups())) - for call in _GO_REGISTER_CALL_RE.finditer(fn["body"]): - events.append(("call", call.start(), call.groups())) + for callee, pos, args in _iter_register_calls(fn["body"]): + events.append(("call", pos, (callee, args))) events.sort(key=lambda e: e[1]) - for kind, _pos, groups in events: + routes = [] + calls = [] + for kind, _pos, payload in events: if kind == "assign": - target, parent, prefix = groups + target, parent, prefix = payload base = var_bases.get(parent) if base is not None: var_bases[target] = (base[0], base[1] + prefix) elif kind == "route": - var, method, path = groups + var, method, path = payload base = var_bases.get(var) if base is not None: routes.append((base[0], method, base[1] + path)) else: # call - callee, arg_var, arg_prefix = groups - base = var_bases.get(arg_var) - if base is not None: - calls.append((callee, base[0], base[1] + (arg_prefix or ""))) - return {"routes": routes, "calls": calls, "file": fn["file"]} + callee, args = payload + resolved_args = [resolve_expr(a) for a in args] + calls.append((callee, resolved_args)) + return { + "routes": routes, + "calls": calls, + "file": fn["file"], + "group_param_index": group_idx, + } analyzed = {name: analyze(fn) for name, fn in functions.items()} @@ -634,7 +854,21 @@ def emit(fn_name: str, param_prefix: str): for base, method, path in info["routes"]: full = (param_prefix + path) if base == "PARAM" else path resolved.append({"method": method, "path": full, "file": info["file"]}) - for callee, base, prefix in info["calls"]: + for callee, args in info["calls"]: + callee_info = analyzed.get(callee) + if callee_info is None: + continue + # Pick the argument that maps to the callee's RouterGroup param; + # fall back to the first resolvable argument. + idx = callee_info.get("group_param_index") + arg = None + if idx is not None and idx < len(args): + arg = args[idx] + if arg is None: + arg = next((a for a in args if a is not None), None) + if arg is None: + continue + base, prefix = arg callee_prefix = (param_prefix + prefix) if base == "PARAM" else prefix emit(callee, callee_prefix) @@ -665,6 +899,19 @@ def emit(fn_name: str, param_prefix: str): routes.sort(key=lambda r: (r["path"], r["method"])) return routes + +def _normalize_path_params(path: str) -> str: + """`/agent/runs/{runId}` -> `/agent/runs/{}` (param-name-insensitive).""" + return re.sub(r"\{[^}]+\}", "{}", path) + + +def parse_openapi_paths(openapi_text: str) -> set[str]: + """Extract normalized path keys from the OpenAPI YAML text.""" + paths = set() + for match in re.finditer(r"(?m)^\s{2}(/[^\s:]+):", openapi_text): + paths.add(_normalize_path_params(match.group(1))) + return paths + # --------------------------------------------------------------------------- # Extraction: slash commands (warp-internal) # --------------------------------------------------------------------------- @@ -687,11 +934,213 @@ def parse_slash_commands(warp_internal: Path) -> list[str]: return sorted(names) # --------------------------------------------------------------------------- -# Extraction: docs changelog entries +# Extraction: settings (warp-internal) +# --------------------------------------------------------------------------- + +_SETTING_TOML_PATH_RE = re.compile(r'toml_path:\s*"([^"]+)"') + + +def _is_test_rs(path: Path) -> bool: + return path.name.endswith("_tests.rs") or path.name == "tests.rs" or "/tests/" in str(path) + + +def parse_settings(warp_internal: Path) -> dict[str, dict]: + """Parse user-facing settings from `define_setting!`-style registrations. + + Every settings.toml-backed setting declares `toml_path: "section.key"` + in its registration block, alongside `private:` and (optionally) + `feature_flag:`. This is the same metadata the JSON-schema generator + (app/src/bin/generate_settings_schema.rs) consumes via inventory. + + Returns {toml_path: {"private": bool, "feature_flag": str|None}}. + """ + settings: dict[str, dict] = {} + roots = [warp_internal / "app" / "src", warp_internal / "crates"] + for rs_file in iter_source_files(roots, ".rs"): + if _is_test_rs(rs_file): + continue + # The macro definition file contains `toml_path:` in doc examples. + if rs_file.name == "macros.rs" and rs_file.parent.name == "src" \ + and rs_file.parent.parent.name == "settings": + continue + try: + content = rs_file.read_text(encoding="utf-8") + except Exception: + continue + if "toml_path:" not in content: + continue + for match in _SETTING_TOML_PATH_RE.finditer(content): + toml_path = match.group(1) + block = _enclosing_brace_block(content, match.start()) + flag_match = re.search( + r"feature_flag:\s*(?:Some\()?\s*(?:[\w:]*::)?FeatureFlag::(\w+)", block) + entry = { + "private": re.search(r"private:\s*true", block) is not None, + "feature_flag": flag_match.group(1) if flag_match else None, + } + existing = settings.get(toml_path) + if existing: + # Same setting registered per-platform: private if any + # registration is private; keep the first flag seen. + entry["private"] = entry["private"] or existing["private"] + entry["feature_flag"] = existing["feature_flag"] or entry["feature_flag"] + settings[toml_path] = entry + return settings + + +def setting_status(info: dict, flag_statuses: dict[str, str]) -> str: + """Classify a setting: private | always_on | ga | preview | dogfood | other | unknown_flag.""" + if info["private"]: + return "private" + flag = info["feature_flag"] + if flag is None: + return "always_on" + return flag_statuses.get(flag, "unknown_flag") + + +def parse_settings_doc(docs_root: Path) -> tuple[dict[str, set[str]], Path | None]: + """Parse all-settings.mdx into {toml_section: {keys}}. + + The reference page lists `**Section**: `[a.b]`` headers followed by + `* `key` — ...` bullets. + """ + page = docs_root / "terminal" / "settings" / "all-settings.mdx" + sections: dict[str, set[str]] = {} + if not page.exists(): + return sections, None + current = "" + for line in page.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + header = re.match(r"\*\*Section\*\*:\s*`\[([^\]]+)\]`", stripped) + if header: + current = header.group(1) + sections.setdefault(current, set()) + continue + bullet = re.match(r"^\*\s+`([A-Za-z0-9_]+)`", stripped) + if bullet: + sections.setdefault(current, set()).add(bullet.group(1)) + return sections, page + +# --------------------------------------------------------------------------- +# Extraction: snapshot-only surfaces (web app, tools, bundled skills) # --------------------------------------------------------------------------- +def parse_webapp_routes(warp_server: Path) -> list[str]: + """Parse Oz web app route paths from the agents package router.""" + app_tsx = warp_server / "client" / "packages" / "agents" / "src" / "AgentsApp.tsx" + if not app_tsx.exists(): + print(f"Warning: {app_tsx} not found", file=sys.stderr) + return [] + paths = set(re.findall(r'path="([^"]+)"', app_tsx.read_text(encoding="utf-8"))) + paths.discard("*") + return sorted(paths) + + +def parse_server_tools(warp_server: Path) -> list[str]: + """Parse agent tool names from the multi_agent tool registries. + + Two definition styles exist: + - `xToolName = "tool_name"` constants + - `native_tools.Create*NativeTool[...]("tool_name", ...)` registrations + (the canonical registry in native_tools/shared/shared_tools.go) + """ + base = warp_server / "logic" / "ai" / "multi_agent" + if not base.exists(): + print(f"Warning: {base} not found", file=sys.stderr) + return [] + names: set[str] = set() + native_tool_re = re.compile( + r'Create\w*NativeTool\[[^\]]*\]\(\s*"([a-z0-9_]+)"', re.DOTALL) + for go_file in iter_source_files([base], ".go"): + if go_file.name.endswith("_test.go"): + continue + try: + content = go_file.read_text(encoding="utf-8") + except Exception: + continue + for match in re.finditer(r'ToolName\s*=\s*"([a-z0-9_]+)"', content): + names.add(match.group(1)) + for match in native_tool_re.finditer(content): + names.add(match.group(1)) + return sorted(names) + + +def parse_bundled_skills(warp_internal: Path) -> dict[str, str]: + """List bundled skills shipped with the client, keyed by channel gating. + + resources/bundled/skills/ ships on all channels ("bundled"); + resources/channel-gated-skills// ships per channel. + """ + skills: dict[str, str] = {} + bundled = warp_internal / "resources" / "bundled" / "skills" + if bundled.exists(): + for entry in sorted(bundled.iterdir()): + if entry.is_dir(): + skills[entry.name] = "bundled" + gated = warp_internal / "resources" / "channel-gated-skills" + if gated.exists(): + for channel_dir in sorted(gated.iterdir()): + if not channel_dir.is_dir(): + continue + for entry in sorted(channel_dir.iterdir()): + if entry.is_dir(): + skills[entry.name] = channel_dir.name + return skills + +# --------------------------------------------------------------------------- +# Extraction: docs sidebar + changelog +# --------------------------------------------------------------------------- + +def parse_sidebar_slugs(repo_root: Path) -> set[str] | None: + """Parse referenced page slugs from src/sidebar.ts. + + Entries appear either as bare string items ('terminal/blocks/find'), + `slug: '...'` objects, or topic `link: '/...'` values. + Returns None when the sidebar file cannot be found (callers should skip + the structure audit rather than reporting everything unlisted). + """ + sidebar = repo_root / "src" / "sidebar.ts" + if not sidebar.exists(): + return None + slugs: set[str] = set() + for line in sidebar.read_text(encoding="utf-8").splitlines(): + stripped = line.strip().rstrip(",") + bare = re.fullmatch(r"'([a-z0-9][a-z0-9/-]*)'", stripped) + if bare: + slugs.add(bare.group(1)) + continue + for match in re.finditer(r"slug:\s*'([^']+)'", line): + slugs.add(match.group(1).strip("/")) + for match in re.finditer(r"link:\s*'([^']*)'", line): + slugs.add(match.group(1).strip("/")) + return slugs + + +def page_slug(md_file: Path, docs_root: Path) -> str: + """Compute the Starlight slug for a docs page (frontmatter override aware).""" + try: + head = md_file.read_text(encoding="utf-8")[:2000] + override = re.search(r"(?m)^slug:\s*['\"]?([^'\"\n]+)['\"]?\s*$", head) + if override: + return override.group(1).strip().strip("/") + except Exception: + pass + rel = md_file.relative_to(docs_root) + slug = str(rel) + for ext in (".mdx", ".md"): + if slug.endswith(ext): + slug = slug[: -len(ext)] + if slug.endswith("/index"): + slug = slug[: -len("/index")] + elif slug == "index": + slug = "" + return slug + + _CHANGELOG_HEADER_RE = re.compile(r"^### (\d{4}\.\d{2}\.\d{2})", re.MULTILINE) -_CHANGELOG_TRACKED_SECTIONS = ("new features", "improvements") +# "Bug fixes" is deliberately untracked: fix bullets rarely create doc surface +# and would double the weekly triage volume. +_CHANGELOG_TRACKED_SECTIONS = ("new features", "improvements", "oz updates") def parse_changelog_entries(repo_root: Path) -> list[dict]: @@ -699,8 +1148,8 @@ def parse_changelog_entries(repo_root: Path) -> list[dict]: Returns [{"version": "2026.06.03", "file": str, "items": [{"category": "new features", "text": str}]}] sorted newest first. - Only "New features" and "Improvements" bullets are tracked — those are the - sections that may represent undocumented feature launches. + "New features", "Improvements", and "Oz updates" bullets are tracked — + the sections that may represent undocumented feature launches. """ changelog_dir = repo_root / "src" / "content" / "docs" / "changelog" if not changelog_dir.exists(): @@ -864,9 +1313,10 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, # --------------------------------------------------------------------------- def audit_cli(warp_internal: Path, docs_root: Path, surface_map: dict, - docs_text: dict[str, str]) -> list[dict]: + docs_text: dict[str, str], + cli_commands: list[dict] | None = None) -> list[dict]: """Audit CLI command and subcommand coverage in docs.""" - commands = parse_cli_commands(warp_internal) + commands = cli_commands if cli_commands is not None else parse_cli_commands(warp_internal) cli_to_doc = surface_map.get("cli_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -929,15 +1379,17 @@ def is_covered(cmd_str: str, search_phrase: str) -> bool: # --------------------------------------------------------------------------- def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, - docs_text: dict[str, str]) -> list[dict]: + docs_text: dict[str, str], + api_routes: list[dict] | None = None) -> list[dict]: """Audit public API endpoint coverage in the OpenAPI spec and API docs. The public docs API reference (docs.warp.dev/api) renders developers/agent-api-openapi.yaml, so a route missing from the spec is a - docs gap. Use the warp-server `update-open-api-spec` skill / docs - `sync-openapi-spec` skill to fix spec drift rather than hand-editing. + docs gap. Spec matching is param-name-insensitive ({runId} == {run_id}). + Use the warp-server `update-open-api-spec` skill / docs `sync-openapi-spec` + skill to fix spec drift rather than hand-editing. """ - routes = parse_public_api_routes(warp_server) + routes = api_routes if api_routes is not None else parse_public_api_routes(warp_server) api_to_doc = surface_map.get("api_to_doc", {}) # Read API docs @@ -963,6 +1415,7 @@ def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, openapi_text = openapi_path.read_text(encoding="utf-8").lower() except Exception: pass + spec_paths = parse_openapi_paths(openapi_text) findings = [] for route in routes: @@ -977,15 +1430,20 @@ def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, if route_str in api_to_doc or rel_route_str in api_to_doc: continue - # Search the OpenAPI spec and API docs for the path - found = False - for candidate in {route["path"].lower(), rel_path.lower()}: - if candidate in openapi_text: - found = True - break - if any(candidate in content for content in api_docs_text.values()): - found = True - break + # Match against the spec's path keys (param-name-insensitive), then + # fall back to substring search in API docs prose. + found = ( + _normalize_path_params(rel_path) in spec_paths + or _normalize_path_params(route["path"]) in spec_paths + ) + if not found: + for candidate in {route["path"].lower(), rel_path.lower()}: + if candidate in openapi_text: + found = True + break + if any(candidate in content for content in api_docs_text.values()): + found = True + break if not found: findings.append({ @@ -1013,9 +1471,10 @@ def _slash_mention_re(name: str) -> re.Pattern: def audit_slash_commands(warp_internal: Path, docs_root: Path, surface_map: dict, - docs_text: dict[str, str]) -> list[dict]: + docs_text: dict[str, str], + slash_commands: list[str] | None = None) -> list[dict]: """Audit static slash command coverage in docs.""" - names = parse_slash_commands(warp_internal) + names = slash_commands if slash_commands is not None else parse_slash_commands(warp_internal) slash_to_doc = surface_map.get("slash_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -1041,7 +1500,150 @@ def audit_slash_commands(warp_internal: Path, docs_root: Path, surface_map: dict return findings # --------------------------------------------------------------------------- -# Audit 5: Docs staleness +# Audit 5: Settings coverage +# --------------------------------------------------------------------------- + +def audit_settings(docs_root: Path, surface_map: dict, + settings: dict[str, dict], + flag_statuses: dict[str, str]) -> list[dict]: + """Audit settings.toml coverage in the all-settings reference page. + + Private settings are skipped; settings gated by dogfood/other flags are + tracked by the snapshot instead. Settings gated by a flag the parser + cannot resolve are flagged conservatively. + """ + settings_to_doc = surface_map.get("settings_to_doc", {}) + repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent + doc_sections, doc_page = parse_settings_doc(docs_root) + if doc_page is None: + return [{ + "setting": "(all)", + "severity": "high", + "reason": ( + "all-settings.mdx not found — the settings reference page moved; " + "update parse_settings_doc() in the audit script" + ), + }] + + findings = [] + for toml_path in sorted(settings): + status = setting_status(settings[toml_path], flag_statuses) + if status in ("private", "dogfood", "other"): + continue + + if toml_path in settings_to_doc: + target = settings_to_doc[toml_path] + if target == "internal": + continue + if resolve_doc_path(target, repo_root) is not None: + continue + + hierarchy, _, key = toml_path.rpartition(".") + if key in doc_sections.get(hierarchy, set()): + continue + # Object-typed settings are documented as their own section (e.g. the + # `notifications.preferences` setting appears as a + # `**Section**: [notifications.preferences]` block of field bullets). + if toml_path in doc_sections or any( + section.startswith(toml_path + ".") for section in doc_sections): + continue + + findings.append({ + "setting": toml_path, + "status": status, + "severity": "medium", + "suggested_doc_path": "src/content/docs/terminal/settings/all-settings.mdx", + "reason": ( + f"Setting '{toml_path}' ({status}) is not documented in the " + "all-settings reference — add it under the " + f"`[{hierarchy or 'top-level'}]` section, or map it as internal" + ), + }) + return findings + +# --------------------------------------------------------------------------- +# Audit 6: Stale doc references (docs pointing at removed code surfaces) +# --------------------------------------------------------------------------- + +def audit_stale_doc_references(warp_internal: Path, docs_root: Path, + settings: dict[str, dict]) -> list[dict]: + """Find doc references to code surfaces that no longer exist. + + - Settings keys documented in all-settings.mdx but absent from the code + settings registry (renamed/removed settings). + - Keybinding action names (`scope:action`) documented on the keyboard + shortcuts page but absent from warp-internal source. + """ + findings = [] + + # Documented settings that no longer exist in code. + doc_sections, doc_page = parse_settings_doc(docs_root) + if doc_page is not None and settings: + known = set() + for toml_path in settings: + hierarchy, _, key = toml_path.rpartition(".") + known.add((hierarchy, key)) + + def is_object_setting_section(section: str) -> bool: + # Fields of object-typed settings (e.g. keys under the + # `[notifications.preferences]` section, where the code setting is + # `notifications.preferences` itself) cannot be validated + # statically — skip them. + return any( + section == code_path or section.startswith(code_path + ".") + for code_path in settings + ) + + for section, keys in sorted(doc_sections.items()): + if is_object_setting_section(section): + continue + for key in sorted(keys): + if (section, key) not in known: + findings.append({ + "kind": "setting", + "reference": f"{section}.{key}" if section else key, + "doc_page": "src/content/docs/terminal/settings/all-settings.mdx", + "severity": "low", + "reason": ( + "Documented setting not found in the code settings " + "registry — it was renamed or removed; update the " + "all-settings page" + ), + }) + + # Documented keybinding actions that no longer exist in code. + shortcuts_page = docs_root / "getting-started" / "keyboard-shortcuts.mdx" + if shortcuts_page.exists(): + text = shortcuts_page.read_text(encoding="utf-8") + actions = sorted(set(re.findall(r"`([a-z0-9_]+:[a-z0-9_]+)`", text))) + remaining = set(actions) + if remaining: + roots = [warp_internal / "app" / "src", warp_internal / "crates"] + for rs_file in iter_source_files(roots, ".rs"): + if not remaining: + break + try: + content = rs_file.read_text(encoding="utf-8") + except Exception: + continue + remaining = {a for a in remaining if a not in content} + for action in sorted(remaining): + findings.append({ + "kind": "keybinding_action", + "reference": action, + "doc_page": "src/content/docs/getting-started/keyboard-shortcuts.mdx", + "severity": "low", + "reason": ( + "Documented keybinding action not found anywhere in " + "warp-internal source — it was renamed or removed; update " + "the keyboard shortcuts page" + ), + }) + + return findings + +# --------------------------------------------------------------------------- +# Audit 7: Docs staleness (terminology) # --------------------------------------------------------------------------- def audit_staleness(warp_internal: Path, docs_root: Path, @@ -1084,12 +1686,53 @@ def audit_staleness(warp_internal: Path, docs_root: Path, return findings # --------------------------------------------------------------------------- -# Audit 6: Surface map hygiene +# Audit 8: Docs structure (pages missing from the sidebar) +# --------------------------------------------------------------------------- + +def audit_unlisted_pages(repo_root: Path, docs_root: Path, surface_map: dict) -> list[dict]: + """Find docs pages that exist on disk but are not referenced in the sidebar. + + Unlisted pages are built but unreachable through navigation — usually a + forgotten `src/sidebar.ts` entry after adding a page. Intentionally + unlisted pages belong in the surface map's "Unlisted docs pages" section. + """ + slugs = parse_sidebar_slugs(repo_root) + if slugs is None: + return [{ + "page": "(all)", + "severity": "high", + "reason": ( + "src/sidebar.ts not found — sidebar definition moved; update " + "parse_sidebar_slugs() in the audit script" + ), + }] + allowlist = surface_map.get("unlisted_ignore", set()) + + findings = [] + for md_file in find_markdown_files(docs_root): + slug = page_slug(md_file, docs_root) + if slug in slugs or slug in allowlist: + continue + findings.append({ + "page": slug or "(root index)", + "file": str(md_file.relative_to(repo_root)), + "severity": "low", + "reason": ( + "Docs page is not referenced in src/sidebar.ts — add it to the " + "sidebar (and astro.config.mjs topic if new) or allow-list it " + "in the surface map's 'Unlisted docs pages' section" + ), + }) + return findings + +# --------------------------------------------------------------------------- +# Audit 9: Surface map hygiene # --------------------------------------------------------------------------- def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], cli_commands: list[dict], api_routes: list[dict], - slash_commands: list[str], docs_root: Path) -> list[dict]: + slash_commands: list[str], settings: dict[str, dict], + docs_root: Path) -> list[dict]: """Flag surface-map entries that reference code surfaces that no longer exist. Dead entries usually mean a feature was renamed or removed — verify the @@ -1140,6 +1783,25 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], ), }) + known_api = set() + for route in api_routes: + known_api.add(route["route"]) + rel_path = route["path"] + if rel_path.startswith("/api/v1"): + rel_path = rel_path[len("/api/v1"):] or "/" + known_api.add(f"{route['method']} {rel_path}") + for key in sorted(surface_map.get("api_to_doc", {})): + if key not in known_api: + findings.append({ + "entry": key, + "section": "API endpoints", + "severity": "low", + "reason": ( + f"Map entry '{key}' does not match any public API route in " + "code — verify and prune or update" + ), + }) + known_slash = set(slash_commands) for name in sorted(surface_map.get("slash_to_doc", {})): if name not in known_slash: @@ -1153,12 +1815,25 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], ), }) + for key in sorted(surface_map.get("settings_to_doc", {})): + if key not in settings: + findings.append({ + "entry": key, + "section": "Settings", + "severity": "low", + "reason": ( + f"Map entry '{key}' does not match any setting in code — " + "verify and prune or update" + ), + }) + # Mapped doc targets that no longer exist (any section). for section, mapping in ( ("Feature flags", surface_map.get("feature_to_doc", {})), ("CLI commands", surface_map.get("cli_to_doc", {})), ("API endpoints", surface_map.get("api_to_doc", {})), ("Slash commands", surface_map.get("slash_to_doc", {})), + ("Settings", surface_map.get("settings_to_doc", {})), ): for key, doc_path in sorted(mapping.items()): if doc_path == "internal": @@ -1181,7 +1856,10 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], # --------------------------------------------------------------------------- def build_snapshot(flag_statuses: dict[str, str], cli_commands: list[dict], - api_routes: list[dict], slash_commands: list[str], + cli_flags: dict[str, list[str]], api_routes: list[dict], + slash_commands: list[str], settings: dict[str, dict], + web_routes: list[str], server_tools: list[str], + bundled_skills: dict[str, str], changelog_entries: list[dict]) -> dict: """Assemble the surface snapshot (deterministic ordering for clean diffs).""" cli_flat = [] @@ -1191,12 +1869,22 @@ def build_snapshot(flag_statuses: dict[str, str], cli_commands: list[dict], cli_flat.append({"command": sub["command"], "hidden": sub["hidden"]}) cli_flat.sort(key=lambda c: c["command"]) + settings_status = { + path: setting_status(info, flag_statuses) + for path, info in settings.items() + } + return { "schema_version": SNAPSHOT_SCHEMA_VERSION, "flags": dict(sorted(flag_statuses.items())), "cli_commands": cli_flat, + "cli_flags": {k: sorted(v) for k, v in sorted(cli_flags.items())}, "api_routes": sorted(r["route"] for r in api_routes), "slash_commands": sorted(slash_commands), + "settings": dict(sorted(settings_status.items())), + "web_routes": sorted(web_routes), + "server_tools": sorted(server_tools), + "bundled_skills": dict(sorted(bundled_skills.items())), "changelog_last_version": ( changelog_entries[0]["version"] if changelog_entries else None ), @@ -1213,6 +1901,38 @@ def load_snapshot(path: Path) -> dict | None: return None +def _diff_sets(findings: list, old: dict, new: dict, field: str, label: str, + added_reason: str, removed_reason: str, severity: str = "medium"): + """Generic added/removed diff for a snapshot list field.""" + if field not in old: + findings.append({ + "change": "surface_type_added", + "surface": field, + "severity": "low", + "reason": ( + f"The snapshot now tracks {label} — baseline established this " + "run; future runs will diff it (regenerate with --update-snapshot)" + ), + }) + return + old_set = set(old.get(field) or []) + new_set = set(new.get(field) or []) + for item in sorted(new_set - old_set): + findings.append({ + "change": f"{field}_added", + "surface": item, + "severity": severity, + "reason": added_reason.format(item=item), + }) + for item in sorted(old_set - new_set): + findings.append({ + "change": f"{field}_removed", + "surface": item, + "severity": severity, + "reason": removed_reason.format(item=item), + }) + + def diff_snapshots(old: dict, new: dict) -> list[dict]: """Compare two snapshots and report added/removed/promoted surfaces.""" findings = [] @@ -1283,51 +2003,175 @@ def diff_snapshots(old: dict, new: dict) -> list[dict]: ), }) - old_api = set(old.get("api_routes", [])) - new_api = set(new.get("api_routes", [])) - for route in sorted(new_api - old_api): + # Per-module CLI flag changes. + if "cli_flags" not in old: findings.append({ - "change": "api_added", - "surface": route, - "severity": "medium", + "change": "surface_type_added", + "surface": "cli_flags", + "severity": "low", "reason": ( - f"New public API route '{route}' — add it to the OpenAPI spec " - "(sync-openapi-spec skill) or map it as internal" - ), - }) - for route in sorted(old_api - new_api): - findings.append({ - "change": "api_removed", - "surface": route, - "severity": "medium", - "reason": ( - f"Public API route '{route}' was removed — verify the OpenAPI " - "spec and API docs no longer document it" + "The snapshot now tracks CLI --flags per module — baseline " + "established this run; future runs will diff it" ), }) + else: + old_flags_by_module = old.get("cli_flags") or {} + new_flags_by_module = new.get("cli_flags") or {} + for module in sorted(set(old_flags_by_module) | set(new_flags_by_module)): + old_set = set(old_flags_by_module.get(module, [])) + new_set = set(new_flags_by_module.get(module, [])) + for flag in sorted(new_set - old_set): + findings.append({ + "change": "cli_flag_added", + "surface": f"{module}: {flag}", + "severity": "low", + "reason": ( + f"New CLI flag '{flag}' in warp_cli/src/{module}.rs — " + "verify the CLI reference documents it" + ), + }) + for flag in sorted(old_set - new_set): + findings.append({ + "change": "cli_flag_removed", + "surface": f"{module}: {flag}", + "severity": "low", + "reason": ( + f"CLI flag '{flag}' removed from warp_cli/src/{module}.rs — " + "verify the CLI reference no longer documents it" + ), + }) - old_slash = set(old.get("slash_commands", [])) - new_slash = set(new.get("slash_commands", [])) - for name in sorted(new_slash - old_slash): + _diff_sets( + findings, old, new, "api_routes", "public API routes", + "New public API route '{item}' — add it to the OpenAPI spec " + "(sync-openapi-spec skill) or map it as internal", + "Public API route '{item}' was removed — verify the OpenAPI spec and " + "API docs no longer document it", + ) + _diff_sets( + findings, old, new, "slash_commands", "slash commands", + "New slash command '{item}' — add it to the slash-commands docs page " + "or map it as internal", + "Slash command '{item}' was removed — update the slash-commands docs " + "page and surface map", + ) + _diff_sets( + findings, old, new, "web_routes", "Oz web app routes", + "New Oz web app route '{item}' — verify the Oz web app docs cover the " + "new page", + "Oz web app route '{item}' was removed — verify the Oz web app docs " + "no longer reference it", + ) + _diff_sets( + findings, old, new, "server_tools", "server-side agent tools", + "New agent tool '{item}' — verify docs cover the new agent capability", + "Agent tool '{item}' was removed — verify docs no longer describe it", + severity="low", + ) + + # Settings (dict field: path -> status). + if "settings" not in old: findings.append({ - "change": "slash_added", - "surface": name, - "severity": "medium", + "change": "surface_type_added", + "surface": "settings", + "severity": "low", "reason": ( - f"New slash command '{name}' — add it to the slash-commands docs " - "page or map it as internal" + "The snapshot now tracks settings — baseline established this " + "run; future runs will diff it" ), }) - for name in sorted(old_slash - new_slash): + else: + old_settings = old.get("settings") or {} + new_settings = new.get("settings") or {} + for path in sorted(set(new_settings) - set(old_settings)): + status = new_settings[path] + user_facing = status in ("always_on", "ga", "preview") + findings.append({ + "change": "setting_added", + "surface": path, + "detail": f"status: {status}", + "severity": "medium" if user_facing else "low", + "reason": ( + f"New setting '{path}' ({status}) — " + + ("document it in the all-settings reference" + if user_facing else "track it; document on promotion") + ), + }) + for path in sorted(set(old_settings) - set(new_settings)): + findings.append({ + "change": "setting_removed", + "surface": path, + "detail": f"was: {old_settings[path]}", + "severity": "medium", + "reason": ( + f"Setting '{path}' was removed or renamed — update the " + "all-settings reference" + ), + }) + for path in sorted(set(old_settings) & set(new_settings)): + if old_settings[path] == new_settings[path]: + continue + now_user_facing = new_settings[path] in ("always_on", "ga", "preview") + findings.append({ + "change": "setting_status_changed", + "surface": path, + "detail": f"{old_settings[path]} -> {new_settings[path]}", + "severity": "medium" if now_user_facing else "low", + "reason": ( + f"Setting '{path}' moved {old_settings[path]} -> " + f"{new_settings[path]}" + + (" — verify the all-settings reference documents it" + if now_user_facing else "") + ), + }) + + # Bundled skills (dict field: name -> channel). + if "bundled_skills" not in old: findings.append({ - "change": "slash_removed", - "surface": name, - "severity": "medium", + "change": "surface_type_added", + "surface": "bundled_skills", + "severity": "low", "reason": ( - f"Slash command '{name}' was removed — update the slash-commands " - "docs page and surface map" + "The snapshot now tracks bundled skills — baseline established " + "this run; future runs will diff it" ), }) + else: + old_skills = old.get("bundled_skills") or {} + new_skills = new.get("bundled_skills") or {} + for name in sorted(set(new_skills) - set(old_skills)): + findings.append({ + "change": "bundled_skill_added", + "surface": name, + "detail": f"channel: {new_skills[name]}", + "severity": "medium" if new_skills[name] == "bundled" else "low", + "reason": ( + f"New bundled skill '{name}' ({new_skills[name]}) — verify " + "the skills docs cover it" + ), + }) + for name in sorted(set(old_skills) - set(new_skills)): + findings.append({ + "change": "bundled_skill_removed", + "surface": name, + "severity": "low", + "reason": ( + f"Bundled skill '{name}' was removed — verify docs no " + "longer reference it" + ), + }) + for name in sorted(set(old_skills) & set(new_skills)): + if old_skills[name] != new_skills[name]: + findings.append({ + "change": "bundled_skill_channel_changed", + "surface": name, + "detail": f"{old_skills[name]} -> {new_skills[name]}", + "severity": "medium" if new_skills[name] == "bundled" else "low", + "reason": ( + f"Bundled skill '{name}' moved channel " + f"{old_skills[name]} -> {new_skills[name]} — verify docs" + ), + }) return findings @@ -1371,12 +2215,18 @@ def changelog_review_findings(changelog_entries: list[dict], lambda i: i.get("route", "")), ("undocumented_slash_commands", "UNDOCUMENTED SLASH COMMANDS", lambda i: i.get("command", "")), + ("undocumented_settings", "UNDOCUMENTED SETTINGS", + lambda i: i.get("setting", "")), ("surface_changes", "SURFACE CHANGES SINCE SNAPSHOT", lambda i: f"{i.get('change', '')}: {i.get('surface', '')}"), ("changelog_review", "CHANGELOG ITEMS TO VERIFY", lambda i: f"{i.get('version', '')} [{i.get('category', '')}] {i.get('text', '')[:100]}"), ("map_hygiene", "SURFACE MAP HYGIENE", lambda i: f"{i.get('section', '')}: {i.get('entry', '')}"), + ("stale_doc_references", "STALE DOC REFERENCES", + lambda i: f"{i.get('kind', '')}: {i.get('reference', '')}"), + ("unlisted_pages", "PAGES MISSING FROM SIDEBAR", + lambda i: i.get("page", "")), ("potentially_stale_docs", "POTENTIALLY STALE DOCS", lambda i: i.get("doc_path", "")), ] @@ -1445,6 +2295,10 @@ def print_report(report: dict) -> None: print(f" Source: {item['source_file']}") if item.get("handler_file"): print(f" Handler: {item['handler_file']}") + if item.get("doc_page"): + print(f" Doc page: {item['doc_page']}") + if item.get("file"): + print(f" File: {item['file']}") if item.get("detail"): print(f" Detail: {item['detail']}") for t in item.get("stale_terms", []): @@ -1475,7 +2329,8 @@ def main(): ) parser.add_argument( "--category", - choices=["features", "cli", "api", "slash", "staleness", "map"], + choices=["features", "cli", "api", "slash", "settings", "structure", + "staleness", "map"], help="Run only a specific audit category", ) parser.add_argument( @@ -1543,24 +2398,56 @@ def main(): findings: dict[str, list] = {} audits_run: list[str] = [] audits_skipped: list[dict] = [] + extraction_ok = True - needs_internal = args.category in (None, "features", "cli", "slash", "staleness", "map") \ + def guard(label: str, count: int) -> bool: + nonlocal extraction_ok + floor = EXTRACTION_FLOORS.get(label, 1) + if count < floor: + extraction_ok = False + audits_skipped.append({ + "audit": f"extraction:{label}", + "reason": ( + f"only {count} {label} extracted (expected >= {floor}) — " + "the source layout likely changed; fix the parser in " + "audit_docs.py before trusting any results" + ), + }) + return False + return True + + internal_categories = ("features", "cli", "slash", "settings", "staleness", "map") + needs_internal = args.category in (None, *internal_categories) \ or args.diff or args.update_snapshot needs_server = args.category in (None, "api", "map") \ or args.diff or args.update_snapshot flag_statuses: dict[str, str] = {} cli_commands: list[dict] = [] + cli_flags: dict[str, list[str]] = {} slash_commands: list[str] = [] + settings: dict[str, dict] = {} api_routes: list[dict] = [] + web_routes: list[str] = [] + server_tools: list[str] = [] + bundled_skills: dict[str, str] = {} if warp_internal and needs_internal: print(f"Using warp-internal: {warp_internal}", file=sys.stderr) flag_statuses = compute_flag_statuses(warp_internal) cli_commands = parse_cli_commands(warp_internal) + cli_flags = parse_cli_flags(warp_internal, cli_commands) slash_commands = parse_slash_commands(warp_internal) + print("Parsing settings registry...", file=sys.stderr) + settings = parse_settings(warp_internal) + bundled_skills = parse_bundled_skills(warp_internal) + + flags_ok = guard("feature flags", len(flag_statuses)) + cli_ok = guard("CLI commands", len(cli_commands)) + slash_ok = guard("slash commands", len(slash_commands)) + settings_ok = guard("settings", len(settings)) - if args.category in (None, "features"): + if args.category in (None, "features") and flags_ok: print("Running feature flag coverage audit...", file=sys.stderr) findings["undocumented_features"] = audit_features( warp_internal, docs_root, surface_map, docs_text, @@ -1568,25 +2455,39 @@ def main(): ) audits_run.append("features") - if args.category in (None, "cli"): + if args.category in (None, "cli") and cli_ok: print("Running CLI command coverage audit...", file=sys.stderr) findings["undocumented_cli_commands"] = audit_cli( - warp_internal, docs_root, surface_map, docs_text) + warp_internal, docs_root, surface_map, docs_text, + cli_commands=cli_commands) audits_run.append("cli") - if args.category in (None, "slash"): + if args.category in (None, "slash") and slash_ok: print("Running slash command coverage audit...", file=sys.stderr) findings["undocumented_slash_commands"] = audit_slash_commands( - warp_internal, docs_root, surface_map, docs_text) + warp_internal, docs_root, surface_map, docs_text, + slash_commands=slash_commands) audits_run.append("slash") + if args.category in (None, "settings") and settings_ok and flags_ok: + print("Running settings coverage audit...", file=sys.stderr) + findings["undocumented_settings"] = audit_settings( + docs_root, surface_map, settings, flag_statuses) + audits_run.append("settings") + if args.category in (None, "staleness"): print("Running docs staleness audit...", file=sys.stderr) findings["potentially_stale_docs"] = audit_staleness( warp_internal, docs_root, docs_text) + # The reverse checks compare docs against extracted code surfaces, + # so they are only meaningful when extraction is healthy. + if flags_ok and settings_ok: + print("Running stale doc reference audit...", file=sys.stderr) + findings["stale_doc_references"] = audit_stale_doc_references( + warp_internal, docs_root, settings) audits_run.append("staleness") elif needs_internal: - for audit in ("features", "cli", "slash", "staleness"): + for audit in ("features", "cli", "slash", "settings", "staleness"): if args.category in (None, audit): audits_skipped.append({ "audit": audit, @@ -1596,10 +2497,14 @@ def main(): if warp_server and needs_server: print(f"Using warp-server: {warp_server}", file=sys.stderr) api_routes = parse_public_api_routes(warp_server) - if args.category in (None, "api"): + web_routes = parse_webapp_routes(warp_server) + server_tools = parse_server_tools(warp_server) + api_ok = guard("API routes", len(api_routes)) + if args.category in (None, "api") and api_ok: print("Running API endpoint coverage audit...", file=sys.stderr) findings["undocumented_api_endpoints"] = audit_api( - warp_server, docs_root, surface_map, docs_text) + warp_server, docs_root, surface_map, docs_text, + api_routes=api_routes) audits_run.append("api") elif needs_server: if args.category in (None, "api"): @@ -1608,27 +2513,39 @@ def main(): "reason": "warp-server repo not found (pass --warp-server)", }) + # Docs structure audit needs only the docs repo. + if args.category in (None, "structure"): + print("Running docs structure audit (sidebar coverage)...", file=sys.stderr) + findings["unlisted_pages"] = audit_unlisted_pages( + repo_root, docs_root, surface_map) + audits_run.append("structure") + if args.category in (None, "map"): - if warp_internal and warp_server: + if warp_internal and warp_server and extraction_ok: print("Running surface map hygiene audit...", file=sys.stderr) findings["map_hygiene"] = audit_map_hygiene( surface_map, flag_statuses, cli_commands, api_routes, - slash_commands, docs_root) + slash_commands, settings, docs_root) audits_run.append("map") else: audits_skipped.append({ "audit": "map", - "reason": "requires both warp-internal and warp-server", + "reason": ( + "requires both warp-internal and warp-server with healthy " + "extraction (dead-entry checks against empty extraction " + "would flag everything)" + ), }) # Change detection (diff + snapshot update) changelog_entries = parse_changelog_entries(repo_root) snapshot_path = Path(args.snapshot) if args.diff or args.update_snapshot: - if warp_internal and warp_server: + if warp_internal and warp_server and extraction_ok: current_snapshot = build_snapshot( - flag_statuses, cli_commands, api_routes, slash_commands, - changelog_entries) + flag_statuses, cli_commands, cli_flags, api_routes, + slash_commands, settings, web_routes, server_tools, + bundled_skills, changelog_entries) if args.diff: previous = load_snapshot(snapshot_path) if previous is None: @@ -1655,7 +2572,12 @@ def main(): else: audits_skipped.append({ "audit": "diff" if args.diff else "update-snapshot", - "reason": "requires both warp-internal and warp-server", + "reason": ( + "requires both warp-internal and warp-server with healthy " + "extraction (see extraction:* skips above)" + if not extraction_ok + else "requires both warp-internal and warp-server" + ), }) # Filter by severity From 3ea28b65d2864ebaa9254a497b42d7f5b3fccc08 Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Thu, 11 Jun 2026 16:45:08 +0000 Subject: [PATCH 03/15] Add completeness accounting and map integrity checks; reclassify GA flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triple-check that every feature is encapsulated in the mapping: - Built-in completeness accounting on every full run: partitions every extracted surface item (277 flags, 74 CLI commands, 71 API routes, 47 slash commands, 201 settings) into exactly one accountability bucket (mapped / ignored / doc-covered / visible finding / snapshot-tracked) and exits 2 with integrity:accounting if anything escapes — an unaccounted item can only mean the audit logic regressed - Map integrity checks in hygiene: entries in both the mapping and the ignore list (ignore silently wins) and duplicate keys within a section are now medium findings - Ignore-list review against computed statuses found ~20 flags filed under 'Non-GA' that have since GA'd: reclassified 15 user-facing ones to real mappings (session sharing trio, AgentHarness, SshRemoteServer, ArtifactCommand, OzIdentityFederation, image-context pair, OzPlatformSkills, WorkflowAliases, ShellSelector, KittyImages, UndoClosedPanes, RevertDiffHunk), surfaced FullScreenZenMode as a visible undocumented-feature finding, and retitled the section so placement no longer asserts rollout status - Re-baselined the snapshot after live drift the diff caught on today's checkouts (SuperGrok dogfood->ga, new /rename-conversation slash command — both now standing coverage findings) - SKILL.md: documented the accounting contract and the end-to-end 'how every change path is caught' chain (new/promoted/removed surfaces, no-code-change launches via changelog net, parser rot via extraction guards, map rot via hygiene) Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 45 +++ .../references/surface_snapshot.json | 3 +- .../skills/missing_docs/scripts/audit_docs.py | 279 +++++++++++++++++- 3 files changed, 313 insertions(+), 14 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 3fe1106a2..aedec623f 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -108,6 +108,51 @@ Adjacent checks owned by other skills (do not duplicate them here): - Broken links and 404s/redirects → `check_for_broken_links` / `weekly-404-monitor` - Terminology/style sweeps → `style_lint` +### Completeness accounting (the no-slip guarantee) + +Every full run computes a completeness accounting and embeds it in the report +(`summary.accounting` in JSON, a `COMPLETENESS ACCOUNTING` block in the printed +output). It partitions every extracted surface item into exactly one +accountability bucket and proves totality: +- **Feature flags**: every GA/Preview flag is `mapped` (surface map verified), + `ignored` (curated internal list), or a visible `finding`; every dogfood/other + flag is `tracked_non_ga` (snapshot diff fires on promotion or removal). +- **CLI commands**: `mapped`, `doc_covered`, `finding`, `parent_flagged` + (suppressed because the parent command is already flagged), or `hidden`. +- **API routes**: `mapped`, `spec_covered`, `docs_covered`, or `finding`. +- **Slash commands**: `mapped`, `doc_covered`, or `finding`. +- **Settings**: `private`, `tracked_non_ga`, `mapped`, `doc_covered`, or `finding`. + +If any item escapes every bucket, the run reports `integrity:accounting` in +`audits_skipped` and exits 2 — an unaccounted item means the audit logic itself +regressed, never that the item is fine. Map hygiene additionally rejects +integrity bugs in the surface map: entries that are both mapped and ignored +(the ignore silently wins) and duplicate keys within a section. + +How every change path is caught, end to end: +1. **New surface item appears** (flag, command, route, slash, setting, web + route, tool, bundled skill) → the snapshot `--diff` reports it AND, once + GA/user-facing, the coverage audit produces a standing finding until it is + documented + mapped or ignored with a comment. +2. **Item is promoted** (dogfood→preview→ga, setting status change, skill + channel change) → `--diff` status-change finding + coverage finding appears. +3. **Item is removed/renamed** → `--diff` removal finding + map hygiene flags + the dead map entry + stale-doc-reference checks flag docs still naming it. +4. **Launch with no client-code change** (server-side experiment flips to 100%, + Oz web app backend feature) → the changelog cross-check is the net: every + "New features"/"Improvements"/"Oz updates" bullet newer than the snapshot + becomes a verification finding. +5. **The audit itself rots** (source layout moves, parser breaks) → extraction + sanity guards trip, dependent audits skip, exit 2. +6. **The map rots** (dead entries, conflicts, duplicates, missing doc targets, + unmapped-but-mentioned features) → map hygiene + fallback-transparency + findings keep pressure until fixed. + +The mapping is updated through three enforced paths: Phase 3 step 8 makes the +map+snapshot update a mandatory part of drafting; the drift-watch triage step +requires a mapping/ignore/allowlist decision for every finding; and map hygiene +findings force pruning when code moves underneath the map. + ### Phase 2: Change detection (diff mode) The snapshot at `references/surface_snapshot.json` records all extracted surfaces diff --git a/.agents/skills/missing_docs/references/surface_snapshot.json b/.agents/skills/missing_docs/references/surface_snapshot.json index 71d9abb32..46d4a0cd0 100644 --- a/.agents/skills/missing_docs/references/surface_snapshot.json +++ b/.agents/skills/missing_docs/references/surface_snapshot.json @@ -247,7 +247,7 @@ "SummarizationCancellationConfirmation": "ga", "SummarizationConversationCommand": "ga", "SummarizationViaMessageReplacement": "dogfood", - "SuperGrok": "dogfood", + "SuperGrok": "ga", "SyncAmbientPlans": "ga", "TabCloseButtonOnLeft": "ga", "TabConfigs": "ga", @@ -822,6 +822,7 @@ "/prompts", "/queue", "/remote-control", + "/rename-conversation", "/rename-tab", "/rewind", "/set-tab-color", diff --git a/.agents/skills/missing_docs/scripts/audit_docs.py b/.agents/skills/missing_docs/scripts/audit_docs.py index 928cd4587..b7149d093 100755 --- a/.agents/skills/missing_docs/scripts/audit_docs.py +++ b/.agents/skills/missing_docs/scripts/audit_docs.py @@ -79,7 +79,11 @@ # --------------------------------------------------------------------------- def parse_surface_map(path: Path) -> dict: - """Parse the feature_surface_map.md into structured data.""" + """Parse the feature_surface_map.md into structured data. + + Duplicate keys within a section are recorded in `duplicates` so map + hygiene can flag them (the last occurrence silently wins otherwise). + """ result = { "feature_to_doc": {}, "cli_to_doc": {}, @@ -88,6 +92,7 @@ def parse_surface_map(path: Path) -> dict: "settings_to_doc": {}, "ignore_flags": set(), "unlisted_ignore": set(), + "duplicates": [], } if not path.exists(): return result @@ -113,9 +118,13 @@ def parse_surface_map(path: Path) -> dict: continue if current_section == "ignore": + if line in result["ignore_flags"]: + result["duplicates"].append(("Flags to ignore", line)) result["ignore_flags"].add(line) continue if current_section == "unlisted": + if line in result["unlisted_ignore"]: + result["duplicates"].append(("Unlisted docs pages", line)) result["unlisted_ignore"].add(line) continue @@ -123,16 +132,18 @@ def parse_surface_map(path: Path) -> dict: key, doc_path = line.split(" -> ", 1) key = key.strip() doc_path = doc_path.strip() - if current_section == "features": - result["feature_to_doc"][key] = doc_path - elif current_section == "cli": - result["cli_to_doc"][key] = doc_path - elif current_section == "api": - result["api_to_doc"][key] = doc_path - elif current_section == "slash": - result["slash_to_doc"][key] = doc_path - elif current_section == "settings": - result["settings_to_doc"][key] = doc_path + section_targets = { + "features": ("Feature flags", result["feature_to_doc"]), + "cli": ("CLI commands", result["cli_to_doc"]), + "api": ("API endpoints", result["api_to_doc"]), + "slash": ("Slash commands", result["slash_to_doc"]), + "settings": ("Settings", result["settings_to_doc"]), + } + if current_section in section_targets: + section_name, mapping = section_targets[current_section] + if key in mapping: + result["duplicates"].append((section_name, key)) + mapping[key] = doc_path return result @@ -1742,6 +1753,31 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent known_flags = set(flag_statuses) + # Map integrity: a flag must not be both mapped and ignored (the audit + # checks the ignore list first, so the mapping would silently lose). + for flag in sorted(set(surface_map.get("feature_to_doc", {})) + & surface_map.get("ignore_flags", set())): + findings.append({ + "entry": flag, + "section": "Feature flags + Flags to ignore", + "severity": "medium", + "reason": ( + f"'{flag}' appears in BOTH the feature mapping and the ignore " + "list — the ignore entry wins silently; remove one" + ), + }) + # Map integrity: duplicate keys within a section (last occurrence wins). + for section_name, key in surface_map.get("duplicates", []): + findings.append({ + "entry": key, + "section": section_name, + "severity": "medium", + "reason": ( + f"Duplicate entry '{key}' in the {section_name} section — the " + "last occurrence silently wins; remove the extra line" + ), + }) + for flag in sorted(surface_map.get("feature_to_doc", {})): if flag not in known_flags: findings.append({ @@ -2202,6 +2238,183 @@ def changelog_review_findings(changelog_entries: list[dict], }) return findings +# --------------------------------------------------------------------------- +# Completeness accounting +# --------------------------------------------------------------------------- + +def compute_accounting(docs_root: Path, surface_map: dict, findings: dict, + flag_statuses: dict[str, str], cli_commands: list[dict], + api_routes: list[dict], slash_commands: list[str], + settings: dict[str, dict], + docs_text: dict[str, str]) -> dict: + """Partition every extracted surface item into exactly one accountability + bucket and prove totality. + + Every item must be mapped, ignored, covered by docs, a visible finding, or + snapshot-tracked (non-GA). `unaccounted` lists anything that escapes all + buckets — it must be empty; a non-empty list means the audit logic + regressed and the run is treated as incomplete (exit 2). + """ + repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent + acc: dict = {} + unaccounted: dict[str, list[str]] = {} + + # Feature flags --------------------------------------------------------- + mapped = set(surface_map.get("feature_to_doc", {})) + ignored = surface_map.get("ignore_flags", set()) + flag_findings = {f.get("flag") for f in findings.get("undocumented_features", [])} + fb = {"total": len(flag_statuses), "ga_preview": 0, "ignored": 0, + "mapped": 0, "finding": 0, "tracked_non_ga": 0} + missing = [] + for flag, status in flag_statuses.items(): + if status not in ("ga", "preview"): + fb["tracked_non_ga"] += 1 + continue + fb["ga_preview"] += 1 + if flag in ignored: + fb["ignored"] += 1 + elif flag in mapped: + fb["mapped"] += 1 + elif flag in flag_findings: + fb["finding"] += 1 + else: + missing.append(flag) + if missing: + unaccounted["feature_flags"] = missing + acc["feature_flags"] = fb + + # CLI commands ----------------------------------------------------------- + cli_map = surface_map.get("cli_to_doc", {}) + cli_findings = {f.get("command") for f in findings.get("undocumented_cli_commands", [])} + cli_text = {} + cli_docs_dir = docs_root / "reference" / "cli" + if cli_docs_dir.exists(): + for f in find_markdown_files(cli_docs_dir): + try: + cli_text[str(f)] = f.read_text(encoding="utf-8").lower() + except Exception: + pass + cb = {"total": 0, "hidden": 0, "mapped": 0, "doc_covered": 0, + "finding": 0, "parent_flagged": 0} + missing = [] + for cmd in cli_commands: + entries = [(cmd["command"], cmd["hidden"], None)] + [ + (s["command"], s["hidden"], cmd["command"]) for s in cmd["subcommands"]] + for name, hidden, parent in entries: + cb["total"] += 1 + if hidden: + cb["hidden"] += 1 + elif name in cli_map: + cb["mapped"] += 1 + elif any(name.split(" ", 1)[1] in t for t in cli_text.values()): + cb["doc_covered"] += 1 + elif name in cli_findings: + cb["finding"] += 1 + elif parent in cli_findings: + cb["parent_flagged"] += 1 + else: + missing.append(name) + if missing: + unaccounted["cli_commands"] = missing + acc["cli_commands"] = cb + + # API routes ------------------------------------------------------------- + api_map = surface_map.get("api_to_doc", {}) + api_findings = {f.get("route") for f in findings.get("undocumented_api_endpoints", [])} + openapi_candidates = [ + repo_root / "developers" / "agent-api-openapi.yaml", + docs_root / "developers" / "agent-api-openapi.yaml", + ] + openapi_path = next((c for c in openapi_candidates if c.exists()), openapi_candidates[0]) + openapi_text = "" + if openapi_path.exists(): + try: + openapi_text = openapi_path.read_text(encoding="utf-8").lower() + except Exception: + pass + spec_paths = parse_openapi_paths(openapi_text) + api_docs_text = {} + api_docs_dir = docs_root / "reference" / "api-and-sdk" + if api_docs_dir.exists(): + for f in find_markdown_files(api_docs_dir): + try: + api_docs_text[str(f)] = f.read_text(encoding="utf-8").lower() + except Exception: + pass + ab = {"total": len(api_routes), "mapped": 0, "spec_covered": 0, + "docs_covered": 0, "finding": 0} + missing = [] + for route in api_routes: + rel = route["path"] + if rel.startswith("/api/v1"): + rel = rel[len("/api/v1"):] or "/" + rel_str = f"{route['method']} {rel}" + if route["route"] in api_map or rel_str in api_map: + ab["mapped"] += 1 + elif (_normalize_path_params(rel) in spec_paths + or _normalize_path_params(route["path"]) in spec_paths): + ab["spec_covered"] += 1 + elif any(c in openapi_text or any(c in t for t in api_docs_text.values()) + for c in {route["path"].lower(), rel.lower()}): + ab["docs_covered"] += 1 + elif rel_str in api_findings: + ab["finding"] += 1 + else: + missing.append(rel_str) + if missing: + unaccounted["api_routes"] = missing + acc["api_routes"] = ab + + # Slash commands ---------------------------------------------------------- + slash_map = surface_map.get("slash_to_doc", {}) + slash_findings = {f.get("command") for f in findings.get("undocumented_slash_commands", [])} + sb = {"total": len(slash_commands), "mapped": 0, "doc_covered": 0, "finding": 0} + missing = [] + for name in slash_commands: + if name in slash_map: + sb["mapped"] += 1 + elif any(_slash_mention_re(name).search(t) for t in docs_text.values()): + sb["doc_covered"] += 1 + elif name in slash_findings: + sb["finding"] += 1 + else: + missing.append(name) + if missing: + unaccounted["slash_commands"] = missing + acc["slash_commands"] = sb + + # Settings ---------------------------------------------------------------- + settings_map = surface_map.get("settings_to_doc", {}) + setting_findings = {f.get("setting") for f in findings.get("undocumented_settings", [])} + doc_sections, _ = parse_settings_doc(docs_root) + tb = {"total": len(settings), "private": 0, "tracked_non_ga": 0, + "mapped": 0, "doc_covered": 0, "finding": 0} + missing = [] + for path, info in settings.items(): + status = setting_status(info, flag_statuses) + if status == "private": + tb["private"] += 1 + continue + if status in ("dogfood", "other"): + tb["tracked_non_ga"] += 1 + continue + hierarchy, _, key = path.rpartition(".") + if path in settings_map: + tb["mapped"] += 1 + elif (key in doc_sections.get(hierarchy, set()) or path in doc_sections + or any(s.startswith(path + ".") for s in doc_sections)): + tb["doc_covered"] += 1 + elif path in setting_findings: + tb["finding"] += 1 + else: + missing.append(path) + if missing: + unaccounted["settings"] = missing + acc["settings"] = tb + + acc["unaccounted"] = unaccounted + return acc + # --------------------------------------------------------------------------- # Report generation # --------------------------------------------------------------------------- @@ -2233,7 +2446,8 @@ def changelog_review_findings(changelog_entries: list[dict], def generate_report(findings_by_category: dict[str, list], audits_run: list[str], - audits_skipped: list[dict], mode: str) -> dict: + audits_skipped: list[dict], mode: str, + accounting: dict | None = None) -> dict: """Assemble the full audit report.""" total = sum(len(v) for v in findings_by_category.values()) report = { @@ -2248,6 +2462,8 @@ def generate_report(findings_by_category: dict[str, list], audits_run: list[str] }, }, } + if accounting is not None: + report["summary"]["accounting"] = accounting for key, _, _ in REPORT_CATEGORIES: report[key] = findings_by_category.get(key, []) return report @@ -2271,6 +2487,24 @@ def print_report(report: dict) -> None: print(f" {category}: {count}") print() + accounting = summary.get("accounting") + if accounting: + print("-" * 60) + print("COMPLETENESS ACCOUNTING (every item in exactly one bucket)") + print("-" * 60) + for surface, buckets in accounting.items(): + if surface == "unaccounted": + continue + parts = ", ".join(f"{k}={v}" for k, v in buckets.items()) + print(f" {surface}: {parts}") + if accounting.get("unaccounted"): + print(" !! UNACCOUNTED ITEMS (audit logic regression):") + for surface, items in accounting["unaccounted"].items(): + print(f" {surface}: {items}") + else: + print(" unaccounted: none — every extracted surface item is accounted for") + print() + severity_order = {"high": 0, "medium": 1, "low": 2} for key, title, describe in REPORT_CATEGORIES: @@ -2580,6 +2814,24 @@ def guard(label: str, count: int) -> bool: ), }) + # Completeness accounting: prove every extracted surface item lands in + # exactly one accountability bucket. Runs on full audits with healthy + # extraction; any unaccounted item means an audit-logic regression and + # the run is treated as incomplete. + accounting = None + if args.category is None and warp_internal and warp_server and extraction_ok: + accounting = compute_accounting( + docs_root, surface_map, findings, flag_statuses, cli_commands, + api_routes, slash_commands, settings, docs_text) + if accounting["unaccounted"]: + audits_skipped.append({ + "audit": "integrity:accounting", + "reason": ( + "surface items escaped every accountability bucket " + f"(audit logic regression): {accounting['unaccounted']}" + ), + }) + # Filter by severity if args.severity: severity_order = {"high": 0, "medium": 1, "low": 2} @@ -2591,7 +2843,8 @@ def guard(label: str, count: int) -> bool: ] mode = "diff" if args.diff else "audit" - report = generate_report(findings, audits_run, audits_skipped, mode) + report = generate_report(findings, audits_run, audits_skipped, mode, + accounting=accounting) print_report(report) From c6710671858041a1523620f63f7068af839673df Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Thu, 11 Jun 2026 20:56:23 +0000 Subject: [PATCH 04/15] Target the public warp repo instead of warp-internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warp's client code is open source at warpdotdev/warp — the audit now treats the public repo as the primary source: - Repo auto-detection prefers a sibling checkout named 'warp' and falls back to 'warp-internal' for transitional environments - New --warp flag is the primary CLI option; --warp-internal remains as a deprecated alias (same destination) so existing invocations keep working - All docstrings, stderr messages, skip reasons, section headers, and SKILL.md guidance (requirements, audit descriptions, drift-watch command, scheduled-agent prompt) now reference the public warp client repo Validated: auto-detect fallback, preferred 'warp' sibling resolution, explicit --warp, and the deprecated alias all run the full audit with clean completeness accounting. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 29 +-- .../skills/missing_docs/scripts/audit_docs.py | 177 ++++++++++-------- 2 files changed, 109 insertions(+), 97 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index aedec623f..ea25c4480 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -2,7 +2,7 @@ name: missing_docs description: >- Find and fill documentation gaps in Warp's Astro Starlight docs by auditing coverage - against code surfaces in warp-internal and warp-server, then drafting missing + against code surfaces in the public warp client repo and warp-server, then drafting missing pages. Use when asked to find missing docs, audit documentation coverage, identify undocumented features, draft docs for new features, detect doc-impacting code changes since the last audit, or do a docs coverage check. Runs a Python @@ -18,10 +18,11 @@ Find documentation gaps, detect doc-impacting code changes, and draft missing pa ## Requirements The audit compares docs against code, so both source repos must be available: -- `warp-internal` and `warp-server`, auto-detected as siblings of the docs repo - root (e.g. `/workspace/docs` next to `/workspace/warp-internal` and - `/workspace/warp-server`), or passed explicitly via `--warp-internal PATH` / - `--warp-server PATH`. +- the public warp client repo ([warpdotdev/warp](https://github.com/warpdotdev/warp)) + and `warp-server`, auto-detected as siblings of the docs repo root (e.g. + `/workspace/docs` next to `/workspace/warp` and `/workspace/warp-server`; a + sibling named `warp-internal` is accepted as a fallback), or passed explicitly + via `--warp PATH` / `--warp-server PATH` (`--warp-internal` is a deprecated alias). The script FAILS LOUD when a repo is missing OR when an extraction sanity guard trips (a parser returning implausibly few surfaces means the source layout @@ -45,7 +46,7 @@ Options: - `--severity high|medium|low` — filter by minimum severity - `--weak-coverage` — also flag GA features whose mapped doc exists but doesn't mention feature keywords (low-severity, noisy) - `--output report.json` — save JSON report to file -- `--warp-internal PATH` / `--warp-server PATH` — explicit repo paths +- `--warp PATH` / `--warp-server PATH` — explicit repo paths (`--warp-internal` is a deprecated alias) - `--diff` — change detection against the committed snapshot (see Phase 2) - `--update-snapshot` — regenerate `references/surface_snapshot.json` (full runs only) @@ -55,7 +56,7 @@ canonical filename even when the on-disk extension differs. The script performs these coverage audits: 1. **Feature flag coverage** — classifies every `FeatureFlag` by rollout status using - the cargo-feature→flag bridge in warp-internal `app/src/features.rs` plus + the cargo-feature→flag bridge in the warp client repo's `app/src/features.rs` plus `RELEASE_FLAGS`/`PREVIEW_FLAGS`/`DOGFOOD_FLAGS` in `crates/warp_features/src/lib.rs`. GA flags must be mapped in the surface map or covered in docs; Preview flags produce low-severity "docs needed soon" findings; dogfood/other flags are tracked by the @@ -71,11 +72,11 @@ The script performs these coverage audits: `{run_id}`) and the API reference docs. For spec drift, run the docs `sync-openapi-spec` skill (or warp-server's `update-open-api-spec`) instead of hand-editing the YAML. -4. **Slash command coverage** — parses the static registry in warp-internal +4. **Slash command coverage** — parses the static registry in the warp client repo's `app/src/search/slash_command_menu/static_commands/` and checks each `/command` is mentioned in docs. 5. **Settings coverage** — parses every `toml_path: "section.key"` setting - registration in warp-internal (the same registry the JSON-schema generator uses) + registration in the warp client repo (the same registry the JSON-schema generator uses) and checks the all-settings reference page documents it. Private and dogfood/other-flagged settings are exempt; object-typed settings documented as their own `[section]` count as covered. @@ -86,7 +87,7 @@ The script performs these coverage audits: 7. **Stale doc references** — reverse checks: settings keys documented in all-settings.mdx that no longer exist in code (catches renames like `agents.oz.*` → `agents.warp_agent.*`), and keybinding actions (`scope:action`) - on the keyboard-shortcuts page that no longer exist anywhere in warp-internal. + on the keyboard-shortcuts page that no longer exist anywhere in the warp client repo. 8. **Docs structure** — pages on disk that are missing from `src/sidebar.ts` (built but unreachable through navigation). Intentionally unlisted pages go in the surface map's "Unlisted docs pages" section. @@ -159,7 +160,7 @@ The snapshot at `references/surface_snapshot.json` records all extracted surface (flags + rollout status, CLI commands and per-module flags, API routes, slash commands, settings + status, Oz web app routes, server-side agent tools, bundled skills) plus the last-seen docs-changelog version. It makes change detection -possible: a feature flag that is deleted after stabilizing (per warp-internal's +possible: a feature flag that is deleted after stabilizing (per the warp repo's remove-feature-flag policy) would otherwise vanish from the audit's universe silently. When a new surface type is introduced, diffing against an older snapshot emits a one-time "surface type newly tracked" note instead of false positives. @@ -196,7 +197,7 @@ For each gap to address (prioritize high → medium → low): 2. Read `AGENTS.md` in the docs repo root for the complete style guide 3. Read 2-3 strong examples in the target section to match formatting patterns 4. Research the relevant source code: - - **Feature gaps** → read the implementation in warp-internal `app/src/`, check UI code, settings, user-facing strings + - **Feature gaps** → read the implementation in the warp client repo's `app/src/`, check UI code, settings, user-facing strings - **CLI gaps** → read command definition in `crates/warp_cli/src/`, extract flags, arguments, help text - **API gaps** → read handler in warp-server `router/handlers/public_api/`, route definition, request/response types; prefer fixing the OpenAPI spec via the `sync-openapi-spec` skill - **Slash command gaps** → read the registry entry and gating flags in `app/src/search/slash_command_menu/` @@ -226,7 +227,7 @@ with the product. Each run: instead of concluding "no gaps": ```bash python3 .agents/skills/missing_docs/scripts/audit_docs.py \ - --warp-internal ../warp-internal --warp-server ../warp-server \ + --warp ../warp --warp-server ../warp-server \ --diff --output /tmp/docs_audit.json ``` 2. **Triage**: work through `surface_changes` and `changelog_review` first (what @@ -250,7 +251,7 @@ with the product. Each run: Recommended scheduled-agent prompt (copy when setting up the agent): > Run the missing_docs skill in drift-watch mode. Use the audit script with explicit -> --warp-internal and --warp-server paths and --diff. If the script exits non-zero with +> --warp (public warpdotdev/warp checkout) and --warp-server paths and --diff. If the script exits non-zero with > skipped audits, report the environment problem and stop. Otherwise triage all > surface_changes and changelog_review findings plus high/medium coverage findings: > draft or update doc pages, update the surface map (mapping or ignore entry with a diff --git a/.agents/skills/missing_docs/scripts/audit_docs.py b/.agents/skills/missing_docs/scripts/audit_docs.py index b7149d093..a3f7e4b17 100755 --- a/.agents/skills/missing_docs/scripts/audit_docs.py +++ b/.agents/skills/missing_docs/scripts/audit_docs.py @@ -2,8 +2,9 @@ """ Missing Docs Audit Script for Warp Astro Starlight Documentation -Compares documentation coverage against code surfaces in warp-internal and -warp-server to identify gaps, and (in --diff mode) detects surface changes +Compares documentation coverage against code surfaces in the warp client +repo (the public warpdotdev/warp checkout; a warp-internal checkout also +works) and warp-server to identify gaps, and (in --diff mode) detects surface changes since the last committed snapshot. Produces a structured JSON report. Audited surfaces: @@ -167,10 +168,12 @@ def parse_stale_terms(path: Path) -> list[tuple[str, str]]: # Generic helpers # --------------------------------------------------------------------------- -def find_repo(name: str, explicit_path: str | None, repo_root: Path) -> Path | None: +def find_repo(names: list[str], explicit_path: str | None, repo_root: Path) -> Path | None: """Find a source repo by explicit path or as a sibling of the docs repo root. - e.g. docs at /workspace/docs -> look for /workspace/. + Candidate names are tried in order, e.g. docs at /workspace/docs with + names ["warp", "warp-internal"] -> prefer /workspace/warp (the public + warpdotdev/warp checkout) and fall back to /workspace/warp-internal. """ if explicit_path: p = Path(explicit_path).resolve() @@ -179,9 +182,10 @@ def find_repo(name: str, explicit_path: str | None, repo_root: Path) -> Path | N print(f"Warning: explicit path {explicit_path} does not exist", file=sys.stderr) return None - sibling = repo_root.parent / name - if sibling.exists(): - return sibling + for name in names: + sibling = repo_root.parent / name + if sibling.exists(): + return sibling return None @@ -409,24 +413,24 @@ def _iter_attr_blocks(content: str, names: tuple[str, ...]): yield content[match.start():i] # --------------------------------------------------------------------------- -# Extraction: feature flags (warp-internal) +# Extraction: feature flags (warp client repo) # --------------------------------------------------------------------------- -def _features_lib_rs(warp_internal: Path) -> Path | None: +def _features_lib_rs(warp_repo: Path) -> Path | None: candidates = [ - warp_internal / "crates" / "warp_features" / "src" / "lib.rs", - warp_internal / "crates" / "warp_core" / "src" / "features.rs", - warp_internal / "app" / "src" / "features.rs", - warp_internal / "warp_core" / "src" / "features.rs", + warp_repo / "crates" / "warp_features" / "src" / "lib.rs", + warp_repo / "crates" / "warp_core" / "src" / "features.rs", + warp_repo / "app" / "src" / "features.rs", + warp_repo / "warp_core" / "src" / "features.rs", ] return next((c for c in candidates if c.exists()), None) -def parse_feature_flags(warp_internal: Path) -> list[str]: +def parse_feature_flags(warp_repo: Path) -> list[str]: """Parse FeatureFlag enum variants from the features lib (brace-safe).""" - features_rs = _features_lib_rs(warp_internal) + features_rs = _features_lib_rs(warp_repo) if features_rs is None: - print("Warning: FeatureFlag enum source not found in warp-internal", file=sys.stderr) + print("Warning: FeatureFlag enum source not found in the warp client repo", file=sys.stderr) return [] enum_body = _extract_enum_block(features_rs.read_text(), "FeatureFlag") @@ -436,9 +440,9 @@ def parse_feature_flags(warp_internal: Path) -> list[str]: return [v["name"] for v in _parse_enum_variants(enum_body)] -def parse_flag_list_const(warp_internal: Path, const_name: str) -> set[str]: +def parse_flag_list_const(warp_repo: Path, const_name: str) -> set[str]: """Parse a `pub const : &[FeatureFlag] = &[...]` block into flag names.""" - features_rs = _features_lib_rs(warp_internal) + features_rs = _features_lib_rs(warp_repo) if features_rs is None: return set() content = features_rs.read_text() @@ -452,8 +456,8 @@ def parse_flag_list_const(warp_internal: Path, const_name: str) -> set[str]: return set(re.findall(r"FeatureFlag::(\w+)", match.group(1))) -def parse_features_bridge(warp_internal: Path) -> dict[str, dict]: - """Parse the cargo-feature -> FeatureFlag bridge from app/src/features.rs. +def parse_features_bridge(warp_repo: Path) -> dict[str, dict]: + """Parse the cargo-feature -> FeatureFlag bridge from the warp client repo\'s app/src/features.rs. The authoritative mapping is the `enabled_features()` extend block: @@ -466,7 +470,7 @@ def parse_features_bridge(warp_internal: Path) -> dict[str, dict]: Returns {flag_name: {"cargo_feature": str, "debug_only": bool}}. """ - bridge_rs = warp_internal / "app" / "src" / "features.rs" + bridge_rs = warp_repo / "app" / "src" / "features.rs" if not bridge_rs.exists(): print(f"Warning: {bridge_rs} not found; GA detection will be incomplete", file=sys.stderr) @@ -486,11 +490,11 @@ def parse_features_bridge(warp_internal: Path) -> dict[str, dict]: return bridge -def parse_default_features(warp_internal: Path) -> set[str]: +def parse_default_features(warp_repo: Path) -> set[str]: """Parse the default feature list from app/Cargo.toml.""" candidates = [ - warp_internal / "app" / "Cargo.toml", - warp_internal / "crates" / "warp_features" / "Cargo.toml", + warp_repo / "app" / "Cargo.toml", + warp_repo / "crates" / "warp_features" / "Cargo.toml", ] cargo_toml = next((c for c in candidates if c.exists()), None) if cargo_toml is None: @@ -507,7 +511,7 @@ def parse_default_features(warp_internal: Path) -> set[str]: return set(re.findall(r'"(\w+)"', features_block)) -def compute_flag_statuses(warp_internal: Path) -> dict[str, str]: +def compute_flag_statuses(warp_repo: Path) -> dict[str, str]: """Classify every FeatureFlag by rollout status. - "ga": gating cargo feature is in app/Cargo.toml default features, or the @@ -518,12 +522,12 @@ def compute_flag_statuses(warp_internal: Path) -> dict[str, str]: may still be enabled via server-side experiments; the docs changelog cross-check covers those launches. """ - flags = parse_feature_flags(warp_internal) - bridge = parse_features_bridge(warp_internal) - default_features = parse_default_features(warp_internal) - release_flags = parse_flag_list_const(warp_internal, "RELEASE_FLAGS") - preview_flags = parse_flag_list_const(warp_internal, "PREVIEW_FLAGS") - dogfood_flags = parse_flag_list_const(warp_internal, "DOGFOOD_FLAGS") + flags = parse_feature_flags(warp_repo) + bridge = parse_features_bridge(warp_repo) + default_features = parse_default_features(warp_repo) + release_flags = parse_flag_list_const(warp_repo, "RELEASE_FLAGS") + preview_flags = parse_flag_list_const(warp_repo, "PREVIEW_FLAGS") + dogfood_flags = parse_flag_list_const(warp_repo, "DOGFOOD_FLAGS") statuses: dict[str, str] = {} for flag in flags: @@ -542,7 +546,7 @@ def compute_flag_statuses(warp_internal: Path) -> dict[str, str]: return statuses # --------------------------------------------------------------------------- -# Extraction: CLI command tree + flags (warp-internal) +# Extraction: CLI command tree + flags (warp client repo) # --------------------------------------------------------------------------- def _resolve_subcommand_enum(module_content: str, referenced_type: str | None) -> str | None: @@ -590,24 +594,24 @@ def _collect_subcommands(src_dir: Path, module_content: str, enum_body: str, return subs -def _cli_src_dir(warp_internal: Path) -> Path | None: +def _cli_src_dir(warp_repo: Path) -> Path | None: candidates = [ - warp_internal / "crates" / "warp_cli" / "src", - warp_internal / "warp_cli" / "src", + warp_repo / "crates" / "warp_cli" / "src", + warp_repo / "warp_cli" / "src", ] return next((c for c in candidates if c.exists()), None) -def parse_cli_commands(warp_internal: Path) -> list[dict]: +def parse_cli_commands(warp_repo: Path) -> list[dict]: """Parse the full `oz` CLI command tree (recursive subcommands). Returns [{"command": "oz agent", "hidden": bool, "source_file": str, "module": str|None, "subcommands": [{"command": "oz agent run", "hidden": bool}]}] """ - src_dir = _cli_src_dir(warp_internal) + src_dir = _cli_src_dir(warp_repo) if src_dir is None: - print("Warning: warp_cli/src not found in warp-internal", file=sys.stderr) + print("Warning: warp_cli/src not found in the warp client repo", file=sys.stderr) return [] lib_rs = src_dir / "lib.rs" @@ -648,7 +652,7 @@ def parse_cli_commands(warp_internal: Path) -> list[dict]: return commands -def parse_cli_flags(warp_internal: Path, cli_commands: list[dict]) -> dict[str, list[str]]: +def parse_cli_flags(warp_repo: Path, cli_commands: list[dict]) -> dict[str, list[str]]: """Extract visible `--long` flags per CLI module for change tracking. Attribution of flags to specific subcommands would require full clap @@ -656,7 +660,7 @@ def parse_cli_flags(warp_internal: Path, cli_commands: list[dict]) -> dict[str, flag was added or removed (the drift agent then reads the module to see which command it belongs to). """ - src_dir = _cli_src_dir(warp_internal) + src_dir = _cli_src_dir(warp_repo) if src_dir is None: return {} @@ -924,13 +928,13 @@ def parse_openapi_paths(openapi_text: str) -> set[str]: return paths # --------------------------------------------------------------------------- -# Extraction: slash commands (warp-internal) +# Extraction: slash commands (warp client repo) # --------------------------------------------------------------------------- -def parse_slash_commands(warp_internal: Path) -> list[str]: +def parse_slash_commands(warp_repo: Path) -> list[str]: """Parse static slash command names from the registry.""" registry_dir = ( - warp_internal / "app" / "src" / "search" / "slash_command_menu" / "static_commands" + warp_repo / "app" / "src" / "search" / "slash_command_menu" / "static_commands" ) if not registry_dir.exists(): print(f"Warning: {registry_dir} not found", file=sys.stderr) @@ -945,7 +949,7 @@ def parse_slash_commands(warp_internal: Path) -> list[str]: return sorted(names) # --------------------------------------------------------------------------- -# Extraction: settings (warp-internal) +# Extraction: settings (warp client repo) # --------------------------------------------------------------------------- _SETTING_TOML_PATH_RE = re.compile(r'toml_path:\s*"([^"]+)"') @@ -955,7 +959,7 @@ def _is_test_rs(path: Path) -> bool: return path.name.endswith("_tests.rs") or path.name == "tests.rs" or "/tests/" in str(path) -def parse_settings(warp_internal: Path) -> dict[str, dict]: +def parse_settings(warp_repo: Path) -> dict[str, dict]: """Parse user-facing settings from `define_setting!`-style registrations. Every settings.toml-backed setting declares `toml_path: "section.key"` @@ -966,7 +970,7 @@ def parse_settings(warp_internal: Path) -> dict[str, dict]: Returns {toml_path: {"private": bool, "feature_flag": str|None}}. """ settings: dict[str, dict] = {} - roots = [warp_internal / "app" / "src", warp_internal / "crates"] + roots = [warp_repo / "app" / "src", warp_repo / "crates"] for rs_file in iter_source_files(roots, ".rs"): if _is_test_rs(rs_file): continue @@ -1076,19 +1080,19 @@ def parse_server_tools(warp_server: Path) -> list[str]: return sorted(names) -def parse_bundled_skills(warp_internal: Path) -> dict[str, str]: +def parse_bundled_skills(warp_repo: Path) -> dict[str, str]: """List bundled skills shipped with the client, keyed by channel gating. resources/bundled/skills/ ships on all channels ("bundled"); resources/channel-gated-skills// ships per channel. """ skills: dict[str, str] = {} - bundled = warp_internal / "resources" / "bundled" / "skills" + bundled = warp_repo / "resources" / "bundled" / "skills" if bundled.exists(): for entry in sorted(bundled.iterdir()): if entry.is_dir(): skills[entry.name] = "bundled" - gated = warp_internal / "resources" / "channel-gated-skills" + gated = warp_repo / "resources" / "channel-gated-skills" if gated.exists(): for channel_dir in sorted(gated.iterdir()): if not channel_dir.is_dir(): @@ -1200,7 +1204,7 @@ def parse_changelog_entries(repo_root: Path) -> list[dict]: # Audit 1: Feature flag coverage # --------------------------------------------------------------------------- -def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, +def audit_features(warp_repo: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str], flag_statuses: dict[str, str] | None = None, weak_coverage: bool = False) -> list[dict]: @@ -1212,7 +1216,7 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, the snapshot diff instead). """ if flag_statuses is None: - flag_statuses = compute_flag_statuses(warp_internal) + flag_statuses = compute_flag_statuses(warp_repo) ignore_flags = surface_map.get("ignore_flags", set()) feature_to_doc = surface_map.get("feature_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -1323,11 +1327,11 @@ def audit_features(warp_internal: Path, docs_root: Path, surface_map: dict, # Audit 2: CLI command coverage # --------------------------------------------------------------------------- -def audit_cli(warp_internal: Path, docs_root: Path, surface_map: dict, +def audit_cli(warp_repo: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str], cli_commands: list[dict] | None = None) -> list[dict]: """Audit CLI command and subcommand coverage in docs.""" - commands = cli_commands if cli_commands is not None else parse_cli_commands(warp_internal) + commands = cli_commands if cli_commands is not None else parse_cli_commands(warp_repo) cli_to_doc = surface_map.get("cli_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -1481,11 +1485,11 @@ def _slash_mention_re(name: str) -> re.Pattern: return re.compile(r"(? list[dict]: """Audit static slash command coverage in docs.""" - names = slash_commands if slash_commands is not None else parse_slash_commands(warp_internal) + names = slash_commands if slash_commands is not None else parse_slash_commands(warp_repo) slash_to_doc = surface_map.get("slash_to_doc", {}) repo_root = DOCS_REPO_ROOT[0] or docs_root.parent.parent.parent @@ -1576,14 +1580,14 @@ def audit_settings(docs_root: Path, surface_map: dict, # Audit 6: Stale doc references (docs pointing at removed code surfaces) # --------------------------------------------------------------------------- -def audit_stale_doc_references(warp_internal: Path, docs_root: Path, +def audit_stale_doc_references(warp_repo: Path, docs_root: Path, settings: dict[str, dict]) -> list[dict]: """Find doc references to code surfaces that no longer exist. - Settings keys documented in all-settings.mdx but absent from the code settings registry (renamed/removed settings). - Keybinding action names (`scope:action`) documented on the keyboard - shortcuts page but absent from warp-internal source. + shortcuts page but absent from the warp client repo source. """ findings = [] @@ -1629,7 +1633,7 @@ def is_object_setting_section(section: str) -> bool: actions = sorted(set(re.findall(r"`([a-z0-9_]+:[a-z0-9_]+)`", text))) remaining = set(actions) if remaining: - roots = [warp_internal / "app" / "src", warp_internal / "crates"] + roots = [warp_repo / "app" / "src", warp_repo / "crates"] for rs_file in iter_source_files(roots, ".rs"): if not remaining: break @@ -1646,7 +1650,7 @@ def is_object_setting_section(section: str) -> bool: "severity": "low", "reason": ( "Documented keybinding action not found anywhere in " - "warp-internal source — it was renamed or removed; update " + "the warp client repo source — it was renamed or removed; update " "the keyboard shortcuts page" ), }) @@ -1657,7 +1661,7 @@ def is_object_setting_section(section: str) -> bool: # Audit 7: Docs staleness (terminology) # --------------------------------------------------------------------------- -def audit_staleness(warp_internal: Path, docs_root: Path, +def audit_staleness(warp_repo: Path, docs_root: Path, docs_text: dict[str, str], stale_terms_path: Path = STALE_TERMS_PATH) -> list[dict]: """Check existing docs for stale terminology. @@ -2549,9 +2553,16 @@ def main(): parser = argparse.ArgumentParser( description="Audit Warp documentation coverage against code surfaces" ) + parser.add_argument( + "--warp", + dest="warp_repo", + help="Path to the public warp client repo (auto-detected as a sibling " + "of the docs repo named 'warp', with 'warp-internal' as fallback)", + ) parser.add_argument( "--warp-internal", - help="Path to warp-internal repo (auto-detected as a sibling of the docs repo)", + dest="warp_repo", + help="Deprecated alias for --warp", ) parser.add_argument( "--warp-server", @@ -2618,8 +2629,8 @@ def main(): # repo_root carries the developers/ openapi spec etc. DOCS_REPO_ROOT[0] = repo_root - warp_internal = find_repo("warp-internal", args.warp_internal, repo_root) - warp_server = find_repo("warp-server", args.warp_server, repo_root) + warp_repo = find_repo(["warp", "warp-internal"], args.warp_repo, repo_root) + warp_server = find_repo(["warp-server"], args.warp_server, repo_root) # Parse surface map surface_map = parse_surface_map(SURFACE_MAP_PATH) @@ -2666,15 +2677,15 @@ def guard(label: str, count: int) -> bool: server_tools: list[str] = [] bundled_skills: dict[str, str] = {} - if warp_internal and needs_internal: - print(f"Using warp-internal: {warp_internal}", file=sys.stderr) - flag_statuses = compute_flag_statuses(warp_internal) - cli_commands = parse_cli_commands(warp_internal) - cli_flags = parse_cli_flags(warp_internal, cli_commands) - slash_commands = parse_slash_commands(warp_internal) + if warp_repo and needs_internal: + print(f"Using warp client repo: {warp_repo}", file=sys.stderr) + flag_statuses = compute_flag_statuses(warp_repo) + cli_commands = parse_cli_commands(warp_repo) + cli_flags = parse_cli_flags(warp_repo, cli_commands) + slash_commands = parse_slash_commands(warp_repo) print("Parsing settings registry...", file=sys.stderr) - settings = parse_settings(warp_internal) - bundled_skills = parse_bundled_skills(warp_internal) + settings = parse_settings(warp_repo) + bundled_skills = parse_bundled_skills(warp_repo) flags_ok = guard("feature flags", len(flag_statuses)) cli_ok = guard("CLI commands", len(cli_commands)) @@ -2684,7 +2695,7 @@ def guard(label: str, count: int) -> bool: if args.category in (None, "features") and flags_ok: print("Running feature flag coverage audit...", file=sys.stderr) findings["undocumented_features"] = audit_features( - warp_internal, docs_root, surface_map, docs_text, + warp_repo, docs_root, surface_map, docs_text, flag_statuses=flag_statuses, weak_coverage=args.weak_coverage, ) audits_run.append("features") @@ -2692,14 +2703,14 @@ def guard(label: str, count: int) -> bool: if args.category in (None, "cli") and cli_ok: print("Running CLI command coverage audit...", file=sys.stderr) findings["undocumented_cli_commands"] = audit_cli( - warp_internal, docs_root, surface_map, docs_text, + warp_repo, docs_root, surface_map, docs_text, cli_commands=cli_commands) audits_run.append("cli") if args.category in (None, "slash") and slash_ok: print("Running slash command coverage audit...", file=sys.stderr) findings["undocumented_slash_commands"] = audit_slash_commands( - warp_internal, docs_root, surface_map, docs_text, + warp_repo, docs_root, surface_map, docs_text, slash_commands=slash_commands) audits_run.append("slash") @@ -2712,20 +2723,20 @@ def guard(label: str, count: int) -> bool: if args.category in (None, "staleness"): print("Running docs staleness audit...", file=sys.stderr) findings["potentially_stale_docs"] = audit_staleness( - warp_internal, docs_root, docs_text) + warp_repo, docs_root, docs_text) # The reverse checks compare docs against extracted code surfaces, # so they are only meaningful when extraction is healthy. if flags_ok and settings_ok: print("Running stale doc reference audit...", file=sys.stderr) findings["stale_doc_references"] = audit_stale_doc_references( - warp_internal, docs_root, settings) + warp_repo, docs_root, settings) audits_run.append("staleness") elif needs_internal: for audit in ("features", "cli", "slash", "settings", "staleness"): if args.category in (None, audit): audits_skipped.append({ "audit": audit, - "reason": "warp-internal repo not found (pass --warp-internal)", + "reason": "warp client repo not found (pass --warp)", }) if warp_server and needs_server: @@ -2755,7 +2766,7 @@ def guard(label: str, count: int) -> bool: audits_run.append("structure") if args.category in (None, "map"): - if warp_internal and warp_server and extraction_ok: + if warp_repo and warp_server and extraction_ok: print("Running surface map hygiene audit...", file=sys.stderr) findings["map_hygiene"] = audit_map_hygiene( surface_map, flag_statuses, cli_commands, api_routes, @@ -2765,7 +2776,7 @@ def guard(label: str, count: int) -> bool: audits_skipped.append({ "audit": "map", "reason": ( - "requires both warp-internal and warp-server with healthy " + "requires both the warp client repo and warp-server with healthy " "extraction (dead-entry checks against empty extraction " "would flag everything)" ), @@ -2775,7 +2786,7 @@ def guard(label: str, count: int) -> bool: changelog_entries = parse_changelog_entries(repo_root) snapshot_path = Path(args.snapshot) if args.diff or args.update_snapshot: - if warp_internal and warp_server and extraction_ok: + if warp_repo and warp_server and extraction_ok: current_snapshot = build_snapshot( flag_statuses, cli_commands, cli_flags, api_routes, slash_commands, settings, web_routes, server_tools, @@ -2807,10 +2818,10 @@ def guard(label: str, count: int) -> bool: audits_skipped.append({ "audit": "diff" if args.diff else "update-snapshot", "reason": ( - "requires both warp-internal and warp-server with healthy " + "requires both the warp client repo and warp-server with healthy " "extraction (see extraction:* skips above)" if not extraction_ok - else "requires both warp-internal and warp-server" + else "requires both the warp client repo and warp-server" ), }) @@ -2819,7 +2830,7 @@ def guard(label: str, count: int) -> bool: # extraction; any unaccounted item means an audit-logic regression and # the run is treated as incomplete. accounting = None - if args.category is None and warp_internal and warp_server and extraction_ok: + if args.category is None and warp_repo and warp_server and extraction_ok: accounting = compute_accounting( docs_root, surface_map, findings, flag_statuses, cli_commands, api_routes, slash_commands, settings, docs_text) From 3a3b3fe6c5e5cb74c4810097a7111f16bb8c4530 Mon Sep 17 00:00:00 2001 From: Hong Yi Chen Date: Tue, 30 Jun 2026 12:14:22 -0700 Subject: [PATCH 05/15] First drift-watch burn-down: settings, keybindings, slash commands, SuperGrok docs (#203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First drift-watch burn-down: settings, keybindings, slash commands, SuperGrok Dogfoods the missing_docs drift-watch workflow on the standing findings backlog (41 findings resolved): - all-settings.mdx: documented 17 missing settings (prompt submission mode, orchestration message display, auto handoff on sleep, agent attribution, handoff kill-switches, OSC 52 clipboard access, async find, directory tab colors, vertical-tabs panel options, hidden files in project explorer, line number mode, force X11, Ctrl+Enter submit for CLI agents, input focus on block selection) with types/defaults/options extracted from the settings registry; moved git_operations_autogen_enabled into [agents.warp_agent.active_ai] reflecting the agents.oz -> agents.warp_agent rename and removed the stale remnant section; added an Experimental section - keyboard-shortcuts.mdx: fixed all 14 dead action names (10 renames like workspace:open_new_tab -> workspace:new_tab, editor_view:cmd_i -> editor_view:inspect_command, terminal:trigger_subshell_bootstrap -> terminal:warpify_subshell; 4 removed actions blanked) - slash-commands.mdx: added /environment, /harness, /host (cloud agent session selectors) and /rename-conversation - bring-your-own-api-key.mdx: documented connecting a SuperGrok subscription instead of an xAI API key (newly GA SuperGrok flag) + map entry - Surface map: allowlisted guides/agent-workflows/warp-vs-claude-code as intentionally unlisted (per its frontmatter note) - Re-applied the GA-flag reclassification from the previous session (15 mappings + FullScreenZenMode surfaced) which had been silently reverted by a 'git checkout' during integrity testing before it was committed \u2014 caught by comparing accounting bucket counts run-over-run - Snapshot re-baselined; audit now reports 0 stale doc references, 0 undocumented settings/slash commands/unlisted pages; remaining backlog: 29 CLI subcommand docs, 24 OpenAPI spec gaps (update-open-api-spec), 29 terminology pages (style_lint), FullScreenZenMode + GroupedTabs Co-Authored-By: Oz * docs: second drift-watch pass — settings, feature flags, map hygiene Re-ran the missing_docs drift-watch audit against current code surfaces and burned down the newly-found in-scope drift (features 5→0, settings 6→0, map hygiene 2→0). Settings (all-settings.mdx): - Document appearance.icon.show_dock_icon (macOS Dock / Cmd-Tab visibility) - Document agents.warp_agent.other.long_running_command_submission_mode - Document code.editor.format_on_save - Document cloud_platform.third_party_api_keys.gemini_enterprise_credentials_enabled - Document warpify.ssh.reuse_existing_control_master - Map warpify.ssh.ssh_tmux_deprecation_notice_pending -> internal (one-time migration banner state, not user-configurable) Feature flags (feature_surface_map.md): - Map CodexPlugin -> cli-agents/codex.md - Map FullScreenZenMode, AsyncFind -> all-settings.mdx (surfaces are documented settings) - Map CustomModelRouters -> inference/model-choice.mdx (new "Custom routers" section) - Ignore GroupedTabs (macOS-only Preview; docs pending GA promotion) Map hygiene: - Prune stale ignore-list flags FreeUserNoAi and WelcomeTab (no longer in code) Co-Authored-By: Oz * docs(missing_docs): codify finding-resolution patterns in SKILL.md Add a "Resolution patterns" subsection capturing the per-type decision rules applied during the second drift-watch pass, so recurring runs resolve findings consistently: - user-facing setting -> document in all-settings - internal/state-only setting -> map `section.key -> internal` - feature flag with a dedicated page -> map to it - feature flag whose only surface is a documented setting -> map to that page - preview/pre-launch feature with no docs -> ignore-list with a comment - stale map entry/doc reference -> prune after confirming removal in code Co-Authored-By: Oz * Update .agents/skills/missing_docs/references/feature_surface_map.md Co-authored-by: oz-for-oss[bot] <277970191+oz-for-oss[bot]@users.noreply.github.com> --------- Co-authored-by: Oz Co-authored-by: oz-for-oss[bot] <277970191+oz-for-oss[bot]@users.noreply.github.com> --- .agents/skills/missing_docs/SKILL.md | 11 +++ .../references/feature_surface_map.md | 81 +++++++++++++----- .../capabilities/slash-commands.mdx | 2 +- .../inference/bring-your-own-api-key.mdx | 2 + .../agent-platform/inference/model-choice.mdx | 6 ++ .../getting-started/keyboard-shortcuts.mdx | 84 +++++++++---------- .../docs/terminal/settings/all-settings.mdx | 38 +++++++-- 7 files changed, 152 insertions(+), 72 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index ea25c4480..fba2dec8a 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -217,6 +217,17 @@ For each gap to address (prioritize high → medium → low): map is how gaps get lost. 9. Run `--update-snapshot` and commit the refreshed snapshot with the same PR. +### Resolution patterns + +Not every finding needs a new doc page — pick the lightest correct fix and verify it against source before applying: + +- **User-facing setting** — document it in `terminal/settings/all-settings.mdx` under its TOML section (type/default/options come from the `toml_path` registration). +- **Internal or state-only setting** (one-time banners, migration flags, telemetry-modeled state) — map `section.key -> internal` in the surface map instead of documenting it. +- **Feature flag with a dedicated doc page** — map the flag to that page. +- **Feature flag whose only user-facing surface is an already-documented setting** — map the flag to that setting's doc page rather than writing a new page (for example, a tab-bar visibility flag maps to the all-settings reference). +- **Preview or pre-launch feature with no docs yet** — add it to the surface-map ignore list with a comment; the snapshot diff re-flags it when it promotes to GA. +- **Stale map entry or doc reference** (map hygiene) — confirm the surface is gone from code, then prune the dead entry. + ### Drift-watch mode (recurring scheduled agent) This is the end-to-end workflow for the scheduled cloud agent that keeps docs in sync diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index 1414fcf94..3fb930716 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -107,6 +107,8 @@ CLIAgentRichInput -> src/content/docs/agent-platform/cli-agents/rich-input.md HOANotifications -> src/content/docs/agent-platform/capabilities/agent-notifications.mdx OpenCodeNotifications -> src/content/docs/agent-platform/cli-agents/opencode.md CodexNotifications -> src/content/docs/agent-platform/cli-agents/codex.md +# Codex Warp plugin marketplace integration; documented alongside Codex notifications. +CodexPlugin -> src/content/docs/agent-platform/cli-agents/codex.md HOARemoteControl -> src/content/docs/agent-platform/cli-agents/remote-control.md GlobalSearch -> src/content/docs/code/overview.md FileBasedMcp -> src/content/docs/agent-platform/capabilities/mcp.mdx @@ -125,6 +127,38 @@ ConfigurableToolbar -> src/content/docs/terminal/windows/configurable-toolbar.md SettingsFile -> src/content/docs/terminal/settings/index.mdx Changelog -> src/content/docs/changelog/index.mdx Autoupdate -> src/content/docs/support-and-community/troubleshooting-and-support/updating-warp.mdx +ShellSelector -> src/content/docs/getting-started/supported-shells.mdx +WorkflowAliases -> src/content/docs/terminal/entry/yaml-workflows.mdx +KittyImages -> src/content/docs/terminal/more-features/full-screen-apps.mdx +UndoClosedPanes -> src/content/docs/terminal/windows/tabs.mdx +RevertDiffHunk -> src/content/docs/code/code-review.mdx +SshRemoteServer -> src/content/docs/terminal/warpify/ssh.mdx + +# Feature flags whose only user-facing surface is a documented setting in the +# all-settings reference (terminal/settings/all-settings.mdx). +# - FullScreenZenMode: the "zen mode" tab-bar visibility knob appearance.tabs.workspace_decoration_visibility +# - AsyncFind: experimental.async_find_enabled +FullScreenZenMode -> src/content/docs/terminal/settings/all-settings.mdx +AsyncFind -> src/content/docs/terminal/settings/all-settings.mdx + +# Session sharing (viewing + ACLs are part of the documented session sharing feature) +ViewingSharedSessions -> src/content/docs/knowledge-and-collaboration/session-sharing/index.mdx +SessionSharingAcls -> src/content/docs/knowledge-and-collaboration/session-sharing/index.mdx +SharedSessionWriteToLongRunningCommands -> src/content/docs/knowledge-and-collaboration/session-sharing/index.mdx + +# CLI-gated features documented in the CLI reference +ArtifactCommand -> src/content/docs/reference/cli/artifacts.mdx +OzIdentityFederation -> src/content/docs/reference/cli/federate.mdx + +# Third-party harness support +AgentHarness -> src/content/docs/agent-platform/cloud-agents/harnesses/index.mdx + +# Image context for cloud agents +AmbientAgentsImageUpload -> src/content/docs/agent-platform/local-agents/agent-context/images-as-context.mdx +CloudModeImageContext -> src/content/docs/agent-platform/local-agents/agent-context/images-as-context.mdx + +# Skills on the Oz platform +OzPlatformSkills -> src/content/docs/agent-platform/capabilities/skills.mdx # Handoff (local <-> cloud, cloud <-> cloud) and snapshots OzHandoff -> src/content/docs/agent-platform/cloud-agents/handoff/index.mdx @@ -144,6 +178,10 @@ NamedAgents -> src/content/docs/agent-platform/cloud-agents/agents.mdx # Inference: BYOK and custom endpoints SoloUserByok -> src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx CustomInferenceEndpoints -> src/content/docs/agent-platform/inference/custom-inference-endpoint.mdx +# Connect a SuperGrok subscription instead of pasting an xAI API key. +SuperGrok -> src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx +# Custom model routers (Settings > AI > Custom Routers) surface in the model picker. +CustomModelRouters -> src/content/docs/agent-platform/inference/model-choice.mdx # Billing & Usage settings page (redesigned) BillingAndUsagePageV2 -> src/content/docs/support-and-community/plans-and-billing/index.mdx @@ -229,10 +267,17 @@ POST /harness-support/upload-snapshot -> internal # for exceptions: settings documented on another page (`section.key -> path`) # or intentionally undocumented (`section.key -> internal`). +# One-time internal state for the deprecated tmux SSH wrapper migration banner; +# not a user-configurable setting. +warpify.ssh.ssh_tmux_deprecation_notice_pending -> internal + ## Unlisted docs pages to ignore # Pages intentionally absent from src/sidebar.ts (one slug per line, e.g. # `guides/some-page`). Everything else on disk must be reachable via the sidebar. +# Per the page's frontmatter comment: not in the Guides sidebar yet, pending +# team feedback. +guides/agent-workflows/warp-vs-claude-code ## Flags to ignore (internal-only, not user-facing) @@ -280,7 +325,6 @@ DefaultAdeberryTheme AutoupdateUIRevamp MinimalistUI AvatarInTabBar -SessionSharingAcls ImeMarkedText NewTabStyling AmbientAgentsRTC @@ -297,9 +341,9 @@ CreateProjectFlow CodeLaunchModal ValidateAutosuggestions ClearAutosuggestionOnEscape -OzPlatformSkills -# Rendering detail for markdown tables in notebooks/AI output; no dedicated doc surface. +# Rendering details for markdown tables/Mermaid in notebooks/AI output; no dedicated doc surface. MarkdownTables +MarkdownMermaid # UI implementation details (not user-facing features) FallbackModelLoadOutputMessaging @@ -321,7 +365,6 @@ HOAOnboardingFlow AgentViewConversationListView BuildPlanAutoReloadBannerToggle BuildPlanAutoReloadPostPurchaseModal -FreeUserNoAi ForceLogin SimulateGithubUnauthed ConversationApi @@ -338,39 +381,33 @@ GitCredentialRefresh OrchestrationViewerStreamer OwnerOrchestrationAncestorStreamer -# Non-GA flags in dogfood/preview only +# Sub-feature toggles and pre-launch flags. Section placement does NOT assert +# rollout status (the audit computes that from code); entries here are ignored +# because the toggle itself isn't a documentable surface, or because the +# feature isn't user-facing yet — the snapshot diff flags promotions. +# Grouped Tabs is a macOS-only Preview feature (organize tabs into named, +# collapsible groups); public docs are pending GA promotion, which the snapshot +# diff will flag. +GroupedTabs LSPAsATool -SshRemoteServer EmbeddedCodeReviewComments InteractiveConversationManagementView MarkdownImages -MarkdownMermaid EditableMarkdownMermaid -OzIdentityFederation -AgentHarness +# Directory-based tab colors: the user-facing knob is the setting +# appearance.tabs.directory_tab_colors, documented in the all-settings reference. DirectoryTabColors -ArtifactCommand -CloudModeImageContext CloudModeHostSelector -AmbientAgentsImageUpload CodebaseIndexSpeedbump CodebaseIndexPersistence -SharedSessionWriteToLongRunningCommands AgentTips AgentViewPromptChip AllowOpeningFileLinksUsingEditorEnv AllowIgnoringInputSuggestions CodeModeChip -UndoClosedPanes -RevertDiffHunk -ViewingSharedSessions -ShellSelector -FullScreenZenMode -WorkflowAliases -KittyImages +# Internal agent file-search tool plumbing (read tools are not individually documented). GrepTool NativeShellCompletions -WelcomeTab DragTabsToWindows SshDragAndDrop ITermImages @@ -384,5 +421,7 @@ PredictAMQueries UseTantivySearch CommandCorrectionsHistoryRule SuggestedAgentModeWorkflows +# Implementation toggle choosing skill-based vs slash-command PR comments; +# the user-facing /pr-comments command is mapped via PRCommentsSlashCommand. PRCommentsSkill FigmaDetection diff --git a/src/content/docs/agent-platform/capabilities/slash-commands.mdx b/src/content/docs/agent-platform/capabilities/slash-commands.mdx index 4f6190c43..5d212c8d4 100644 --- a/src/content/docs/agent-platform/capabilities/slash-commands.mdx +++ b/src/content/docs/agent-platform/capabilities/slash-commands.mdx @@ -18,7 +18,7 @@ As you type, the menu filters results in real time, making it easy to find and r Warp currently supports the following built-in Slash Commands: -
Slash CommandDescription
/add-mcpAdd a new MCP server.
/add-promptAdd a new Agent Prompt in Warp Drive.
/add-ruleAdd a new Global Rule for the Agent.
/agentStart a new agent conversation. Optionally include a prompt to send immediately.
/changelogOpen the latest Warp changelog.
/cloud-agentStart a new cloud agent conversation. {'*'}
/compactFree up context by summarizing conversation history.
/compact-andCompact the current conversation and then send a follow-up prompt.
/conversationsOpen conversation history.
/costToggle credit usage details in the current conversation.
/create-environmentCreate a Warp Environment (Docker image + repos) via guided setup. {'*'}
/create-new-projectHave the Agent walk you through creating a new coding project. {'*'}
/export-to-clipboardExport the current conversation to clipboard in markdown format.
/export-to-fileExport the current conversation to a markdown file.
/feedbackOpen the static feedback experience. See Using /feedback in Warp for details.
/forkForks the current conversation into a new thread with the full context and history of the original.

You can optionally include a prompt that will be sent immediately in the forked conversation.
/fork-and-compactForks the current conversation and automatically compacts the forked version.

Useful when you want a fresh, summarized starting point that preserves relevant context while trimming the rest.
/fork-fromOpen a searchable menu to fork the conversation from a specific query. Select a query to create a fork that includes everything up to that point.
/indexIndex the current codebase using Codebase Context.
/initIndex the current codebase and generate an AGENTS.md file. {'*'}
/modelSwitch the base agent model for the current conversation.
/newStart a new agent conversation (alias for /agent).
/open-code-reviewOpen the code review pane.
/open-fileOpen a file for editing in Warp's code editor.
/open-mcp-serversView the status of your MCP servers.
/open-project-rulesOpen the Project Rules file (AGENTS).
/open-repoSwitch to another indexed repository.
/open-rulesView all of your global and project rules.
/open-settings-fileOpen the Warp settings file (settings.toml) in Warp's code editor.
/open-skillOpen an interactive menu to browse and edit project or global skills.
/orchestrateBreak a task into subtasks and run them in parallel with multiple agents. {'*'}
/planPrompt the Agent to do some research and create a plan for a task.
/pr-commentsPull GitHub PR review comments into Warp. {'*'}
/profileSwitch the active execution profile.
/promptsSearch saved prompts.
/queueQueue a prompt to send after the agent finishes responding. See Prompt Queueing.
/rename-tabRename the current tab. Include the new tab name as an argument (for example, /rename-tab deploy).
/rewindRewind to a previous point in the conversation.
/skillsInvoke a skill from a searchable menu.
/usageOpen billing and usage settings.
+
Slash CommandDescription
/add-mcpAdd a new MCP server.
/add-promptAdd a new Agent Prompt in Warp Drive.
/add-ruleAdd a new Global Rule for the Agent.
/agentStart a new agent conversation. Optionally include a prompt to send immediately.
/changelogOpen the latest Warp changelog.
/cloud-agentStart a new cloud agent conversation. {'*'}
/compactFree up context by summarizing conversation history.
/compact-andCompact the current conversation and then send a follow-up prompt.
/conversationsOpen conversation history.
/costToggle credit usage details in the current conversation.
/create-environmentCreate a Warp Environment (Docker image + repos) via guided setup. {'*'}
/create-new-projectHave the Agent walk you through creating a new coding project. {'*'}
/environmentSwitch the environment for the current cloud agent conversation.
/export-to-clipboardExport the current conversation to clipboard in markdown format.
/export-to-fileExport the current conversation to a markdown file.
/feedbackOpen the static feedback experience. See Using /feedback in Warp for details.
/forkForks the current conversation into a new thread with the full context and history of the original.

You can optionally include a prompt that will be sent immediately in the forked conversation.
/fork-and-compactForks the current conversation and automatically compacts the forked version.

Useful when you want a fresh, summarized starting point that preserves relevant context while trimming the rest.
/fork-fromOpen a searchable menu to fork the conversation from a specific query. Select a query to create a fork that includes everything up to that point.
/harnessSwitch the harness for the current cloud agent conversation.
/hostSwitch the execution host for the current cloud agent conversation.
/indexIndex the current codebase using Codebase Context.
/initIndex the current codebase and generate an AGENTS.md file. {'*'}
/modelSwitch the base agent model for the current conversation.
/newStart a new agent conversation (alias for /agent).
/open-code-reviewOpen the code review pane.
/open-fileOpen a file for editing in Warp's code editor.
/open-mcp-serversView the status of your MCP servers.
/open-project-rulesOpen the Project Rules file (AGENTS).
/open-repoSwitch to another indexed repository.
/open-rulesView all of your global and project rules.
/open-settings-fileOpen the Warp settings file (settings.toml) in Warp's code editor.
/open-skillOpen an interactive menu to browse and edit project or global skills.
/orchestrateBreak a task into subtasks and run them in parallel with multiple agents. {'*'}
/planPrompt the Agent to do some research and create a plan for a task.
/pr-commentsPull GitHub PR review comments into Warp. {'*'}
/profileSwitch the active execution profile.
/promptsSearch saved prompts.
/queueQueue a prompt to send after the agent finishes responding. See Prompt Queueing.
/rename-conversationRename the current conversation.
/rename-tabRename the current tab. Include the new tab name as an argument (for example, /rename-tab deploy).
/rewindRewind to a previous point in the conversation.
/skillsInvoke a skill from a searchable menu.
/usageOpen billing and usage settings.
:::caution Slash commands marked with a `*` consume credits to complete the task. diff --git a/src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx b/src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx index d959e6c1d..1681efc2e 100644 --- a/src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx +++ b/src/content/docs/agent-platform/inference/bring-your-own-api-key.mdx @@ -11,6 +11,8 @@ This lets you use your own API keys for model access, giving you control over mo BYOK provides greater flexibility in model access and ensures Warp **never consumes your** [AI credits](/support-and-community/plans-and-billing/credits/) for requests routed through your own keys. +For xAI's Grok models, you can also connect your SuperGrok subscription instead of entering an API key. In the Warp app, go to **Settings** > **AI** and choose to connect your SuperGrok subscription — Warp opens your browser to complete the connection. + :::note BYOK is available on Free and all eligible paid plans for individual users and organizations with 10 or fewer employees, subject to Warp's [Terms of Service](https://www.warp.dev/legal/terms-of-service). Larger organizations need a Business or Enterprise plan. See [Warp pricing](https://www.warp.dev/pricing) for current availability. ::: diff --git a/src/content/docs/agent-platform/inference/model-choice.mdx b/src/content/docs/agent-platform/inference/model-choice.mdx index 3c0651e9f..3321d23ba 100644 --- a/src/content/docs/agent-platform/inference/model-choice.mdx +++ b/src/content/docs/agent-platform/inference/model-choice.mdx @@ -122,6 +122,12 @@ You can configure the base model for each [Agent Profile](/agent-platform/capabi Edit your default profile or any other profile directly in **Settings** > **Agents** > **Profiles**. +### Custom routers + +Custom model routers automatically route each task to a specific model based on task complexity or rules you define, instead of using a single fixed model for every prompt. Your custom routers appear in the [model selector](#how-to-change-models) alongside Warp's built-in models. + +Add and manage custom routers in **Settings** > **AI** > **Custom Routers**. + ### Zero data retention policies Warp integrates with multiple large language model (LLM) providers to power its AI-driven features. diff --git a/src/content/docs/getting-started/keyboard-shortcuts.mdx b/src/content/docs/getting-started/keyboard-shortcuts.mdx index 9c2e767e2..8e116a41f 100644 --- a/src/content/docs/getting-started/keyboard-shortcuts.mdx +++ b/src/content/docs/getting-started/keyboard-shortcuts.mdx @@ -43,8 +43,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-R` | Workflows | `input:toggle_workflows` | | `` CTRL-` `` | Generate | `input:toggle_natural_language_command_search` | | `CMD-L` | Focus Terminal Input | `terminal:focus_input` | - | `CTRL-I` | Warpify Subshell | `terminal:trigger_subshell_bootstrap` | - | `CMD-\` | Warp Drive | `terminal:toggle_warp_drive` | + | `CTRL-I` | Warpify Subshell | `terminal:warpify_subshell` | + | `CMD-\` | Warp Drive | `workspace:toggle_warp_drive` | | `CMD-O` | File search | | | `CMD-P` | Open Command Palette | @@ -64,7 +64,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-M` | Open Block Context Menu | `terminal:open_block_list_context_menu_via_keybinding` | | `SHIFT-CMD-C` | Copy Command | `terminal:copy_commands` | | `SHIFT-CMD-I` | Reinput Selected Commands as Root | `terminal:reinput_commands_with_sudo` | - | `SHIFT-CMD-S` | Share Selected Block | `terminal:open_share_modal` | + | `SHIFT-CMD-S` | Share Selected Block | `terminal:open_share_block_modal` | | `SHIFT-DOWN` | Expand Selected Blocks Below | `terminal:expand_block_selection_below` | | `SHIFT-UP` | Expand Selected Blocks Above | `terminal:expand_block_selection_above` | @@ -72,10 +72,10 @@ Keybinds that conflict with others are highlighted with an orange border. | Shortcut | Command | Action | | ---------------- | -------------------------------------------- | --------------------------------------------------------- | - | `PAGE UP` | Scroll Up One Page | `terminal:page_up` | - | `PAGE DOWN` | Scroll Down One Page | `terminal:page_down` | - | `HOME` | Scroll to Top | `terminal:home` | - | `END` | Scroll to Bottom | `terminal:end` | + | `PAGE UP` | Scroll Up One Page | `terminal:scroll_up_one_page` | + | `PAGE DOWN` | Scroll Down One Page | `terminal:scroll_down_one_page` | + | `HOME` | Scroll to Top | | + | `END` | Scroll to Bottom | | | `SHIFT-CMD-UP` | Scroll to Top of Selected Block | `terminal:scroll_to_top_of_selected_block` | | `SHIFT-CMD-DOWN` | Scroll to Bottom of Selected Block | `terminal:scroll_to_bottom_of_selected_block` | | | Scroll Terminal Output Up One Line | `terminal:scroll_up_one_line` | @@ -98,7 +98,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CMD-BACKSPACE` | Delete All Left | `editor_view:delete_all_left` | | `CMD-DELETE` | Delete All Right | `editor_view:delete_all_right` | | `CMD-DOWN` | Move Cursor to the Bottom | `editor_view:cmd_down` | - | `CMD-I` | Inspect Command | `editor_view:cmd_i` | + | `CMD-I` | Inspect Command | `editor_view:inspect_command` | | `CMD-LEFT` | Home | `editor_view:home` | | `CMD-RIGHT` | End | `editor_view:end` | | `CTRL-A` | Move to Start of Line | `editor_view:move_to_line_start` | @@ -118,9 +118,9 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-B` | Select One Character to the Left | `editor_view:select_left` | | `CTRL-SHIFT-DOWN` | Add Cursor Below | `editor_view:add_cursor_below` | | `CTRL-SHIFT-E` | Select to End of Line | `editor:select_to_line_end` | - | `CMD-Z` | Undo | `editor:undo` | - | `CMD-SHIFT-Z` | Redo | `editor:redo` | - | `CTRL-SHIFT-F` | Select One Character to the Right | `editor:select_right` | + | `CMD-Z` | Undo | | + | `CMD-SHIFT-Z` | Redo | | + | `CTRL-SHIFT-F` | Select One Character to the Right | `editor_view:select_right` | | `CTRL-SHIFT-N` | Select Down | `editor_view:select_down` | | `CTRL-SHIFT-P` | Select Up | `editor_view:select_up` | | `CTRL-SHIFT-UP` | Add Cursor Above | `editor_view:add_cursor_above` | @@ -150,7 +150,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `ALT-CMD-UP` | Switch Panes Up | `pane_group:navigate_up` | | `ALT-CMD-V` | \[a11y] Set Concise Accessibility Announcements | `workspace:set_a11y_concise_verbosity_level` | | `ALT-CMD-V` | \[a11y] Set Verbose Accessibility Announcements | `workspace:set_a11y_verbose_verbosity_level` | - | `CMD-,` | Open Settings | `workspace:show_settings_modal` | + | `CMD-,` | Open Settings | `workspace:show_settings` | | `CMD-,` | Open Settings: Account | `workspace:show_settings_account_page` | | `CMD-G` | Find the Next Occurrence of Your Search Query | `find:find_next_occurrence` | | `CMD-P` | Toggle Command Palette | `workspace:toggle_command_palette` | @@ -187,8 +187,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CMD-C` | Copy | `terminal:copy` | | `CMD-F` | Find | `terminal:find` | | `CMD-V` | Paste | `terminal:paste` | - | `CMD-T` | Open New Tab | `workspace:open_new_tab` | - | `SHIFT-CMD-T` | Reopen Closed Tab | `workspace:reopen_closed_tab` | + | `CMD-T` | Open New Tab | `workspace:new_tab` | + | `SHIFT-CMD-T` | Reopen Closed Tab | `app:reopen_closed_session` | | `CTRL-SHIFT-LEFT` | Move Tab Left | `workspace:move_tab_left` | | `CTRL-SHIFT-RIGHT` | Move Tab Right | `workspace:move_tab_right` | | `SHIFT-CMD-{` | Activate Previous Tab | `workspace:activate_prev_tab` | @@ -206,8 +206,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-R` | Workflows | `input:toggle_workflows` | | `` CTRL-` `` | Generate | `input:toggle_natural_language_command_search` | | `CTRL-SHIFT-L` | Focus Terminal Input | `terminal:focus_input` | - | `CTRL-I` | Warpify Subshell | `terminal:trigger_subshell_bootstrap` | - | `CTRL-SHIFT-\` | Warp Drive | `terminal:toggle_warp_drive` | + | `CTRL-I` | Warpify Subshell | `terminal:warpify_subshell` | + | `CTRL-SHIFT-\` | Warp Drive | `workspace:toggle_warp_drive` | **Blocks** @@ -225,7 +225,7 @@ Keybinds that conflict with others are highlighted with an orange border. | | Open Block Context Menu | `terminal:open_block_list_context_menu_via_keybinding` | | `CTRL-SHIFT-C` | Copy Command | `terminal:copy_commands` | | | Reinput Selected Commands as Root | `terminal:reinput_commands_with_sudo` | - | `CTRL-SHIFT-S` | Share Selected Block | `terminal:open_share_modal` | + | `CTRL-SHIFT-S` | Share Selected Block | `terminal:open_share_block_modal` | | `SHIFT-DOWN` | Expand Selected Blocks Below | `terminal:expand_block_selection_below` | | `SHIFT-UP` | Expand Selected Blocks Above | `terminal:expand_block_selection_above` | @@ -233,10 +233,10 @@ Keybinds that conflict with others are highlighted with an orange border. | Shortcut | Command | Action | | ------------------ | -------------------------------------------- | --------------------------------------------------------- | - | `PAGE UP` | Scroll Up One Page | `terminal:page_up` | - | `PAGE DOWN` | Scroll Down One Page | `terminal:page_down` | - | `HOME` | Scroll to Top | `terminal:home` | - | `END` | Scroll to Bottom | `terminal:end` | + | `PAGE UP` | Scroll Up One Page | `terminal:scroll_up_one_page` | + | `PAGE DOWN` | Scroll Down One Page | `terminal:scroll_down_one_page` | + | `HOME` | Scroll to Top | | + | `END` | Scroll to Bottom | | | `CTRL-SHIFT-UP` | Scroll to Top of Selected Block | `terminal:scroll_to_top_of_selected_block` | | `CTRL-SHIFT-DOWN` | Scroll to Bottom of Selected Block | `terminal:scroll_to_bottom_of_selected_block` | | | Scroll Terminal Output Up One Line | `terminal:scroll_up_one_line` | @@ -259,7 +259,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-Y` | Delete All Left | `editor_view:delete_all_left` | | | Delete All Right | `editor_view:delete_all_right` | | `CTRL-END` | Move Cursor to the Bottom | `editor_view:cmd_down` | - | `CTRL-I` | Inspect Command | `editor_view:cmd_i` | + | `CTRL-I` | Inspect Command | `editor_view:inspect_command` | | `HOME` | Home | `editor_view:home` | | `END` | End | `editor_view:end` | | `CTRL-A` | Move to Start of Line | `editor_view:move_to_line_start` | @@ -279,9 +279,9 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-B` | Select One Character to the Left | `editor_view:select_left` | | `CTRL-SHIFT-DOWN` | Add Cursor Below | `editor_view:add_cursor_below` | | | Select to End of Line | `editor:select_to_line_end` | - | `CTRL-Z` | Undo | `editor:undo` | - | `CTRL-SHIFT-Z` | Redo | `editor:redo` | - | `CTRL-SHIFT-F` | Select One Character to the Right | `editor:select_right` | + | `CTRL-Z` | Undo | | + | `CTRL-SHIFT-Z` | Redo | | + | `CTRL-SHIFT-F` | Select One Character to the Right | `editor_view:select_right` | | | Select Down | `editor_view:select_down` | | `CTRL-SHIFT-P` | Select Up | `editor_view:select_up` | | `CTRL-SHIFT-UP` | Add Cursor Above | `editor_view:add_cursor_above` | @@ -310,7 +310,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-ALT-UP` | Switch Panes Up | `pane_group:navigate_up` | | `CTRL-ALT-V` | \[a11y] Set Concise Accessibility Announcements | `workspace:set_a11y_concise_verbosity_level` | | `CTRL-ALT-V` | \[a11y] Set Verbose Accessibility Announcements | `workspace:set_a11y_verbose_verbosity_level` | - | `CTRL-,` | Open Settings | `workspace:show_settings_modal` | + | `CTRL-,` | Open Settings | `workspace:show_settings` | | `CTRL-,` | Open Settings: Account | `workspace:show_settings_account_page` | | `F3` | Find the Next Occurrence of Your Search Query | `find:find_next_occurrence` | | `CTRL-SHIFT-P` | Toggle Command Palette | `workspace:toggle_command_palette` | @@ -347,8 +347,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-C` | Copy | `terminal:copy` | | `CTRL-SHIFT-F` | Find | `terminal:find` | | `CTRL-SHIFT-V` | Paste | `terminal:paste` | - | `CTRL-SHIFT-T` | Open New Tab | `workspace:open_new_tab` | - | `CTRL-ALT-T` | Reopen Closed Tab | `workspace:reopen_closed_tab` | + | `CTRL-SHIFT-T` | Open New Tab | `workspace:new_tab` | + | `CTRL-ALT-T` | Reopen Closed Tab | `app:reopen_closed_session` | | `CTRL-SHIFT-LEFT` | Move Tab Left | `workspace:move_tab_left` | | `CTRL-SHIFT-RIGHT` | Move Tab Right | `workspace:move_tab_right` | | `CTRL-PAGEUP` | Activate Previous Tab | `workspace:activate_prev_tab` | @@ -366,8 +366,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-R` | Workflows | `input:toggle_workflows` | | `` CTRL-` `` | Generate | `input:toggle_natural_language_command_search` | | `CTRL-SHIFT-L` | Focus Terminal Input | `terminal:focus_input` | - | `CTRL-I` | Warpify Subshell | `terminal:trigger_subshell_bootstrap` | - | `CTRL-SHIFT-\` | Warp Drive | `terminal:toggle_warp_drive` | + | `CTRL-I` | Warpify Subshell | `terminal:warpify_subshell` | + | `CTRL-SHIFT-\` | Warp Drive | `workspace:toggle_warp_drive` | **Blocks** @@ -385,7 +385,7 @@ Keybinds that conflict with others are highlighted with an orange border. | | Open Block Context Menu | `terminal:open_block_list_context_menu_via_keybinding` | | `CTRL-SHIFT-C` | Copy Command | `terminal:copy_commands` | | | Reinput Selected Commands as Root | `terminal:reinput_commands_with_sudo` | - | `CTRL-SHIFT-S` | Share Selected Block | `terminal:open_share_modal` | + | `CTRL-SHIFT-S` | Share Selected Block | `terminal:open_share_block_modal` | | `SHIFT-DOWN` | Expand Selected Blocks Below | `terminal:expand_block_selection_below` | | `SHIFT-UP` | Expand Selected Blocks Above | `terminal:expand_block_selection_above` | @@ -393,10 +393,10 @@ Keybinds that conflict with others are highlighted with an orange border. | Shortcut | Command | Action | | ------------------ | -------------------------------------------- | --------------------------------------------------------- | - | `PAGE UP` | Scroll Up One Page | `terminal:page_up` | - | `PAGE DOWN` | Scroll Down One Page | `terminal:page_down` | - | `HOME` | Scroll to Top | `terminal:home` | - | `END` | Scroll to Bottom | `terminal:end` | + | `PAGE UP` | Scroll Up One Page | `terminal:scroll_up_one_page` | + | `PAGE DOWN` | Scroll Down One Page | `terminal:scroll_down_one_page` | + | `HOME` | Scroll to Top | | + | `END` | Scroll to Bottom | | | `CTRL-SHIFT-UP` | Scroll to Top of Selected Block | `terminal:scroll_to_top_of_selected_block` | | `CTRL-SHIFT-DOWN` | Scroll to Bottom of Selected Block | `terminal:scroll_to_bottom_of_selected_block` | | | Scroll Terminal Output Up One Line | `terminal:scroll_up_one_line` | @@ -419,7 +419,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-Y` | Delete All Left | `editor_view:delete_all_left` | | | Delete All Right | `editor_view:delete_all_right` | | `CTRL-END` | Move Cursor to the Bottom | `editor_view:cmd_down` | - | `CTRL-I` | Inspect Command | `editor_view:cmd_i` | + | `CTRL-I` | Inspect Command | `editor_view:inspect_command` | | `HOME` | Home | `editor_view:home` | | `END` | End | `editor_view:end` | | `CTRL-A` | Move to Start of Line | `editor_view:move_to_line_start` | @@ -439,9 +439,9 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-B` | Select One Character to the Left | `editor_view:select_left` | | `CTRL-SHIFT-DOWN` | Add Cursor Below | `editor_view:add_cursor_below` | | | Select to End of Line | `editor:select_to_line_end` | - | `CTRL-Z` | Undo | `editor:undo` | - | `CTRL-SHIFT-Z` | Redo | `editor:redo` | - | `CTRL-SHIFT-F` | Select One Character to the Right | `editor:select_right` | + | `CTRL-Z` | Undo | | + | `CTRL-SHIFT-Z` | Redo | | + | `CTRL-SHIFT-F` | Select One Character to the Right | `editor_view:select_right` | | | Select Down | `editor_view:select_down` | | `CTRL-SHIFT-P` | Select Up | `editor_view:select_up` | | `CTRL-SHIFT-UP` | Add Cursor Above | `editor_view:add_cursor_above` | @@ -470,7 +470,7 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-ALT-UP` | Switch Panes Up | `pane_group:navigate_up` | | `CTRL-ALT-V` | \[a11y] Set Concise Accessibility Announcements | `workspace:set_a11y_concise_verbosity_level` | | `CTRL-ALT-V` | \[a11y] Set Verbose Accessibility Announcements | `workspace:set_a11y_verbose_verbosity_level` | - | `CTRL-,` | Open Settings | `workspace:show_settings_modal` | + | `CTRL-,` | Open Settings | `workspace:show_settings` | | `CTRL-,` | Open Settings: Account | `workspace:show_settings_account_page` | | `F3` | Find the Next Occurrence of Your Search Query | `find:find_next_occurrence` | | `CTRL-SHIFT-P` | Toggle Command Palette | `workspace:toggle_command_palette` | @@ -507,8 +507,8 @@ Keybinds that conflict with others are highlighted with an orange border. | `CTRL-SHIFT-C` | Copy | `terminal:copy` | | `CTRL-SHIFT-F` | Find | `terminal:find` | | `CTRL-SHIFT-V` | Paste | `terminal:paste` | - | `CTRL-SHIFT-T` | Open New Tab | `workspace:open_new_tab` | - | `CTRL-ALT-T` | Reopen Closed Tab | `workspace:reopen_closed_tab` | + | `CTRL-SHIFT-T` | Open New Tab | `workspace:new_tab` | + | `CTRL-ALT-T` | Reopen Closed Tab | `app:reopen_closed_session` | | `CTRL-SHIFT-LEFT` | Move Tab Left | `workspace:move_tab_left` | | `CTRL-SHIFT-RIGHT` | Move Tab Right | `workspace:move_tab_right` | | `CTRL-PAGEUP` | Activate Previous Tab | `workspace:activate_prev_tab` | diff --git a/src/content/docs/terminal/settings/all-settings.mdx b/src/content/docs/terminal/settings/all-settings.mdx index b8bdbbfa6..23704ac10 100644 --- a/src/content/docs/terminal/settings/all-settings.mdx +++ b/src/content/docs/terminal/settings/all-settings.mdx @@ -21,6 +21,7 @@ Top-level settings that control Warp's startup behavior, session management, and * `login_item` — Whether to launch Warp automatically when you log in. Type: boolean. Default: `true`. * `mouse_scroll_multiplier` — The scroll speed multiplier for mouse scroll events. Type: number. Default: `3.0`. * `new_tab_placement` — Where new tabs are placed in the tab bar. Type: string. Default: `"after_current_tab"`. Options: `"after_current_tab"`, `"after_all_tabs"`. +* `preserve_input_focus_on_block_selection` — Whether the input box keeps focus when you select a block. Type: boolean. Default: `false`. * `quit_on_last_window_closed` — Whether to quit Warp when the last window is closed. Type: boolean. Default: `false`. * `restore_session` — Whether to restore the previous session when Warp starts up. Type: boolean. Default: `true`. * `should_confirm_close_session` — Whether to show a confirmation dialog when closing a session. Type: boolean. Default: `true`. @@ -100,6 +101,7 @@ theme = { custom = { name = "My Theme", path = "~/.warp/themes/my-theme.yaml" } * `tab_close_button_position` — Position of the close button on tabs. Type: string. Default: `"right"`. Options: `"right"`, `"left"`. * `show_indicators_button` — Whether to show activity indicators on tabs. Type: boolean. Default: `true`. * `preserve_active_tab_color` — Whether to preserve the active tab's color when switching tabs. Type: boolean. Default: `false`. +* `directory_tab_colors` — Mapping of directory paths to their tab color assignments, populated when you assign tab colors by directory. Type: object. Default: `{}`. * `header_toolbar_chip_selection` — Configuration for the header toolbar chips in the vertical tab panel header. Type: string or object. Default: `"default"`. ### Vertical tabs @@ -116,6 +118,8 @@ theme = { custom = { name = "My Theme", path = "~/.warp/themes/my-theme.yaml" } * `show_diff_stats` — Whether to show diff stats on vertical tabs. Type: boolean. Default: `true`. * `show_pr_link` — Whether to show PR links on vertical tabs. Type: boolean. Default: `true`. * `use_latest_prompt_as_title` — Whether vertical tab names for agent conversations use the latest user prompt. Type: boolean. Default: `false`. +* `hide_title_bar_search_bar` — Whether to hide the title bar search bar when using the vertical tab layout. Search stays available via the Command Palette and keyboard shortcuts. Type: boolean. Default: `false`. +* `show_panel_in_restored_windows` — Whether restored windows open the vertical tabs panel even if it was closed when the session was saved. Type: boolean. Default: `false`. ### Panes @@ -141,6 +145,7 @@ theme = { custom = { name = "My Theme", path = "~/.warp/themes/my-theme.yaml" } **Section**: `[appearance.icon]` * `app_icon` — The app icon displayed in the dock. Type: string. Default: `"default"`. Options: `"default"`, `"aurora"`, `"classic1"`, `"classic2"`, `"classic3"`, `"comets"`, `"cow"`, `"glass_sky"`, `"glitch"`, `"glow"`, `"holographic"`, `"mono"`, `"neon"`, `"original"`, `"starburst"`, `"sticker"`, `"warp_one"`. +* `show_dock_icon` — Whether Warp is shown in the macOS Dock and `Cmd-Tab` switcher. Type: boolean. Default: `true`. macOS only. ### Window @@ -166,6 +171,7 @@ Settings that control terminal behavior, input, and event handling. * `mouse_reporting_enabled` — Whether to forward mouse events to full-screen terminal applications. Type: boolean. Default: `true`. * `scroll_reporting_enabled` — Whether to forward scroll events to full-screen terminal applications. Type: boolean. Default: `true`. * `maximum_grid_size` — The maximum number of rows in the terminal grid. Type: integer. Default: `50000`. +* `osc52_clipboard_access` — Whether terminal programs can access the system clipboard via OSC 52 escape sequences. Type: string. Default: `"deny"`. Options: `"deny"`, `"write_only"`, `"read_write"`. * `use_audible_bell` — Whether to play an audible bell sound on terminal bell events. Type: boolean. Default: `false`. * `show_terminal_zero_state_block` — Whether to show the AI zero-state block in new terminal sessions. Type: boolean. Default: `true`. @@ -293,6 +299,8 @@ Settings for Warp's agents, including model behavior, permissions, knowledge, MC * `code_suggestions_enabled` — Controls whether AI code suggestions are enabled. Type: boolean. Default: `true`. * `intelligent_autosuggestions_enabled` — Controls whether AI-powered intelligent autosuggestions are enabled. Type: boolean. Default: `true`. * `agent_mode_query_suggestions_enabled` — Controls whether prompt suggestions are shown in Agent Mode. Type: boolean. Default: `true`. +* `git_operations_autogen_enabled` — Whether AI auto-generates commit messages and PR titles and bodies in the code review dialogs. Type: boolean. Default: `true`. +* `rule_suggestions_enabled` — Whether the agent suggests rules to save after responses. Type: boolean. Default: `true`. * `shared_block_title_generation_enabled` — Controls whether titles are auto-generated when sharing blocks. Type: boolean. Default: `true`. #### Input @@ -318,14 +326,13 @@ Settings for Warp's agents, including model behavior, permissions, knowledge, MC * `should_show_oz_updates_in_zero_state` — Whether the "What's new" section is shown in the agent view. Type: boolean. Default: `true`. * `should_render_use_agent_toolbar_for_user_commands` — Whether to show the "Use Agent" footer for terminal commands. Type: boolean. Default: `true`. * `cloud_agent_computer_use_enabled` — Whether computer use is enabled for cloud agent conversations. Type: boolean. Default: `false`. - -### Code review autogeneration - -Controls AI-driven autogeneration in the code review dialogs. This setting currently lives under `[agents.oz.active_ai]` rather than alongside the other `[agents.warp_agent.active_ai]` settings. - -**Section**: `[agents.oz.active_ai]` - -* `git_operations_autogen_enabled` — Controls whether AI auto-generates commit messages and PR title and body in the code review dialogs. Type: boolean. Default: `true`. +* `agent_attribution_enabled` — Whether the Warp Agent adds an attribution co-author line to commit messages and pull requests it creates. Type: boolean. Default: `true`. +* `auto_handoff_on_sleep_enabled` — Whether Warp automatically hands off local agent conversations to the cloud when your computer is about to sleep (macOS). See [local-to-cloud handoff](/agent-platform/cloud-agents/handoff/local-to-cloud/). Type: boolean. Default: `false`. +* `default_prompt_submission_mode` — Default behavior when submitting a new prompt while the agent is still responding. See [Prompt Queueing](/agent-platform/local-agents/interacting-with-agents/prompt-queueing/). Type: string. Default: `"interrupt"`. Options: `"interrupt"`, `"queue"`. +* `long_running_command_submission_mode` — What happens when you submit a prompt while the agent is running an agent-requested long-running command. Only applies when `default_prompt_submission_mode` is `"interrupt"`. See [Prompt Queueing](/agent-platform/local-agents/interacting-with-agents/prompt-queueing/). Type: string. Default: `"queue_until_command_completes"`. Options: `"send_immediately"`, `"queue_until_command_completes"`. +* `orchestration_message_display_mode` — How child-agent message bodies are displayed during [multi-agent runs](/agent-platform/cloud-agents/orchestration/multi-agent-runs/). Type: string. Default: `"always_collapse"`. Options: `"show_and_collapse"`, `"always_show"`, `"always_collapse"`. +* `should_force_disable_cloud_handoff` — Whether to force-disable [local-to-cloud handoff](/agent-platform/cloud-agents/handoff/local-to-cloud/). Type: boolean. Default: `false`. +* `should_force_disable_ampersand_handoff` — Whether to force-disable the `&` prefix that composes a prompt for cloud handoff. Type: boolean. Default: `false`. ### Third-party (CLI agents) @@ -337,6 +344,7 @@ Controls AI-driven autogeneration in the code review dialogs. This setting curre * `auto_dismiss_composer_after_submit` — Whether CLI agent Rich Input automatically closes after the user submits a prompt. Type: boolean. Default: `false`. * `cli_agent_toolbar_chip_selection_setting` — Controls the layout of context chips in the CLI Agent toolbar. Type: string or object. Default: `"default"`. * `cli_agent_toolbar_enabled_commands` — Maps custom toolbar command patterns to specific CLI agents. Type: object. Default: `{}`. +* `submit_on_ctrl_enter` — Whether the CLI agent Rich Input editor submits on `Ctrl+Enter` instead of `Enter` (with `Enter` inserting a newline). Type: boolean. Default: `false`. ### Voice @@ -365,7 +373,9 @@ Settings for Warp's built-in code editor, file handling, and codebase indexing. * `show_code_review_diff_stats` — Whether to show lines added/removed counts on the code review button. Type: boolean. Default: `true`. * `show_project_explorer` — Whether the project explorer is shown in the tools panel. Type: boolean. Default: `true`. * `show_global_search` — Whether global file search is shown in the tools panel. Type: boolean. Default: `true`. +* `show_hidden_files` — Whether hidden files (dotfiles) are shown in the project explorer. Type: boolean. Default: `false`. * `use_warp_as_default_editor` — Whether Warp is used as the default code editor. Type: boolean. Default: `false`. +* `format_on_save` — Whether the language server automatically formats the file on save. Other LSP features (hover, go-to-definition, references, diagnostics) are unaffected. Type: boolean. Default: `true`. ### Indexing @@ -437,6 +447,7 @@ Low-level system and rendering settings. * `prefer_low_power_gpu` — Whether to prefer the integrated (low-power) GPU. Type: boolean. Default: `false`. * `preferred_graphics_backend` — The preferred graphics backend (Windows). Type: string or null. Default: `null`. Options: `"dx12"`, `"vulkan"`, `"gl"`, `"metal"`, `null`. * `linux_selection_clipboard` — Whether the Linux primary selection clipboard is used. Type: boolean. Default: `true`. +* `force_x11` — Whether to force X11 instead of Wayland on Linux. Type: boolean. Default: `true` (`false` under WSL). ## Text editing @@ -444,11 +455,20 @@ Settings that control text editing behavior in the input editor. **Section**: `[text_editing]` +* `code_editor_line_number_mode` — How line numbers are displayed in code editors. Type: string. Default: `"absolute"`. Options: `"absolute"`, `"relative"`. * `vim_mode_enabled` — Whether Vim keybindings are enabled. Type: boolean. Default: `false`. * `vim_status_bar` — Whether the Vim status bar is displayed. Type: boolean. Default: `true`. * `vim_unnamed_system_clipboard` — Whether the Vim unnamed register uses the system clipboard. Type: boolean. Default: `false`. * `autocomplete_symbols` — Whether matching symbols like brackets and quotes are auto-completed. Type: boolean. Default: `true`. +## Experimental + +Opt-in settings for features that are still being refined. + +**Section**: `[experimental]` + +* `async_find_enabled` — Use an improved implementation of find that keeps the UI responsive while searching large outputs. Type: boolean. Default: `false`. + ## Warp Drive Settings for Warp Drive (shared workflows, notebooks, and prompts). @@ -469,6 +489,7 @@ Settings for Warp features in SSH sessions and subshells. * `enable_ssh_warpification` — Whether to enable Warp features in SSH sessions. Type: boolean. Default: `true`. * `enable_legacy_ssh_wrapper` — Whether the legacy SSH wrapper is enabled for SSH sessions. Type: boolean. Default: `true`. * `use_ssh_tmux_wrapper` — Whether to use a tmux-based wrapper for SSH warpification. Type: boolean. Default: `false`. +* `reuse_existing_control_master` — Whether the legacy SSH wrapper attaches to an existing SSH `ControlMaster` for the destination host instead of always creating its own. Type: boolean. Default: `false`. * `ssh_extension_install_mode` — Controls SSH extension installation behavior. Type: string. Default: `"always_ask"`. Options: `"always_ask"` (always prompt before installing), `"always_install"` (auto-install and connect without prompting), `"never_install"` (fall back to legacy warpification). * `ssh_hosts_denylist` — SSH hosts that should not trigger the warpification prompt. Type: array of strings. Default: `[]`. @@ -515,6 +536,7 @@ Settings for third-party API key integration and cloud model configuration. * `aws_bedrock_profile` — The AWS profile name to use for Bedrock credentials. Type: string. Default: `"default"`. * `aws_bedrock_auto_login` — Whether to automatically run the AWS login command when Bedrock credentials expire. Type: boolean. Default: `false`. * `aws_bedrock_auth_refresh_command` — The command to run to refresh AWS credentials for Bedrock. Type: string. Default: `"aws login"`. +* `gemini_enterprise_credentials_enabled` — Whether Warp routes eligible requests through your workspace's Gemini Enterprise Google Cloud project. Requires a workspace admin to configure Gemini Enterprise. Type: boolean. Default: `false`. * `can_use_warp_credits_with_byok` — Whether Warp credits can be used even when providing your own API key. Type: boolean. Default: `false`. ## Global hotkey From 888cf8717446f63a2f33e14ae9b4f11280efc7ea Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 21:43:30 +0000 Subject: [PATCH 06/15] docs(missing_docs): add reviewer routing from code ownership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/suggest_reviewers.py, which maps the source surface behind each drift-watch finding to its owning engineer using the CODEOWNERS-format ownership files already maintained in the code repos: - warp client: .github/STAKEHOLDERS - warp-server: .github/STAKEHOLDERS (advisory) + .github/CODEOWNERS (enforced) It resolves with standard last-match-wins precedence, dedupes owners into users/teams, and prints a ready-to-run `gh pr edit --add-reviewer` command. Unresolved paths are non-fatal so a scheduled run is never blocked. Wire it into SKILL.md: a new "Reviewer routing" section (per-category source-file hints), a drift-watch "Route reviewers" step before opening the PR, an updated scheduled-agent prompt, and a References entry. Ownership stays sourced from STAKEHOLDERS/CODEOWNERS (kept fresh by sync-stakeholders) — never hardcoded. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 47 +++++- .../missing_docs/scripts/suggest_reviewers.py | 153 ++++++++++++++++++ 2 files changed, 194 insertions(+), 6 deletions(-) create mode 100755 .agents/skills/missing_docs/scripts/suggest_reviewers.py diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index fba2dec8a..0f91d2e61 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -228,6 +228,32 @@ Not every finding needs a new doc page — pick the lightest correct fix and ver - **Preview or pre-launch feature with no docs yet** — add it to the surface-map ignore list with a comment; the snapshot diff re-flags it when it promotes to GA. - **Stale map entry or doc reference** (map hygiene) — confirm the surface is gone from code, then prune the dead entry. +### Reviewer routing + +Assign the engineer who owns the *code* behind each change, so a human with real context reviews the PR. Every finding traces to a source surface; map that surface's defining file to its owner using the ownership files that already live in the code repos (CODEOWNERS format, last-match-wins): +- warp client repo: `.github/STAKEHOLDERS` +- warp-server: `.github/STAKEHOLDERS` (advisory) + `.github/CODEOWNERS` (enforced) + +These are the source of truth (warp-server keeps STAKEHOLDERS fresh via the `sync-stakeholders` skill), so never hardcode owner lists here. + +For each addressed finding, note the defining source file you already consulted in Phase 3 step 4: +- **Setting** → the file holding its `toml_path` registration (usually under `app/src/settings/`). +- **Slash command** → `app/src/search/slash_command_menu/static_commands/`. +- **Feature flag** → the flag's primary usage site in `app/src/` (grep the flag name); fall back to `crates/warp_features/src/lib.rs`. +- **CLI command** → `crates/warp_cli/src/`. +- **API route** → warp-server `router/handlers/public_api/` (API gaps usually go to `sync-openapi-spec`). + +Resolve owners and get a ready-to-run assignment command: + +```bash +python3 .agents/skills/missing_docs/scripts/suggest_reviewers.py \ + --warp ../warp --warp-server ../warp-server \ + warp:app/src/settings/ssh.rs \ + warp:app/src/search/slash_command_menu/static_commands/commands.rs +``` + +Then assign the resolved reviewers on the PR with `gh pr edit --add-reviewer `. Unresolved paths are non-fatal — leave them for manual assignment rather than blocking the run. + ### Drift-watch mode (recurring scheduled agent) This is the end-to-end workflow for the scheduled cloud agent that keeps docs in sync @@ -255,9 +281,13 @@ with the product. Each run: ``` 5. **Validate**: `npm run build` if doc pages changed; re-run the audit and confirm the addressed findings are gone. -6. **Open a PR** with the doc pages + map + snapshot changes together, using the - `create_pr` skill. Summarize remaining (deferred) findings in the PR body so - nothing is silently dropped. +6. **Route reviewers**: run `scripts/suggest_reviewers.py` (see Reviewer routing) + with the source files behind the addressed findings to resolve the owning + engineers for the PR. +7. **Open a PR** with the doc pages + map + snapshot changes together, using the + `create_pr` skill. Assign the reviewers from step 6 (`gh pr edit + --add-reviewer ...`), and summarize remaining (deferred) findings in the PR body + so nothing is silently dropped. Recommended scheduled-agent prompt (copy when setting up the agent): @@ -267,9 +297,11 @@ Recommended scheduled-agent prompt (copy when setting up the agent): > surface_changes and changelog_review findings plus high/medium coverage findings: > draft or update doc pages, update the surface map (mapping or ignore entry with a > comment) for every triaged flag, and use the sync-openapi-spec skill for API spec -> gaps. Regenerate the surface snapshot with --update-snapshot. Open a single PR with -> the doc pages, feature_surface_map.md, and surface_snapshot.json changes, and list -> any findings you deferred in the PR body. +> gaps. Regenerate the surface snapshot with --update-snapshot. Resolve reviewers by +> running scripts/suggest_reviewers.py against the source files behind each addressed +> finding. Open a single PR with the doc pages, feature_surface_map.md, and +> surface_snapshot.json changes, assign the resolved owners as reviewers, and list any +> findings you deferred in the PR body. ### Invocation modes @@ -299,4 +331,7 @@ The user can trigger any subset: `--diff`. Regenerate with `--update-snapshot`; never hand-edit. - `references/stale_terms.md` — renamed/removed-feature terms to flag during staleness audits. Pure terminology/style policing belongs to the `style_lint` skill. +- `scripts/suggest_reviewers.py` — resolves PR reviewers from the warp and warp-server + `.github/STAKEHOLDERS` and `CODEOWNERS` files (CODEOWNERS-format, last-match-wins), + given the source files behind each finding. Used by the drift-watch reviewer-routing step. - `AGENTS.md` (docs repo root) — full documentation style guide diff --git a/.agents/skills/missing_docs/scripts/suggest_reviewers.py b/.agents/skills/missing_docs/scripts/suggest_reviewers.py new file mode 100755 index 000000000..68ad5b48c --- /dev/null +++ b/.agents/skills/missing_docs/scripts/suggest_reviewers.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Suggest PR reviewers for missing_docs drift-watch changes. + +Every docs change the drift-watch flow makes traces back to a concrete source +surface (a feature flag, CLI command, API route, slash command, or setting). This +script maps those *source* files to the engineers who own them, using the +CODEOWNERS-format ownership files that already live in the code repos: + + - warp client repo : .github/STAKEHOLDERS (advisory, broad coverage) + - warp-server : .github/STAKEHOLDERS (advisory) + .github/CODEOWNERS (enforced) + +Those files are the source of truth for ownership (warp-server keeps STAKEHOLDERS +fresh via the `sync-stakeholders` skill), so this script never duplicates owner +lists — it just resolves against them with standard CODEOWNERS precedence +(last matching rule wins). + +Usage: + python3 suggest_reviewers.py \ + --warp ../warp --warp-server ../warp-server \ + warp:app/src/settings/ssh.rs \ + warp:app/src/search/slash_command_menu/static_commands/commands.rs \ + warp-server:router/handlers/public_api/runs.go + +Source paths may also be piped on stdin (one `repo:relpath` per line). `repo` +is `warp` (the client repo passed via --warp; `warp-internal` is accepted as an +alias) or `warp-server`. + +Output: a per-path resolution table, the deduped reviewer set (users and teams), +and a ready-to-run `gh pr edit --add-reviewer` snippet. Exit code is always 0; +unresolved paths are reported but never fatal (so a scheduled run is not blocked +by an ownership gap — it just falls back to the default owners or none). +""" + +import argparse +import fnmatch +import sys +from pathlib import Path + + +def parse_ownership(path): + """Parse a CODEOWNERS-format file into an ordered list of (pattern, [owners]).""" + rules = [] + if not path or not path.is_file(): + return rules + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + pattern = parts[0] + owners = [tok for tok in parts[1:] if tok.startswith("@")] + if owners: + rules.append((pattern, owners)) + return rules + + +def pattern_matches(pattern, rel_path): + """Practical CODEOWNERS matching for a repo-relative POSIX path.""" + pat = pattern.lstrip("/") # all our rules are root-anchored + p = rel_path.lstrip("/") + if pat in ("", "*", "**"): + return True # default fallback rule (e.g. `/ @org/team`) + if pat.endswith("/"): # directory prefix: matches the dir and everything under it + return p == pat[:-1] or p.startswith(pat) + if any(ch in pat for ch in "*?["): # glob pattern + return fnmatch.fnmatch(p, pat) or fnmatch.fnmatch(p, pat + "/*") + # bare path: exact file, or a directory given without a trailing slash + return p == pat or p.startswith(pat + "/") + + +def owners_for(rel_path, rules): + """Return (owners, matched_pattern) using last-match-wins precedence.""" + match = None + for pattern, owners in rules: + if pattern_matches(pattern, rel_path): + match = (owners, pattern) + return match if match else (None, None) + + +def main(): + ap = argparse.ArgumentParser(description="Suggest PR reviewers from code ownership.") + ap.add_argument("--warp", help="Path to the warp client repo root (warp-internal accepted).") + ap.add_argument("--warp-server", dest="warp_server", help="Path to the warp-server repo root.") + ap.add_argument("paths", nargs="*", help="Source paths as repo:relpath.") + args = ap.parse_args() + + # Build per-repo rule lists (STAKEHOLDERS first, then CODEOWNERS so enforced + # rules take precedence as later matches). + repos = {} + if args.warp: + root = Path(args.warp) + repos["warp"] = parse_ownership(root / ".github" / "STAKEHOLDERS") + parse_ownership( + root / ".github" / "CODEOWNERS" + ) + repos["warp-internal"] = repos["warp"] # alias + if args.warp_server: + root = Path(args.warp_server) + repos["warp-server"] = parse_ownership(root / ".github" / "STAKEHOLDERS") + parse_ownership( + root / ".github" / "CODEOWNERS" + ) + + inputs = list(args.paths) + if not sys.stdin.isatty(): + inputs += [ln.strip() for ln in sys.stdin if ln.strip()] + + if not inputs: + print("No source paths given. Pass repo:relpath args or pipe them on stdin.", file=sys.stderr) + return 0 + + users, teams = [], [] + unresolved = [] + print("Reviewer resolution:") + for item in inputs: + if ":" not in item: + unresolved.append(item) + print(f" ? {item} — missing repo prefix (use warp: or warp-server:)") + continue + repo, rel = item.split(":", 1) + rules = repos.get(repo) + if rules is None: + unresolved.append(item) + print(f" ? {item} — no ownership file loaded for repo '{repo}'") + continue + owners, pattern = owners_for(rel, rules) + if not owners: + unresolved.append(item) + print(f" ? {repo}:{rel} — no owner match") + continue + print(f" - {repo}:{rel} -> {' '.join(owners)} (matched: {pattern})") + for o in owners: + handle = o.lstrip("@") + bucket = teams if "/" in handle else users + if handle not in bucket: + bucket.append(handle) + + print() + print(f"Reviewers (users): {', '.join(users) if users else '(none)'}") + print(f"Reviewers (teams): {', '.join(teams) if teams else '(none)'}") + if unresolved: + print(f"Unresolved paths: {len(unresolved)} (left for manual assignment)") + + # gh accepts users by login and teams as org/team; both via --add-reviewer. + review_args = users + teams + if review_args: + joined = ",".join(review_args) + print() + print("Suggested command (replace with the PR number):") + print(f" gh pr edit --add-reviewer {joined}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 5863b8c917ca0387c65091e4a23b572080721f46 Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 21:46:33 +0000 Subject: [PATCH 07/15] docs(missing_docs): address review comments - SKILL.md: Phase 3 step 7 now points to src/sidebar.ts (the real sidebar source the structure audit checks); astro.config.mjs only for a new top-level topic. - all-settings.mdx: correct code.editor.show_hidden_files default to `true` (matches the client setting definition). - surface_snapshot.json: regenerate so it includes the newly documented settings (code.editor.format_on_save, appearance.icon.show_dock_icon, agents.warp_agent.other.long_running_command_submission_mode, warpify.ssh.reuse_existing_control_master, cloud_platform.third_party_api_keys.gemini_enterprise_credentials_enabled) and current flag/CLI/API state. Audit: exit 0, audits_skipped none, unaccounted none. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 2 +- .../references/surface_snapshot.json | 88 +++++++++++++++++-- .../docs/terminal/settings/all-settings.mdx | 2 +- 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 0f91d2e61..edf28e15c 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -209,7 +209,7 @@ For each gap to address (prioritize high → medium → low): - Correct terminology (Agent, Agent Mode, Warp Drive, Oz, etc.) - Bold + dash format for list items: `* **Term** - Description` 6. Create the markdown file at the suggested path -7. Add new pages to the sidebar config in `astro.config.mjs` +7. Add new pages to the sidebar in `src/sidebar.ts` (only a brand-new top-level topic also needs an `astro.config.mjs` change) 8. **Update `references/feature_surface_map.md` in the same PR**: add a `Flag -> src/content/docs/...` mapping for every feature you documented (or add the flag to the ignore list with a comment if you confirmed it is internal-only). This diff --git a/.agents/skills/missing_docs/references/surface_snapshot.json b/.agents/skills/missing_docs/references/surface_snapshot.json index 46d4a0cd0..22e157e43 100644 --- a/.agents/skills/missing_docs/references/surface_snapshot.json +++ b/.agents/skills/missing_docs/references/surface_snapshot.json @@ -39,7 +39,7 @@ "AmbientAgentsRTC": "ga", "ArtifactCommand": "ga", "AskUserQuestion": "ga", - "AsyncFind": "dogfood", + "AsyncFind": "preview", "AtMenuOutsideOfAIMode": "ga", "AutoOpenCodeReviewPane": "ga", "Autoupdate": "ga", @@ -64,6 +64,7 @@ "CloudModeInputV2": "ga", "CloudModeSetupV2": "ga", "CloudObjects": "other", + "CloudRunners": "dogfood", "CocoaSentry": "other", "CodeFindReplace": "ga", "CodeLaunchModal": "ga", @@ -74,13 +75,14 @@ "CodebaseIndexPersistence": "dogfood", "CodebaseIndexSpeedbump": "dogfood", "CodexNotifications": "ga", - "CodexPlugin": "dogfood", + "CodexPlugin": "ga", "CommandCorrectionKey": "ga", "CommandCorrectionsHistoryRule": "other", "CommandPaletteFileSearch": "ga", "ConfigurableToolbar": "ga", "ContextChips": "other", "ContextLineReviewComments": "dogfood", + "ContextWindowUsageBreakdown": "dogfood", "ContextWindowUsageV2": "ga", "ConversationApi": "ga", "ConversationArtifacts": "ga", @@ -92,6 +94,7 @@ "CrossRepoContext": "dogfood", "CustomInferenceEndpoints": "ga", "CustomInferenceEndpointsEnterprise": "other", + "CustomModelRouters": "ga", "CycleNextCommandSuggestion": "other", "DebugMode": "other", "DefaultAdeberryTheme": "other", @@ -118,10 +121,10 @@ "ForceClassicCompletions": "ga", "ForceLogin": "other", "ForkFromCommand": "ga", - "FreeUserNoAi": "other", "FullScreenZenMode": "ga", "FullSourceCodeEmbedding": "dogfood", "GPTConfigurableContextWindow": "dogfood", + "GeminiEnterprise": "other", "GeminiNotifications": "dogfood", "GetStartedTab": "ga", "GitCredentialRefresh": "ga", @@ -195,11 +198,12 @@ "PRCommentsV2": "ga", "PartialNextCommandSuggestions": "other", "PendingUserQueryIndicator": "ga", - "PinnedTabs": "other", + "PinnedTabs": "dogfood", "PluggableNotifications": "ga", "PredictAMQueries": "other", "ProfilesDesignRevamp": "ga", "Projects": "dogfood", + "PromptCacheExpiryWarning": "dogfood", "PromptSuggestionsViaMAA": "other", "ProviderCommand": "dogfood", "QueueSlashCommand": "ga", @@ -223,7 +227,6 @@ "RunAgentsTool": "ga", "RunGeneratorsWithCmdExe": "dogfood", "RuntimeFeatureFlags": "other", - "SSHTmuxWrapper": "dogfood", "ScheduledAmbientAgents": "ga", "SearchCodebaseUI": "ga", "SelectablePrompt": "other", @@ -267,14 +270,14 @@ "VerticalTabsSummaryMode": "ga", "ViewingSharedSessions": "ga", "VimCodeEditor": "ga", - "WarpControlCli": "other", + "WaitForEventsParentRegistration": "dogfood", + "WarpControlCli": "dogfood", "WarpManagedSecrets": "ga", "WarpPacks": "ga", "WarpifyFooter": "ga", "WebFetchUI": "ga", "WebSearchUI": "ga", "WelcomeBlock": "other", - "WelcomeTab": "other", "WelcomeTips": "other", "WithSandboxTelemetry": "other", "WorkflowAliases": "ga" @@ -456,6 +459,50 @@ "command": "oz mcp list", "hidden": false }, + { + "command": "oz memory", + "hidden": false + }, + { + "command": "oz memory create", + "hidden": false + }, + { + "command": "oz memory delete", + "hidden": false + }, + { + "command": "oz memory list", + "hidden": false + }, + { + "command": "oz memory update", + "hidden": false + }, + { + "command": "oz memory versions", + "hidden": false + }, + { + "command": "oz memory-store", + "hidden": false + }, + { + "command": "oz memory-store get", + "hidden": false + }, + { + "command": "oz memory-store list", + "hidden": false + }, + { + "command": "oz memory-store list-store-agents", + "hidden": false + }, + { + "command": "oz memory-store update", + "hidden": false + }, { "command": "oz model", "hidden": false @@ -606,6 +653,7 @@ "--remove-secret", "--remove-skill", "--repo", + "--runner", "--saved-prompt", "--secret", "--skill", @@ -652,6 +700,13 @@ "--prompt", "--remove-mcp" ], + "memory_store": [ + "--content", + "--description", + "--reason", + "--store", + "--version" + ], "model": [ "--model" ], @@ -710,6 +765,7 @@ "api_routes": [ "DELETE /api/v1/agent/identities/{uid}", "DELETE /api/v1/agent/schedules/{id}", + "DELETE /api/v1/factory/{uid}", "DELETE /api/v1/memory_stores/{uid}", "DELETE /api/v1/memory_stores/{uid}/memories/{memoryUid}", "GET /.well-known/openid-configuration", @@ -735,6 +791,8 @@ "GET /api/v1/agent/sessions/{session_uuid}/redirect", "GET /api/v1/agent/tasks", "GET /api/v1/agent/tasks/{id}", + "GET /api/v1/factory", + "GET /api/v1/factory/{uid}", "GET /api/v1/harness-support/transcript", "GET /api/v1/memory_stores", "GET /api/v1/memory_stores/{uid}", @@ -744,6 +802,7 @@ "GET /api/v1/oauth/authorize", "GET /api/v1/oauth/jwks.json", "PATCH /api/v1/agent/runs/{runId}/event-sequence", + "PATCH /api/v1/factory/{uid}", "POST /api/v1/agent/events/{run_id}", "POST /api/v1/agent/handoff/upload-snapshot", "POST /api/v1/agent/identities", @@ -761,6 +820,7 @@ "POST /api/v1/agent/schedules/{id}/pause", "POST /api/v1/agent/schedules/{id}/resume", "POST /api/v1/agent/tasks/{id}/cancel", + "POST /api/v1/factory", "POST /api/v1/harness-support/block-snapshot", "POST /api/v1/harness-support/external-conversation", "POST /api/v1/harness-support/finish-task", @@ -870,6 +930,7 @@ "agents.warp_agent.other.auto_handoff_on_sleep_enabled": "always_on", "agents.warp_agent.other.cloud_agent_computer_use_enabled": "always_on", "agents.warp_agent.other.default_prompt_submission_mode": "ga", + "agents.warp_agent.other.long_running_command_submission_mode": "ga", "agents.warp_agent.other.open_conversation_layout_preference": "always_on", "agents.warp_agent.other.orchestration_message_display_mode": "always_on", "agents.warp_agent.other.should_force_disable_ampersand_handoff": "always_on", @@ -888,6 +949,7 @@ "appearance.cursor.cursor_display_type": "always_on", "appearance.full_screen_apps.alt_screen_padding": "always_on", "appearance.icon.app_icon": "always_on", + "appearance.icon.show_dock_icon": "always_on", "appearance.input.input_mode": "always_on", "appearance.panes.focus_pane_on_hover": "always_on", "appearance.panes.should_dim_inactive_panes": "always_on", @@ -937,7 +999,9 @@ "cloud_platform.third_party_api_keys.aws_bedrock_credentials_enabled": "always_on", "cloud_platform.third_party_api_keys.aws_bedrock_profile": "always_on", "cloud_platform.third_party_api_keys.can_use_warp_credits_with_byok": "always_on", + "cloud_platform.third_party_api_keys.gemini_enterprise_credentials_enabled": "always_on", "code.editor.auto_open_code_review_pane_on_first_agent_change": "always_on", + "code.editor.format_on_save": "always_on", "code.editor.open_code_panels_file_editor": "always_on", "code.editor.open_file_editor": "always_on", "code.editor.open_file_layout": "always_on", @@ -1025,8 +1089,10 @@ "warp_drive.sorting_choice": "always_on", "warpify.ssh.enable_legacy_ssh_wrapper": "always_on", "warpify.ssh.enable_ssh_warpification": "always_on", + "warpify.ssh.reuse_existing_control_master": "always_on", "warpify.ssh.ssh_extension_install_mode": "always_on", "warpify.ssh.ssh_hosts_denylist": "always_on", + "warpify.ssh.ssh_tmux_deprecation_notice_pending": "always_on", "warpify.ssh.use_ssh_tmux_wrapper": "always_on", "warpify.subshells.added_subshell_commands": "always_on", "warpify.subshells.subshell_commands_denylist": "always_on", @@ -1035,6 +1101,7 @@ "web_routes": [ ":agentId", ":envId", + ":managedMcpServerId", ":runId", ":scheduleId", ":secretId", @@ -1090,6 +1157,7 @@ "mark_todo_as_done", "notify_user", "open_code_review", + "openai_web_search", "read_executed_shell_command_output", "read_files", "read_mcp_resource", @@ -1118,6 +1186,7 @@ "transfer_control_to_user", "upload_artifact", "wait_for_events", + "web_search", "write_block", "write_line", "write_raw_bytes" @@ -1135,7 +1204,8 @@ "test-warp-ui": "dogfood", "triage-vulnerabilities": "dogfood", "update-tab-config": "bundled", - "verify-ui-change-in-cloud": "dogfood" + "verify-ui-change-in-cloud": "dogfood", + "warpctrl": "bundled" }, - "changelog_last_version": "2026.06.03" + "changelog_last_version": "2026.06.24" } diff --git a/src/content/docs/terminal/settings/all-settings.mdx b/src/content/docs/terminal/settings/all-settings.mdx index cf83620f7..c3c11b081 100644 --- a/src/content/docs/terminal/settings/all-settings.mdx +++ b/src/content/docs/terminal/settings/all-settings.mdx @@ -373,7 +373,7 @@ Settings for Warp's built-in code editor, file handling, and codebase indexing. * `show_code_review_diff_stats` — Whether to show lines added/removed counts on the code review button. Type: boolean. Default: `true`. * `show_project_explorer` — Whether the project explorer is shown in the tools panel. Type: boolean. Default: `true`. * `show_global_search` — Whether global file search is shown in the tools panel. Type: boolean. Default: `true`. -* `show_hidden_files` — Whether hidden files (dotfiles) are shown in the project explorer. Type: boolean. Default: `false`. +* `show_hidden_files` — Whether hidden files (dotfiles) are shown in the project explorer. Type: boolean. Default: `true`. * `use_warp_as_default_editor` — Whether Warp is used as the default code editor. Type: boolean. Default: `false`. * `format_on_save` — Whether the language server automatically formats the file on save. Other LSP features (hover, go-to-definition, references, diagnostics) are unaffected. Type: boolean. Default: `true`. From 94fa141efbc0631bf1ac4306934616ad0e62f2b5 Mon Sep 17 00:00:00 2001 From: Hong Yi Chen Date: Tue, 30 Jun 2026 14:55:12 -0700 Subject: [PATCH 08/15] demo(missing_docs): CLI drift burn-down sample (api-key / schedule / secret) (#278) Sample output of one missing_docs drift-watch pass over the CLI backlog, to evaluate the skill's efficacy. Resolves 14 of 31 undocumented CLI commands: - Draft: document the `oz api-key list/create/expire` subcommands in the CLI reference (reference/cli/api-keys.mdx), with flags/args extracted from crates/warp_cli/src/api_key.rs. - Map (no duplication): `oz schedule *` and `oz secret *` subcommands are already documented in their feature pages, so add surface-map entries pointing there instead of re-drafting. Reviewer routing (suggest_reviewers.py): crates/warp_cli ownership -> @bnavetta, @ianhodge. Audit after: CLI findings 31 -> 17, exit 0, unaccounted none. Co-authored-by: Oz --- .../references/feature_surface_map.md | 16 +++++++ src/content/docs/reference/cli/api-keys.mdx | 44 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index 45bcddd3c..d97ad5884 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -204,6 +204,22 @@ oz provider -> src/content/docs/reference/cli/index.mdx oz federate -> src/content/docs/reference/cli/federate.mdx oz artifact -> src/content/docs/reference/cli/artifacts.mdx oz api-key -> src/content/docs/reference/cli/api-keys.mdx + +# Scheduled-agent CLI subcommands are fully documented in the scheduled-agents feature page. +oz schedule create -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule list -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule get -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule update -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule pause -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule unpause -> src/content/docs/platform/triggers/scheduled-agents.mdx +oz schedule delete -> src/content/docs/platform/triggers/scheduled-agents.mdx + +# Warp-managed secret CLI subcommands are documented in the secrets feature page. +oz secret create -> src/content/docs/platform/secrets.mdx +oz secret list -> src/content/docs/platform/secrets.mdx +oz secret update -> src/content/docs/platform/secrets.mdx +oz secret delete -> src/content/docs/platform/secrets.mdx + # Internal/hidden command — not a user-facing surface, so no public docs. oz harness-support -> internal diff --git a/src/content/docs/reference/cli/api-keys.mdx b/src/content/docs/reference/cli/api-keys.mdx index 06d2b44ab..dd998e131 100644 --- a/src/content/docs/reference/cli/api-keys.mdx +++ b/src/content/docs/reference/cli/api-keys.mdx @@ -104,6 +104,50 @@ To delete an API key, find it in either the Oz web app or the Warp app's API Key Deleted keys are immediately invalidated and cannot be recovered. Any services or scripts using the deleted key will lose access and may return an [`authentication_required` error](/reference/api-and-sdk/troubleshooting/errors/authentication-required/). +## Manage API keys from the CLI + +In addition to the web and Warp app surfaces, you can manage API keys directly with the [Oz CLI](/reference/cli/). These commands are useful for scripting key rotation and for headless environments. + +### List keys + +`oz api-key list` prints your active API keys. + +* `--sort-by ` — Sort by `name`, `created-at`, `last-used-at`, `expires-at`, or `scope`. +* `--sort-order ` — Sort direction: `asc` or `desc`. +* `--jq ` — Filter the JSON output with a jq expression (for example, `--jq '.[].name'`). + +```bash +oz api-key list --sort-by last-used-at --sort-order desc +``` + +### Create a key + +`oz api-key create ` creates a key and prints the raw secret once. Store it securely — you can't retrieve it again. + +* `` — A name to identify the key (required). +* `--agent ` — Create an agent key that runs as the given cloud agent, instead of a personal key. +* One expiration choice is required: `--expires-in ` (for example, `30d`, `12h`, or `90m`), `--expires-at ` (an exact timestamp), or `--no-expiration`. +* `--jq ` — Filter the JSON output with a jq expression. + +```bash +# A personal key that expires in 30 days +oz api-key create "ci-pipeline" --expires-in 30d + +# An agent key scoped to a cloud agent, with no expiration +oz api-key create "nightly-triage" --agent AGENT_UID --no-expiration +``` + +### Expire a key + +`oz api-key expire ` immediately invalidates a key (also available as `oz api-key delete`). Expired keys can't be recovered. + +* `` — The name or UID of the key to expire (required). +* `--force` — Skip the confirmation prompt. + +```bash +oz api-key expire "ci-pipeline" --force +``` + ## Best practices * **Use personal keys for runs that should act as you.** When code changes should be attributed to your GitHub account, a personal key is the right choice. Use agent keys for automation that isn't tied to a specific user. From 9af8fde07434e07f461e2c84e7069a380d6219c2 Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 21:55:53 +0000 Subject: [PATCH 09/15] test(missing_docs): add stdlib test suite for the skill scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_suggest_reviewers.py: 15 unit tests for reviewer resolution (CODEOWNERS matching incl. anchored dir-prefix / exact-file / glob / default rule, last-match-wins precedence, user vs team split, dedup, unresolved paths, warp-internal alias, stdin) — all via temp ownership files. - test_audit_docs.py: 6 behavioral integration tests that run audit_docs.py against the sibling code repos — clean exit + completeness accounting (unaccounted empty), --category scoping, --severity filtering, fail-loud (exit 2) on a missing repo, committed-snapshot currency, and that --update-snapshot honors --snapshot without mutating the committed snapshot. Skips gracefully when the code repos aren't checked out. Both suites use only the Python stdlib (unittest) — no third-party deps. 21/21 pass. Documented under a new "## Tests" section in SKILL.md. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 12 ++ .../missing_docs/scripts/test_audit_docs.py | 153 +++++++++++++++ .../scripts/test_suggest_reviewers.py | 174 ++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100755 .agents/skills/missing_docs/scripts/test_audit_docs.py create mode 100755 .agents/skills/missing_docs/scripts/test_suggest_reviewers.py diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index edf28e15c..4bf268562 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -320,6 +320,18 @@ The user can trigger any subset: - For API docs: include request/response schemas, auth requirements, curl examples - Use `codebase_semantic_search` and `grep` on source repos for technical accuracy +## Tests + +The skill's scripts have a stdlib-only test suite (no third-party dependencies): + +```bash +python3 .agents/skills/missing_docs/scripts/test_suggest_reviewers.py +python3 .agents/skills/missing_docs/scripts/test_audit_docs.py +``` + +- `test_suggest_reviewers.py` unit-tests reviewer resolution (CODEOWNERS matching, last-match-wins, user/team split, dedup, unresolved paths). +- `test_audit_docs.py` runs behavioral checks against the sibling code repos — clean exit, completeness accounting (`unaccounted` empty), category/severity scoping, fail-loud (exit 2) on a missing repo, and snapshot round-trip — and skips gracefully when those repos aren't checked out. + ## References - `references/feature_surface_map.md` — curated mapping of flags/commands/routes/slash diff --git a/.agents/skills/missing_docs/scripts/test_audit_docs.py b/.agents/skills/missing_docs/scripts/test_audit_docs.py new file mode 100755 index 000000000..ceeebe331 --- /dev/null +++ b/.agents/skills/missing_docs/scripts/test_audit_docs.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Integration tests for audit_docs.py. + +These run the audit as a subprocess against the sibling code repos (warp client + +warp-server) and assert behavioral invariants: clean exit, completeness +accounting totality, category/severity scoping, fail-loud on a missing repo, and +that --update-snapshot honors --snapshot without mutating the committed snapshot. + +Tests are skipped (not failed) when the sibling code repos aren't checked out, so +the suite is safe to run anywhere. + +Run with: python3 .agents/skills/missing_docs/scripts/test_audit_docs.py +(stdlib unittest only; no third-party deps). +""" + +import hashlib +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +_AUDIT = _HERE / "audit_docs.py" +_DOCS_ROOT = _HERE.parents[3] # scripts -> missing_docs -> skills -> .agents -> docs +_DEFAULT_SNAPSHOT = _HERE.parent / "references" / "surface_snapshot.json" +_SIBLINGS = _DOCS_ROOT.parent + + +def _find_warp(): + for name in ("warp", "warp-internal"): + if (_SIBLINGS / name / ".github").exists() or (_SIBLINGS / name / "app").exists(): + return _SIBLINGS / name + return None + + +def _find_server(): + p = _SIBLINGS / "warp-server" + return p if p.exists() else None + + +WARP = _find_warp() +SERVER = _find_server() +_REPOS_AVAILABLE = WARP is not None and SERVER is not None + + +def _run_audit(extra_args, capture_report=True): + """Run audit_docs.py; return (returncode, report_dict_or_None).""" + out_path = None + args = [sys.executable, str(_AUDIT), "--warp", str(WARP), "--warp-server", str(SERVER)] + if capture_report: + out_path = Path(tempfile.mkstemp(suffix=".json")[1]) + args += ["--output", str(out_path)] + args += extra_args + proc = subprocess.run(args, capture_output=True, text=True, stdin=subprocess.DEVNULL) + report = None + if capture_report and out_path and out_path.exists() and out_path.stat().st_size > 0: + try: + report = json.loads(out_path.read_text()) + except json.JSONDecodeError: + report = None + return proc.returncode, report, proc.stderr + + +def _sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +@unittest.skipUnless(_REPOS_AVAILABLE, "warp/warp-server repos not checked out as siblings") +class TestAuditBehavior(unittest.TestCase): + def test_full_run_is_clean_and_accounts_for_everything(self): + rc, report, stderr = _run_audit([]) + self.assertEqual(rc, 0, f"audit should exit 0 on a healthy run; stderr={stderr}") + self.assertIsNotNone(report, "audit should emit a JSON report") + summary = report["summary"] + self.assertEqual(summary.get("audits_skipped"), [], "no audits should be skipped") + self.assertEqual( + summary["accounting"].get("unaccounted"), {}, "every surface must be accounted for" + ) + + def test_category_scopes_to_one_audit(self): + rc, report, stderr = _run_audit(["--category", "settings"]) + self.assertEqual(rc, 0, stderr) + audits_run = report["summary"].get("audits_run", []) + self.assertIn("settings", audits_run) + self.assertNotIn("cli", audits_run) + # CLI category did not run, so its findings should be absent/zero. + self.assertEqual(report["summary"]["by_category"].get("undocumented_cli_commands", 0), 0) + + def test_severity_filter_excludes_lower_severities(self): + rc, report, _ = _run_audit(["--severity", "high"]) + self.assertEqual(rc, 0) + bad = [] + for key, value in report.items(): + if isinstance(value, list): + for item in value: + if isinstance(item, dict) and item.get("severity") in ("low", "medium"): + bad.append((key, item.get("severity"))) + self.assertEqual(bad, [], f"--severity high must drop low/medium findings, found: {bad[:5]}") + + def test_fail_loud_on_missing_repo(self): + # Point --warp at a nonexistent path; the script must exit 2, not pretend "no gaps". + out_path = Path(tempfile.mkstemp(suffix=".json")[1]) + proc = subprocess.run( + [ + sys.executable, + str(_AUDIT), + "--warp", + str(_SIBLINGS / "definitely-not-a-real-repo"), + "--warp-server", + str(SERVER), + "--output", + str(out_path), + ], + capture_output=True, + text=True, + stdin=subprocess.DEVNULL, + ) + self.assertEqual(proc.returncode, 2, f"missing repo must exit 2; stderr={proc.stderr}") + + def test_diff_against_committed_snapshot_is_current(self): + # The committed snapshot should reflect current code (no pending surface drift). + rc, report, stderr = _run_audit(["--diff"]) + self.assertEqual(rc, 0, stderr) + self.assertEqual( + report["summary"]["by_category"].get("surface_changes", 0), + 0, + "committed snapshot is stale; regenerate with --update-snapshot", + ) + + def test_update_snapshot_respects_snapshot_flag_and_roundtrips(self): + before = _sha(_DEFAULT_SNAPSHOT) + with tempfile.TemporaryDirectory() as d: + tmp_snap = Path(d) / "snap.json" + # Regenerate into the temp path (must NOT touch the committed snapshot). + rc, _, stderr = _run_audit( + ["--update-snapshot", "--snapshot", str(tmp_snap)], capture_report=False + ) + self.assertEqual(rc, 0, stderr) + self.assertTrue(tmp_snap.exists() and tmp_snap.stat().st_size > 0, + "--update-snapshot should write to the --snapshot path") + self.assertEqual( + _sha(_DEFAULT_SNAPSHOT), before, "--update-snapshot must not mutate the committed snapshot" + ) + # Diffing current code against the just-generated snapshot shows no drift. + rc2, report2, _ = _run_audit(["--diff", "--snapshot", str(tmp_snap)]) + self.assertEqual(rc2, 0) + self.assertEqual(report2["summary"]["by_category"].get("surface_changes", 0), 0) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/.agents/skills/missing_docs/scripts/test_suggest_reviewers.py b/.agents/skills/missing_docs/scripts/test_suggest_reviewers.py new file mode 100755 index 000000000..c03f6bb60 --- /dev/null +++ b/.agents/skills/missing_docs/scripts/test_suggest_reviewers.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Unit tests for suggest_reviewers.py. + +Run with: python3 .agents/skills/missing_docs/scripts/test_suggest_reviewers.py +(stdlib unittest only; no third-party deps). +""" + +import importlib.util +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +_MODULE_PATH = _HERE / "suggest_reviewers.py" + +_spec = importlib.util.spec_from_file_location("suggest_reviewers", _MODULE_PATH) +sr = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(sr) + + +class TestPatternMatches(unittest.TestCase): + def test_anchored_dir_prefix(self): + self.assertTrue(sr.pattern_matches("/app/src/settings/", "app/src/settings/ssh.rs")) + # The directory path itself (no trailing slash on the candidate) matches. + self.assertTrue(sr.pattern_matches("/app/src/settings/", "app/src/settings")) + + def test_dir_prefix_does_not_match_sibling_prefix(self): + # "settingsX" must NOT match the "settings/" directory rule. + self.assertFalse(sr.pattern_matches("/app/src/settings/", "app/src/settingsX/y.rs")) + + def test_exact_file(self): + self.assertTrue(sr.pattern_matches("/app/src/tab.rs", "app/src/tab.rs")) + self.assertFalse(sr.pattern_matches("/app/src/tab.rs", "app/src/tab.rs.bak")) + self.assertFalse(sr.pattern_matches("/app/src/tab.rs", "app/src/other.rs")) + + def test_dir_without_trailing_slash(self): + # A bare path can match a file under it (treated as a directory). + self.assertTrue(sr.pattern_matches("/app/src/code", "app/src/code/file_tree.rs")) + self.assertTrue(sr.pattern_matches("/app/src/code", "app/src/code")) + self.assertFalse(sr.pattern_matches("/app/src/code", "app/src/codex/y.rs")) + + def test_glob(self): + self.assertTrue(sr.pattern_matches("*.md", "README.md")) + self.assertFalse(sr.pattern_matches("*.md", "main.rs")) + + def test_default_root_rule(self): + # `/ @team` is the catch-all fallback and matches anything. + self.assertTrue(sr.pattern_matches("/", "literally/anything/here")) + + +class TestOwnersFor(unittest.TestCase): + def setUp(self): + self.rules = [ + ("/", ["@warpdotdev/oss-maintainers"]), + ("/app/src/settings/", ["@lucie"]), + ("/app/src/search/slash_command_menu/", ["@moira"]), + ("/app/src/search/slash_command_menu/static_commands/commands.rs", ["@lucie2"]), + ] + + def test_last_match_wins_specific_over_broad(self): + owners, pat = sr.owners_for( + "app/src/search/slash_command_menu/static_commands/commands.rs", self.rules + ) + self.assertEqual(owners, ["@lucie2"]) + self.assertEqual(pat, "/app/src/search/slash_command_menu/static_commands/commands.rs") + + def test_dir_match(self): + owners, _ = sr.owners_for("app/src/search/slash_command_menu/menu.rs", self.rules) + self.assertEqual(owners, ["@moira"]) + + def test_settings_dir_match(self): + owners, _ = sr.owners_for("app/src/settings/ssh.rs", self.rules) + self.assertEqual(owners, ["@lucie"]) + + def test_fallback_to_default(self): + owners, pat = sr.owners_for("crates/warp_features/src/lib.rs", self.rules) + self.assertEqual(owners, ["@warpdotdev/oss-maintainers"]) + self.assertEqual(pat, "/") + + def test_no_rules_returns_none(self): + self.assertEqual(sr.owners_for("anything", []), (None, None)) + + +class TestParseOwnership(unittest.TestCase): + def test_parses_and_skips_comments_blanks(self): + with tempfile.TemporaryDirectory() as d: + f = Path(d) / "STAKEHOLDERS" + f.write_text( + "# a comment\n" + "\n" + "/app/ @alice @bob\n" + "/lib/ @warpdotdev/team\n" + "/no-owner-here/\n", # line without owners is ignored + encoding="utf-8", + ) + rules = sr.parse_ownership(f) + self.assertEqual(rules, [("/app/", ["@alice", "@bob"]), ("/lib/", ["@warpdotdev/team"])]) + + def test_missing_file_returns_empty(self): + self.assertEqual(sr.parse_ownership(Path("/nope/does/not/exist")), []) + + +class TestMainCLI(unittest.TestCase): + """End-to-end test of the script: ownership files -> deduped reviewers + teams.""" + + def _make_repo(self, root, stakeholders): + gh = Path(root) / ".github" + gh.mkdir(parents=True, exist_ok=True) + (gh / "STAKEHOLDERS").write_text(stakeholders, encoding="utf-8") + + def test_resolution_dedup_and_team_split(self): + with tempfile.TemporaryDirectory() as d: + warp = Path(d) / "warp" + server = Path(d) / "warp-server" + self._make_repo( + warp, + "/ @warpdotdev/oss-maintainers\n" + "/app/src/settings/ @lucie\n" + "/app/src/ai/agent/ @zach\n", + ) + self._make_repo(server, "/router/ @ian\n") + result = subprocess.run( + [ + sys.executable, + str(_MODULE_PATH), + "--warp", + str(warp), + "--warp-server", + str(server), + "warp:app/src/settings/ssh.rs", + "warp:app/src/settings/code.rs", # same owner -> dedup + "warp:app/src/ai/agent/api.rs", + "warp:crates/warp_features/src/lib.rs", # default team fallback + "warp-server:router/handlers/x.go", + "warp-server:nope/unmatched.go", # no match -> unresolved + ], + capture_output=True, + text=True, + stdin=subprocess.DEVNULL, + ) + out = result.stdout + self.assertEqual(result.returncode, 0, result.stderr) + # Users deduped (lucie once) and ordered; teams separated. + self.assertIn("Reviewers (users): lucie, zach, ian", out) + self.assertIn("Reviewers (teams): warpdotdev/oss-maintainers", out) + # gh snippet present. + self.assertIn("--add-reviewer lucie,zach,ian,warpdotdev/oss-maintainers", out) + # The unmatched server path is reported, not fatal. + self.assertIn("no owner match", out) + + def test_warp_internal_alias(self): + with tempfile.TemporaryDirectory() as d: + warp = Path(d) / "warp" + self._make_repo(warp, "/app/ @alice\n") + result = subprocess.run( + [ + sys.executable, + str(_MODULE_PATH), + "--warp", + str(warp), + "warp-internal:app/x.rs", # alias should resolve against --warp + ], + capture_output=True, + text=True, + stdin=subprocess.DEVNULL, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Reviewers (users): alice", result.stdout) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 80e107f28d8249f1eaf7b1863e6fbe32bbf75930 Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 22:02:27 +0000 Subject: [PATCH 10/15] docs(missing_docs): encode public vs. private surface boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make explicit that the skill must only document publicly released surfaces: - New "Public vs. private surfaces" section in SKILL.md: the OSS warp client repo is public; warp-server is a PRIVATE repo whose only public surface is the released Oz Agent API already in the OpenAPI spec. Two gates (source/exposure + GA rollout); never document private or unreleased surfaces. - Woven into the API audit description, Phase 3 API-gap research, and Resolution patterns: warp-server endpoints not in the released spec are not auto- documentable — route released ones via sync-openapi-spec, else `-> internal`/defer. Apply the rule to detected drift: Agent Memory is research preview, so its `oz memory*` / `oz memory-store*` CLI and `/memory_stores/*` REST API are mapped `-> internal` with comments. Added a public/private POLICY note to the surface map's API section. Audit after: CLI 17->15, API 29->18, audits_skipped none, unaccounted none. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 20 +++++++++-- .../references/feature_surface_map.md | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 4bf268562..782cbcdc9 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -31,6 +31,17 @@ audits in the report's `audits_skipped` field (`extraction:*` entries identify broken parsers). Never treat an exit-2 run as a clean audit — fix the problem and re-run. Exit 0 means all requested audits ran (findings may still exist). +## Public vs. private surfaces (what you may document) + +Only document surfaces that are **publicly released**. This is the most important guardrail in this skill: do not reveal private or unreleased surfaces in public docs. Two independent gates, both required: + +1. **Source / exposure.** The OSS warp client repo ([warpdotdev/warp](https://github.com/warpdotdev/warp); locally `warp`, or the `warp-internal` fallback) is public — its feature flags, CLI commands, settings, and slash commands are documentable. **`warp-server` is a private repo** and is not public until released; its source and most of its surfaces must NOT be documented. The one exception is the public **Oz Agent API**, whose released surface is exactly the set of endpoints already present in the OpenAPI spec (`developers/agent-api-openapi.yaml`). +2. **Rollout status.** Even for public-repo surfaces, only document **GA** features. Never document dogfood, preview, or research-preview surfaces (for example, Agent Memory is research preview, so its `oz memory*` CLI and `/memory_stores` API must not be documented yet). + +Rules of thumb: +- A `warp-server` API endpoint that is **not already in the OpenAPI spec** is treated as not-yet-public: do NOT hand-write docs for it. Either confirm it has been publicly released and let the `sync-openapi-spec` skill bring it into the spec, or map it `-> internal` / defer it. When unsure, defer — never expose an unreleased endpoint or feature in public docs. +- The audit still *detects* these as gaps (useful signal), but detection is not permission to document. Every resolution must respect this boundary. + ## Workflows ### Phase 1: Audit (coverage) @@ -71,7 +82,11 @@ The script performs these coverage audits: `developers/agent-api-openapi.yaml` (param-name-insensitive: `{runId}` matches `{run_id}`) and the API reference docs. For spec drift, run the docs `sync-openapi-spec` skill (or warp-server's `update-open-api-spec`) instead of - hand-editing the YAML. + hand-editing the YAML. **warp-server is private** (see Public vs. private + surfaces): a flagged endpoint is documentable only once it is part of the + released public Oz Agent API. Never hand-draft API docs or reveal an unreleased + endpoint — resolve released endpoints via `sync-openapi-spec`, and `-> internal`/ + defer the rest. 4. **Slash command coverage** — parses the static registry in the warp client repo's `app/src/search/slash_command_menu/static_commands/` and checks each `/command` is mentioned in docs. @@ -199,7 +214,7 @@ For each gap to address (prioritize high → medium → low): 4. Research the relevant source code: - **Feature gaps** → read the implementation in the warp client repo's `app/src/`, check UI code, settings, user-facing strings - **CLI gaps** → read command definition in `crates/warp_cli/src/`, extract flags, arguments, help text - - **API gaps** → read handler in warp-server `router/handlers/public_api/`, route definition, request/response types; prefer fixing the OpenAPI spec via the `sync-openapi-spec` skill + - **API gaps** → read handler in warp-server `router/handlers/public_api/`, route definition, request/response types; prefer fixing the OpenAPI spec via the `sync-openapi-spec` skill. Only act on endpoints already publicly released (see Public vs. private surfaces); never draft docs for unreleased warp-server endpoints. - **Slash command gaps** → read the registry entry and gating flags in `app/src/search/slash_command_menu/` 5. Draft the doc following style guide conventions: - YAML frontmatter with description @@ -227,6 +242,7 @@ Not every finding needs a new doc page — pick the lightest correct fix and ver - **Feature flag whose only user-facing surface is an already-documented setting** — map the flag to that setting's doc page rather than writing a new page (for example, a tab-bar visibility flag maps to the all-settings reference). - **Preview or pre-launch feature with no docs yet** — add it to the surface-map ignore list with a comment; the snapshot diff re-flags it when it promotes to GA. - **Stale map entry or doc reference** (map hygiene) — confirm the surface is gone from code, then prune the dead entry. +- **warp-server API endpoint not in the released OpenAPI spec** — do not hand-document it (warp-server is private). If it is part of the released public Oz Agent API, hand it to the `sync-openapi-spec` skill; if it is unreleased or internal, map it `-> internal` with a comment. Never expose an unreleased endpoint or feature in public docs. ### Reviewer routing diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index d97ad5884..f9c95a230 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -220,6 +220,20 @@ oz secret list -> src/content/docs/platform/secrets.mdx oz secret update -> src/content/docs/platform/secrets.mdx oz secret delete -> src/content/docs/platform/secrets.mdx +# Agent Memory is in research preview — not publicly released, so its CLI is +# intentionally undocumented for now (see "Public vs. private surfaces" in SKILL.md). +oz memory -> internal +oz memory create -> internal +oz memory delete -> internal +oz memory list -> internal +oz memory update -> internal +oz memory versions -> internal +oz memory-store -> internal +oz memory-store get -> internal +oz memory-store list -> internal +oz memory-store list-store-agents -> internal +oz memory-store update -> internal + # Internal/hidden command — not a user-facing surface, so no public docs. oz harness-support -> internal @@ -227,6 +241,13 @@ oz harness-support -> internal # Paths are relative to /api/v1 and use OpenAPI-style {param} segments. # Public API endpoints documented via the OpenAPI spec (developers/agent-api-openapi.yaml). +# +# POLICY: warp-server is a private repo. Only endpoints that are part of the +# released public Oz Agent API (already in the OpenAPI spec) may be documented. +# Endpoints not in the spec are NOT auto-documentable: confirm release status and +# route released ones through the sync-openapi-spec skill, or mark `-> internal` +# (unreleased/internal). Never document an unreleased endpoint. See SKILL.md +# "Public vs. private surfaces". POST /agent/run -> src/content/docs/reference/api-and-sdk/index.mdx GET /agent/runs -> src/content/docs/reference/api-and-sdk/index.mdx GET /agent/runs/{runId} -> src/content/docs/reference/api-and-sdk/index.mdx @@ -269,6 +290,20 @@ POST /harness-support/finish-task -> internal POST /harness-support/report-shutdown -> internal POST /harness-support/upload-snapshot -> internal +# Agent Memory REST API — research preview, not publicly released. Intentionally +# undocumented until GA (see "Public vs. private surfaces" in SKILL.md). +GET /memory_stores -> internal +POST /memory_stores -> internal +GET /memory_stores/{uid} -> internal +PUT /memory_stores/{uid} -> internal +DELETE /memory_stores/{uid} -> internal +GET /memory_stores/{uid}/agents -> internal +GET /memory_stores/{uid}/memories -> internal +POST /memory_stores/{uid}/memories -> internal +DELETE /memory_stores/{uid}/memories/{memoryUid} -> internal +PUT /memory_stores/{uid}/memories/{memoryUid} -> internal +GET /memory_stores/{uid}/memories/{memoryUid}/versions -> internal + ## Slash commands -> doc pages # Most documented commands are matched automatically against the From 21329903610293e95a36b0a883cdaebf0a330f5c Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 22:05:38 +0000 Subject: [PATCH 11/15] test(missing_docs): cover public/private boundary + run skill tests in CI - test_audit_docs.py: add test_research_preview_surfaces_are_deferred, a regression guard asserting Agent Memory's CLI (`oz memory*`) and REST API (`/memory_stores/*`) are never flagged for documentation (public/private boundary). 22 tests total, all green. - ci.yml: run the skill's stdlib test suites on every PR. The reviewer-resolver unit tests run fully; the audit integration tests skip gracefully since the warp/warp-server code repos aren't checked out in docs CI. - SKILL.md: note the new boundary test in the Tests section. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 2 +- .../missing_docs/scripts/test_audit_docs.py | 16 ++++++++++++++++ .github/workflows/ci.yml | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 782cbcdc9..5392501c1 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -346,7 +346,7 @@ python3 .agents/skills/missing_docs/scripts/test_audit_docs.py ``` - `test_suggest_reviewers.py` unit-tests reviewer resolution (CODEOWNERS matching, last-match-wins, user/team split, dedup, unresolved paths). -- `test_audit_docs.py` runs behavioral checks against the sibling code repos — clean exit, completeness accounting (`unaccounted` empty), category/severity scoping, fail-loud (exit 2) on a missing repo, and snapshot round-trip — and skips gracefully when those repos aren't checked out. +- `test_audit_docs.py` runs behavioral checks against the sibling code repos — clean exit, completeness accounting (`unaccounted` empty), category/severity scoping, fail-loud (exit 2) on a missing repo, snapshot round-trip, and research-preview deferral (the public/private boundary) — and skips gracefully when those repos aren't checked out. ## References diff --git a/.agents/skills/missing_docs/scripts/test_audit_docs.py b/.agents/skills/missing_docs/scripts/test_audit_docs.py index ceeebe331..bc8f9ce6f 100755 --- a/.agents/skills/missing_docs/scripts/test_audit_docs.py +++ b/.agents/skills/missing_docs/scripts/test_audit_docs.py @@ -148,6 +148,22 @@ def test_update_snapshot_respects_snapshot_flag_and_roundtrips(self): self.assertEqual(rc2, 0) self.assertEqual(report2["summary"]["by_category"].get("surface_changes", 0), 0) + def test_research_preview_surfaces_are_deferred(self): + # Public vs. private boundary: Agent Memory is research preview (not public), + # so its CLI (`oz memory*`) and REST API (`/memory_stores/*`) must never be + # flagged for documentation. Guards the surface-map deferrals from regressing. + rc, report, _ = _run_audit([]) + self.assertEqual(rc, 0) + flagged = [] + for cat in ("undocumented_cli_commands", "undocumented_api_endpoints"): + for item in report.get(cat, []): + name = item.get("command") or item.get("endpoint") or "" + if "memory" in name.lower(): + flagged.append(name) + self.assertEqual( + flagged, [], f"research-preview Agent Memory surfaces must stay deferred, found: {flagged}" + ) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df80aba77..1bf8c5950 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,14 @@ jobs: - name: Check internal links run: python3 .agents/skills/check_for_broken_links/check_links.py --internal-only + # Stdlib-only tests for the missing_docs skill scripts. The reviewer-resolver + # unit tests run fully here; the audit integration tests skip gracefully + # because the warp/warp-server code repos aren't checked out in docs CI. + - name: Test missing_docs skill scripts + run: | + python3 .agents/skills/missing_docs/scripts/test_suggest_reviewers.py + python3 .agents/skills/missing_docs/scripts/test_audit_docs.py + # Production-only audit; gate on high+ so dev-only deprecation chatter # doesn't break PRs. The `|| true` keeps it informational; tighten this # to a hard fail once we're confident the noise is gone. From 5f08115508cdea41ba53066fc8b7b7fe90c47c1b Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 22:15:39 +0000 Subject: [PATCH 12/15] feat(missing_docs): rollout-gate CLI/API surfaces via gated: Fixes the limitation that the CLI and API audits flagged commands/routes regardless of whether their feature had shipped (so non-GA surfaces like Agent Memory needed a permanent `-> internal`). audit_docs.py: - New `gated:` surface-map target for CLI commands and API routes. The audit resolves the gating flag's rollout status (the same machinery used for feature flags + settings): * non-GA (preview/dogfood/other) -> deferred (new `gated_non_ga` accounting bucket), not a finding; * GA -> falls through to normal coverage so it auto-surfaces as a finding; * unknown flag -> conservative (still a finding) + map-hygiene error so the annotation can't silently rot. - audit_cli / audit_api now take flag_statuses; main computes it for API runs. - Completeness accounting keeps totality (gated_non_ga counted; unaccounted none). feature_surface_map.md: migrate Agent Memory's `oz memory*` / `oz memory-store*` CLI and `/memory_stores/*` API from `-> internal` to `-> gated:AIMemories`, so they auto-surface for docs when AIMemories goes GA. Documented the `gated:` sentinel in the header. SKILL.md: document `gated:` in Public vs. private surfaces + Resolution patterns. tests: add TestGatedLogic (helper, non-GA deferral, GA auto-surface, unknown-flag conservatism, map-hygiene validation). 27 tests pass; audit exit 0, unaccounted none. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 2 + .../references/feature_surface_map.md | 57 ++++++------ .../skills/missing_docs/scripts/audit_docs.py | 93 ++++++++++++++++--- .../missing_docs/scripts/test_audit_docs.py | 58 ++++++++++++ 4 files changed, 172 insertions(+), 38 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index 5392501c1..c8f211e2d 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -40,6 +40,7 @@ Only document surfaces that are **publicly released**. This is the most importan Rules of thumb: - A `warp-server` API endpoint that is **not already in the OpenAPI spec** is treated as not-yet-public: do NOT hand-write docs for it. Either confirm it has been publicly released and let the `sync-openapi-spec` skill bring it into the spec, or map it `-> internal` / defer it. When unsure, defer — never expose an unreleased endpoint or feature in public docs. +- A CLI command or API route gated by a **non-GA feature flag** should be mapped `-> gated:` (for example, `gated:AIMemories`) rather than `-> internal`: the audit auto-defers it while the flag is non-GA and auto-surfaces it for docs once the flag goes GA. (Feature flags and settings already auto-defer by rollout status; `gated:` extends that to CLI/API.) - The audit still *detects* these as gaps (useful signal), but detection is not permission to document. Every resolution must respect this boundary. ## Workflows @@ -243,6 +244,7 @@ Not every finding needs a new doc page — pick the lightest correct fix and ver - **Preview or pre-launch feature with no docs yet** — add it to the surface-map ignore list with a comment; the snapshot diff re-flags it when it promotes to GA. - **Stale map entry or doc reference** (map hygiene) — confirm the surface is gone from code, then prune the dead entry. - **warp-server API endpoint not in the released OpenAPI spec** — do not hand-document it (warp-server is private). If it is part of the released public Oz Agent API, hand it to the `sync-openapi-spec` skill; if it is unreleased or internal, map it `-> internal` with a comment. Never expose an unreleased endpoint or feature in public docs. +- **CLI command or API route gated by a non-GA feature flag** — map it `-> gated:` so it auto-defers while the flag is non-GA and auto-surfaces for docs when it GAs (e.g. Agent Memory's `oz memory*` and `/memory_stores/*` use `gated:AIMemories`). Prefer this over `-> internal`, which never re-surfaces. ### Reviewer routing diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index f9c95a230..4fd203106 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -9,6 +9,9 @@ verified rather than flagged. Format: `CodeIdentifier -> src/content/docs/path/to/page.md` (one per line within each section). Lines starting with `#` are comments. Blank lines are ignored. The sentinel target `internal` marks surfaces that intentionally have no public docs. +The sentinel target `gated:` (CLI commands and API routes) ties a surface to +its gating FeatureFlag's rollout: it is deferred while the flag is non-GA and +auto-surfaces for docs once the flag goes GA. # Maintenance policy: # - When a feature ships (GA or Preview), add a mapping here in the same PR that @@ -220,19 +223,20 @@ oz secret list -> src/content/docs/platform/secrets.mdx oz secret update -> src/content/docs/platform/secrets.mdx oz secret delete -> src/content/docs/platform/secrets.mdx -# Agent Memory is in research preview — not publicly released, so its CLI is -# intentionally undocumented for now (see "Public vs. private surfaces" in SKILL.md). -oz memory -> internal -oz memory create -> internal -oz memory delete -> internal -oz memory list -> internal -oz memory update -> internal -oz memory versions -> internal -oz memory-store -> internal -oz memory-store get -> internal -oz memory-store list -> internal -oz memory-store list-store-agents -> internal -oz memory-store update -> internal +# Agent Memory is research preview (gating flag AIMemories is non-GA), so its CLI +# is deferred via `gated:` — it auto-surfaces for docs when AIMemories goes GA. +# See "Public vs. private surfaces" in SKILL.md. +oz memory -> gated:AIMemories +oz memory create -> gated:AIMemories +oz memory delete -> gated:AIMemories +oz memory list -> gated:AIMemories +oz memory update -> gated:AIMemories +oz memory versions -> gated:AIMemories +oz memory-store -> gated:AIMemories +oz memory-store get -> gated:AIMemories +oz memory-store list -> gated:AIMemories +oz memory-store list-store-agents -> gated:AIMemories +oz memory-store update -> gated:AIMemories # Internal/hidden command — not a user-facing surface, so no public docs. oz harness-support -> internal @@ -290,19 +294,20 @@ POST /harness-support/finish-task -> internal POST /harness-support/report-shutdown -> internal POST /harness-support/upload-snapshot -> internal -# Agent Memory REST API — research preview, not publicly released. Intentionally -# undocumented until GA (see "Public vs. private surfaces" in SKILL.md). -GET /memory_stores -> internal -POST /memory_stores -> internal -GET /memory_stores/{uid} -> internal -PUT /memory_stores/{uid} -> internal -DELETE /memory_stores/{uid} -> internal -GET /memory_stores/{uid}/agents -> internal -GET /memory_stores/{uid}/memories -> internal -POST /memory_stores/{uid}/memories -> internal -DELETE /memory_stores/{uid}/memories/{memoryUid} -> internal -PUT /memory_stores/{uid}/memories/{memoryUid} -> internal -GET /memory_stores/{uid}/memories/{memoryUid}/versions -> internal +# Agent Memory REST API — research preview (gating flag AIMemories is non-GA), +# deferred via `gated:` and auto-surfaces when AIMemories goes GA. See +# "Public vs. private surfaces" in SKILL.md. +GET /memory_stores -> gated:AIMemories +POST /memory_stores -> gated:AIMemories +GET /memory_stores/{uid} -> gated:AIMemories +PUT /memory_stores/{uid} -> gated:AIMemories +DELETE /memory_stores/{uid} -> gated:AIMemories +GET /memory_stores/{uid}/agents -> gated:AIMemories +GET /memory_stores/{uid}/memories -> gated:AIMemories +POST /memory_stores/{uid}/memories -> gated:AIMemories +DELETE /memory_stores/{uid}/memories/{memoryUid} -> gated:AIMemories +PUT /memory_stores/{uid}/memories/{memoryUid} -> gated:AIMemories +GET /memory_stores/{uid}/memories/{memoryUid}/versions -> gated:AIMemories ## Slash commands -> doc pages diff --git a/.agents/skills/missing_docs/scripts/audit_docs.py b/.agents/skills/missing_docs/scripts/audit_docs.py index a3f7e4b17..8afed9df8 100755 --- a/.agents/skills/missing_docs/scripts/audit_docs.py +++ b/.agents/skills/missing_docs/scripts/audit_docs.py @@ -149,6 +149,24 @@ def parse_surface_map(path: Path) -> dict: return result +# Gating statuses that mean "not publicly released" — a `gated:` surface +# whose flag has one of these statuses is intentionally deferred (undocumented). +_NON_GA_STATUSES = ("preview", "dogfood", "other") + + +def _gated_flag(value) -> str | None: + """Return the gating FeatureFlag name for a `gated:` surface-map target. + + `gated:` ties a CLI command or API route to its gating feature flag's + rollout: while the flag is non-GA the surface is intentionally undocumented + (private/unreleased), and once the flag goes GA the surface auto-surfaces as + a normal coverage finding. Returns None for plain doc paths and `internal`. + """ + if isinstance(value, str) and value.startswith("gated:"): + return value[len("gated:"):].strip() + return None + + def parse_stale_terms(path: Path) -> list[tuple[str, str]]: """Parse stale_terms.md into a list of (term, reason) tuples.""" terms = [] @@ -1329,7 +1347,8 @@ def audit_features(warp_repo: Path, docs_root: Path, surface_map: dict, def audit_cli(warp_repo: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str], - cli_commands: list[dict] | None = None) -> list[dict]: + cli_commands: list[dict] | None = None, + flag_statuses: dict[str, str] | None = None) -> list[dict]: """Audit CLI command and subcommand coverage in docs.""" commands = cli_commands if cli_commands is not None else parse_cli_commands(warp_repo) cli_to_doc = surface_map.get("cli_to_doc", {}) @@ -1352,7 +1371,13 @@ def is_covered(cmd_str: str, search_phrase: str) -> bool: # intentionally have no public docs (matches API audit semantics). if doc_path == "internal": return True - if resolve_doc_path(doc_path, repo_root) is not None: + gflag = _gated_flag(doc_path) + if gflag is not None: + # Defer while the gating flag is non-GA; once GA, fall through so + # the command must be documented (auto-surfaces as a finding). + if (flag_statuses or {}).get(gflag) in _NON_GA_STATUSES: + return True + elif resolve_doc_path(doc_path, repo_root) is not None: return True return any(search_phrase in content for content in cli_docs_text.values()) @@ -1395,7 +1420,8 @@ def is_covered(cmd_str: str, search_phrase: str) -> bool: def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, docs_text: dict[str, str], - api_routes: list[dict] | None = None) -> list[dict]: + api_routes: list[dict] | None = None, + flag_statuses: dict[str, str] | None = None) -> list[dict]: """Audit public API endpoint coverage in the OpenAPI spec and API docs. The public docs API reference (docs.warp.dev/api) renders @@ -1442,8 +1468,17 @@ def audit_api(warp_server: Path, docs_root: Path, surface_map: dict, if rel_path.startswith("/api/v1"): rel_path = rel_path[len("/api/v1"):] or "/" rel_route_str = f"{route['method']} {rel_path}" - if route_str in api_to_doc or rel_route_str in api_to_doc: - continue + map_val = api_to_doc.get(route_str) + if map_val is None: + map_val = api_to_doc.get(rel_route_str) + if map_val is not None: + gflag = _gated_flag(map_val) + if gflag is None: + continue # plain doc path or `internal` sentinel: suppressed + # Defer while the gating flag is non-GA; once GA, fall through so the + # endpoint must reach the OpenAPI spec (auto-surfaces as a finding). + if (flag_statuses or {}).get(gflag) in _NON_GA_STATUSES: + continue # Match against the spec's path keys (param-name-insensitive), then # fall back to substring search in API docs prose. @@ -1867,6 +1902,24 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], ), }) + # Gated surface-map targets must reference a real FeatureFlag. + for section_name, mapping in ( + ("CLI commands", surface_map.get("cli_to_doc", {})), + ("API endpoints", surface_map.get("api_to_doc", {})), + ): + for key, val in sorted(mapping.items()): + gflag = _gated_flag(val) + if gflag is not None and gflag not in known_flags: + findings.append({ + "entry": key, + "section": section_name, + "severity": "medium", + "reason": ( + f"Gated target 'gated:{gflag}' references a FeatureFlag that " + "does not exist in code \u2014 fix the flag name or remove the gate" + ), + }) + # Mapped doc targets that no longer exist (any section). for section, mapping in ( ("Feature flags", surface_map.get("feature_to_doc", {})), @@ -1876,7 +1929,7 @@ def audit_map_hygiene(surface_map: dict, flag_statuses: dict[str, str], ("Settings", surface_map.get("settings_to_doc", {})), ): for key, doc_path in sorted(mapping.items()): - if doc_path == "internal": + if doc_path == "internal" or _gated_flag(doc_path) is not None: continue if resolve_doc_path(doc_path, repo_root) is None: findings.append({ @@ -2299,16 +2352,21 @@ def compute_accounting(docs_root: Path, surface_map: dict, findings: dict, except Exception: pass cb = {"total": 0, "hidden": 0, "mapped": 0, "doc_covered": 0, - "finding": 0, "parent_flagged": 0} + "finding": 0, "parent_flagged": 0, "gated_non_ga": 0} missing = [] for cmd in cli_commands: entries = [(cmd["command"], cmd["hidden"], None)] + [ (s["command"], s["hidden"], cmd["command"]) for s in cmd["subcommands"]] for name, hidden, parent in entries: cb["total"] += 1 + val = cli_map.get(name) + gflag = _gated_flag(val) + deferred = gflag is not None and flag_statuses.get(gflag) in _NON_GA_STATUSES if hidden: cb["hidden"] += 1 - elif name in cli_map: + elif deferred: + cb["gated_non_ga"] += 1 + elif val is not None and gflag is None: cb["mapped"] += 1 elif any(name.split(" ", 1)[1] in t for t in cli_text.values()): cb["doc_covered"] += 1 @@ -2346,14 +2404,21 @@ def compute_accounting(docs_root: Path, surface_map: dict, findings: dict, except Exception: pass ab = {"total": len(api_routes), "mapped": 0, "spec_covered": 0, - "docs_covered": 0, "finding": 0} + "docs_covered": 0, "finding": 0, "gated_non_ga": 0} missing = [] for route in api_routes: rel = route["path"] if rel.startswith("/api/v1"): rel = rel[len("/api/v1"):] or "/" rel_str = f"{route['method']} {rel}" - if route["route"] in api_map or rel_str in api_map: + map_val = api_map.get(route["route"]) + if map_val is None: + map_val = api_map.get(rel_str) + gflag = _gated_flag(map_val) + deferred = gflag is not None and flag_statuses.get(gflag) in _NON_GA_STATUSES + if deferred: + ab["gated_non_ga"] += 1 + elif map_val is not None and gflag is None: ab["mapped"] += 1 elif (_normalize_path_params(rel) in spec_paths or _normalize_path_params(route["path"]) in spec_paths): @@ -2704,7 +2769,7 @@ def guard(label: str, count: int) -> bool: print("Running CLI command coverage audit...", file=sys.stderr) findings["undocumented_cli_commands"] = audit_cli( warp_repo, docs_root, surface_map, docs_text, - cli_commands=cli_commands) + cli_commands=cli_commands, flag_statuses=flag_statuses) audits_run.append("cli") if args.category in (None, "slash") and slash_ok: @@ -2746,10 +2811,14 @@ def guard(label: str, count: int) -> bool: server_tools = parse_server_tools(warp_server) api_ok = guard("API routes", len(api_routes)) if args.category in (None, "api") and api_ok: + # gated: deferral needs flag rollout statuses; compute them if a + # full run hasn't already (e.g. an isolated `--category api` run). + if not flag_statuses and warp_repo: + flag_statuses = compute_flag_statuses(warp_repo) print("Running API endpoint coverage audit...", file=sys.stderr) findings["undocumented_api_endpoints"] = audit_api( warp_server, docs_root, surface_map, docs_text, - api_routes=api_routes) + api_routes=api_routes, flag_statuses=flag_statuses) audits_run.append("api") elif needs_server: if args.category in (None, "api"): diff --git a/.agents/skills/missing_docs/scripts/test_audit_docs.py b/.agents/skills/missing_docs/scripts/test_audit_docs.py index bc8f9ce6f..3636069fb 100755 --- a/.agents/skills/missing_docs/scripts/test_audit_docs.py +++ b/.agents/skills/missing_docs/scripts/test_audit_docs.py @@ -14,6 +14,7 @@ """ import hashlib +import importlib.util import json import subprocess import sys @@ -44,6 +45,11 @@ def _find_server(): SERVER = _find_server() _REPOS_AVAILABLE = WARP is not None and SERVER is not None +# Import the audit module directly for repo-free unit tests of pure logic. +_spec = importlib.util.spec_from_file_location("audit_docs", _AUDIT) +audit_docs = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(audit_docs) + def _run_audit(extra_args, capture_report=True): """Run audit_docs.py; return (returncode, report_dict_or_None).""" @@ -165,5 +171,57 @@ def test_research_preview_surfaces_are_deferred(self): ) +class TestGatedLogic(unittest.TestCase): + """Repo-free unit tests for the `gated:` rollout-aware deferral.""" + + def test_gated_flag_helper(self): + self.assertEqual(audit_docs._gated_flag("gated:AIMemories"), "AIMemories") + self.assertEqual(audit_docs._gated_flag("gated: Spaced "), "Spaced") + self.assertIsNone(audit_docs._gated_flag("internal")) + self.assertIsNone(audit_docs._gated_flag("src/content/docs/x.mdx")) + self.assertIsNone(audit_docs._gated_flag(None)) + + def _run_cli(self, status_map): + """Run audit_cli on one gated command with the given flag statuses.""" + with tempfile.TemporaryDirectory() as d: + surface_map = {"cli_to_doc": {"oz memx": "gated:MemFlag"}} + commands = [{"command": "oz memx", "hidden": False, + "subcommands": [], "source_file": None}] + return audit_docs.audit_cli( + None, Path(d), surface_map, {}, + cli_commands=commands, flag_statuses=status_map) + + def test_gated_non_ga_cli_is_deferred(self): + findings = self._run_cli({"MemFlag": "other"}) + self.assertEqual(findings, [], "non-GA gated CLI command must be deferred") + + def test_gated_ga_cli_auto_surfaces(self): + findings = self._run_cli({"MemFlag": "ga"}) + cmds = [f["command"] for f in findings] + self.assertIn("oz memx", cmds, "a GA gated command must surface as a finding") + + def test_gated_unknown_flag_cli_surfaces(self): + # Unknown gating flag is treated conservatively (not silently deferred). + findings = self._run_cli({}) + self.assertIn("oz memx", [f["command"] for f in findings]) + + def test_map_hygiene_flags_unknown_gated_flag(self): + surface_map = { + "cli_to_doc": {"oz good": "gated:KnownFlag", "oz bad": "gated:BogusFlag"}, + "feature_to_doc": {}, "api_to_doc": {}, "slash_to_doc": {}, + "settings_to_doc": {}, "ignore_flags": set(), "duplicates": [], + } + cli_commands = [ + {"command": "oz good", "hidden": False, "subcommands": []}, + {"command": "oz bad", "hidden": False, "subcommands": []}, + ] + with tempfile.TemporaryDirectory() as d: + findings = audit_docs.audit_map_hygiene( + surface_map, {"KnownFlag": "other"}, cli_commands, [], [], {}, Path(d)) + gated_findings = [f for f in findings if "Gated target" in f["reason"]] + self.assertEqual(len(gated_findings), 1, gated_findings) + self.assertEqual(gated_findings[0]["entry"], "oz bad") + + if __name__ == "__main__": unittest.main(verbosity=2) From bddaea6955ce578dab9bad7e6e0ae9752cf305ae Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 22:31:56 +0000 Subject: [PATCH 13/15] docs(missing_docs): sync SKILL.md accounting buckets with gated_non_ga The gated: work added a `gated_non_ga` bucket to the CLI and API completeness accounting, but SKILL.md's bucket list still omitted it. Document `gated_non_ga` for CLI commands and API routes, and note the `gated:` sentinel alongside `internal` in the References section so the documented accounting matches what the audit actually emits. Co-Authored-By: Oz --- .agents/skills/missing_docs/SKILL.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.agents/skills/missing_docs/SKILL.md b/.agents/skills/missing_docs/SKILL.md index c8f211e2d..475a35a20 100644 --- a/.agents/skills/missing_docs/SKILL.md +++ b/.agents/skills/missing_docs/SKILL.md @@ -134,9 +134,11 @@ accountability bucket and proves totality: - **Feature flags**: every GA/Preview flag is `mapped` (surface map verified), `ignored` (curated internal list), or a visible `finding`; every dogfood/other flag is `tracked_non_ga` (snapshot diff fires on promotion or removal). -- **CLI commands**: `mapped`, `doc_covered`, `finding`, `parent_flagged` +- **CLI commands**: `mapped`, `doc_covered`, `gated_non_ga` (deferred via + `gated:` while its gating flag is non-GA), `finding`, `parent_flagged` (suppressed because the parent command is already flagged), or `hidden`. -- **API routes**: `mapped`, `spec_covered`, `docs_covered`, or `finding`. +- **API routes**: `mapped`, `spec_covered`, `docs_covered`, `gated_non_ga` + (deferred via `gated:` while its gating flag is non-GA), or `finding`. - **Slash commands**: `mapped`, `doc_covered`, or `finding`. - **Settings**: `private`, `tracked_non_ga`, `mapped`, `doc_covered`, or `finding`. @@ -354,9 +356,10 @@ python3 .agents/skills/missing_docs/scripts/test_audit_docs.py - `references/feature_surface_map.md` — curated mapping of flags/commands/routes/slash commands/settings to doc pages, ignore list for internal flags, allowlist for - intentionally unlisted pages, and the `internal` sentinel for surfaces that - intentionally have no public docs. Update it with every docs PR that ships a - feature. + intentionally unlisted pages, the `internal` sentinel for surfaces that + intentionally have no public docs, and the `gated:` sentinel for CLI/API + surfaces deferred until their gating flag goes GA. Update it with every docs PR + that ships a feature. - `references/surface_snapshot.json` — generated snapshot of all code surfaces used by `--diff`. Regenerate with `--update-snapshot`; never hand-edit. - `references/stale_terms.md` — renamed/removed-feature terms to flag during staleness From 738b7b8fce6ff83b4777d86e1fe002266fd47a74 Mon Sep 17 00:00:00 2001 From: Hong Yi Chen Date: Tue, 30 Jun 2026 15:44:27 -0700 Subject: [PATCH 14/15] docs(cli): resolve missing_docs CLI drift (oz agent, oz run messaging, oz provider) (#279) Demonstrates the missing_docs drift-watch loop end to end on real drift. The audit flagged 15 undocumented `oz` subcommands; this resolves all of them according to each command's GA rollout status: - GA (NamedAgents): document the `oz agent` named-agent management group (list/get/create/update/delete + `oz agent skills`) and fix the existing `oz agent list` entry, which incorrectly described skill listing. - GA (ConversationApi): document `oz run conversation get` and the `oz run message` inbox commands (list/read/watch/send/mark-delivered). - Non-GA (ProviderCommand = dogfood): defer the whole `oz provider` group via `gated:ProviderCommand` so it auto-surfaces for docs when it goes GA. All command flags drafted from crates/warp_cli source (agent.rs, task.rs, provider.rs). CLI audit now reports 0 gaps; cli_commands gated_non_ga = 14. Co-authored-by: Oz --- .../references/feature_surface_map.md | 9 +- src/content/docs/reference/cli/index.mdx | 100 +++++++++++++++++- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/.agents/skills/missing_docs/references/feature_surface_map.md b/.agents/skills/missing_docs/references/feature_surface_map.md index 4fd203106..b3baa644f 100644 --- a/.agents/skills/missing_docs/references/feature_surface_map.md +++ b/.agents/skills/missing_docs/references/feature_surface_map.md @@ -203,7 +203,6 @@ oz whoami -> src/content/docs/reference/cli/index.mdx oz integration -> src/content/docs/reference/cli/integration-setup.mdx oz schedule -> src/content/docs/reference/cli/index.mdx oz secret -> src/content/docs/reference/cli/index.mdx -oz provider -> src/content/docs/reference/cli/index.mdx oz federate -> src/content/docs/reference/cli/federate.mdx oz artifact -> src/content/docs/reference/cli/artifacts.mdx oz api-key -> src/content/docs/reference/cli/api-keys.mdx @@ -238,6 +237,14 @@ oz memory-store list -> gated:AIMemories oz memory-store list-store-agents -> gated:AIMemories oz memory-store update -> gated:AIMemories +# `oz provider` (link third-party services like Slack and Linear) is gated by +# ProviderCommand, which is non-GA (dogfood), so the whole command group is +# deferred via `gated:` — it auto-surfaces for docs when ProviderCommand goes +# GA. See "Public vs. private surfaces" in SKILL.md. +oz provider -> gated:ProviderCommand +oz provider setup -> gated:ProviderCommand +oz provider list -> gated:ProviderCommand + # Internal/hidden command — not a user-facing surface, so no public docs. oz harness-support -> internal diff --git a/src/content/docs/reference/cli/index.mdx b/src/content/docs/reference/cli/index.mdx index 9aad722de..5792ffe0e 100644 --- a/src/content/docs/reference/cli/index.mdx +++ b/src/content/docs/reference/cli/index.mdx @@ -365,13 +365,68 @@ The `--share` flag can be repeated, and uses the following syntax: The following commands are available for managing and inspecting Oz resources. -### `oz agent list` +### Managing named agents -List all available skills discovered from your environments. Optionally filter by repository: +[Named agents](/platform/agents/) are reusable agent configurations — a name, description, skills, secrets, base model, and default environment — that you can run with `oz agent run-cloud --agent ` and scope API keys to. + +List the named agents on your team, optionally sorting by name or creation time: ```sh oz agent list -oz agent list --repo owner/repo +oz agent list --sort-by created-at --sort-order desc +``` + +Show the full configuration for a single agent by its UID: + +```sh +oz agent get +``` + +Create a named agent. Only `--name` is required; attach skills and secrets by repeating the flags: + +```sh +oz agent create \ + --name "release-notes" \ + --description "Drafts release notes from merged PRs" \ + --skill "myorg/repo:release-notes" \ + --secret GITHUB_TOKEN \ + --base-model \ + --environment +``` + +Update an existing agent. Each attribute has an explicit add/remove flag so changes are unambiguous: + +```sh +# Rename the agent and swap one of its skills +oz agent update --name "weekly-release-notes" \ + --remove-skill "myorg/repo:old" --add-skill "myorg/repo:release-notes" + +# Clear attributes entirely +oz agent update --remove-description --remove-all-secrets +``` + +Common `oz agent update` flags: + +* `--name ` (`-n`) — rename the agent. +* `--description ` / `--remove-description` — set or clear the description. +* `--add-secret ` / `--remove-secret ` / `--remove-all-secrets` — manage attached secrets. +* `--add-skill ` / `--remove-skill ` / `--remove-all-skills` — manage attached skills. +* `--base-model ` / `--remove-base-model` — set or clear the base model. +* `--environment ` (`-e`) / `--remove-environment` — set or clear the default cloud environment. + +Delete a named agent by its UID: + +```sh +oz agent delete +``` + +### `oz agent skills` + +List the agent skills discovered across your environments. Pass `--repo` (`-r`) to list skills from a specific GitHub repository instead: + +```sh +oz agent skills +oz agent skills --repo owner/repo ``` ### `oz run list` / `oz run get` @@ -387,6 +442,45 @@ oz run list --limit 20 oz run get ``` +### Run conversations and messages + +Read an agent run's transcript and exchange messages between runs — for example, between a parent orchestrator and its [child agents](/platform/orchestration/multi-agent-runs/). + +Retrieve a conversation transcript by its ID: + +```sh +oz run conversation get +``` + +List and read the messages in a run's inbox: + +```sh +# List inbox message headers +oz run message list +oz run message list --unread --since 2026-01-01T00:00:00Z --limit 100 + +# Read one message body +oz run message read + +# Stream new messages as they arrive (resume reconnects with --since-sequence) +oz run message watch +oz run message watch --since-sequence 42 +``` + +Send a message from one run to one or more recipient runs, then mark it delivered once handled: + +```sh +oz run message send \ + --sender-run-id \ + --to \ + --subject "build status" \ + --body "tests are green" + +oz run message mark-delivered +``` + +Repeat `--to` to fan a message out to multiple recipient runs. + ### `oz model list` List all available models: From 6bf2f74cde28c21359086aff3eaba8724f49953b Mon Sep 17 00:00:00 2001 From: hongyi-chen Date: Tue, 30 Jun 2026 22:57:46 +0000 Subject: [PATCH 15/15] docs(cli): cross-link run-cloud --agent to named-agent management The new "Managing named agents" section says agents are run with `oz agent run-cloud --agent `, but that flag was absent from the run-cloud key-flags list. Document `--agent ` (from RunCloudArgs in crates/warp_cli/src/agent.rs) and cross-link the two sections so readers can get from creating a named agent to running one. Co-Authored-By: Oz --- src/content/docs/reference/cli/index.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/content/docs/reference/cli/index.mdx b/src/content/docs/reference/cli/index.mdx index 5792ffe0e..4e1f70442 100644 --- a/src/content/docs/reference/cli/index.mdx +++ b/src/content/docs/reference/cli/index.mdx @@ -266,6 +266,7 @@ oz agent run-cloud \ * `--no-environment` — run without an environment (not recommended). * `--open` — view the agent's session in Warp once it's available. * `--name ` (`-n`) — label the run for grouping and traceability (see [Naming runs](/reference/cli/#naming-runs) below). +* `--agent ` — run as a saved [named agent](/platform/agents/), applying its configuration (skills, secrets, base model, and default environment) and attributing credit usage to it (see [Managing named agents](/reference/cli/#managing-named-agents) below). * `--mcp ` — start one or more MCP servers before execution (UUID, JSON file path, or inline JSON). Can be repeated. * `--model ` — override the default model. * `--skill ` — use a skill from the environment's repository as the base prompt (see [Using Skills](/reference/cli/#using-skills)).