first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -32,7 +32,7 @@ Code syntax is a surface area in Figma for codebase translation context. You can
### Scope
`variable.scopes: VariableScope[]` specifies which properties in Figma the variable can be used for. This is important when you create and when you use variables. **Always set specific scopes rather than leaving the default `ALL_SCOPES`** — it pollutes every property picker with irrelevant tokens. The more specific the better. For the canonical scope-to-use-case mapping, see [token-creation.md § Variable Scopes — Complete Reference Table](../../figma-generate-library/references/token-creation.md).
`variable.scopes: VariableScope[]` specifies which properties in Figma the variable can be used for. This is important when you create and when you use variables. **Always set specific scopes rather than leaving the default `ALL_SCOPES`** — it pollutes every property picker with irrelevant tokens. The more specific the better. For the canonical scope-to-use-case mapping, see [token-creation.md § Variable Scopes — Complete Reference Table](../../../figma-generate-library/references/token-creation.md).
Common scope values:
@@ -1,5 +1,5 @@
---
name: add-mcp-server
name: agent-add-mcp
description: Use this skill when helping users add MCP servers to their Warp configuration.
---
@@ -0,0 +1,84 @@
---
name: change-keybinding
description: Customize Warp keyboard shortcuts (keybindings, keymappings) by editing the user's keybindings.yaml file. Use when the user asks to remap a key combination, rebind an action, change a shortcut, or remove a default keybinding (e.g. "change ctrl+space to ctrl+s", "rebind the command palette to cmd+p", "remove the default for X").
---
# change-keybinding
Use this skill when the user wants to remap, rebind, or remove a Warp keyboard shortcut.
## Keybindings file
User customizations live at:
```
{{keybindings_file_path}}
```
This is the exact path Warp reads at launch — it is platform- and channel-specific (e.g. under `~/.warp*/` on macOS, under XDG config dirs like `~/.config/warp-terminal/` on Linux, and under `%LocalAppData%` on Windows). Use this path verbatim — do not infer a different one from the user's home directory layout. Create the file (and any missing parent directories) if it does not exist.
## File format
A flat YAML map of `action_name``key_trigger`. Action names contain a colon, so they **must be quoted**:
```yaml
"workspace:toggle_ai_assistant": ctrl-s
"editor_view:delete_all_left": cmd-shift-A
"workspace:toggle_command_palette": none
```
## Keystroke encoding rules
Triggers use Warp's normalized form — get this exactly right or the binding silently fails to load.
- **Modifiers** (in this order when combined): `ctrl-alt-shift-cmd-meta-`. Cross-platform alias: `cmdorctrl-` (becomes `cmd` on macOS, `ctrl` elsewhere).
- **Letter casing**: applies only to single-letter keys. Without `shift`, the letter is lowercase (`ctrl-s`). With `shift`, the letter is **uppercase** (`shift-A`, never `shift-a`). Mixing them is invalid.
- **Special keys**: `space`, `enter`, `escape`, `tab`, `backspace`, `delete`, `insert`, `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown`, `f1``f20`, `numpadenter`. Always lowercase, even with `shift` (`ctrl-shift-space`, `shift-tab` — never `ctrl-shift-SPACE`). Use the literal word `space` — not `" "`.
- **Punctuation** is the bare character: `cmd-=`, `cmd-,`, `cmdorctrl-/`.
- **Remove a default binding**: set the value to the literal string `none`. The action becomes unbound.
Translate user phrasing into this form: `Ctrl+S``ctrl-s`, `Cmd+Shift+P``cmd-shift-P`, `Ctrl+Space``ctrl-space`.
## Identifying the action
Defaults are compiled into Warp and are **not** discoverable from the keybindings file on disk. There is no catalog the agent can consult to map a description or current shortcut to an action name. Pick the right strategy based on how the user described the change:
1. **By action name** ("set workspace:toggle_command_palette to cmd-p"): the user already gave you the name — write it directly.
2. **By description or current key combo** ("rebind the command palette to cmd-p", "change ctrl+space to ctrl+s"): you don't have the action name and cannot reliably guess it. Do not invent one. Direct the user to the **keybindings editor** (`workspace:show_keybinding_settings`, default `cmd-ctrl-k` on macOS; **Settings → Keyboard Shortcuts** on other platforms) — they can search by description or current shortcut there and either edit the binding in place or share the canonical `namespace:action_name` so you can write it.
## Workflow
1. Determine which action to remap and the new trigger (see "Identifying the action").
2. Read the existing keybindings file at `{{keybindings_file_path}}` if present. **Preserve every existing entry** — only add or update the one you're changing.
3. Write the file (creating parent directories if necessary). Make sure the action key is quoted and the value is normalized (see encoding rules).
4. Tell the user that **Warp must be restarted** for the change to take effect — the keybindings file is loaded only at app launch, unlike `settings.toml` which hot-reloads. They can quit with `cmd-Q` (macOS) or the equivalent on their platform and reopen Warp.
## Examples
Remap an existing custom binding by old trigger:
```yaml
# before
"workspace:toggle_ai_assistant": ctrl-space
# after
"workspace:toggle_ai_assistant": ctrl-s
```
Remove a default shortcut:
```yaml
"workspace:toggle_keybindings_page": none
```
Shift combined with a special key (note the special key stays lowercase):
```yaml
"workspace:toggle_ai_assistant": ctrl-shift-space
```
Cross-platform binding using the `cmdorctrl-` alias (resolves to `cmd` on macOS, `ctrl` elsewhere):
```yaml
"workspace:toggle_command_palette": cmdorctrl-shift-P
```
@@ -227,7 +227,7 @@ Once all runs are done:
2. **Aggregate into benchmark** — run the aggregation script from the skill-creator directory:
```bash
python -m scripts.aggregate_benchmark <workspace>/iteration-N --skill-name <name>
python {{skill_dir}}/scripts/aggregate_benchmark.py <workspace>/iteration-N --skill-name <name>
```
This produces `benchmark.json` and `benchmark.md` with pass_rate, time, and tokens for each configuration, with mean ± stddev and the delta. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects.
Put each with_skill version before its baseline counterpart.
@@ -236,7 +236,7 @@ Put each with_skill version before its baseline counterpart.
4. **Launch the viewer** with both qualitative outputs and quantitative data:
```bash
nohup python <skill-creator-path>/eval-viewer/generate_review.py \
nohup python {{skill_dir}}/eval-viewer/generate_review.py \
<workspace>/iteration-N \
--skill-name "my-skill" \
--benchmark <workspace>/iteration-N/benchmark.json \
-240
View File
@@ -1,240 +0,0 @@
---
name: feedback
description: Turn rough feedback about the Warp app into a filed GitHub issue or duplicate-issue response for `warpdotdev/warp`. Use when the user shares a Warp bug report, regression, confusing UX note, feature gap, or short complaint and wants it clarified into reproduction steps, expected vs actual behavior, concrete impact, and grounded source references from local Warp repos when available.
---
# Feedback
Turn rough Warp app feedback into a crisp filed issue or duplicate-issue response for `warpdotdev/warp`.
Treat Warp client, Warp app, Warp terminal, and Warp UX feedback as `warpdotdev/warp` unless the user clearly asks for a different destination.
## Overview
- Use the `gh` CLI to search for and fetch code from `warpdotdev/warp` when product or implementation context would improve the report.
- If those repos are not available, draft the issue from the user's report alone rather than blocking on more context.
- This skill is strictly for issue filing and duplicate detection. Never modify code, generate patches, propose implementation diffs, or open a pull request as part of this workflow.
- If you cannot file an issue, say so explicitly in the response instead of attempting another side effect.
- The helper script applies the `in-app-feedback` label to filed issues for tracking.
## Code access boundaries
This skill runs in environments where Warp source code may be present in the current working directory or on disk. The following rules apply unconditionally regardless of what source code is visible locally:
- **Never write, edit, or delete any source file.** Do not use Edit, Write, or any tool that modifies files on disk, even if asked to do so as part of filing feedback or "while you're in the code."
- **Never create or modify any git artifact.** Do not stage files, create commits, create branches, produce patches, or modify any git state.
- **Local source code is read-only context at most.** You may read local Warp source files (e.g., with Read or grep) only to find concrete file paths, symbol names, or setting names that would make a source reference more precise. Never read local files to produce a code fix or diff.
- **Prefer `gh` CLI for code lookups.** Use `gh` to search and fetch code from `warpdotdev/warp` rather than reading the local checkout when both are available.
- **The presence of local source code is not an invitation to fix it.** Observing that you are inside a Warp source directory changes nothing about the permitted outputs of this skill: issue filed, duplicate found, or explicit refusal.
Load the bundled reference files only when relevant:
- platform and OS-version resolution, plus operating-system-specific behavior: `references/platforms.md`
- logs and crash artifacts: `references/logs.md`
- output calibration examples: `references/examples.md`
## Workflow
### 1. Confirm scope and classify the report
- This skill only handles feedback about the Warp product that could plausibly be addressed by a code or docs change to the Warp client, server, or SDKs. Before drafting anything, verify the request is in scope.
- **Decline and exit the skill (do not call the helper script) when the report is clearly out of scope.** Out-of-scope categories include, but are not limited to:
- Account, billing, subscription, plan, credits, refund, or invoice questions.
- Login, authentication, SSO, password, or session-expiry problems.
- Requests to contact human support, sales, or legal.
- General venting, praise, or commentary with no actionable product signal.
- Questions about third-party tools or the user's own shell, machine, or network configuration that are not about Warp's behavior.
- Anything the user explicitly says is not about Warp, or that they just want to talk through.
- When you decline, respond in one or two sentences that (a) say you won't file an issue, (b) name the reason in plain language, and (c) point the user at the right channel: account/billing/support concerns go to the in-app Help menu or `support@warp.dev`, community discussion goes to the Warp Slack community, and security reports go to `security@warp.dev`. Do not apologize performatively and do not offer to retry the same flow.
- Only if the request is in scope, classify it as `bug`, `regression`, `ux issue`, or `feature request` before drafting.
### 2. Ask only for missing facts that materially improve the draft
- Before drafting, decide whether the report already contains the minimum actionable information: what the user was doing, what they expected, and what happened (for bugs, regressions, and UX issues) or what they want to be able to do and why (for feature requests). If any of those pieces is missing, run a single focused clarifying round.
- Use the `ask_user_question` tool for that round. Ask 3-4 high-value multiple-choice questions in a single call, focused on user experience and expectations: what the user was trying to do, what felt confusing or broken, what they expected to happen instead, where in the product they hit the issue, and how much it blocked them.
- Follow the tool guidance where possible: only ask when necessary, do not add labels like `Select One` or `Select All that Apply`, and if fixed options are too limiting, include an `Other` option. If the user skips a question, proceed with your best judgment on what they did answer.
- **Run at most one clarifying round.** If after that round the minimum actionable information is still missing, decline to file rather than drafting a weak issue. Tell the user in one or two sentences exactly which specifics would unblock a future report (for example: "A short description of what you were doing when it happened and what you expected instead would let us turn this into an actionable bug report."). Do not file a placeholder issue just to close the loop.
- For bugs and regressions, first read `references/platforms.md` and try to resolve Warp version and operating system from the bundled version metadata and available context. Ask for reproduction steps only when they are not already clear, and for regressions in particular only when the flow is not readily available from the report or supporting context.
- For crashes, startup failures, rendering bugs, sync issues, or hard-to-reproduce regressions, ask for logs or crash artifacts only when they are likely to help. Read `references/logs.md` only when needed.
- If operating system version, Warp version, or operating-system-specific behavior is relevant, read `references/platforms.md` and follow the bundled metadata guidance there yourself when possible. Ask the user only if you still cannot determine the necessary platform details.
### 3. Check whether the feature or capability is already supported
- Before concluding that something is missing from Warp (feature requests, "it doesn't do X" complaints, "I wish it could Y" asks, or any UX complaint that could be explained by an existing setting or workflow), you **must** consult the docs first.
- Call the `search_warp_documentation` tool with the user's own phrasing. If the first query is vague or returns nothing actionable, try one shorter variant that keeps the same user-visible problem.
- If the search returns a clear match, respond with a concise, direct answer that cites the docs page (title + URL) and explains how the existing functionality addresses the user's ask. Do not file an issue and do not invoke the helper script.
- If the search returns an ambiguous or partial match, briefly summarize what does exist and ask one clarifying question about whether that satisfies the user's intent before deciding whether to file.
- If the search turns up nothing relevant, proceed to step 4. Do not invent workarounds, and do not imply a feature is missing when the docs already answer the question.
- Docs-first checking applies primarily to feature-request-shaped reports. For reproducible bugs and regressions, skip ahead to step 4 unless docs would clarify whether the current behavior is intended.
### 4. Ground the report in product and code context when helpful
- Search the `warpdotdev/warp` repo via the `gh` CLI for matching product language, expected workflows, setting names, or UX intent when that context would make the draft more actionable.
- Search the `warpdotdev/warp` repo via the `gh` CLI for matching components, settings surfaces, feature flags, and likely code paths when implementation context would help triage.
- Add source references only when they point to real files, symbols, settings names, or spec text that plausibly relate to the feedback.
- Never invent a root cause just to make the report sound complete.
### 5. Draft the issue
- Keep the title concrete and user-visible.
- Rewrite rough notes into a polished issue body with the shared section structure below.
- Preserve the user's meaning while making the report easier for an engineer to act on.
- If the exact reproduction steps are still uncertain, write the best-supported scenario and call out what is still unknown.
- Make the title specific enough that it can be used as the primary duplicate-detection query.
### 6. Handle image attachments, if present
- If the user's query includes one or more image attachments visible to you as multimodal context, apply the rules in this step in addition to the normal drafting workflow.
- Incorporate what you can see in each image into the drafted issue body. At minimum, describe the relevant visual content in prose in the `Problem`, `Actual behavior`, or similar section so the report remains coherent even if the images do not end up attached to the filed issue.
- In the `Artifacts` section, emit one entry per attached image, in the order you encountered them. Each entry must include both a **caption** describing what the screenshot depicts and a drop-target placeholder, so the issue body still conveys what each screenshot was meant to show even if the user never uploads the image. Use this format per image, numbered starting at 1:
```md
**Screenshot 1:** <one-sentence caption describing what this screenshot shows, written from what you saw in multimodal context>.
_Paste screenshot 1 here_
```
Captions should be concrete (for example, "Agent footer with the Send button misaligned below the text input") rather than generic ("a screenshot" or "the bug"). Do not invent details that aren't visible; if an image is ambiguous or unreadable, say so in the caption.
- When invoking the helper script (see the `Output` section below), pass `--use browser` instead of `--use gh`. This makes the script skip `gh issue create` and open the prefilled new-issue page in the browser so the user can upload images through GitHub's native drag-and-drop.
- In your final response, explicitly instruct the user to paste or drag each attached image into the body at the corresponding `_Paste screenshot N here_` line(s) and then submit the issue. Reference the count of attached images so the user knows how many to paste. Do not claim the issue has been filed until the user submits.
- If the user's query has no image attachments, do not add captions or placeholders, pass `--use gh` as usual, and do not add drag-and-drop instructions to your final response.
### 7. Check for likely duplicates before filing
- Before invoking `scripts/file_feedback_issue.py`, search issues in `warpdotdev/warp` for likely title matches using the drafted title as the primary query.
- Use a lightweight title-based check only. Prefer precision over recall, and do not run a broad semantic fishing expedition.
- Start with the exact drafted title. If the exact title returns no clear title match, try one shorter normalized variant that removes filler words while preserving the same user-visible problem.
- A suitable command is:
```bash
GH_PAGER=cat gh issue list \
--repo warpdotdev/warp \
--state all \
--limit 10 \
--search "<title> in:title" \
--json number,title,url,state
```
- Treat a result as a duplicate candidate only when the existing issue title clearly refers to the same underlying problem or request.
- If you find a clear title match, do not file a new issue. Respond by pointing the user to the existing issue and explain briefly why it appears to match.
- If no clear title match is found, proceed to file the new issue.
## Issue Structure
Use these sections in order when they apply:
- Summary
- Problem
- Reproduction steps or desired workflow
- Artifacts
- Warp version
- Operating system
For bugs, regressions, and UX issues, also include:
- Expected behavior
- Actual behavior
If you found grounded repo evidence, append:
- Possible source references
Section rules:
- If a required field is unknown, say `Unknown`.
- If an optional section does not apply, omit it.
- If no artifacts are attached, say `None attached`.
- For feature requests, the `Problem` section can describe the current friction or missing capability, and the `Reproduction steps or desired workflow` section can describe the desired flow instead of literal repro steps.
- For feature requests, `Expected behavior` and `Actual behavior` are usually omitted unless they genuinely clarify the request.
## Source Reference Rules
- Prefer concrete file paths, symbols, settings names, and spec headings over broad guesses like "probably in the terminal code."
- Cite spec references when they clarify the intended workflow, wording, or product expectation.
- Cite implementation references when they localize the likely surface area or affected component.
- Keep each reference brief and explain why it is relevant.
- Omit references that are speculative, weakly related, or only tangentially connected.
## Writing Rules
- Turn vague feedback into specific behavior without changing the user's meaning.
- Convert fuzzy narratives into numbered reproduction steps when the sequence can be inferred responsibly.
- Call out severity, frequency, and regression context when available.
- Be explicit about uncertainty or missing details instead of smoothing them over.
- Do not mention this skill, private internal discussion, or speculative implementation theories in the issue body.
- Do not pad the issue with source references unless they genuinely improve debugging or triage.
- Never treat this workflow as permission to implement a fix. This skill may only file an issue, decline to file, or point to an existing issue.
- Never suggest code changes, patches, or implementation diffs in the issue body, in your response, or as a follow-up action — even informally or "as a starting point." If the user asks for a fix, decline and redirect them to the filed issue.
- **Refuse to file in three situations:** (1) the report is out of scope per step 1, (2) the minimum actionable information is still missing after the single clarifying round in step 2, or (3) step 3 found a clear docs match for what the user is asking about. In each case, explain the decision in one or two plain sentences and do not call the helper script. Never imply a feature is missing when docs already answer the question, and never file a placeholder issue just to acknowledge the user.
## Output
Use the bundled helper script `scripts/file_feedback_issue.py` to file the issue in `warpdotdev/warp` instead of calling `gh` directly. The script requires a `--use` flag that selects the filing method explicitly:
- `--use gh`: creates the issue with `gh issue create`. Requires `gh` to be installed and authenticated for `github.com`. Prints a `created` result with `issue_url` on success, or `unavailable` when `gh` is missing or unauthenticated. Does not silently fall back to the browser.
- `--use browser`: opens the prefilled new-issue page in the browser so the user can upload image attachments via GitHub's web UI. Prints a `browser_opened` result on success. If the browser cannot be opened, automatically falls back to `gh issue create` and prints a `created` result with `browser_unavailable: true`; if both are unavailable, prints `failed`. Use this whenever the user attached one or more images to the query.
- The script always targets `warpdotdev/warp` on `github.com`.
Write the final body to a temporary UTF-8 file and pass the final title directly as an argument. When the user has no image attachments:
```bash
python3 scripts/file_feedback_issue.py \
--use gh \
--title "<title>" \
--body-file <body-file>
```
When the user has one or more image attachments:
```bash
# Opens the prefilled new-issue page in the browser so the user can drop
# their images into the issue body via GitHub's web UI.
python3 scripts/file_feedback_issue.py \
--use browser \
--title "<title>" \
--body-file <body-file>
```
The title and body should be structured as follows:
Issue title: `<title>`
Issue body:
```md
<!-- warp-feedback-skill:v1 -->
## Summary
...
## Problem
...
<!-- Include these sections for bugs, regressions, and UX issues when applicable. -->
## Expected behavior
...
## Actual behavior
...
## Reproduction steps or desired workflow
1. ...
## Artifacts
...
## Warp version
...
## Operating system
...
<!-- Omit this section if there are no grounded references. -->
## Possible source references
- path or symbol: why it may be relevant
```
After completing the duplicate-check and filing workflow:
- If the duplicate-check step finds an existing matching issue, respond with the existing issue link and a brief summary (24 sentences max) explaining that a likely duplicate already exists, including the matching title, whether that issue is open or closed, and why it appears to match. Do not create another issue.
- If the JSON result has `status: "created"` and `browser_unavailable: true`, explicitly tell the user that the browser could not be opened (include the `message` field from the result) and that the issue was filed programmatically with the available text contents. Make clear that image attachments were not uploaded to the issue. Then provide the issue link and a brief summary (35 sentences max) of what was filed.
- If the JSON result has `status: "created"` (without `browser_unavailable`), respond with only the created issue link and a brief summary (35 sentences max) of what was filed: the classification, the core problem, and any notable missing details.
- If the JSON result has `status: "unavailable"`, say that you could not file the issue because the GitHub CLI was not installed or authenticated, and include the returned message.
- If the JSON result has `status: "browser_opened"` (seen when `--use browser` was used for image-bearing feedback), do not claim the issue has been filed. Pass the returned `message` to the user, and explicitly instruct them to paste or drag each attached image into the placeholder line(s) in the issue body and then submit the issue. When the result includes a `body` field (the drafted body was too long to prefill in the URL), surface that body so the user can paste it into the issue form before attaching images.
- If the JSON result has `status: "failed"`, say that you could not file the issue because the filing flow failed, and include the returned `error` or `gh_error` message. When `--use browser` was used and filing failed (meaning both the browser and the `gh` CLI fallback were unavailable), make it explicit that image attachments were not handed off and no issue was filed.
- If issue filing is not possible for any other reason, say explicitly that no issue was filed.
Read `references/examples.md` only if you need a compact example of the expected polish level.
@@ -1,39 +0,0 @@
# Examples
Use these only to calibrate tone and structure. Do not copy them mechanically.
## Example 1: Bug
Rough feedback:
`Warp loses my selected theme after restart on macOS. I changed it twice and it keeps going back.`
Draft shape:
- classification: `bug`
- title: `Theme selection resets after restarting Warp on macOS`
- missing details: Warp version, macOS version, whether the issue reproduces every restart
Key body moves:
- turn the complaint into a concrete restart flow
- state the expected persisted theme behavior
- mark unknown version details as `Unknown`
## Example 2: Feature Request
Rough feedback:
`The command palette is too hard to scan. I want better grouping for settings-related actions.`
Draft shape:
- classification: `feature request`
- title: `Improve Command Palette grouping for settings-related actions`
- missing details: concrete commands that are hardest to find, screenshots if the user has them
Key body moves:
- describe the current friction instead of inventing reproduction steps
- focus the request on the desired workflow and user impact
- draft immediately unless a specific missing fact blocks clarity
@@ -1,33 +0,0 @@
# Log And Crash Artifact Guidance
Use this only for crashes, startup failures, rendering bugs, sync issues, or hard-to-reproduce regressions.
- Ask for logs only when they are likely to improve the report.
- Note in the issue that logs or crash reports were attached, but do not claim they contain console input or output.
- In the `Artifacts` section, mention the exact file names or bundles that were attached.
macOS paths and commands:
- Logs live under `~/Library/Logs/`
- Stable app logs are typically `~/Library/Logs/warp.log*`
- Preview app logs are typically `~/Library/Logs/warp_preview.log*`
- Stable zip command: `zip -j ~/Desktop/warp-logs.zip ~/Library/Logs/warp.log*`
- Preview zip command: `zip -j ~/Desktop/warp_preview-logs.zip ~/Library/Logs/warp_preview.log*`
- If Warp still opens, the user can search `View Warp Logs` in the Command Palette
- Crash reports may also exist under `~/Library/Logs/DiagnosticReports/` as Warp `.ips` files
Linux paths:
- Logs live under Warp's state directory.
- Stable app logs are typically `~/.local/state/warp-terminal/warp.log*`
- Preview app logs are typically `~/.local/state/warp-terminal-preview/warp_preview.log*`
- If the exact channel is unclear, ask the user to open the nearest `warp*.log*` files under `~/.local/state/`
Windows paths:
- Logs live under Warp's local app data state directory.
- Stable app logs are typically `%LOCALAPPDATA%\warp\Warp\data\logs\warp.log*`
- Preview app logs are typically `%LOCALAPPDATA%\warp\WarpPreview\data\logs\warp_preview.log*`
- If the exact channel is unclear, ask the user to look under `%LOCALAPPDATA%\warp\` for the relevant `Warp*` folder and attach the matching `warp*.log*` files from its `data\logs\` directory
If no artifacts are available, say so plainly instead of implying they were checked.
@@ -1,29 +0,0 @@
# Platform Guidance
Use this only when the operating system, Warp version, or operating-system-specific behavior is relevant and missing.
- Resolve the execution surface first: packaged native Warp app or web session.
- Prefer the bundled helper scripts and metadata files over prose or ad hoc shell inspection.
- If the user already gave a sufficiently specific OS version or Warp version, do not ask again.
- Include both OS name and version in the `Operating system` section when available.
- Include the `Warp version` section when available, and note when the report is about a web session rather than a packaged native install.
## Operating system
Resolve the OS from the machine where the reported behavior actually happens. Do not substitute the OS of a different host, container, or remote target unless that is where the bug occurs.
Run the bundled helper script when you need to resolve OS name and version:
```bash
python3 scripts/resolve_platform.py
```
Use the script output directly when filling `Operating system`. Ask the user only if Python is unavailable or the output still does not identify the relevant environment precisely enough.
## Warp version
For packaged native Warp installs, read the bundled version metadata file directly:
The bundled version metadata file lives at `../../metadata/version.json` relative to the skill root. Read its `warp_version` field and use that value directly.
Use the file contents directly when filling `Warp version`. Ask the user only if Python is unavailable, the bundled metadata file is missing or unreadable, or the report is about a browser or web session rather than a packaged native install.
- Browser or web session with no local Warp executable: use the version or build identifier from the session URL or surrounding session metadata when present. If there is no concrete version string, record that it was a web session and leave `Warp version` as `Unknown` rather than guessing.
@@ -1,308 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import platform
import urllib.parse
import webbrowser
from pathlib import Path
import shutil
import subprocess
import sys
DEFAULT_REPO = "warpdotdev/warp"
DEFAULT_HOSTNAME = "github.com"
FEEDBACK_LABEL = "in-app-feedback"
# GitHub's new-issue page accepts a prefilled title and body via query
# parameters, but browsers and intermediate servers commonly cap URLs around
# 8 KB. Keep a conservative threshold that leaves headroom for the base URL
# and percent-encoding overhead.
MAX_PREFILL_URL_LENGTH = 8000
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"File a GitHub issue in warpdotdev/warp. The caller must choose "
"the filing method via --use: `gh` to create the issue directly with the "
"gh CLI, or `browser` to open the prefilled new-issue page in the browser "
"(used when attachments require manual upload via GitHub's web UI)."
)
)
parser.add_argument(
"--use",
dest="use_method",
required=True,
choices=["gh", "browser"],
help=(
"Filing method. `gh` creates the issue via the gh CLI (requires gh to be "
"installed and authenticated). `browser` opens the prefilled new-issue "
"page in the user's browser so attachments can be uploaded via GitHub's "
"web UI."
),
)
parser.add_argument("--title", required=True, help="Issue title.")
parser.add_argument(
"--body-file",
type=Path,
required=True,
help="Path to a UTF-8 file containing the issue body.",
)
return parser.parse_args()
def read_text(path: Path, field_name: str) -> str:
try:
return path.read_text(encoding="utf-8")
except OSError as exc:
raise SystemExit(f"Failed to read {field_name} from {path}: {exc}") from exc
def normalize_title(title: str) -> str:
normalized_title = " ".join(title.splitlines()).strip()
if not normalized_title:
raise SystemExit("Issue title must not be empty.")
return normalized_title
def normalize_body(body: str) -> str:
normalized_body = body.rstrip("\n")
if not normalized_body.strip():
raise SystemExit("Issue body must not be empty.")
return normalized_body
def gh_path_if_authenticated() -> str | None:
gh_path = shutil.which("gh")
if gh_path is None:
return None
auth_result = subprocess.run(
[gh_path, "auth", "status", "--hostname", DEFAULT_HOSTNAME],
capture_output=True,
text=True,
check=False,
)
if auth_result.returncode != 0:
return None
return gh_path
def create_issue_with_gh(
gh_path: str,
title: str,
body: str,
) -> tuple[str | None, str | None]:
command = [
gh_path, "issue", "create",
"--repo", DEFAULT_REPO,
"--title", title,
"--body", body,
"--label", FEEDBACK_LABEL,
]
result = subprocess.run(command, capture_output=True, text=True, check=False)
if result.returncode != 0:
error_output = "\n".join(part for part in [result.stdout.strip(), result.stderr.strip()] if part).strip()
return None, error_output or "gh issue create failed"
issue_url = result.stdout.strip().splitlines()[-1].strip()
if not issue_url:
return None, "gh issue create succeeded but did not return an issue URL"
return issue_url, None
def build_new_issue_url(title: str, body: str | None) -> str:
"""Build a GitHub new-issue URL with the provided title and optional body prefilled."""
base = f"https://{DEFAULT_HOSTNAME}/{DEFAULT_REPO}/issues/new"
params: list[tuple[str, str]] = [("title", title)]
if body is not None:
params.append(("body", body))
return f"{base}?{urllib.parse.urlencode(params, quote_via=urllib.parse.quote)}"
def browser_is_available() -> tuple[bool, str | None]:
"""Return whether opening a browser is likely to succeed on this system.
On macOS and Windows a GUI session is effectively always present for the
user running this script. On Linux and other unix-likes ``webbrowser.open``
can return True without actually opening a browser when no display server is
running (for example, in a headless SSH session), so we require ``DISPLAY``
or ``WAYLAND_DISPLAY`` to be set.
"""
system = platform.system()
if system in ("Darwin", "Windows"):
return True, None
if os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"):
return True, None
return (
False,
(
"No graphical display is available (neither DISPLAY nor WAYLAND_DISPLAY "
"is set), so the GitHub new-issue page cannot be opened in a browser."
),
)
def open_in_browser(url: str) -> bool:
try:
return webbrowser.open(url, new=2)
except webbrowser.Error:
return False
def fallback_to_browser(title: str, body: str) -> int:
"""Open the GitHub new-issue page with a prefilled title (and body when it fits).
Intended for the caller-selected ``--use browser`` path, which the feedback skill
uses when image attachments need to be uploaded via GitHub's web UI. When the full
prefill URL would exceed ``MAX_PREFILL_URL_LENGTH``, the body is omitted from the
URL and surfaced in the JSON result under a ``body`` field so the caller can tell
the user to paste it into the issue form manually.
When the browser cannot be opened, the function attempts a ``gh issue create``
fallback so the issue is still filed with the available text contents. The returned
payload includes ``browser_unavailable: true`` when that fallback is used, so the
caller can inform the user that the browser could not be opened and images were not
uploaded. If both the browser and ``gh`` are unavailable, filing fails.
"""
full_url = build_new_issue_url(title, body)
body_fits_in_url = len(full_url) <= MAX_PREFILL_URL_LENGTH
url_to_open = full_url if body_fits_in_url else build_new_issue_url(title, None)
browser_available, browser_unavailable_reason = browser_is_available()
base_payload: dict[str, object] = {
"method": "browser",
"repo": DEFAULT_REPO,
"url": url_to_open,
}
if not body_fits_in_url:
# Surface the body so the caller can instruct the user to paste it in.
base_payload["body"] = body
browser_failure_reason: str | None = None
if not browser_available:
browser_failure_reason = browser_unavailable_reason or "No display available."
elif not open_in_browser(url_to_open):
browser_failure_reason = "Unable to open a web browser for the prefilled new-issue page."
if browser_failure_reason is not None:
# Browser unavailable — try gh CLI as a fallback so the issue is still filed.
gh_path = gh_path_if_authenticated()
if gh_path is not None:
issue_url, _gh_error = create_issue_with_gh(gh_path, title, body)
if issue_url is not None:
print_result({
"status": "created",
"method": "gh",
"repo": DEFAULT_REPO,
"issue_url": issue_url,
"browser_unavailable": True,
"message": (
f"{browser_failure_reason} "
"The issue was filed programmatically with the available text contents. "
"Image attachments were not uploaded."
),
})
return 0
base_payload["status"] = "failed"
base_payload["error"] = (
browser_failure_reason
+ " Image attachments could not be handed off through the browser flow; "
"no issue has been filed."
)
print_result(base_payload)
return 1
# The browser path is only used by the skill when image attachments are
# present, so the user-facing message always references pasting/dropping them.
if body_fits_in_url:
message = (
"Opened the GitHub new-issue page in your browser with the title and body prefilled. "
"Paste or drag your attached screenshot(s) into the body at the placeholder line(s), "
"then submit the issue."
)
else:
message = (
"Opened the GitHub new-issue page in your browser with the title prefilled. "
"The drafted body was too long to include in the URL; paste the body (returned in "
"this result's `body` field) into the issue form first, then paste or drag your "
"attached screenshot(s) into the placeholder line(s), then submit."
)
base_payload["status"] = "browser_opened"
base_payload["message"] = message
print_result(base_payload)
return 0
def print_result(payload: dict[str, object]) -> None:
json.dump(payload, sys.stdout)
sys.stdout.write("\n")
def file_with_gh(title: str, body: str) -> int:
"""File the issue via the gh CLI. Returns status `unavailable` when gh isn't
installed or not authenticated; the caller is responsible for choosing a
different --use method in that case (no automatic fallback happens here).
"""
gh_path = gh_path_if_authenticated()
if gh_path is None:
print_result(
{
"status": "unavailable",
"method": "gh",
"repo": DEFAULT_REPO,
"message": f"GitHub CLI is not installed or not authenticated for {DEFAULT_HOSTNAME}.",
}
)
return 0
issue_url, gh_error = create_issue_with_gh(gh_path, title, body)
if issue_url is not None:
print_result(
{
"status": "created",
"method": "gh",
"repo": DEFAULT_REPO,
"issue_url": issue_url,
}
)
return 0
print_result(
{
"status": "failed",
"method": "gh",
"repo": DEFAULT_REPO,
"gh_error": gh_error,
}
)
return 1
def main() -> int:
args = parse_args()
title = normalize_title(args.title)
body = normalize_body(read_text(args.body_file, "body"))
if args.use_method == "browser":
return fallback_to_browser(title, body)
# argparse enforces choices=["gh", "browser"], so the remaining case is "gh".
return file_with_gh(title, body)
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,75 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
from pathlib import Path
import platform
import subprocess
import sys
def linux_info() -> dict[str, str]:
fields: dict[str, str] = {}
os_release = Path("/etc/os-release")
if os_release.exists():
for line in os_release.read_text(encoding="utf-8", errors="replace").splitlines():
if "=" not in line or line.startswith("#"):
continue
key, value = line.split("=", 1)
fields[key] = value.strip().strip(chr(34))
return {
"os": fields.get("PRETTY_NAME") or fields.get("NAME") or "Linux",
"os_version": fields.get("VERSION_ID") or fields.get("VERSION") or platform.release(),
"kernel": platform.release(),
}
def mac_info() -> dict[str, str]:
build = None
result = subprocess.run(
["sw_vers", "-buildVersion"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
build = result.stdout.strip() or None
return {
"os": "macOS",
"os_version": platform.mac_ver()[0] or None,
"os_build": build,
}
def windows_info() -> dict[str, str]:
version = sys.getwindowsversion()
return {
"os": "Windows",
"os_version": f"{version.major}.{version.minor}.{version.build}",
}
def main() -> int:
system = platform.system()
if system == "Darwin":
info = mac_info()
elif system == "Linux":
info = linux_info()
elif system == "Windows":
info = windows_info()
else:
info = {
"os": system or "Unknown",
"os_version": platform.version() or None,
}
json.dump({k: v for k, v in info.items() if v}, sys.stdout, sort_keys=True)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
"""Unit tests for file_feedback_issue.py.
Run with:
python3 resources/channel-gated-skills/dogfood/feedback/scripts/test_file_feedback_issue.py
"""
from __future__ import annotations
import contextlib
import importlib.util
import io
import json
import sys
import unittest
import urllib.parse
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
MODULE_PATH = SCRIPT_DIR / "file_feedback_issue.py"
def load_module():
spec = importlib.util.spec_from_file_location("file_feedback_issue", MODULE_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
ffi = load_module()
class BuildNewIssueUrlTests(unittest.TestCase):
def test_url_includes_repo_and_title(self):
url = ffi.build_new_issue_url("Hello world", None)
self.assertTrue(url.startswith(f"https://{ffi.DEFAULT_HOSTNAME}/{ffi.DEFAULT_REPO}/issues/new?"))
parsed = urllib.parse.urlparse(url)
qs = urllib.parse.parse_qs(parsed.query)
self.assertEqual(qs["title"], ["Hello world"])
self.assertNotIn("body", qs)
def test_url_includes_body_when_provided(self):
url = ffi.build_new_issue_url("T", "body text with spaces & symbols?")
parsed = urllib.parse.urlparse(url)
qs = urllib.parse.parse_qs(parsed.query)
self.assertEqual(qs["title"], ["T"])
self.assertEqual(qs["body"], ["body text with spaces & symbols?"])
def test_special_characters_are_percent_encoded(self):
url = ffi.build_new_issue_url("crash: `foo`", "<script>alert(1)</script>")
# Spaces should be %20, not +, because we use quote_via=quote.
self.assertIn("%20", url)
self.assertNotIn("+", url.split("?", 1)[1])
# Angle brackets and backticks are percent-encoded.
self.assertIn("%3C", url)
self.assertIn("%3E", url)
class FallbackToBrowserTests(unittest.TestCase):
def _run_fallback(
self,
title,
body,
open_ok=True,
browser_available=(True, None),
gh_path=None,
gh_create_result=(None, "gh not configured"),
):
with mock.patch.object(
ffi, "open_in_browser", return_value=open_ok
) as open_mock, mock.patch.object(
ffi, "browser_is_available", return_value=browser_available
), mock.patch.object(
ffi, "gh_path_if_authenticated", return_value=gh_path
), mock.patch.object(
ffi, "create_issue_with_gh", return_value=gh_create_result
):
buf = io.StringIO()
with redirect_stdout(buf):
rc = ffi.fallback_to_browser(title, body)
payload = json.loads(buf.getvalue().strip())
return rc, payload, open_mock
def test_short_body_is_embedded_in_url(self):
rc, payload, open_mock = self._run_fallback("t", "small body")
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "browser_opened")
self.assertEqual(payload["method"], "browser")
# Short bodies fit in the URL; no `body` field is surfaced separately.
self.assertNotIn("body", payload)
open_mock.assert_called_once()
called_url = open_mock.call_args.args[0]
parsed = urllib.parse.urlparse(called_url)
qs = urllib.parse.parse_qs(parsed.query)
self.assertEqual(qs["title"], ["t"])
self.assertEqual(qs["body"], ["small body"])
def test_long_body_surfaces_body_in_payload_and_opens_title_only_url(self):
huge_body = "A" * (ffi.MAX_PREFILL_URL_LENGTH + 500)
rc, payload, open_mock = self._run_fallback("t", huge_body)
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "browser_opened")
# Long bodies are returned in the payload so the caller can instruct
# the user to paste them; no clipboard copy happens.
self.assertEqual(payload["body"], huge_body)
called_url = open_mock.call_args.args[0]
parsed = urllib.parse.urlparse(called_url)
qs = urllib.parse.parse_qs(parsed.query)
self.assertEqual(qs["title"], ["t"])
self.assertNotIn("body", qs)
def test_browser_failure_falls_back_to_gh_when_available(self):
rc, payload, _ = self._run_fallback(
"t",
"small body",
open_ok=False,
gh_path="/usr/bin/gh",
gh_create_result=("https://github.com/warpdotdev/warp/issues/7", None),
)
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "created")
self.assertEqual(payload["method"], "gh")
self.assertTrue(payload["issue_url"].endswith("/issues/7"))
self.assertTrue(payload.get("browser_unavailable"))
self.assertIn("attachment", payload["message"].lower())
def test_browser_failure_reports_failed_when_gh_also_unavailable(self):
rc, payload, open_mock = self._run_fallback("t", "small body", open_ok=False)
# Filing failed, so exit code must be non-zero for shell callers that
# check `$?` to distinguish "issue filed" from "issue not filed".
self.assertEqual(rc, 1)
self.assertEqual(payload["status"], "failed")
self.assertEqual(payload["method"], "browser")
self.assertIn("url", payload)
# Failure messaging must acknowledge attachments so the user understands
# why the image workflow couldn't complete.
self.assertIn("attachment", payload["error"].lower())
def test_browser_unavailable_falls_back_to_gh_when_available(self):
rc, payload, open_mock = self._run_fallback(
"t",
"small body",
browser_available=(False, "No DISPLAY"),
gh_path="/usr/bin/gh",
gh_create_result=("https://github.com/warpdotdev/warp/issues/8", None),
)
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "created")
self.assertEqual(payload["method"], "gh")
self.assertTrue(payload["issue_url"].endswith("/issues/8"))
self.assertTrue(payload.get("browser_unavailable"))
self.assertIn("No DISPLAY", payload["message"])
self.assertIn("attachment", payload["message"].lower())
open_mock.assert_not_called()
def test_browser_unavailable_reports_failed_when_gh_also_unavailable(self):
rc, payload, open_mock = self._run_fallback(
"t",
"small body",
browser_available=(False, "No DISPLAY"),
)
self.assertEqual(rc, 1)
self.assertEqual(payload["status"], "failed")
self.assertEqual(payload["method"], "browser")
self.assertIn("No DISPLAY", payload["error"])
self.assertIn("attachment", payload["error"].lower())
open_mock.assert_not_called()
class BrowserIsAvailableTests(unittest.TestCase):
def test_darwin_is_always_available(self):
with mock.patch.object(ffi.platform, "system", return_value="Darwin"), \
mock.patch.dict(ffi.os.environ, {}, clear=True):
ok, reason = ffi.browser_is_available()
self.assertTrue(ok)
self.assertIsNone(reason)
def test_windows_is_always_available(self):
with mock.patch.object(ffi.platform, "system", return_value="Windows"), \
mock.patch.dict(ffi.os.environ, {}, clear=True):
ok, reason = ffi.browser_is_available()
self.assertTrue(ok)
self.assertIsNone(reason)
def test_linux_without_display_is_unavailable(self):
with mock.patch.object(ffi.platform, "system", return_value="Linux"), \
mock.patch.dict(ffi.os.environ, {}, clear=True):
ok, reason = ffi.browser_is_available()
self.assertFalse(ok)
self.assertIsNotNone(reason)
def test_linux_with_display_is_available(self):
with mock.patch.object(ffi.platform, "system", return_value="Linux"), \
mock.patch.dict(ffi.os.environ, {"DISPLAY": ":0"}, clear=True):
ok, reason = ffi.browser_is_available()
self.assertTrue(ok)
self.assertIsNone(reason)
def test_linux_with_wayland_display_is_available(self):
with mock.patch.object(ffi.platform, "system", return_value="Linux"), \
mock.patch.dict(ffi.os.environ, {"WAYLAND_DISPLAY": "wayland-0"}, clear=True):
ok, reason = ffi.browser_is_available()
self.assertTrue(ok)
self.assertIsNone(reason)
class FileWithGhTests(unittest.TestCase):
def _run(self, **mocks):
patches = []
for attr, value in mocks.items():
patches.append(mock.patch.object(ffi, attr, return_value=value))
buf = io.StringIO()
with contextlib.ExitStack() as stack:
for p in patches:
stack.enter_context(p)
with redirect_stdout(buf):
rc = ffi.file_with_gh("hello", "body")
return rc, json.loads(buf.getvalue().strip())
def test_reports_unavailable_when_gh_missing(self):
rc, payload = self._run(gh_path_if_authenticated=None)
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "unavailable")
self.assertEqual(payload["method"], "gh")
self.assertIn("message", payload)
def test_creates_when_gh_available(self):
rc, payload = self._run(
gh_path_if_authenticated="/usr/bin/gh",
create_issue_with_gh=(
"https://github.com/warpdotdev/warp/issues/42",
None,
),
)
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "created")
self.assertEqual(payload["method"], "gh")
self.assertTrue(payload["issue_url"].endswith("/issues/42"))
def test_reports_failed_when_gh_create_fails(self):
rc, payload = self._run(
gh_path_if_authenticated="/usr/bin/gh",
create_issue_with_gh=(None, "gh failed to create issue"),
)
# Filing failed, so exit code must be non-zero.
self.assertEqual(rc, 1)
self.assertEqual(payload["status"], "failed")
self.assertEqual(payload["method"], "gh")
self.assertEqual(payload["gh_error"], "gh failed to create issue")
class MainTests(unittest.TestCase):
"""Tests for the --use dispatch in main().
The caller must pick a filing method explicitly; main() does not fall back
between the two paths.
"""
def setUp(self):
self.body_file = SCRIPT_DIR / "_tmp_body.txt"
self.body_file.write_text("my body", encoding="utf-8")
def tearDown(self):
self.body_file.unlink(missing_ok=True)
def _run_main(self, argv_extras):
argv = [
"file_feedback_issue.py",
"--title",
"hello",
"--body-file",
str(self.body_file),
*argv_extras,
]
buf = io.StringIO()
with mock.patch.object(sys, "argv", argv):
with redirect_stdout(buf):
rc = ffi.main()
return rc, json.loads(buf.getvalue().strip())
def test_use_gh_creates_when_gh_available(self):
with mock.patch.object(
ffi, "gh_path_if_authenticated", return_value="/usr/bin/gh"
), mock.patch.object(
ffi,
"create_issue_with_gh",
return_value=(
"https://github.com/warpdotdev/warp/issues/999",
None,
),
):
rc, payload = self._run_main(["--use", "gh"])
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "created")
self.assertEqual(payload["method"], "gh")
self.assertTrue(payload["issue_url"].endswith("/issues/999"))
def test_use_gh_reports_unavailable_and_does_not_open_browser(self):
with mock.patch.object(
ffi, "gh_path_if_authenticated", return_value=None
), mock.patch.object(ffi, "open_in_browser", return_value=True) as open_mock:
rc, payload = self._run_main(["--use", "gh"])
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "unavailable")
self.assertEqual(payload["method"], "gh")
# Critical invariant: --use gh must not silently fall back to the browser.
open_mock.assert_not_called()
def test_use_browser_does_not_touch_gh(self):
with mock.patch.object(
ffi, "gh_path_if_authenticated"
) as gh_mock, mock.patch.object(
ffi, "create_issue_with_gh"
) as create_mock, mock.patch.object(
ffi, "browser_is_available", return_value=(True, None)
), mock.patch.object(ffi, "open_in_browser", return_value=True) as open_mock:
rc, payload = self._run_main(["--use", "browser"])
self.assertEqual(rc, 0)
self.assertEqual(payload["status"], "browser_opened")
self.assertEqual(payload["method"], "browser")
open_mock.assert_called_once()
gh_mock.assert_not_called()
create_mock.assert_not_called()
def test_missing_use_flag_exits_with_argparse_error(self):
stderr = io.StringIO()
with mock.patch.object(
sys,
"argv",
[
"file_feedback_issue.py",
"--title",
"hello",
"--body-file",
str(self.body_file),
],
), mock.patch.object(sys, "stderr", stderr), contextlib.suppress(SystemExit):
ffi.main()
self.assertIn("--use", stderr.getvalue())
def test_invalid_use_value_is_rejected(self):
stderr = io.StringIO()
with mock.patch.object(
sys,
"argv",
[
"file_feedback_issue.py",
"--use",
"carrier-pigeon",
"--title",
"hello",
"--body-file",
str(self.body_file),
],
), mock.patch.object(sys, "stderr", stderr), contextlib.suppress(SystemExit):
ffi.main()
self.assertIn("invalid choice", stderr.getvalue())
if __name__ == "__main__":
unittest.main()
@@ -33,7 +33,7 @@ grep -i "font" {{settings_schema_path}}
Once you have a candidate key name, run the bundled script to get the **full dotted path**, the setting's properties, and any parent context. This is critical — the schema has multiple sections with similar names (e.g. several `input` keys), so never assume the nesting from grep output alone.
```sh
python3 <skill_dir>/scripts/find_setting.py {{settings_schema_path}} <key_name>
python3 {{skill_dir}}/scripts/find_setting.py {{settings_schema_path}} <key_name>
```
The output gives you the unambiguous full path (e.g. `properties.appearance.properties.input.properties.input_mode`) and the setting's full definition including valid values.
@@ -0,0 +1,285 @@
---
name: oz-platform
description: Use Warp's REST API and command line to run, configure, and inspect Oz cloud agents
---
# oz-platform
Use the Oz REST API and CLI to:
* Spawn cloud agents
* Get the status of a cloud agent
* Schedule cloud agents to run repeatedly
* Create and manage the environments in which cloud agents run
* Provide secrets for cloud agents to use
## Command Line
The Oz CLI is installed as `{{warp_cli_binary_name}}`. To get help output, use `{{warp_cli_binary_name}} help` or `{{warp_cli_binary_name}} help <subcommand>`.
Prefer `--output-format text` to review the response, or `--output-format json` to parse fields with `jq`.
You can find more information at https://docs.warp.dev/reference/cli.
The most important commands are:
* `{{warp_cli_binary_name}} agent run-cloud`: Spawn a new cloud agent. You can configure the prompt, model, environment, and other settings.
* `{{warp_cli_binary_name}} run list` and `{{warp_cli_binary_name}} run get <run-id>`: List all cloud agent runs, and get details about a particular run.
* `{{warp_cli_binary_name}} environment list` and `{{warp_cli_binary_name}} environment get`: List available environments, and get more information about a particular environment.
* `{{warp_cli_binary_name}} schedule list` and `{{warp_cli_binary_name}} schedule get`: List scheduled tasks with most recent runs, and get more information about a particular scheduled run.
Most subcommands support the `--output-format json` flag to produce JSON output, which you can pipe into `jq` or other commands.
### Examples
Start a cloud agent, and then monitor its status:
```sh
$ {{warp_cli_binary_name}} agent run-cloud --prompt "Update the login error to be more specific" --environment UA17BXYZ
# ...
Spawned agent with run ID: 5972cca4-a410-42af-930a-e56bc23e07ac
```
```sh
$ {{warp_cli_binary_name}} run get 5972cca4-a410-42af-930a-e56bc23e07ac
# ...
```
Schedule an agent to summarize feedback every day at 8am UTC:
```sh
$ {{warp_cli_binary_name}} schedule create --cron "0 8 * * *" \
--name "GitHub issue summary" \
--prompt "Collect all feedback from new GitHub issues and provide a summary report" \
--environment UA17BXYZ
```
List and inspect scheduled agents:
```sh
$ {{warp_cli_binary_name}} schedule list
$ {{warp_cli_binary_name}} schedule get <schedule-id>
```
Create a secret for cloud agents to use:
```sh
$ {{warp_cli_binary_name}} secret create JIRA_API_KEY --team --value-file jira_key.txt --description "API key to access Jira"
```
## REST API
Oz has a REST API for starting and inspecting cloud agents.
All API requests require authentication using an API key. The user can generate API keys in their Warp settings, on the `Platform` page (accessible via `{{warp_url_scheme}}://settings/platform`).
You can find the full OpenAPI specification here: https://docs.warp.dev/reference/api-and-sdk
### TypeScript / JavaScript SDK
The TypeScript SDK is available via NPM. It is fully async, and works with Node, Bun, and Deno.
* Package link: https://www.npmjs.com/package/oz-agent-sdk
* Source Code: https://github.com/warpdotdev/oz-sdk-typescript
* API reference: https://raw.githubusercontent.com/warpdotdev/oz-sdk-typescript/HEAD/api.md
### Python SDK
The Python SDK is available from PyPi. It can be used synchronously or asynchronously.
* Package link: https://pypi.org/project/oz-agent-sdk/
* Source Code: https://github.com/warpdotdev/oz-sdk-python
* API reference: https://raw.githubusercontent.com/warpdotdev/oz-sdk-python/refs/heads/main/api.md
### API Examples
```sh
curl -L -X POST {{warp_server_url}}/api/v1/agent/run \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"prompt": "Update the login error to be more specific",
"config": {
"environment_id": "UA17BXYZ"
}
}'
```
```sh
curl -L -X GET {{warp_server_url}}/api/v1/agent/runs/5972cca4-a410-42af-930a-e56bc23e07ac \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json'
```
## GitHub Actions Integration
You can trigger Oz cloud agents from GitHub Actions workflows. This enables automation like:
* Triaging issues when they're created or labeled
* Running checks on pull requests
* Responding to CI events or deployment triggers
Use GitHub Actions when the trigger itself lives in GitHub: An event like an issue being opened, a PR being labeled, a push, or a CI workflow completing.
For periodic/recurring work, prefer `{{warp_cli_binary_name}} schedule create` to enhance scheduled run tracking with the Oz platform.
The agent will have access to the `gh` CLI to communicate back to the repository. Prefer prompting the agent to use `gh` vs. requiring the agent to respond with structured output for the GitHub workflow to parse.
### Action Setup
Use `warpdotdev/oz-agent-action@main` in your workflow. Required inputs:
* `prompt`: The task description for the agent
* `warp_api_key`: API key (store in GitHub secrets, e.g., `${{ secrets.WARP_API_KEY }}`)
* `profile`: Optional agent profile identifier (can use repo variable, e.g., `${{ vars.WARP_AGENT_PROFILE || '' }}`)
The action outputs `agent_output` with the agent's response.
### Minimal Workflow Example
```yaml
name: Run Oz Agent
on:
issues:
types: [opened, labeled]
jobs:
agent:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: warpdotdev/oz-agent-action@main
id: agent
with:
prompt: |
Analyze the GitHub issue and provide a summary.
Issue: ${{ github.event.issue.title }}
${{ github.event.issue.body }}
Respond to the issue with a comment containing your summary using the `gh` CLI.
warp_api_key: ${{ secrets.WARP_API_KEY }}
profile: ${{ vars.WARP_AGENT_PROFILE || '' }}
- name: Use Agent Output
run: echo "${{ steps.agent.outputs.agent_output }}"
```
### Common Patterns
**Conditional steps**: Use `if: steps.agent.outputs.agent_output` to branch on agent results.
**Templating**: Use `actions/github-script@v7` to construct dynamic prompts from issue templates, repo context, or code.
**Error handling**: Check action success with `if: success()` or `if: failure()`.
**Git operations**: The action runs with checked-out code and Git credentials, so agents can commit and push changes.
## Environments
All cloud agents run in an environment. The environment defines:
* Which programs are preinstalled for the agent (based on a Docker image)
* The Git repositories to check out before the agent starts
* Setup commands to run, such as `npm install` or `cargo fetch`
You should almost always run cloud agents in an environment. Otherwise, they may not have the necessary code or tools available.
Cloud agents run in a sandbox, so they _can_ install additional programs into their environment. They also have Git credentials to create PRs and push branches.
Cloud environments DO NOT store secret values, like API keys. Use the `{{warp_cli_binary_name}} secret` commands instead.
## Using Third-Party Coding CLIs
Oz environments support running third-party coding agent CLIs such as Claude Code, Codex, Gemini CLI, Amp, Copilot CLI, and OpenCode. The `-agents` tagged variants of prebuilt Oz Docker images (e.g. `warpdotdev/dev-rust:1.85-agents`) come with the most popular CLIs preinstalled. Base tags (without `-agents`) do not include coding agent CLIs.
For detailed per-CLI documentation (installation, authentication, non-interactive flags, and artifact reporting), see [references/third-party-clis.md](./references/third-party-clis.md).
### For Interactive Agents: Launching Cloud Agents with Third-Party CLIs
When you are an interactive agent launching a cloud agent to use a third-party CLI:
1. **Environment Selection**: First, ask the user which environment to use. Present the public `-agents` image options from [warpdotdev/oz-dev-environments](https://github.com/warpdotdev/oz-dev-environments):
- `warpdotdev/dev-base:latest-agents`
- `warpdotdev/dev-go:1.23-agents`
- `warpdotdev/dev-rust:1.83-agents`
- `warpdotdev/dev-rust:1.85-agents`
- `warpdotdev/dev-java:21-agents`
- `warpdotdev/dev-dotnet:8.0-agents`
- `warpdotdev/dev-ruby:3.3-agents`
- `warpdotdev/dev-web:latest-agents`
- `warpdotdev/dev-full:latest-agents`
Also ask if they want to use an existing environment instead. To list their environments:
```sh
{{warp_cli_binary_name}} environment list --output-format text
```
If they choose a public image without an existing environment, create one with `{{warp_cli_binary_name}} environment create ...`
2. **Prompt Construction**: Construct a simple prompt that delegates CLI invocation to the cloud agent:
```sh
{{warp_cli_binary_name}} agent run-cloud \
--environment <ENV_ID> \
--prompt 'Read the oz-platform skill for instructions on using [CLI name] to solve: <task description>'
```
**Do not** include the CLI command syntax in the prompt. The cloud agent will read the oz-platform skill and follow those instructions.
**Example**:
```sh
$ {{warp_cli_binary_name}} agent run-cloud \
--environment <ENV_ID> \
--prompt 'Read the oz-platform skill for instructions on using Claude Code to solve: Summarize the architecture of this project'
```
### For Cloud Agents: Invoking Third-Party CLIs
When you are a cloud agent instructed to use a third-party CLI:
1. **Environment**: You are already running in an environment with the CLI preinstalled (if it's in an `-agents` image).
2. **Authentication**: API keys are available as environment variables (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). These were configured as Oz secrets by the user.
3. **Task Delegation (IMPORTANT)**: The user's task should be completed **entirely by the third-party CLI**. Do NOT use Warp's built-in tools to complete the task yourself:
- Do NOT use `edit_files`, `read_files`, `grep`, `codebase_semantic_search`, or other Warp coding tools to perform the user's task
- The third-party CLI should do all the coding, file editing, searching, and analysis work
- Your role is to:
- Set up the CLI (e.g., authenticate if needed)
- Construct the prompt for the CLI with the user's task
- Run the CLI and monitor its execution
- Debug any issues with the CLI itself
- Report artifacts back to Warp (see below)
4. **CLI Invocation**: Read [references/third-party-clis.md](./references/third-party-clis.md) for detailed instructions on:
- Non-interactive mode flags for each CLI (e.g. `claude -p`, `codex exec`, `gemini -p`)
- Authentication setup steps if needed (e.g. Codex requires `printenv OPENAI_API_KEY | codex login --with-api-key`)
- Useful flags and options
- Example commands
5. **Artifact Reporting**: When the third-party CLI creates a PR, parse its output for the PR URL and branch name, then call `report_pr` to register the artifact in the Warp UI.
**Example workflow**:
```sh
# 1. Read this skill and references/third-party-clis.md to understand CLI usage
# 2. Set up authentication if needed (e.g., for Codex)
# For Claude Code, ANTHROPIC_API_KEY is already available
# 3. Run the CLI with the user's task - let it do ALL the work
$ claude -p "Summarize the architecture of this project"
# 4. If a PR was created, parse the CLI output and report the artifact
# Example: report_pr(pr_url="https://github.com/...", branch="feature-branch")
```
**What NOT to do**:
```sh
# ❌ Don't read files yourself to help the CLI
$ read_files ...
# ❌ Don't search the codebase yourself
$ grep ...
# ❌ Don't edit files yourself
$ edit_files ...
# ✅ Instead, let the third-party CLI handle everything
$ claude -p "Complete the entire task: <user's task>"
```
@@ -12,7 +12,7 @@ Fetch all review comments from the current branch's GitHub PR and display them v
1. Run the bundled script (must be inside a git repo with an open PR on the current branch).
Use `do_not_summarize_output: true` when running this shell command so the JSON output is not truncated.
```bash
python3 <skill_dir>/scripts/fetch_github_review_comments.py
python3 {{skill_dir}}/scripts/fetch_github_review_comments.py
```
The script prints JSON to stdout.
If the script fails to fetch comments, run the fallback `gh` commands instead.
+174
View File
@@ -0,0 +1,174 @@
---
name: warpctrl
description: Control and inspect the currently running local Warp application with the warpctrl CLI. Use this skill whenever the user asks the agent to manipulate Warp's own windows, tabs, panes, sessions, input buffer, themes, or UI surfaces; open a file in Warp; inspect local Warp state; or explain how to invoke Warp Control manually.
---
# Warp Control
Use `{{warpctrl_binary_name}}` to inspect or control the already-running local Warp application that provided this skill. The command name and wrapper path in this skill are injected for the current Warp channel, so do not inspect running processes or guess which channel is active.
Prefer `{{warpctrl_binary_name}}` when the requested action changes Warp itself rather than the user's project or operating system. Examples include creating a Warp tab, splitting a pane, staging text in Warp's input, opening Warp settings, or focusing a Warp window.
## How to invoke Warp Control
Warp Control is bundled into the Warp application. It is not a separate standalone binary; it is a hidden control mode served by the running Warp process.
- Command for the current Warp channel: `{{warpctrl_binary_name}}`
- Bundled wrapper for the current Warp channel: `{{warpctrl_wrapper_path}}`
- Optional PATH symlink: `/usr/local/bin/{{warpctrl_binary_name}}`
### Ensure the command is available
Before invoking Warp Control for the first time in a task, prefer the shortest available path and avoid unnecessary setup research:
1. If `command -v {{warpctrl_binary_name}}` succeeds, use `{{warpctrl_binary_name}}` for the rest of the task. Do not inspect the bundled wrapper or verify the symlink unless a later command fails.
2. If `command -v {{warpctrl_binary_name}}` fails, verify that `{{warpctrl_wrapper_path}}` exists and is executable. If it is missing, tell the user that this Warp build does not contain the expected wrapper and stop.
3. Inspect `/usr/local/bin/{{warpctrl_binary_name}}`. Treat setup as complete only when it is a symlink that resolves to the exact `{{warpctrl_wrapper_path}}` bundled wrapper.
4. If the expected symlink is missing, broken, or points elsewhere, use the `ask_user_question` tool to ask whether the user wants to install it at `/usr/local/bin/{{warpctrl_binary_name}}` pointing to `{{warpctrl_wrapper_path}}`. Offer **Install command** as the recommended option and **Not now** as the alternative. Do not create or replace a symlink without an affirmative response.
5. After approval, create or update only the expected symlink by running `ln -sf "{{warpctrl_wrapper_path}}" "/usr/local/bin/{{warpctrl_binary_name}}"`. Try without elevation first. If macOS permissions prevent the change, run that same command through `osascript` with administrator privileges; never request or expose the user's password directly.
6. Verify the result with `command -v {{warpctrl_binary_name}}`, `readlink /usr/local/bin/{{warpctrl_binary_name}}`, and `{{warpctrl_binary_name}} app version`.
If the user chooses **Not now**, do not create the symlink. Use the bundled wrapper at `{{warpctrl_wrapper_path}}` directly for the current task.
The Warp UI also exposes **Install Warp Control CLI command** and **Uninstall Warp Control CLI command** in the Command Palette and an install control under **Settings > Scripting**.
## Workflow
Always prefer discovering commands from `{{warpctrl_binary_name}}` itself rather than guessing or inventing them. The CLI provides full help and an action catalog that is the authoritative source of truth for what the installed build supports.
### Execute serially and validate results
Run Warp Control commands serially. Never dispatch multiple `{{warpctrl_binary_name}}` commands through parallel shell-tool calls, even when the commands appear independent. They act on the same running app and may change the active target or the terminal context used to execute and observe later commands. For multi-step requests, prefer one shell-tool call that chains commands sequentially, or issue separate shell-tool calls one at a time.
After an action that creates, activates, navigates, or focuses a window, tab, pane, session, or surface, do not assume the active target is unchanged. Use explicit selectors for later commands when exact targeting matters, or rerun `{{warpctrl_binary_name}} app active` before continuing.
Validate that each result corresponds to the command that was invoked. If output describes a different action, reports an unexpected instance or channel, or otherwise conflicts with the request, stop and rerun `{{warpctrl_binary_name}} instance list` serially before retrying. Do not report success until the requested final state has been verified when a corresponding `list`, `inspect`, or `get` command is available.
### Route by intent
Before discovering commands, route the request to the narrowest matching top-level group:
1. Requests to open, show, view, or toggle a named Warp UI destination, panel, picker, or settings page use `surface`. Convert natural-language names to kebab case, such as "Warp Drive" to `warp-drive` and "code review" to `code-review`. Prefer `surface <name> open` when the requested final state is open. Use `surface list` or `surface help` when the destination or supported verb is unknown. Do not infer an internal action name for a UI destination.
2. Requests about windows, tabs, panes, or sessions use the matching `window`, `tab`, `pane`, or `session` group.
3. Requests to stage or inspect editor input use `input`.
4. Requests to open a file in Warp use `file`.
5. Requests about themes, appearance, settings, or keybindings use the matching `theme`, `appearance`, `setting`, or `keybinding` group.
6. Use the generic `action` catalog only when no dedicated CLI group matches. Internal or catalog action names are not guaranteed to be reachable as standalone parser commands.
1. Discover running Warp instances from the current Warp channel:
```sh
{{warpctrl_binary_name}} instance list
```
2. If exactly one same-channel instance is running, commands select it automatically. If multiple same-channel instances are running, select one explicitly with `--instance <instance_id>` or `--pid <pid>`.
3. Discover the exact command and parameters from the routed group instead of guessing. This is the preferred source of truth for the available command surface:
```sh
{{warpctrl_binary_name}} help
{{warpctrl_binary_name}} <group> help
{{warpctrl_binary_name}} <group> <command> --help
```
Only when no dedicated group matches, inspect the generic action catalog:
```sh
{{warpctrl_binary_name}} action list
{{warpctrl_binary_name}} action inspect <action.name>
```
4. Inspect the active target chain or list the relevant targets before changing them:
```sh
{{warpctrl_binary_name}} app active
{{warpctrl_binary_name}} window list
{{warpctrl_binary_name}} tab list
{{warpctrl_binary_name}} pane list
{{warpctrl_binary_name}} session list
```
5. Invoke the narrowest action that satisfies the request, then verify the result with the corresponding `list`, `inspect`, or `get` command when useful.
## Common actions
These are frequently used commands that are safe to invoke directly. For less common commands, route by intent and use `{{warpctrl_binary_name}} <group> help` or `{{warpctrl_binary_name}} <group> <command> --help` to discover the exact syntax supported by the running build. Inspect the generic action catalog only when no dedicated group matches.
```sh
# Create and manage tabs and panes
{{warpctrl_binary_name}} tab create
{{warpctrl_binary_name}} tab create --type agent
{{warpctrl_binary_name}} tab rename "server logs"
{{warpctrl_binary_name}} pane split --direction right
{{warpctrl_binary_name}} pane navigate --direction next
# Stage text in Warp's input without submitting it
{{warpctrl_binary_name}} input insert "git status"
{{warpctrl_binary_name}} input replace "cargo test"
# Open or toggle Warp UI surfaces
{{warpctrl_binary_name}} surface list
{{warpctrl_binary_name}} surface settings open
{{warpctrl_binary_name}} surface command-palette open --query "theme"
{{warpctrl_binary_name}} surface command-search open
{{warpctrl_binary_name}} surface theme-picker open
{{warpctrl_binary_name}} surface keybindings open
{{warpctrl_binary_name}} surface warp-drive open
{{warpctrl_binary_name}} surface resource-center toggle
{{warpctrl_binary_name}} surface ai-assistant toggle
{{warpctrl_binary_name}} surface project-explorer open
{{warpctrl_binary_name}} surface global-search open
{{warpctrl_binary_name}} surface conversation-list open
{{warpctrl_binary_name}} surface code-review open
{{warpctrl_binary_name}} surface left-panel toggle
{{warpctrl_binary_name}} surface right-panel toggle
{{warpctrl_binary_name}} surface vertical-tabs open
{{warpctrl_binary_name}} surface agent-management open
# Open a file in Warp
{{warpctrl_binary_name}} file open ./src/main.rs --line 42
# Inspect and update supported state
{{warpctrl_binary_name}} theme get
{{warpctrl_binary_name}} theme set "Dracula"
{{warpctrl_binary_name}} appearance get
{{warpctrl_binary_name}} setting list
{{warpctrl_binary_name}} keybinding list
```
Add `--output-format json` when structured output is easier to consume:
```sh
{{warpctrl_binary_name}} --output-format json tab list
```
## Targeting
Target selectors can be combined when the action supports their scope:
- Instance: `--instance <instance_id>` or `--pid <pid>`
- Window: `--window <id>`, `--window-index <n>`, or `--window-title <exact-title>`
- Tab: `--tab <id>`, `--tab-index <n>`, or `--tab-title <exact-title>`
- Pane: `--pane <id>` or `--pane-index <n>`
- Session: `--session <id>`
Use IDs returned by `list`, `inspect`, or `app active` when exact targeting matters. If a selector is omitted, most scoped actions operate on the active target. Prefer explicit selectors when more than one target could reasonably match the user's request.
Use `surface list` before a walkthrough or multi-step UI workflow. It reports both available and unavailable destinations with stable names and reasons. The direct `surface ... open` commands are idempotent; use them instead of toggle commands when the final open state matters. `surface list` accepts `--instance` or `--pid` for process selection but rejects window, tab, pane, and session selectors.
## Safety and limitations
- Invoke close actions only when the user explicitly asks to close something. Close actions flow through normal Warp close behavior and may trigger existing app warnings.
- `input insert` and `input replace` only stage text. Warp Control intentionally does not provide an action that submits or runs the input.
- Do not invent unsupported commands. Use the matching group's `help` first, then use `action list` or `action inspect` only when no dedicated group matches.
- Warp Control affects only a running local Warp application owned by the same user. It does not control remote or cloud Warp instances.
- Each channel-specific Warp Control CLI lists and targets only Warp instances from its own channel.
- On Windows, local-control publication is disabled until authenticated broker transport is supported.
## Manual setup and troubleshooting
Warp Control availability depends on the build channel and the **Settings > Scripting** toggle. The local-control mode defaults to enabled on internal dogfood builds (e.g., WarpDev) and disabled on public channels (Stable, Preview, OSS). On any channel, the final gate is the **Settings > Scripting** toggle. The installed `{{warpctrl_binary_name}}` wrapper invokes the matching channel-specific Warp executable.
If `{{warpctrl_binary_name}} instance list` is empty, confirm that a compatible same-channel Warp app is running and Scripting is enabled. If a command reports multiple instances, rerun it with `--instance <instance_id>`.
If the symlink is not on `PATH`, follow the confirmation-gated setup flow in **How to invoke Warp Control** or use `{{warpctrl_wrapper_path}}` directly.