diff --git a/.agents/skills/changelog-draft/SKILL.md b/.agents/skills/changelog-draft/SKILL.md new file mode 100644 index 00000000..ebccf21b --- /dev/null +++ b/.agents/skills/changelog-draft/SKILL.md @@ -0,0 +1,284 @@ +--- +name: changelog-draft +description: Generate a reviewable changelog draft from PRs merged in a release range. Extracts explicit CHANGELOG markers, classifies unmarked PRs, adds external contributor attribution, and outputs markdown + JSON artifacts. Does NOT mutate channel_versions.json. +--- + +# Changelog Draft Generator + +## Inputs + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `channel` | yes | Release channel: `stable`, `preview`, or `dev` | +| `release_tag` | yes | The release tag to generate the changelog for (e.g. `v0.2026.05.06.09.12.stable_00`) | +| `output_dir` | no | Directory to write output files. Defaults to `$RUNNER_TEMP` or `/tmp/changelog-draft` | +| `attribution` | no | Attribution mode: `external-only` (default), `all`, or `none` | + +## Workflow + +### Step 1 — Determine the release range + +Infer the previous release **cut** for comparison. Release tags follow the pattern `v0.YYYY.MM.DD.HH.MM._NN`, where `_NN` is the RC/hotfix number within that release cut. Multiple tags can share the same date prefix (e.g. `_00`, `_01`, `_02` are all part of one release cut). + +The base tag must be the `_00` tag of the **previous** release cut (i.e. a different date), not just the previous tag. For example, if generating a changelog for `v0.2026.04.29.08.57.stable_01`, the base should be `v0.2026.04.22.08.57.stable_00`, not `v0.2026.04.29.08.57.stable_00`. + +```bash +# 1. Extract the date prefix from the release_tag (everything before _NN) +release_date_prefix="${release_tag%_*}" + +# 2. List all _00 tags for the channel (these are release cut points), sorted descending +git tag --list "v0.*.${channel}_00" --sort=-version:refname + +# 3. Pick the first _00 tag whose date prefix differs from release_date_prefix +``` + +Record the range as `previous_cut_tag..release_tag`. + +### Step 2 — Fetch PR data + +Run the `fetch_prs.py` script to collect all public-release PRs merged in the release range and extract explicit changelog markers. Pass the repository that the workflow checked out, not necessarily the public repository. Release workflows run from `warpdotdev/warp-internal`, and the script deterministically resolves `warp-repo-sync[bot]` PRs back to their original public `warpdotdev/warp` PR metadata before emitting JSON. When running from `warpdotdev/warp-internal`, the script intentionally omits PRs that were not authored by the repo-sync bot, because those are private internal changes that must not be exposed to the changelog agent or generated artifacts. + +```bash +python3 .agents/skills/changelog-draft/scripts/fetch_prs.py \ + --repo "${GITHUB_REPOSITORY:-warpdotdev/warp}" \ + --base-ref \ + --head-ref +``` + +The script outputs JSON to stdout with this structure: +```json +{ + "range": { "base": "", "head": "" }, + "prs": [ + { + "number": 1234, + "url": "https://github.com/warpdotdev/warp/pull/1234", + "title": "...", + "author": "username", + "body": "...", + "labels": ["..."], + "merged_at": "2026-05-01T...", + "explicit_entries": [ + { "category": "NEW-FEATURE", "text": "Added dark mode" } + ], + "linked_issues": [5678], + "changed_files": ["app/src/ai/agent.rs", "crates/warp_features/src/lib.rs"], + "source_repo": "warpdotdev/warp", + "internal_pr": { + "number": 25712, + "url": "https://github.com/warpdotdev/warp-internal/pull/25712", + "author": "warp-repo-sync[bot]", + "title": "...", + "repo": "warpdotdev/warp-internal" + } + } + ] +} +``` + +Use the top-level `number`, `url`, `author`, `body`, `labels`, `changed_files`, and `source_repo` fields as the source of truth. `internal_pr` is audit-only and must never be used for contributor attribution or user-facing changelog links. If `url` is empty, omit the PR link from user-facing markdown rather than synthesizing one. + +### Step 3 — Classify contributors + +Run the `classify_contributors.py` script with the unique author logins from Step 2: + +```bash +python3 .agents/skills/changelog-draft/scripts/classify_contributors.py \ + --org warpdotdev \ + --authors author1,author2,author3 +``` + +Output JSON: +```json +{ + "internal": ["author1"], + "external": ["author3"], + "bot": ["author2"], + "unknown": [] +} +``` + +### Step 4 — Extract feature flags + +Run the `extract_feature_flags.py` script to get the current flag gate lists: + +```bash +python3 .agents/skills/changelog-draft/scripts/extract_feature_flags.py \ + --file crates/warp_features/src/lib.rs +``` + +Output JSON: +```json +{ + "release_flags": ["Autoupdate", "Changelog", ...], + "preview_flags": ["Orchestration", ...], + "dogfood_flags": ["LogExpensiveFramesInSentry", ...] +} +``` + +### Step 5 — Fetch issue reporters + +Collect all unique `linked_issues` from Step 2 and fetch the original reporter for each. Pass `--org` so the script checks org membership and filters out internal reporters automatically: + +```bash +python3 .agents/skills/changelog-draft/scripts/fetch_issue_reporters.py \ + --repo warpdotdev/warp \ + --org warpdotdev \ + --issues 5678,9012 +``` + +Output JSON (only external reporters are included): +```json +{ + "issue_reporters": [ + { + "issue_number": 5678, + "title": "Crash when opening large file", + "reporter": "community-user", + "reporter_url": "https://github.com/community-user", + "url": "https://github.com/warpdotdev/warp/issues/5678" + } + ] +} +``` + +The `--org` flag checks each reporter's org membership via the GitHub API, filtering out internal members so they aren't misattributed as external community reporters. These reporters will be credited in the "Community" section of the changelog. +Whenever the markdown draft credits a PR author, contributor, or issue reporter, render the username as a GitHub profile link such as `[@username](https://github.com/username)`. + +### Step 6 — Classify unmarked PRs + +For each PR that has no explicit `CHANGELOG-*` entries, decide whether to include it and under which category. + +Follow the classification guidance in `.agents/skills/classify-changelog-pr/SKILL.md`. + +For each unmarked PR, produce a classification: +```json +{ + "pr_number": 1234, + "include": true, + "category": "IMPROVEMENT", + "text": "Proposed changelog line", + "confidence": "high", + "rationale": "...", + "feature_flag": null, + "needs_review": false +} +``` + +**Key rules:** +- PRs that only touch CI, tests, docs, or internal tooling → `include: false` +- PRs behind dogfood-only feature flags → `include: false` for stable channel +- PRs behind preview flags → `include: false` for stable, `include: true` for preview +- When in doubt, set `needs_review: true` and `confidence: "low"` +- Bot PRs (dependabot, renovate, etc.) → `include: false` + +**Feature-flag detection:** Use the `changed_files` list from Step 2 to check if any PR touches `crates/warp_features/src/lib.rs` or references a `FeatureFlag` variant in its title/body. Cross-reference with the flag lists from Step 4 to determine channel visibility. + +**Unknown contributors:** Authors in the `unknown` bucket (org membership check failed due to auth) should be treated conservatively — do not attribute them as external. Note them in the output for manual verification. + +### Step 7 — Assemble the draft + +Combine explicit entries (Step 2) and inferred entries (Step 6) into the final report. Group by category in this order: + +1. `NEW-FEATURE` — New Features +2. `IMPROVEMENT` — Improvements +3. `BUG-FIX` — Bug Fixes +4. `OZ` — Oz Updates + +PRs marked with `CHANGELOG-NONE` are explicitly opted out and must never appear in the changelog markdown. + +When creating entries, copy `pr_number`, `url`, `author`, `source_repo`, and `internal_pr` from the normalized PR record. The release JSON converter uses `url` directly; do not invent public PR URLs from PR numbers. + +### Step 8 — Write output files + +Write two files to `output_dir`: + +**`changelog-draft.md`** — Human-reviewable markdown, ready for Slack/Notion: + +```markdown +# Changelog Draft +**Channel:** stable +**Range:** v0.2026.05.01... → v0.2026.05.06... +**Generated:** 2026-05-06T15:00:00Z + +## New Features +- Added dark mode ([#1234](https://github.com/warpdotdev/warp/pull/1234)) — [@external-contributor](https://github.com/external-contributor) ✨ + +## Improvements +- Faster tab switching ([#1235](https://github.com/warpdotdev/warp/pull/1235)) + +## Bug Fixes +- Fixed crash on startup ([#1236](https://github.com/warpdotdev/warp/pull/1236)) + +## Oz Updates +- Improved agent memory ([#1237](https://github.com/warpdotdev/warp/pull/1237)) + +## Community +### Contributors +- [@contributor1](https://github.com/contributor1) — [#1234](https://github.com/warpdotdev/warp/pull/1234) ✨ + +### Issue Reporters +Thanks to the community members who reported issues fixed in this release: +- [@reporter1](https://github.com/reporter1) — [#5678](https://github.com/warpdotdev/warp/issues/5678) "Crash when opening large file" +``` + +The markdown draft must **not** include "Needs Review" or "Skipped PRs" sections — those are internal details that belong only in the JSON audit artifact. + +**`changelog-draft.json`** — Machine-readable audit artifact (internal only): + +```json +{ + "channel": "stable", + "range": { "base": "v0...", "head": "v0..." }, + "generated_at": "2026-05-06T15:00:00Z", + "entries": [ + { + "pr_number": 1234, + "url": "https://github.com/warpdotdev/warp/pull/1234", + "category": "NEW-FEATURE", + "text": "Added dark mode", + "source": "explicit", + "author": "external-contributor", + "is_external": true, + "confidence": "high", + "rationale": null, + "feature_flag": null, + "source_repo": "warpdotdev/warp", + "internal_pr": null + } + ], + "skipped": [...], + "needs_review": [...], + "issue_reporters": [...] +} +``` + +The JSON artifact retains `skipped`, `needs_review`, and `issue_reporters` for audit purposes — every PR in the range must appear in either `entries`, `skipped`, or `needs_review`. + +### Step 9 — Generate release-pipeline JSON + +Run the conversion script to deterministically produce `changelog-release.json` from the audit artifact: + +```bash +python3 .agents/skills/changelog-draft/scripts/convert_to_release_json.py \ + --input /changelog-draft.json \ + --output /changelog-release.json +``` + +This produces the flat JSON structure consumed by the `create_release` workflow for Slack and the in-app "What's New" dialog. Do **not** generate this file manually — always use the script so the output is deterministic and consistent. + +## Constraints + +- **Never** write to `channel_versions.json` or any production config file. +- **Never** push commits, create branches, or open PRs. +- All output goes to `output_dir` only. +- The markdown draft should be copy-pasteable into Slack or Notion for review. +- Keep the JSON artifact complete enough for audit: every PR in the range should appear in either `entries`, `skipped`, or `needs_review`. + +## Validation + +After generating output, verify: +1. Every PR in the range is accounted for (entries + skipped + needs_review = total PRs). +2. Explicit marker entries match what `fetch_prs.py` extracted (no dropped markers). +3. No duplicate PR numbers across sections. +4. The markdown renders cleanly (no broken links or formatting). diff --git a/.agents/skills/changelog-draft/examples/changelog-draft-example.md b/.agents/skills/changelog-draft/examples/changelog-draft-example.md new file mode 100644 index 00000000..037d5f7e --- /dev/null +++ b/.agents/skills/changelog-draft/examples/changelog-draft-example.md @@ -0,0 +1,96 @@ +# Changelog Draft +**Channel:** stable +**Range:** v0.2026.04.29.08.56.stable_00 → v0.2026.05.06.09.12.stable_00 +**Generated:** 2026-05-06T19:00:00Z +**Total PRs in range:** 211 | **Explicit markers:** 57 | **Unmarked:** 154 + +--- + +## New Features +- You can now drag tabs out of a window into their own window, or between windows, similar to Chrome. ([#9275](https://github.com/warpdotdev/warp/pull/9275)) +- Added a `/set-tab-color` slash command for setting or clearing the current tab's color from the input bar. ([#9305](https://github.com/warpdotdev/warp/pull/9305)) + +## Improvements +- Added tab context menu actions to copy visible tab and pane metadata when available. ([#10120](https://github.com/warpdotdev/warp/pull/10120)) +- The conversation details panel can now be opened and closed with a configurable keyboard shortcut. ([#9837](https://github.com/warpdotdev/warp/pull/9837)) +- Conversation details side panel is now available for local Warp Agent conversations, not just cloud Oz runs. Click the info button in the pane header to open it for any active AI conversation. ([#9493](https://github.com/warpdotdev/warp/pull/9493)) +- Reduced memory usage and CPU work in the agent runs management view while a conversation is streaming. ([#9866](https://github.com/warpdotdev/warp/pull/9866)) +- Added support for drag-and-drop of image files into an active CLI agent session (e.g. Claude Code). ([#9553](https://github.com/warpdotdev/warp/pull/9553)) +- Warp now renders inline local images and Mermaid diagrams in agent block output. ([#9993](https://github.com/warpdotdev/warp/pull/9993)) +- Warp now silently falls back to a regular SSH session on remote hosts where the prebuilt remote-server binary is incompatible (e.g. glibc < 2.31), instead of attempting an install that would fail at runtime. ([#9681](https://github.com/warpdotdev/warp/pull/9681)) +- HTML files using the .htm extension now open with HTML syntax highlighting in Warp's editor. ([#9360](https://github.com/warpdotdev/warp/pull/9360)) +- Recognize Block's `goose` CLI agent — running `goose` now activates the CLI-agent toolbar, status, brand color, and icon like other recognized third-party agents. ([#9497](https://github.com/warpdotdev/warp/pull/9497)) +- Added a `/continue-locally` slash command to continue cloud conversations locally. ([#9500](https://github.com/warpdotdev/warp/pull/9500)) +- Added a "Show in Finder" (macOS) / "Show containing folder" (Linux/Windows) option to the tooltip that appears when clicking a detected file link. ([#9475](https://github.com/warpdotdev/warp/pull/9475)) +- Tighten orchestration event subscription scope so SSE runs only for active parent and child agent runs. ([#9273](https://github.com/warpdotdev/warp/pull/9273)) +- Fix macOS IME candidate popup positioning in code editor panes so it anchors to the editor caret instead of stale terminal/input positions. ([#9555](https://github.com/warpdotdev/warp/pull/9555)) + +## Bug Fixes +- Fixed /feedback recording "Unknown" instead of the installed Warp version on packaged builds. ([#10219](https://github.com/warpdotdev/warp/pull/10219)) +- Fixed find (cmd+f) selection jumping to a different match when new output streams into the active block. ([#10057](https://github.com/warpdotdev/warp/pull/10057)) +- Fix Japanese IME losing the last character of a phrase that ends right before a punctuation mark on macOS. ([#9730](https://github.com/warpdotdev/warp/pull/9730)) +- Fixed local file tree blinking/reshuffling when connected to an SSH session ([#10184](https://github.com/warpdotdev/warp/pull/10184)) +- Fixed terminal text selection not auto-scrolling when dragging beyond bounds ([#9448](https://github.com/warpdotdev/warp/pull/9448)) +- Fixed Ctrl-G not closing CLI agent rich input on linux when editor is focused ([#10030](https://github.com/warpdotdev/warp/pull/10030)) +- Pressing backspace in the agent view when the buffer is empty no longer resets the conversation. ([#10114](https://github.com/warpdotdev/warp/pull/10114)) +- Fixed unnecessary reconnect attempts for remote SSH sessions after system sleep, reducing error noise ([#10096](https://github.com/warpdotdev/warp/pull/10096)) +- Fixes issue with repeated TUI redraws for CLI agents on terminal pane resize. ([#9877](https://github.com/warpdotdev/warp/pull/9877)) +- Fix new-session "+" dropdown alignment when the Tabs Panel is placed on the right side of the header toolbar. ([#9492](https://github.com/warpdotdev/warp/pull/9492)) +- Copy keybinding now prioritizes selected text in the input over a selected block when both are active. ([#9491](https://github.com/warpdotdev/warp/pull/9491)) +- [Windows] Fix hotkey window. ([#9891](https://github.com/warpdotdev/warp/pull/9891)) +- [Windows] Symlink traversal fixed. ([#9863](https://github.com/warpdotdev/warp/pull/9863)) +- Fixed a crash on Windows when handing off a Web conversation to the native client. ([#9987](https://github.com/warpdotdev/warp/pull/9987)) +- Fixed a bug where multiple 'open skill' buttons shared hover state. ([#9437](https://github.com/warpdotdev/warp/pull/9437)) +- Fixed the OSS Linux desktop entry so WarpOss launches through the packaged `warp-terminal-oss` command. ([#9424](https://github.com/warpdotdev/warp/pull/9424)) +- Fixed Ctrl/Cmd shortcuts (e.g. copy, paste) failing on Windows when a non-Latin keyboard layout was active. ([#9476](https://github.com/warpdotdev/warp/pull/9476)) +- Fixed background colour bleeding in alt screen programs (e.g. delta, diff-so-fancy). ([#9852](https://github.com/warpdotdev/warp/pull/9852)) +- Clip the warping indicator's action chips onto a new line on narrow panes instead of overflowing. ([#9297](https://github.com/warpdotdev/warp/pull/9297)) +- Inline `.bmp`, `.tiff` / `.tif`, and `.ico` images in agent block output now render correctly. ([#9397](https://github.com/warpdotdev/warp/pull/9397)) +- If user attaches an image in block input we should lock in agent mode, without running the NLD classifier. ([#9366](https://github.com/warpdotdev/warp/pull/9366)) +- Remote-server installs no longer fail when the staging-directory cleanup hits a race. ([#9681](https://github.com/warpdotdev/warp/pull/9681)) +- `.command` shell scripts now open with shell syntax highlighting in Warp's editor. ([#9345](https://github.com/warpdotdev/warp/pull/9345)) +- Fix git diff chip flickering between tracked-only and all-files count when untracked files are present ([#9244](https://github.com/warpdotdev/warp/pull/9244)) +- `Open File → Default App` now opens files in the running Warp channel instead of routing to a different installed Warp. ([#9285](https://github.com/warpdotdev/warp/pull/9285)) +- Fixed vertical tabs settings popup items being unclickable ([#9540](https://github.com/warpdotdev/warp/pull/9540)) +- Fixed a macOS memory leak that occurred when Warp enumerated system fonts or built a font fallback chain. ([#9665](https://github.com/warpdotdev/warp/pull/9665)) +- Executable shell scripts opened from a `file://` URL now run in the terminal instead of opening in the editor. ([#9503](https://github.com/warpdotdev/warp/pull/9503)) +- Fixed Option+Enter, Option+Tab, and Option+Escape sending literal key names instead of correct escape sequences ([#9514](https://github.com/warpdotdev/warp/pull/9514)) +- Fixed read_files tool showing an empty box when the LLM requests line ranges beyond the end of a file. ([#9326](https://github.com/warpdotdev/warp/pull/9326)) +- Prevent Warp from consuming too much memory when identifying filepaths in long block outputs. ([#9617](https://github.com/warpdotdev/warp/pull/9617)) +- Don't trigger the agent onboarding tutorial when Warp is running in headless SDK/CLI mode. ([#9590](https://github.com/warpdotdev/warp/pull/9590)) +- Added `--version` flag support in the Oz CLI ([#9252](https://github.com/warpdotdev/warp/pull/9252)) +- Fixed file tree flickering when transitioning to an SSH remote session ([#9320](https://github.com/warpdotdev/warp/pull/9320)) +- Fixed scroll-to-start/end of selected block keybinding not working when the input is focused. ([#9332](https://github.com/warpdotdev/warp/pull/9332)) +- Fix the terminal pane background appearing darker in horizontal tabs mode with background image or custom opacity. ([#9474](https://github.com/warpdotdev/warp/pull/9474)) +- AI code blocks tagged `vue`, `xml`, `dockerfile`, `jsx`, `tsx`, etc. now render with syntax highlighting. ([#9471](https://github.com/warpdotdev/warp/pull/9471)) +- Reopen Closed Session is now reachable from the new-session menu on Linux and Windows. ([#9347](https://github.com/warpdotdev/warp/pull/9347)) +- Fixed missing syntax highlighting for C++ header files using `.hpp`, `.hxx`, or `.H` extensions. ([#9388](https://github.com/warpdotdev/warp/pull/9388)) +- Fixed `/open-file` handling for relative WSL paths so Unix separators are preserved. ([#9322](https://github.com/warpdotdev/warp/pull/9322)) + +## Oz Updates +- Add Codex as a supported harness for local child agents. ([#10176](https://github.com/warpdotdev/warp/pull/10176)) +- Configurable max context window per profile. ([#9352](https://github.com/warpdotdev/warp/pull/9352)) + +--- + +## Community +### Contributors +- @Abdalla-Eldoumani ✨ +- @Akeuuh — [#9655](https://github.com/warpdotdev/warp/pull/9655) ✨ +- @AntonVishal ✨ +- @BennyWaitWhat ✨ +- @Faizanq ✨ +- @JamieMcMillan ✨ +- @R3flector ✨ +- @amriksingh0786 ✨ +- @princepal9120 ✨ +- @webdevtodayjason ✨ +- @zerone0x ✨ + +### Issue Reporters +Thanks to the community members who reported issues fixed in this release: +- @user123 — [#5678](https://github.com/warpdotdev/warp/issues/5678) "Crash when opening large file" + +--- + +*This draft was generated by the `changelog-draft` Oz skill. Needs Review and Skipped PRs are available in the JSON audit artifact.* diff --git a/.agents/skills/changelog-draft/scripts/build_slack_payload.py b/.agents/skills/changelog-draft/scripts/build_slack_payload.py new file mode 100644 index 00000000..f6415c28 --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/build_slack_payload.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Build a Slack Block Kit payload from release-pipeline changelog JSON.""" + +from __future__ import annotations + +import argparse +import html +import json +import re +import sys +from pathlib import Path +from typing import Iterable +from urllib.parse import quote + +MAX_SECTION_TEXT_LENGTH = 3000 +MAX_MESSAGE_BLOCKS = 50 + +SECTION_ORDER = ( + ("newFeatures", "New Features"), + ("improvements", "Improvements"), + ("bugFixes", "Bug Fixes"), + ("images", "Image"), + # Keep the existing label stable for compatibility with recent Slack posts. + ("oz_updates", "oz_updates"), +) + +MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\((https?://[^)\s]+)\)") +SLACK_LINK_URL_SAFE_CHARS = "/:?&=#%+~@!$'()*;,[]" + + +def escape_slack_text(text: str) -> str: + """Escape Slack control characters in ordinary mrkdwn text.""" + return html.escape(text, quote=False) + + +def escape_slack_link_url(url: str) -> str: + """Percent-encode Slack link delimiters before embedding a URL in mrkdwn.""" + return quote(url, safe=SLACK_LINK_URL_SAFE_CHARS) + + +def slack_link(url: str, label: str) -> str: + """Build a Slack mrkdwn link from an already-validated URL and label.""" + return f"<{escape_slack_link_url(url)}|{escape_slack_text(label)}>" + + +def markdown_links_to_slack(text: str) -> str: + """Convert standard Markdown links to Slack mrkdwn links.""" + parts: list[str] = [] + last_end = 0 + for match in MARKDOWN_LINK_RE.finditer(text): + parts.append(escape_slack_text(text[last_end : match.start()])) + label, url = match.groups() + parts.append(slack_link(url, label)) + last_end = match.end() + parts.append(escape_slack_text(text[last_end:])) + return "".join(parts) + + +def slack_lines(changelog: dict) -> list[str]: + """Render non-empty changelog sections as Slack mrkdwn lines.""" + lines: list[str] = [] + for key, title in SECTION_ORDER: + values = changelog.get(key, []) + if not isinstance(values, list) or not values: + continue + lines.append(f"*{title}*") + for value in values: + lines.append(f" • {markdown_links_to_slack(str(value))}") + return lines + + +def split_overlong_line(line: str) -> Iterable[str]: + """Split a pathological line so every Slack section remains valid.""" + while len(line) > MAX_SECTION_TEXT_LENGTH - 1: + yield line[: MAX_SECTION_TEXT_LENGTH - 1] + line = line[MAX_SECTION_TEXT_LENGTH - 1 :] + yield line + + +def chunk_lines(lines: list[str]) -> list[str]: + """Split text into section-sized chunks while retaining copy boundaries.""" + chunks: list[str] = [] + buffer = "" + for raw_line in lines: + for line in split_overlong_line(raw_line): + candidate = line if not buffer else f"{buffer}\n{line}" + # Reserve a character for a trailing newline. Keeping it in each + # section preserves a separator when Slack copies adjacent blocks. + if len(candidate) + 1 > MAX_SECTION_TEXT_LENGTH: + chunks.append(f"{buffer}\n") + buffer = line + else: + buffer = candidate + if buffer: + chunks.append(f"{buffer}\n") + return chunks + + +def artifact_link_block(markdown_artifact_url: str) -> dict | None: + """Build a Slack block linking to the downloadable Markdown artifact.""" + if not markdown_artifact_url: + return None + return { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "Raw Markdown changelog: " + f"{slack_link(markdown_artifact_url, 'Download raw Markdown changelog artifact')}" + ), + }, + } + + +def build_payload( + changelog: dict, release_tag: str, markdown_artifact_url: str = "" +) -> dict: + """Build a Block Kit message and reject payloads Slack cannot accept.""" + chunks = chunk_lines(slack_lines(changelog)) + if not chunks: + return {"blocks": []} + + blocks = [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": f"Changelog for {release_tag}", + }, + } + ] + artifact_block = artifact_link_block(markdown_artifact_url) + if artifact_block is not None: + blocks.append(artifact_block) + blocks.extend( + { + "type": "section", + "expand": True, + "text": {"type": "mrkdwn", "text": chunk}, + } + for chunk in chunks + ) + + if len(blocks) > MAX_MESSAGE_BLOCKS: + raise ValueError( + "Slack payload would require " + f"{len(blocks)} blocks, exceeding Slack's {MAX_MESSAGE_BLOCKS}-block limit" + ) + return {"blocks": blocks} + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Build Slack payload JSON from changelog release JSON" + ) + parser.add_argument("--input", required=True, help="Path to changelog JSON") + parser.add_argument("--release-tag", required=True, help="Release tag header text") + parser.add_argument( + "--markdown-artifact-url", + default="", + help="Download URL for the raw Markdown changelog artifact", + ) + parser.add_argument("--output", required=True, help="Payload JSON output path") + args = parser.parse_args() + + with open(args.input) as f: + changelog = json.load(f) + + payload = build_payload(changelog, args.release_tag, args.markdown_artifact_url) + Path(args.output).write_text(json.dumps(payload, separators=(",", ":")) + "\n") + print(f"Built Slack payload with {len(payload['blocks'])} blocks", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/changelog-draft/scripts/classify_contributors.py b/.agents/skills/changelog-draft/scripts/classify_contributors.py new file mode 100644 index 00000000..ebdd73d7 --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/classify_contributors.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Classify GitHub usernames as internal, external, or bot. + +Uses `gh api` to check org membership — stdlib only, no pip deps. + +Usage: + python3 classify_contributors.py --org warpdotdev --authors user1,user2,user3 + +Outputs JSON to stdout. +""" + +import argparse +import json +import subprocess +import sys + +KNOWN_BOTS = frozenset( + { + "dependabot", + "dependabot[bot]", + "renovate", + "renovate[bot]", + "github-actions", + "github-actions[bot]", + "codecov", + "codecov[bot]", + "warp-bot", + "warp-bot[bot]", + } +) + + +def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess: + return subprocess.run(cmd, capture_output=True, text=True, check=check) + + +def check_org_membership(org: str, username: str) -> str: + """Check if a user is a member of the given GitHub org via gh api. + + Returns: + 'internal' if the user is an org member (HTTP 204), + 'external' if the user is confirmed not a member (HTTP 404), + 'unknown' if the check failed due to auth/permission issues. + """ + result = run( + ["gh", "api", f"orgs/{org}/members/{username}", "--silent"], + check=False, + ) + if result.returncode == 0: + return "internal" + # Distinguish auth failures from genuine "not a member" responses. + # gh api exits non-zero for both 404 (not a member) and 403/401 (no + # read:org scope). Only treat an explicit 404 as "external"; + # everything else (network errors, rate limits, auth issues) is "unknown" + # to avoid publicly crediting internal or unverified users. + stderr = result.stderr.lower() + if "404" in stderr: + return "external" + return "unknown" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Classify contributor types") + parser.add_argument("--org", required=True, help="GitHub org to check membership") + parser.add_argument( + "--authors", + required=True, + help="Comma-separated list of GitHub usernames", + ) + args = parser.parse_args() + + authors = [a.strip() for a in args.authors.split(",") if a.strip()] + + internal: list[str] = [] + external: list[str] = [] + bot: list[str] = [] + unknown: list[str] = [] + + for author in authors: + if author.lower() in KNOWN_BOTS or author.endswith("[bot]"): + bot.append(author) + else: + status = check_org_membership(args.org, author) + if status == "internal": + internal.append(author) + elif status == "unknown": + unknown.append(author) + else: + external.append(author) + + output = {"internal": internal, "external": external, "bot": bot, "unknown": unknown} + json.dump(output, sys.stdout, indent=2) + print() + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/changelog-draft/scripts/convert_to_release_json.py b/.agents/skills/changelog-draft/scripts/convert_to_release_json.py new file mode 100644 index 00000000..1c80efe6 --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/convert_to_release_json.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Convert changelog-draft.json to the release-pipeline-compatible changelog-release.json. + +Reads the audit artifact produced by the changelog-draft skill and emits the +flat JSON structure consumed by the create_release workflow (Slack payload +builder + in-app changelog.json step). + +Usage: + python3 convert_to_release_json.py --input --output + +The output schema: + { + "newFeatures": ["..."], + "improvements": ["..."], + "bugFixes": ["..."], + "images": ["..."], + "oz_updates": ["..."] + } +""" + +import argparse +import json +import sys + +# Map from changelog-draft.json category names to release JSON keys. +CATEGORY_MAP = { + "NEW-FEATURE": "newFeatures", + "IMPROVEMENT": "improvements", + "BUG-FIX": "bugFixes", + "OZ": "oz_updates", + "IMAGE": "images", +} + + +def github_profile_link(username: str) -> str: + """Format a GitHub username as a markdown profile link.""" + return f"[@{username}](https://github.com/{username})" + + +def format_entry(entry: dict) -> str: + """Format a single changelog entry as a text line with a PR link. + + Includes external contributor attribution when applicable. + """ + text = entry["text"] + pr_number = entry.get("pr_number") or entry.get("number") + url = entry.get("url") or entry.get("pr_url") + + link = "" + if url and pr_number: + link = f" ([#{pr_number}]({url}))" + + attribution = "" + if entry.get("is_external") and entry.get("author"): + attribution = f" — {github_profile_link(entry['author'])} ✨" + return f"{text}{link}{attribution}" + + +def convert(draft: dict) -> dict: + """Convert a changelog-draft.json dict to changelog-release.json dict.""" + release: dict[str, list[str]] = { + "newFeatures": [], + "improvements": [], + "bugFixes": [], + "images": [], + "oz_updates": [], + } + + for entry in draft.get("entries", []): + category = entry.get("category", "") + release_key = CATEGORY_MAP.get(category) + if release_key is None: + continue + + if category == "IMAGE": + # IMAGE entries store a URL in "text" — pass through directly. + release["images"].append(entry["text"]) + else: + release[release_key].append(format_entry(entry)) + + return release + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Convert changelog-draft.json to changelog-release.json" + ) + parser.add_argument( + "--input", + required=True, + help="Path to changelog-draft.json", + ) + parser.add_argument( + "--output", + required=True, + help="Path to write changelog-release.json", + ) + args = parser.parse_args() + + with open(args.input) as f: + draft = json.load(f) + + release = convert(draft) + + with open(args.output, "w") as f: + json.dump(release, f, indent=2) + f.write("\n") + + # Summary to stdout for CI logs + for key, items in release.items(): + print(f" {key}: {len(items)} entries") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/changelog-draft/scripts/extract_feature_flags.py b/.agents/skills/changelog-draft/scripts/extract_feature_flags.py new file mode 100644 index 00000000..49bbf25a --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/extract_feature_flags.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Extract RELEASE_FLAGS, PREVIEW_FLAGS, and DOGFOOD_FLAGS from warp_features. + +Parses crates/warp_features/src/lib.rs to find the const arrays and extracts +the FeatureFlag variant names. Stdlib only, no pip deps. + +Usage: + python3 extract_feature_flags.py --file crates/warp_features/src/lib.rs + +Outputs JSON to stdout. +""" + +import argparse +import json +import re +import sys + + +def extract_flag_list(source: str, const_name: str) -> list[str]: + """Extract FeatureFlag variant names from a const array definition.""" + # Match: pub const CONST_NAME: &[FeatureFlag] = &[ ... ]; + pattern = rf"pub\s+const\s+{re.escape(const_name)}\s*:\s*&\[FeatureFlag\]\s*=\s*&\[(.*?)\];" + m = re.search(pattern, source, re.DOTALL) + if not m: + return [] + + block = m.group(1) + # Extract FeatureFlag::VariantName entries, ignoring #[cfg(...)] attributes + variants = re.findall(r"FeatureFlag::(\w+)", block) + return variants + + +def main() -> None: + parser = argparse.ArgumentParser(description="Extract feature flag gate lists") + parser.add_argument( + "--file", + required=True, + help="Path to warp_features lib.rs", + ) + args = parser.parse_args() + + with open(args.file) as f: + source = f.read() + + output = { + "release_flags": extract_flag_list(source, "RELEASE_FLAGS"), + "preview_flags": extract_flag_list(source, "PREVIEW_FLAGS"), + "dogfood_flags": extract_flag_list(source, "DOGFOOD_FLAGS"), + } + json.dump(output, sys.stdout, indent=2) + print() + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/changelog-draft/scripts/fetch_issue_reporters.py b/.agents/skills/changelog-draft/scripts/fetch_issue_reporters.py new file mode 100644 index 00000000..58c5e208 --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/fetch_issue_reporters.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Fetch the original reporters for GitHub issues linked to PRs in a release. + +Uses `gh` CLI (must be authenticated) — stdlib only, no pip deps. + +Usage: + python3 fetch_issue_reporters.py --repo warpdotdev/warp --issues 1234,5678,9012 + +Outputs JSON to stdout mapping issue numbers to reporter info. +""" + +import argparse +import json +import subprocess +import sys + + +def run(cmd: list[str], *, check: bool = True) -> str: + result = subprocess.run(cmd, capture_output=True, text=True, check=check) + return result.stdout.strip() + + +def run_full(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess: + return subprocess.run(cmd, capture_output=True, text=True, check=check) + + +def is_org_member(org: str, username: str) -> bool: + """Check if a user is a member of the given GitHub org. + + Returns True for members (HTTP 204), False for non-members (HTTP 404), + and True (conservative) for auth failures so internal users aren't + misattributed as external. + """ + result = run_full( + ["gh", "api", f"orgs/{org}/members/{username}", "--silent"], + check=False, + ) + if result.returncode == 0: + return True + stderr = result.stderr.lower() + # Auth failure — be conservative, treat as internal + if "403" in stderr or "401" in stderr or "saml" in stderr: + return True + return False + + +def fetch_issue_reporter(repo: str, issue_number: int) -> dict | None: + """Fetch the reporter (author) of a GitHub issue via gh CLI.""" + raw = run( + [ + "gh", + "issue", + "view", + str(issue_number), + "--repo", + repo, + "--json", + "number,title,author,url", + ], + check=False, + ) + if not raw: + return None + try: + data = json.loads(raw) + except json.JSONDecodeError: + return None + + author = "" + if isinstance(data.get("author"), dict): + author = data["author"].get("login", "") + elif isinstance(data.get("author"), str): + author = data["author"] + + return { + "issue_number": data.get("number", issue_number), + "title": data.get("title", ""), + "reporter": author, + "reporter_url": f"https://github.com/{author}" if author else "", + "url": data.get("url", ""), + } + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Fetch issue reporters for linked issues" + ) + parser.add_argument("--repo", required=True, help="GitHub repo (owner/name)") + parser.add_argument( + "--org", + required=False, + default="", + help="GitHub org to filter out internal reporters (e.g. warpdotdev)", + ) + parser.add_argument( + "--issues", + required=True, + help="Comma-separated issue numbers", + ) + args = parser.parse_args() + + issue_numbers = [ + int(n.strip()) for n in args.issues.split(",") if n.strip().isdigit() + ] + + org = args.org + reporters: list[dict] = [] + seen_reporters: set[str] = set() + for num in issue_numbers: + info = fetch_issue_reporter(args.repo, num) + if not info or not info["reporter"]: + continue + username = info["reporter"] + # Skip internal org members when --org is provided + if org and username not in seen_reporters and is_org_member(org, username): + seen_reporters.add(username) + continue + if username not in seen_reporters: + seen_reporters.add(username) + reporters.append(info) + + json.dump({"issue_reporters": reporters}, sys.stdout, indent=2) + print() # trailing newline + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/changelog-draft/scripts/fetch_prs.py b/.agents/skills/changelog-draft/scripts/fetch_prs.py new file mode 100644 index 00000000..5db8f9ac --- /dev/null +++ b/.agents/skills/changelog-draft/scripts/fetch_prs.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Fetch PRs merged in a release range and extract explicit CHANGELOG markers. + +Uses `gh` CLI (must be authenticated) and `git` — stdlib only, no pip deps. + +Usage: + python3 fetch_prs.py --repo warpdotdev/warp --base-ref --head-ref + +Outputs JSON to stdout. +""" + +import argparse +import json +import re +import subprocess +import sys + +# Matches lines like: CHANGELOG-NEW-FEATURE: Added dark mode +MARKER_RE = re.compile( + r"^CHANGELOG-(NEW-FEATURE|IMPROVEMENT|BUG-FIX|IMAGE|OZ|NONE)\s*:?\s*(.*)$", + re.MULTILINE, +) + +# Matches issue-closing keywords: Fixes #123, Closes #456, Resolves #789 +LINKED_ISSUE_RE = re.compile( + r"(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)", + re.IGNORECASE, +) +PUBLIC_REPO = "warpdotdev/warp" +INTERNAL_REPO = "warpdotdev/warp-internal" +REPO_SYNC_AUTHORS = frozenset( + { + "app/warp-repo-sync", + "warp-repo-sync", + "warp-repo-sync[bot]", + } +) +PUBLIC_PR_URL_RE = re.compile(r"https://github\.com/warpdotdev/warp/pull/(\d+)") + + +def run(cmd: list[str], *, check: bool = True) -> str: + result = subprocess.run(cmd, capture_output=True, text=True, check=check) + return result.stdout.strip() + + +def get_commits(base_ref: str, head_ref: str) -> list[str]: + """Return SHAs of first-parent commits between base and head.""" + log = run( + [ + "git", + "log", + "--first-parent", + "--format=%H", + f"{base_ref}..{head_ref}", + ] + ) + if not log: + return [] + return log.splitlines() + + +def extract_pr_number(sha: str) -> int | None: + """Extract PR number from a squash-merge commit subject line. + + Expects the GitHub squash format: 'feat: something (#1234)'. + Matches the trailing parenthesized (#N) to avoid grabbing issue + numbers from titles like 'Fixes #123 (#456)'. + """ + msg = run(["git", "log", "-1", "--format=%s", sha]) + # Match the last (#N) in the subject — GitHub always appends the PR number + m = re.search(r"\(#(\d+)\)\s*$", msg) + if m: + return int(m.group(1)) + # Fallback: first bare #N (for non-standard subjects) + m = re.search(r"#(\d+)", msg) + if m: + return int(m.group(1)) + return None + + +def get_merged_commits(sha: str) -> list[str]: + """For a merge commit, return the SHAs brought in by the merge. + + A merge commit has two parents: the first parent is the mainline, the + second parent is the tip of the merged branch. The commits unique to + the merge are those reachable from the second parent but not the first. + Returns an empty list for non-merge commits. + """ + parents = run(["git", "log", "-1", "--format=%P", sha]).split() + if len(parents) < 2: + return [] + log = run( + ["git", "log", "--format=%H", f"{parents[0]}..{parents[1]}"], + check=False, + ) + if not log: + return [] + return log.splitlines() + + +def fetch_pr_data(repo: str, pr_number: int) -> dict | None: + """Fetch PR metadata and changed file paths via gh CLI.""" + fields = "number,title,author,body,labels,mergedAt,files,url" + raw = run( + ["gh", "pr", "view", str(pr_number), "--repo", repo, "--json", fields], + check=False, + ) + if not raw: + return None + try: + return json.loads(raw) + except json.JSONDecodeError: + return None + + +def fetch_pr_commit_messages(repo: str, pr_number: int) -> list[str]: + """Fetch commit messages for a PR via the GitHub API.""" + raw = run( + ["gh", "api", f"repos/{repo}/pulls/{pr_number}/commits"], + check=False, + ) + if not raw: + return [] + try: + commits = json.loads(raw) + except json.JSONDecodeError: + return [] + + messages = [] + for commit in commits: + if not isinstance(commit, dict): + continue + commit_data = commit.get("commit") + if isinstance(commit_data, dict): + message = commit_data.get("message") + if message: + messages.append(message) + return messages + + +def get_author_login(data: dict) -> str: + """Extract a GitHub login from a gh PR JSON object.""" + if isinstance(data.get("author"), dict): + return data["author"].get("login", "") + if isinstance(data.get("author"), str): + return data["author"] + return "" + + +def get_label_names(data: dict) -> list[str]: + """Extract label names from a gh PR JSON object.""" + label_names = [] + for lbl in data.get("labels", []) or []: + if isinstance(lbl, dict): + label_names.append(lbl.get("name", "")) + else: + label_names.append(str(lbl)) + return label_names + + +def get_file_paths(data: dict) -> list[str]: + """Extract changed file paths from a gh PR JSON object.""" + file_paths = [] + for f in data.get("files", []) or []: + if isinstance(f, dict): + file_paths.append(f.get("path", "")) + return file_paths + + +def is_repo_sync_pr(data: dict) -> bool: + """Return whether this PR was created by the public-to-internal repo sync bot.""" + return get_author_login(data) in REPO_SYNC_AUTHORS + + +def should_include_pr(repo: str, data: dict) -> bool: + """Return whether a PR should be exposed to changelog generation. + + Releases are cut from warp-internal, but non-sync-bot PRs merged there are + private/internal changes. Do not expose them to the Oz changelog agent or to + generated artifacts. + """ + return repo != INTERNAL_REPO or is_repo_sync_pr(data) + + +def extract_public_pr_number(text: str) -> int | None: + """Extract a public warpdotdev/warp PR number from text.""" + if not text: + return None + m = PUBLIC_PR_URL_RE.search(text) + if m: + return int(m.group(1)) + # Repo-sync commits commonly preserve the original public squash-merge + # subject, such as "feat: add thing (#1234)". + m = re.search(r"\(#(\d+)\)\s*$", text.splitlines()[0] if text else "") + if m: + return int(m.group(1)) + return None + + +def resolve_public_pr_number(repo: str, pr_number: int, data: dict) -> int | None: + """Resolve a repo-sync PR back to its original public warpdotdev/warp PR.""" + public_pr_number = extract_public_pr_number(data.get("body", "") or "") + if public_pr_number is not None: + return public_pr_number + + for message in fetch_pr_commit_messages(repo, pr_number): + public_pr_number = extract_public_pr_number(message) + if public_pr_number is not None: + return public_pr_number + return None + + +def pr_reference(repo: str, pr_number: int, data: dict) -> dict: + """Build a compact audit reference to a PR.""" + return { + "number": data.get("number", pr_number), + "url": data.get("url", ""), + "author": get_author_login(data), + "title": data.get("title", ""), + "repo": repo, + } + + +def normalize_pr_data(repo: str, pr_number: int, data: dict) -> tuple[str, dict, dict | None]: + """Resolve repo-sync PRs to public PR metadata. + + The release workflow runs from warp-internal, where public PRs are mirrored + as warp-repo-sync[bot] PRs with different PR numbers. For changelog output + and contributor attribution, use the original public PR metadata when it can + be resolved, and keep the internal PR under `internal_pr` for audit only. + """ + internal_pr = pr_reference(repo, pr_number, data) if repo != PUBLIC_REPO else None + if repo == PUBLIC_REPO or not is_repo_sync_pr(data): + return repo, data, internal_pr + + public_pr_number = resolve_public_pr_number(repo, pr_number, data) + if public_pr_number is None: + return repo, data, internal_pr + + public_data = fetch_pr_data(PUBLIC_REPO, public_pr_number) + if public_data is None: + return repo, data, internal_pr + + return PUBLIC_REPO, public_data, internal_pr + + +def extract_linked_issues(body: str) -> list[int]: + """Extract issue numbers from closing keywords in a PR body.""" + if not body: + return [] + return sorted(set(int(m.group(1)) for m in LINKED_ISSUE_RE.finditer(body))) + + +def strip_html_comments(text: str) -> str: + """Remove HTML comment blocks () from text. + + This prevents template placeholders inside HTML comments from being + parsed as real CHANGELOG markers. + """ + return re.sub(r"", "", text, flags=re.DOTALL) + + +def extract_markers(body: str) -> list[dict]: + """Extract CHANGELOG-* markers from a PR body.""" + if not body: + return [] + # Strip HTML comments so template placeholders aren't treated as real markers + cleaned = strip_html_comments(body) + entries = [] + has_opt_out = False + for m in MARKER_RE.finditer(cleaned): + category = m.group(1) + text = m.group(2).strip() + # CHANGELOG-NONE is an explicit opt-out — skip all other markers + if category == "NONE": + has_opt_out = True + continue + # Skip template placeholders + if text.startswith("{{") or text.startswith("{text") or not text: + continue + entries.append({"category": category, "text": text}) + # If the PR explicitly opted out, return a special marker + if has_opt_out: + return [{"category": "NONE", "text": ""}] + return entries + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fetch PRs in a release range") + parser.add_argument("--repo", required=True, help="GitHub repo (owner/name)") + parser.add_argument("--base-ref", required=True, help="Previous release tag") + parser.add_argument("--head-ref", required=True, help="Current release tag") + args = parser.parse_args() + + commit_shas = get_commits(args.base_ref, args.head_ref) + + seen_prs: set[int] = set() + prs: list[dict] = [] + + def process_pr(pr_num: int) -> None: + """Fetch and record a single PR by number.""" + data = fetch_pr_data(args.repo, pr_num) + if data is None: + return + if not should_include_pr(args.repo, data): + return + source_repo, data, internal_pr = normalize_pr_data(args.repo, pr_num, data) + author_login = get_author_login(data) + label_names = get_label_names(data) + + body = data.get("body", "") or "" + explicit_entries = extract_markers(body) + linked_issues = extract_linked_issues(body) + file_paths = get_file_paths(data) + + pr = { + "number": data.get("number", pr_num), + "url": data.get("url", "") if source_repo == PUBLIC_REPO else "", + "title": data.get("title", ""), + "author": author_login, + "body": body, + "labels": label_names, + "merged_at": data.get("mergedAt", ""), + "explicit_entries": explicit_entries, + "linked_issues": linked_issues, + "changed_files": file_paths, + "source_repo": source_repo, + } + if internal_pr is not None: + pr["internal_pr"] = internal_pr + prs.append(pr) + + for sha in commit_shas: + pr_num = extract_pr_number(sha) + if pr_num is not None and pr_num not in seen_prs: + # Normal squash-merge commit + seen_prs.add(pr_num) + process_pr(pr_num) + else: + # Merge commit fallback: walk the merged-in commits for PR numbers. + # This handles branches merged via merge commit (e.g. security-patches) + # rather than the usual squash merge. + for merged_sha in get_merged_commits(sha): + inner_pr = extract_pr_number(merged_sha) + if inner_pr is not None and inner_pr not in seen_prs: + seen_prs.add(inner_pr) + process_pr(inner_pr) + + output = { + "range": {"base": args.base_ref, "head": args.head_ref}, + "prs": prs, + } + json.dump(output, sys.stdout, indent=2) + print() # trailing newline + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/classify-changelog-pr/SKILL.md b/.agents/skills/classify-changelog-pr/SKILL.md new file mode 100644 index 00000000..21950663 --- /dev/null +++ b/.agents/skills/classify-changelog-pr/SKILL.md @@ -0,0 +1,55 @@ +--- +name: classify-changelog-pr +description: Reference guidance for classifying whether an unmarked PR should appear in the changelog and under which category. Used inline by the changelog-draft skill — not dispatched as a separate agent. +--- + +# Classify Changelog PR + +This document provides classification rules for PRs that lack explicit `CHANGELOG-*` markers. The changelog-draft agent follows these rules inline when deciding whether to include an unmarked PR. + +## Categories + +- **NEW-FEATURE** — A substantial new user-facing capability. Reserve for features that would warrant docs, marketing, or social media attention. +- **IMPROVEMENT** — Enhances an existing feature in a way users would notice (performance, UX, new options). +- **BUG-FIX** — Fixes a user-visible bug or regression. +- **OZ** — Changes to Oz / AI agent capabilities. At most 4 per release in the stable changelog. +- **NONE** — Explicitly opt out of changelog inclusion. Handled upstream by `fetch_prs.py` marker extraction. + +## Decision rules + +### Always exclude +- PRs with an explicit `CHANGELOG-NONE` marker (contributor opted out) +- PRs authored by known bots (dependabot, renovate, github-actions, codecov) +- PRs that exclusively modify CI workflows (`.github/workflows/`), test files, or dev tooling +- PRs that only update internal docs, comments, or README files +- Dependency bumps with no user-facing behavior change +- Refactors with no observable behavior change (code moves, renames, formatting) + +### Always include +- PRs with explicit `CHANGELOG-*` markers (handled before this guidance applies) +- PRs that fix a crash, data loss, or security issue — even without a marker + +### Conditional on channel +- **Stable channel:** Only include changes that are live for all users. Exclude PRs gated behind `DOGFOOD_FLAGS` or `PREVIEW_FLAGS`. +- **Preview channel:** Include PRs gated behind `PREVIEW_FLAGS`. Still exclude `DOGFOOD_FLAGS`-only changes. +- **Dev channel:** Include everything that's user-visible, regardless of flag gates. + +### Feature-flagged PRs +If a PR mentions a `FeatureFlag` variant in its diff or title: +1. Check which flag list it belongs to (`RELEASE_FLAGS`, `PREVIEW_FLAGS`, `DOGFOOD_FLAGS`). +2. Apply the channel rules above. +3. If the flag is in `RELEASE_FLAGS` or enabled by default in `app/Cargo.toml`, treat it as live. +4. Set `feature_flag` in the classification output to the flag name. + +### Confidence levels +- **high** — Clear user-visible change with obvious category. +- **medium** — Likely user-visible but category or scope is somewhat ambiguous. +- **low** — Unclear whether users would notice; or the PR touches both internal and user-facing code. Set `needs_review: true`. + +## Writing changelog text + +- Write from the user's perspective: "Added X", "Fixed Y", "Improved Z". +- Keep it to one sentence, ≤ 120 characters. +- Don't reference internal implementation details, file paths, or function names. +- Don't start with "PR" or the PR number — those are added as metadata. +- Use active voice and present tense for new features ("Adds dark mode"), past tense for fixes ("Fixed crash on startup"). diff --git a/.agents/skills/create-launch-modal/SKILL.md b/.agents/skills/create-launch-modal/SKILL.md new file mode 100644 index 00000000..e397999d --- /dev/null +++ b/.agents/skills/create-launch-modal/SKILL.md @@ -0,0 +1,406 @@ +--- +name: create-launch-modal +description: Create a one-time launch modal in the Warp client (feature announcement, onboarding, etc.). Use when adding a new modal that should appear exactly once per user on startup, gated by a feature flag, with colors sourced from Warp theme tokens and terminal theme colors. +--- + +# create-launch-modal + +Create a one-time launch modal — the feature-announcement design used for launches like "Orchestrate any agent, anywhere" or "Warp is now open-source." + +## Reference implementation + +`app/src/workspace/view/orchestration_launch_modal/` — the canonical, most up-to-date example of this pattern. + +## Checklist + +- [ ] Feature flag in `warp_features/src/lib.rs` +- [ ] Settings field in `app/src/settings/ai.rs` +- [ ] Trigger logic in `app/src/workspace/one_time_modal_model.rs` +- [ ] View files under `app/src/workspace/view/_launch_modal/` +- [ ] Workspace wiring in `app/src/workspace/view.rs` and `app/src/workspace/mod.rs` +- [ ] Debug actions in `app/src/workspace/action.rs` +- [ ] Hero image at `app/assets/async/png/onboarding/_launch_banner.png` +- [ ] Any custom icons added to `crates/warp_core/src/ui/icons.rs` + SVG in `app/assets/bundled/svg/` + +--- + +## Step 0 – Custom icons (if needed) + +If the modal uses icons not yet in the `Icon` enum, add them before writing the view. + +In `crates/warp_core/src/ui/icons.rs`: + +```rust +// Add to enum +YourIconName, + +// Add to From for &'static str match +Icon::YourIconName => "bundled/svg/your-icon-name.svg", +``` + +Drop the SVG file at `app/assets/bundled/svg/your-icon-name.svg`. Use the same 24×24 viewBox format as existing icons. + +--- + +## Step 1 – Feature flag + +Add to `crates/warp_features/src/lib.rs`: + +```rust +/// Enables the launch modal. +LaunchModal, +``` + +Enable for dogfood: + +```rust +pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ + FeatureFlag::LaunchModal, + // ... +]; +``` + +--- + +## Step 2 – Settings field + +Add to `app/src/settings/ai.rs` inside `define_settings_group!(AISettings, ...)`. +Pattern: one boolean field per modal, globally synced (not respecting user sync), private. + +```rust +// This is not a user-visible setting - it's merely a one-time flag to track if the +// launch modal has been shown to the user. +// +// We model it as a setting so it's only shown once to a given user regardless of the number of +// devices they use. +did_check_to_trigger__launch_modal: DidShowLaunchModal { + type: bool, + default: false, + supported_platforms: SupportedPlatforms::ALL, + sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), + private: true, +} +``` + +--- + +## Step 3 – OneTimeModalModel + +File: `app/src/workspace/one_time_modal_model.rs` + +### 3a. Add field to struct + +```rust +is__launch_modal_open: bool, +``` + +### 3b. Initialize to false in `new()` + +```rust +is__launch_modal_open: false, +``` + +### 3c. Pre-dismiss for new users (critical) + +In the `AuthComplete` → `!is_existing_user` branch, add to the `AISettings::handle` update block alongside the other pre-dismissals. **Without this, new users see the modal on their second startup after onboarding.** + +```rust +if let Err(e) = settings + .did_check_to_trigger__launch_modal + .set_value(true, ctx) +{ + log::warn!("Failed to mark launch modal as dismissed: {e}"); +} +``` + +### 3d. Public API methods + +```rust +pub fn is__launch_modal_open(&self) -> bool { + self.is__launch_modal_open && self.target_window_id.is_some() +} + +pub fn mark__launch_modal_dismissed(&mut self, ctx: &mut ModelContext) { + self.set__launch_modal_open(false, ctx); +} + +#[cfg(debug_assertions)] +pub fn force_open__launch_modal(&mut self, ctx: &mut ModelContext) { + self.set__launch_modal_open(true, ctx); +} +``` + +### 3e. Private setter + +```rust +fn set__launch_modal_open(&mut self, is_open: bool, ctx: &mut ModelContext) -> bool { + if self.is__launch_modal_open != is_open { + self.is__launch_modal_open = is_open; + ctx.emit(OneTimeModalEvent::VisibilityChanged { is_open }); + return true; + } + false +} +``` + +### 3f. Add to `is_any_modal_open` + +```rust +|| self.is__launch_modal_open +``` + +### 3g. Trigger function + +```rust +fn check_and_trigger__launch_modal(&mut self, ctx: &mut ModelContext) -> bool { + if !FeatureFlag::LaunchModal.is_enabled() { + return false; + } + + let ai_settings = AISettings::as_ref(ctx); + if *ai_settings.did_check_to_trigger__launch_modal { + return false; + } + + AISettings::handle(ctx).update(ctx, |settings, ctx| { + if let Err(e) = settings + .did_check_to_trigger__launch_modal + .set_value(true, ctx) + { + log::warn!("Failed to mark launch modal as dismissed: {e}"); + } + }); + + let should_show = !matches!(ChannelState::channel(), Channel::Integration); + self.set__launch_modal_open(should_show, ctx); + should_show +} +``` + +### 3h. Call from `check_and_trigger_all_modals` + +Insert before `check_and_trigger_hoa_onboarding`: + +```rust +if self.check_and_trigger__launch_modal(ctx) { + return; +} +``` + +--- + +## Step 4 – View + +Create `app/src/workspace/view/_launch_modal/mod.rs`: + +```rust +mod view; +pub use view::{init, LaunchModal, LaunchModalEvent}; +``` + +Create `app/src/workspace/view/_launch_modal/view.rs`. Copy from `orchestration_launch_modal/view.rs` and adapt. Key details: + +### Color sources (important) + +- Prefer Warp theme tokens for modal backgrounds, text, overlays, and borders: + - background surfaces: `appearance.theme().surface_3()` (or another `surface_*` token when needed) + - primary/subtext: `appearance.theme().main_text_color(...)` and `appearance.theme().sub_text_color(...)` + - overlays/hover fills: `appearance.theme().surface_overlay_1()` / `surface_overlay_2()` + - subtle borders: `appearance.theme().outline()` +- Use terminal theme colors for terminal-color accents (for example, magenta launch badge accents): + - `appearance.theme().terminal_colors().normal.magenta` + - `appearance.theme().ansi_overlay_1(magenta)` for low-alpha backgrounds +- Avoid hardcoded hex colors. + +### Hero image + +- Store at `app/assets/async/png/onboarding/_launch_banner.png` +- **Aspect ratio matters**: if the image is wider than `MODAL_WIDTH/HERO_HEIGHT` (420/92 ≈ 4.57), wrap the hero `ConstrainedBox` in `Clipped::new(...)` to prevent horizontal bleed when `cover()` scales it +- Images pre-sized to exactly 420×92 need no `Clipped`; images only taller (aspect ratio < 4.57) are fine without it + +```rust +const MODAL_WIDTH: f32 = 420.; +const HERO_HEIGHT: f32 = 92.; +const HERO_IMAGE_PATH: &str = "async/png/onboarding/_launch_banner.png"; + +fn render_hero(&self) -> Box { + let hero = Clipped::new( // only needed if image ratio > 4.57 + ConstrainedBox::new( + Image::new(AssetSource::Bundled { path: HERO_IMAGE_PATH }, CacheOption::Original) + .with_corner_radius(CornerRadius::with_top(Radius::Pixels(8.))) + .cover() + .top_aligned() + .finish(), + ) + .with_width(MODAL_WIDTH) + .with_height(HERO_HEIGHT) + .finish(), + ) + .finish(); + // ... close button overlay via Stack + add_positioned_child +} +``` + +### "New" badge + +Use the standard badge — 24 px tall, 8 px horizontal padding, 14 px font, pill corners, with magenta sourced from terminal theme colors: + +```rust +fn render_badge(appearance: &Appearance) -> Box { + let magenta = appearance.theme().terminal_colors().normal.magenta; + let text = Text::new_inline("New".to_string(), appearance.ui_font_family(), 14.) + .with_color(magenta.into()) + .finish(); + ConstrainedBox::new( + Container::new( + Flex::row() + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_main_axis_size(MainAxisSize::Min) + .with_child(text) + .finish(), + ) + .with_horizontal_padding(8.) + .with_background(Fill::Solid(appearance.theme().ansi_overlay_1(magenta))) + .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) + .finish(), + ) + .with_height(24.) + .finish() +} +``` + +### URLs + +Always use `https://`, not `http://`: + +```rust +const LEARN_MORE_URL: &str = "https://warp.dev/your-blog-link"; +``` + +--- + +## Step 5 – Workspace wiring + +### `app/src/workspace/view.rs` + +```rust +// Module declaration (top) +pub(crate) mod _launch_modal; + +// Import +use crate::workspace::view::_launch_modal::{LaunchModal, LaunchModalEvent}; + +// Struct field +_launch_modal: ViewHandle<LaunchModal>, + +// In Workspace::new() +let _launch_view = ctx.add_typed_action_view(LaunchModal::new); +ctx.subscribe_to_view(&_launch_view, |me, _, event, ctx| { + me.handle__launch_modal_event(event, ctx); +}); + +// In struct initialization +_launch_modal: _launch_view, + +// In OneTimeModalModel subscription handler +} else if model_ref.is__launch_modal_open() { + me.focus__launch_modal(ctx); + +// In View::render (inside the should_show_modal block) +if should_show_modal && one_time_modal_model.is__launch_modal_open() { + stack.add_child(ChildView::new(&self._launch_modal).finish()); +} +``` + +Add event handler and focus helper: + +```rust +fn handle__launch_modal_event(&mut self, event: &LaunchModalEvent, ctx: &mut ViewContext) { + match event { + LaunchModalEvent::Close => { + OneTimeModalModel::handle(ctx).update(ctx, |model, ctx| { + model.mark__launch_modal_dismissed(ctx); + }); + self.focus_active_tab(ctx); + ctx.notify(); + } + } +} + +fn focus__launch_modal(&mut self, ctx: &mut ViewContext) { + ctx.focus(&self._launch_modal); +} +``` + +### `app/src/workspace/mod.rs` + +```rust +// In pub fn init() +view::_launch_modal::init(app); + +// In debug bindings block +EditableBinding::new( + "workspace:open__launch_modal", + "[Debug] Open Launch Modal", + WorkspaceAction::OpenLaunchModal, +) +.with_context_predicate(id!("Workspace")), +EditableBinding::new( + "workspace:reset__launch_modal_state", + "[Debug] Reset Launch Modal State", + WorkspaceAction::ResetLaunchModalState, +) +.with_context_predicate(id!("Workspace")), +``` + +--- + +## Step 6 – Debug actions + +In `app/src/workspace/action.rs`: + +```rust +/// Open the Launch Modal (for debugging) +#[cfg(debug_assertions)] +OpenLaunchModal, +/// Reset the launch modal dismissed state (for debugging) +#[cfg(debug_assertions)] +ResetLaunchModalState, +``` + +Add both variants to the `is_visible_in_command_palette` `false` arm. + +In `app/src/workspace/view.rs` `TypedActionView::handle_action`: + +```rust +#[cfg(debug_assertions)] +OpenLaunchModal => { + OneTimeModalModel::handle(ctx).update(ctx, |model, ctx| { + model.force_open__launch_modal(ctx); + }); + ctx.notify(); +} +#[cfg(debug_assertions)] +ResetLaunchModalState => { + AISettings::handle(ctx).update(ctx, |settings, ctx| { + if let Err(e) = settings + .did_check_to_trigger__launch_modal + .set_value(false, ctx) + { + log::warn!("Failed to reset launch modal state: {e}"); + } + }); +} +``` + +--- + +## Behavior summary + +| User type | Sees modal? | +|---|---| +| New signup | No — pre-dismissed in `AuthComplete` new-user branch | +| Not signed in | No — trigger never fires without `AuthComplete` | +| Existing user, flag enabled | Yes — on first startup after cloud prefs load | +| Integration channel | No — suppressed by `Channel::Integration` check | +| Already seen it | No — setting persists globally across devices | diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md deleted file mode 100644 index 01f1b4fa..00000000 --- a/.agents/skills/create-pr/SKILL.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -name: create-pr -description: Create a pull request in the warp repository for the current branch. Use when the user mentions opening a PR, creating a pull request, submitting changes for review, or preparing code for merge. ---- - -# create-pr - -## Overview - -This guide covers best practices for creating pull requests in the warp repository, including merging master, running presubmit checks, linking Linear tasks, ensuring appropriate test coverage, and structuring your PR for effective review. - -## Related Skills - -- `fix-errors` - Fix presubmit failures (formatting, linting, tests) before opening PR -- `rust-unit-tests` - Write unit tests for your changes, if applicable (see "Testing Requirements" below) -- `warp-integration-test` - Add or update integration coverage for user-visible flows, regressions, and P0 use cases -- `add-feature-flag` - Gate changes behind feature flags - -## Pre-PR Checklist - -### 1. Merge master into your feature branch - -**Always merge master into your feature branch before starting the review process.** - -```bash -git fetch origin -git merge origin/master -``` - -Resolve any merge conflicts locally before opening the PR. - -### 2. Run presubmit checks for code changes - -If the PR includes code changes, run the relevant presubmit checks before opening or updating it: - -```bash -./script/presubmit -``` - -`./script/presubmit` runs: -- `cargo fmt` - Code formatting -- `cargo clippy` - Linting with all warnings as errors -- All tests (unit, doc, and integration) -If the PR is documentation-only (for example, skills, markdown, or other non-code content), you do not need to run `cargo fmt` or `cargo clippy` just to open or update the PR. - -If presubmit fails for a code-changing PR, use the `fix-errors` skill to resolve issues. - -**You must run `cargo fmt` and `cargo clippy` before:** -- Opening a new PR that includes code changes -- Pushing new commits that include code changes to an existing PR branch -- Any reviewed branch update that changes code - -### 3. Review your changes - -Before creating a PR, review what changes you're about to submit: - -```bash -# View commits in your branch (comparing against base branch) -git --no-pager log ..HEAD --oneline - -# View file statistics for changes -git --no-pager diff ...HEAD --stat - -# View full diff -git --no-pager diff ...HEAD -``` - -This helps you: -- Verify all intended changes are included -- Catch unintended changes before review -- Write an accurate PR description -- Ensure you're comparing against the correct base branch -- **Tests:** Include tests when required—bug fixes (regression test), algorithmic code (unit tests), UI components (layout test), P0 use cases (integration test). See Testing Requirements below. - -### 4. Link to Linear task - -When possible, PRs should be associated with a Linear task. Use the Linear MCP tool (if available) to find corresponding issues. - -**Branch naming convention:** -Remote branches should be prefixed with your name (e.g., `zheng/feature`, `alice/fix-bug`). - -**How to link PRs to Linear:** -Include the issue ID in the PR title (e.g., `[WARP-1234] Add new feature`). Do this **before** creating the PR for automatic linking. - -### 5. Open the PR - -Use the PR template at `.github/pull_request_template.md` when opening PRs. - -Add changelog entries when appropriate using the format at the bottom of the PR template. Some examples: -- Feature: "Global search in files across your current directories. Use CMD-F/CTRL-SHIFT-F to open." -- Improvement: "Added horizontal autoscrolling when jumping to line/column." -- Bug fix: "Fixed session viewer input being cleared when agent runs commands. - -**CLI workflow:** - -- **Check if PR exists** for current branch: - ```bash - gh pr view --json number,url - ``` - Exit code 0 if PR exists, 1 if not. - -- **Create a new PR:** - ```bash - # With title and body - gh pr create --title "Title" --body "Description" --draft - - # Auto-fill from commits - gh pr create --fill --draft - - # Use PR template file - gh pr create --body-file .github/pull_request_template.md --title "Title" --draft - ``` - Key flags: `--draft` / `-d`, `--fill` / `-f`, `--body-file` / `-F`, `--web` / `-w` - -- **Update an existing PR:** - ```bash - gh pr edit --title "New title" --body "New body" - gh pr edit --add-reviewer username --add-label bug - ``` - -- **Mark PR ready for review:** - ```bash - gh pr ready - ``` - -### 6. Include co-author attribution - -When committing changes or creating a PR, include attribution at the end of every commit message or PR description: - -``` -Co-Authored-By: Warp -``` - -## Testing Requirements - -### Bug fixes require regression tests - -**All bug fixes should be accompanied by a regression test.** This helps prevent re-breaking something that was already broken once. - -The test should: -- Reproduce the original bug (would fail before the fix) -- Pass after the fix is applied -- Be clearly named to indicate what bug it's preventing - -### Algorithmic code requires unit tests - -Code with non-trivial logic should have unit tests to validate functionality: - -**Examples of what needs unit tests:** -- Custom data structures (e.g., `SumTree`) -- Search-related APIs that should return expected results for a given query -- Core layout code in the UI framework -- Any algorithmic or computational logic - -**Not required for:** -- Sufficiently-simple functions -- Trivial getters/setters - -See the `rust-unit-tests` skill for guidance on writing unit tests. - -### UI components need layout validation tests - -**All UI components (implementations of `View`) should have a simple unit test** to validate that they can be laid out without a panic. - -This provides high-level coverage over rendering "safety" (though not "correctness"): - -```rust -#[test] -fn test_component_can_layout() { - use warpui::App; - use warp::test_util::{terminal::initialize_app_for_terminal_view, add_window_with_terminal}; - - App::test((), |mut app| async move { - initialize_app_for_terminal_view(&mut app); - let term = add_window_with_terminal(&mut app, None); - - // Render the component - should not panic - term.update(&mut app, |view, ctx| { - // Create and layout your component - }); - }) -} -``` - -### Ask before skipping integration coverage - -If the PR changes a user-visible flow, fixes an end-to-end regression, or otherwise looks like it would benefit from integration coverage, use the `ask_user_question` tool before creating or updating the PR to ask whether the user wants an integration test added as part of the work. - -Prefer a direct choice such as: - -- `Yes, add an integration test before creating the PR` -- `No, continue without an integration test` - -If the user chooses to add one, use the `warp-integration-test` skill. - -### P0 use cases require integration tests - -**All "P0 use cases" require an integration test** that covers the behavior/flow in question. - -**A "P0 use case" is defined as:** Any behavior of the application that, if broken, warrants an out-of-band release. - -Integration tests should: -- Exercise the full user-facing flow -- Validate end-to-end functionality -- Be placed in the `integration/` directory - -Use the `warp-integration-test` skill for implementation details, test registration steps, and validation workflow. - -## PR Description Guidelines - -Your PR summary under the "Description" section should include: - -1. **What** - What changes are being made -2. **Why** - Why these changes are necessary (link to Linear task if applicable) -3. **How** - Brief explanation of the approach taken - -## After Opening the PR - -1. **Monitor CI checks** - Ensure all automated checks pass -2. **Respond to review comments** - Address feedback promptly -3. **Keep the PR up to date** - Merge master if conflicts arise -4. **Re-run relevant validation** - After making changes based on review feedback. For code changes, re-run `cargo fmt`/`cargo clippy` (and other relevant checks); for documentation-only changes, this is not required. - -## Best Practices - -- **Keep PRs focused** - One logical change per PR when possible -- **Write clear commit messages** - Explain what and why, not just what -- **Self-review first** - Review your own diff before requesting review -- **Update tests** - Ensure test coverage reflects your changes -- **Document breaking changes** - Call out any API changes or breaking modifications -- **Use feature flags** - Gate risky changes behind feature flags when appropriate (see the `add-feature-flag` skill) diff --git a/.agents/skills/dedupe-issue-local/SKILL.md b/.agents/skills/dedupe-issue-local/SKILL.md index af3085c8..7252cdc2 100644 --- a/.agents/skills/dedupe-issue-local/SKILL.md +++ b/.agents/skills/dedupe-issue-local/SKILL.md @@ -1,10 +1,15 @@ --- name: dedupe-issue-local specializes: dedupe-issue -description: Repo-specific dedupe guidance for warp-external. Only the categories declared overridable by the core dedupe-issue skill may be specialized here. +specializes_source: warpdotdev/oz-for-oss:.agents/skills/dedupe-issue +description: Repo-specific dedupe guidance for warp. Only the categories declared overridable by the core dedupe-issue skill may be specialized here. --- -# Repo-specific dedupe guidance for `warp-external` +# Repo-specific dedupe guidance for `warp` + +## Prerequisite: install the parent skill + +This skill specializes the core `dedupe-issue` skill (named in the `specializes` frontmatter field) and is not functional on its own. Before applying its guidance, confirm the parent skill is installed and resolvable at `.agents/skills/dedupe-issue/SKILL.md`. If it is missing, install it first by copying the skill directory from the source declared in the `specializes_source` frontmatter field (`warpdotdev/oz-for-oss:.agents/skills/dedupe-issue`). Then continue with the guidance below. This file is a companion to the core `dedupe-issue` skill. It does not redefine the duplicate-detection algorithm, the similarity thresholds, diff --git a/.agents/skills/diagnose-ci-failures/SKILL.md b/.agents/skills/diagnose-ci-failures/SKILL.md deleted file mode 100644 index 827c5ed4..00000000 --- a/.agents/skills/diagnose-ci-failures/SKILL.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -name: diagnose-ci-failures -description: Diagnose CI failures for a PR using the GitHub CLI, extract error logs, and generate a plan to fix them. Use when the user asks to check CI status, pull CI issues, triage test failures, or investigate PR build failures. ---- - -# diagnose-ci-failures - -Programmatically diagnose CI failures for a PR and generate a plan to fix them. - -## Overview - -This skill provides a deterministic workflow to check CI status for a PR, extract failure logs, analyze errors, and create a plan (not code changes) to resolve issues. The output is always a plan document that can be reviewed before execution. - -## Workflow - -### 1. Verify PR exists for current branch - -Get the current branch and check if a PR exists: - -```bash -# Get current branch -git branch --show-current - -# Check for PR -gh --no-pager pr view --json number,title,url,state -``` - -If no PR exists, inform the user and offer to create one using the `create-pr` skill. - -### 2. Check CI status - -Fetch the status of all CI checks: - -```bash -gh pr view --json statusCheckRollup -``` - -Parse the output to identify: -- Completed checks vs. in-progress checks -- Successful checks -- Failed checks with their names and details URLs - -If CI is still running, inform the user which checks have already failed or passed, highlight the checks that are still running, and suggest waiting for completion before diagnosis. - -### 3. Extract failure logs - -For each failed check, pull the logs using the run ID from the status check: - -```bash -gh run view --log-failed -``` - -Focus on extracting: -- Error messages and their locations (file paths, line numbers) -- Compilation errors (unused imports, type mismatches, etc.) -- Linting/clippy errors with specific lint names -- Test failure messages and stack traces -- Build failures and their root causes - -### 4. Categorize errors - -Group errors by type: -- **Formatting issues**: `cargo fmt` failures -- **Linting issues**: `cargo clippy` warnings/errors -- **Compilation errors**: Type errors, missing imports, signature mismatches -- **Test failures**: Failing tests with their names and failure reasons -- **Platform-specific issues**: WASM, Linux, macOS, Windows-specific failures - -### 5. Generate fix plan - -Create a plan document (using `create_plan` tool) with: -- **Problem Statement**: Summary of failing checks -- **Current State**: What errors were found and where -- **Proposed Changes**: Specific fixes needed for each error category -- **Validation Steps**: Commands to verify fixes (fmt, clippy, tests, presubmit) - -The plan should reference the `fix-errors` skill for detailed guidance on resolving specific error types. - -## Important Notes - -- **Always create a plan first**: Never make code changes directly. Generate a plan for user review -- **Check test status in CI**: Even if tests fail locally, verify they passed in CI before flagging as issues -- **Unrelated test failures**: If tests passed in CI but fail locally, they may be environment-specific or flaky -- **Multiple error types**: Fix one category at a time (e.g., all clippy errors before tests) -- **Cross-reference fix-errors skill**: For detailed error resolution strategies, use the `fix-errors` skill - -## Common CI Check Names - -- `Formatting + Clippy (MacOS)` -- `Formatting + Clippy (Linux)` -- `Run MacOS tests` -- `Run Linux tests` -- `Run Windows tests` -- `Check CI results` (summary check) -- `WASM build` - -## Example Commands - -**Get PR status with details:** -```bash -gh --no-pager pr view --json number,title,state,statusCheckRollup -``` - -**Get logs from specific failed run:** -```bash -gh run view 12345678 --log-failed -``` - -**Check for specific error in logs:** -```bash -gh run view 12345678 --log-failed 2>&1 | grep -A 5 "error:" -``` diff --git a/.agents/skills/fix-errors/SKILL.md b/.agents/skills/fix-errors/SKILL.md deleted file mode 100644 index b9a0bd3e..00000000 --- a/.agents/skills/fix-errors/SKILL.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -name: fix-errors -description: Fix compilation errors, linting issues, and test failures in the warp Rust codebase. Covers presubmit checks, WASM-specific errors, and running specific tests. Use when the user hits build errors, clippy or fmt failures, test failures, or needs to run or interpret presubmit before a PR. ---- - -# fix-errors - -Fix compilation errors, linting issues, and test failures in the warp Rust codebase. - -## Overview - -This skill helps resolve common issues encountered during development, including: -- Compilation errors (unused imports, type mismatches, etc.) -- Linting failures (clippy warnings) -- Formatting violations -- WASM-specific errors -- Test failures - -Before opening or updating a pull request, all presubmit checks must pass. - -## Presubmit Checks - -Run all presubmit checks at once: - -```bash -./script/presubmit -``` - -This runs formatting, linting, and all tests. If it passes, you're ready to open a PR. - -### Individual Checks - -Run checks separately when debugging specific issues: - -**Rust formatting:** -```bash -cargo fmt -- --check -``` - -**Clippy (full workspace):** -```bash -cargo clippy --workspace --exclude warp_completer --all-targets --all-features --tests -- -D warnings -cargo clippy -p warp_completer --all-targets --tests -- -D warnings -``` - -**WASM Clippy:** -```bash -cargo clippy --target wasm32-unknown-unknown --profile release-wasm-debug_assertions --no-deps -``` - -**Objective-C/C/C++ formatting:** -```bash -./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/ -``` - -**All tests:** -```bash -cargo nextest run --no-fail-fast --workspace --exclude command-signatures-v2 -cargo nextest run -p warp_completer --features v2 -``` - -**Doc tests:** -```bash -cargo test --doc -``` - -## Running Specific Tests - -**Single package:** -```bash -cargo nextest run -p -``` - -**Filter by test name:** -```bash -cargo nextest run -E 'test()' -``` - -**Specific package with filter:** -```bash -cargo nextest run -p -E 'test()' -``` - -**With output (no capture):** -```bash -cargo nextest run -p --nocapture -``` - -## Common Error Types - -### Unused Imports -Remove unused `use` statements identified by the compiler. - -### Unused Constants -Remove constants that are defined but never used. - -### Unknown Imports -Add the correct `use` statement for undefined types. Search the codebase to find the correct module path. - -### Type Mismatches -Update function calls to pass arguments of the correct type. Common fixes: -- Use `.as_str()` instead of `.clone()` when a `&str` is expected -- Use `&value` when a reference is needed -- Use `.to_string()` when `String` is expected but `&str` is provided - -### Struct Field Changes -When a struct adds/removes fields, update all places where it's constructed or destructured: -- Struct initialization -- Pattern matching (`match`, `if let`) -- Destructuring assignments - -### Function Signature Changes -When a function adds a new parameter, update all call sites to provide the new argument: -- For `bool` params: pass `true` or `false` based on context -- For `Option` params: pass `None` as default or `Some(value)` if needed - -### Enum Variant Changes -When adding a new enum variant, update exhaustive `match` statements: -- Add a new match arm with appropriate handling -- Mirror the implementation pattern of similar variants - -### Incorrect Trait Implementation -Fix trait implementations that return the wrong type or don't satisfy trait bounds. - -### WASM-Specific Errors - -WASM builds (`wasm32-unknown-unknown` target) don't support filesystem operations. Code that uses filesystem APIs must be gated behind the `local_fs` feature flag. - -**Common WASM errors:** -- Dead code warnings for code only used in non-WASM builds -- Unused code that's only relevant when `local_fs` is available -- Tests that require filesystem access - -**Fixes:** - -**Gate tests behind `local_fs`:** -```rust -#[test] -#[cfg(feature = "local_fs")] -fn test_find_git_repo_with_worktree() { - // Test that uses filesystem operations -} -``` - -**Conditionally allow dead code for types only used when `local_fs` is enabled:** -```rust -#[cfg_attr(not(feature = "local_fs"), allow(dead_code))] -#[derive(Clone, EnumDiscriminants, Serialize)] -pub enum ExampleType { - // Variants only used when local_fs is enabled - Variant1, - Variant2, - Variant3, -} -``` - -WASM errors are discovered by running: - -```bash -cargo clippy --target wasm32-unknown-unknown --profile release-wasm-debug_assertions --no-deps -``` - -## Best Practices - -**Before fixing:** -- Read the full error message to understand the root cause -- Check if multiple errors are related (fixing one may resolve others) -- For trait/type errors, verify you understand the expected vs actual types -- For WASM errors, check if code needs to be gated behind `local_fs` - -**When fixing:** -- Fix one error type at a time when there are multiple issues -- Run `cargo check` frequently to verify fixes -- For WASM errors, run WASM clippy to verify the fix -- For complex changes, run relevant tests after fixing - -**After fixing:** -- Always run `cargo fmt` and `cargo clippy` before pushing -- Run the full presubmit script before opening or updating a PR. Use the `create-pr` skill for more detailed instructions -- Verify tests pass in the areas you modified diff --git a/.agents/skills/implement-specs/SKILL.md b/.agents/skills/implement-specs/SKILL.md deleted file mode 100644 index d30b84c9..00000000 --- a/.agents/skills/implement-specs/SKILL.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: implement-specs -description: Implement an approved feature from PRODUCT.md and TECH.md, keeping specs and code aligned in the same PR as implementation evolves. Use after the product and tech specs are approved and the next step is building the feature. ---- - -# implement-specs - -Implement an approved feature from `PRODUCT.md` and `TECH.md`. - -## Overview - -Use this skill after the product and tech specs are approved. The goal is to build the feature described by the specs while keeping the checked-in specs and the implementation aligned as the work evolves. - -Approved specs should live directly under a ticket-named directory in `specs/`, for example `specs/APP-1234/PRODUCT.md` and `specs/APP-1234/TECH.md`. - -In many cases, the implementation should be pushed in the same PR as the product and tech specs. As the engineer iterates, changes to `PRODUCT.md`, `TECH.md`, and the code should all be pushed in that same PR so review stays anchored to the feature that will actually ship. - -## Prerequisites - -Before using this skill: - -- confirm that `PRODUCT.md` exists -- confirm that `TECH.md` exists when the feature warranted one -- confirm that the relevant specs have been reviewed and approved enough to start implementation - -## Workflow - -### 1. Read the approved specs first - -Treat: - -- `PRODUCT.md` as the source of truth for user-facing behavior -- `TECH.md` as the source of truth for architecture, sequencing, and implementation shape - -Make sure you understand the expected behavior, constraints, risks, and validation plan before writing code. - -### 2. Offer optional implementation aids for large features - -For large or long-running features, optionally offer one of these aids to the user before implementation begins: - -- `PROJECT_LOG.md` to track checkpoints, explored paths, partial findings, and current implementation state -- `DECISIONS.md` to capture concrete product and technical decisions made during the PRD and tech design process - -These are optional aids, not required deliverables. Offer them when they would reduce confusion or help future agents avoid re-exploring the same paths. - -### 3. Plan and implement against the specs - -Break the work into concrete implementation steps, then implement the feature against the approved specs. - -During implementation: - -- keep behavior aligned with `PRODUCT.md` -- keep architecture and sequencing aligned with `TECH.md` -- add or update tests and verification artifacts as the work lands - -Use the same PR for the specs and implementation when practical so the full feature evolution is reviewable in one place. - -### 4. Update specs as the implementation evolves - -If implementation reveals that the intended behavior or design should change, update the checked-in specs rather than letting them go stale. - -In particular: - -- update `PRODUCT.md` when user-facing behavior, UX, edge cases, or success criteria change -- update `TECH.md` when architecture, sequencing, module boundaries, or validation strategy change -- keep those updates in the same PR as the corresponding code changes - -The PR should describe the feature that actually ships, not just the initial draft of the specs. - -### 5. Verify against the specs - -Before considering the work complete, verify that the code matches the current specs. - -Prefer: - -- `rust-unit-tests` for unit tests and regression coverage -- integration or end-to-end tests for important user flows - -## Best Practices - -- Keep specs and code synchronized throughout implementation. -- Prefer updating the spec immediately when decisions change rather than batching spec cleanup until the end. -- Use optional tracking documents only when they add real value for a complex feature. -- Keep the same PR coherent: spec updates, code changes, tests, and optional tracking docs should all support the same feature narrative. - -## Related Skills - -- `spec-driven-implementation` -- `write-product-spec` -- `write-tech-spec` -- `rust-unit-tests` diff --git a/.agents/skills/onboarding-verification-skill/SKILL.md b/.agents/skills/onboarding-verification-skill/SKILL.md new file mode 100644 index 00000000..40f28503 --- /dev/null +++ b/.agents/skills/onboarding-verification-skill/SKILL.md @@ -0,0 +1,300 @@ +--- +name: onboarding-verification-skill +description: Launch two parallel Oz cloud agents with computer use to download and install the latest stable Linux Warp build, capture screenshots while walking through first-time onboarding in both logged-out and logged-in states, then selectively fan out follow-up cloud agents for distinct onboarding branches proposed by those initial explorers. Use this whenever the user asks to test, document, screenshot, or walk through the Warp first-time install/onboarding experience in a cloud Linux environment. +--- + +# Onboarding verification skill + +Use this skill to verify the first-time Warp install and onboarding flow on Linux with broader branch coverage than a single linear walkthrough. + +The parent agent should not perform the walkthrough locally. Launch two parallel Oz cloud agents with computer use. Both initial children install the latest stable Warp Linux package appropriate for their platform and capture screenshots at every visible onboarding step until Warp reaches a usable terminal session. One child verifies the login-free flow. The other child verifies the logged-in flow using the managed secret `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN`. + +Those two baseline explorers are also responsible for noticing meaningful alternate onboarding branches and returning concrete plans for follow-up cloud agents. The parent agent should synthesize those plans, deduplicate overlapping suggestions, and launch a bounded second wave of targeted follow-up agents to improve coverage of paths a real user might encounter. + +## Parent workflow + +1. Launch exactly two remote Oz cloud agents in a single parallel `run_agents` batch with computer use enabled. +2. Use no environment-specific assumptions unless the user provided an environment. If no environment was provided, omit the environment ID and let Warp choose the default remote environment. +3. Give both baseline child agents the shared child prompt below, plus the appropriate flow-specific prompt. +4. Wait for both baseline agents' reports. Each report must include: + - The completed baseline walkthrough result and artifacts. + - A concise list of observed UI quality issues, suspected bugs, error states, or rough edges, with screenshots when visible. + - A prioritized follow-up coverage plan describing distinct onboarding paths worth exploring with additional cloud agents. +5. Treat the authenticated baseline child as blocked if `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` is missing or does not authenticate successfully. +6. Build a combined coverage map from the two baseline reports. Deduplicate suggestions that reach the same visible state or exercise the same decision surface. +7. Launch a second `run_agents` batch with computer use enabled for the most valuable follow-up onboarding branches: + - Prefer branches that materially change visible UI, available controls, downstream screens, auth state, or setup outcomes. + - Favor paths likely to expose correctness, polish, layout, truncation, loading, or validation problems. + - Default to at most four follow-up agents total unless the user explicitly asked for exhaustive coverage or the baseline reports show more than four clearly distinct high-value branches. + - Do not launch speculative follow-ups when the baseline agents did not observe a concrete branch point; report that coverage stopped after the baseline pass instead. +8. Give each follow-up child the shared child prompt, the follow-up flow prompt below, the logged-out or logged-in flow prompt that matches its assigned auth state, and one synthesized branch assignment from the baseline reports. +9. Wait for all follow-up reports before summarizing coverage, issues, artifacts, and any still-unexplored branches worth a later run. + +## Managed FTUE auth secret + +- `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` is an internal-team managed secret for cloud agents, not a repo file or prompt literal. +- The secret should authenticate as a dedicated non-employee, non-`warp.dev` FTUE test user. +- Rotate the secret with `oz-dev secret update --team --value-file ONBOARDING_AGENT_FTUE_REFRESH_TOKEN`. +- Treat the private token file as local scratch material only. Do not read it into chat, print it, stage it, commit it, upload it, or include it in artifacts. Delete it after the managed secret is updated. +- Children should receive the secret only through the managed environment variable injected into the remote run. + +Use the initial `run_agents` call shaped like this: + +```text +summary: Launching two baseline cloud agents with computer use to compare logged-out and logged-in Warp onboarding screenshots and propose follow-up coverage branches. +remote.computer_use_enabled: true +agent_run_configs: +- name: "warp-onboarding-logged-out" + prompt: the logged-out flow prompt below +- name: "warp-onboarding-logged-in" + prompt: the logged-in flow prompt below +base_prompt: the shared child prompt below +``` + +When the baseline reports identify concrete follow-up branches, use a second `run_agents` call shaped like this: + +```text +summary: Launching targeted cloud follow-up agents to explore distinct onboarding branches identified by the baseline onboarding explorers. +remote.computer_use_enabled: true +agent_run_configs: +- name: "warp-onboarding-followup-theme-choice" + prompt: the follow-up flow prompt below, the logged-out flow prompt below, and one synthesized logged-out branch assignment +- name: "warp-onboarding-followup-model-choice" + prompt: the follow-up flow prompt below, the logged-in flow prompt below, and one synthesized logged-in branch assignment +base_prompt: the shared child prompt below +``` + +## Shared child prompt + +Give both cloud agents these shared instructions: + +```text +You are verifying the first-time Warp install and onboarding experience on Linux. + +Goal: +- Download and install the latest stable Warp Linux build appropriate for this cloud environment's distro and CPU architecture. +- Launch Warp in a fresh first-run state. +- Take a screenshot at every visible onboarding step. +- Continue until Warp reaches a usable terminal session, or stop and report a blocker if the assigned flow cannot proceed. +- Notice alternate onboarding decisions that lead to meaningfully different screens, states, or outcomes, and return concrete follow-up cloud-agent plans for the parent orchestrator. +- Treat visual polish, missing assets, misalignment, overlapping content, clipped text, poor contrast, broken loading states, unexpected errors, and confusing controls as verification findings rather than ignoring them. + +Install requirements: +- Use official stable Warp downloads only. +- Do not use Warp Preview, Alpha, source builds, or a repository development build. +- Detect CPU architecture with `uname -m`. +- Detect the package manager or distro before choosing the package format. +- Prefer native packages over AppImage because they install dependencies and register the app normally. + +Stable Linux package mapping: +- Debian/Ubuntu with amd64 or x86_64: https://app.warp.dev/download?package=deb +- Debian/Ubuntu with arm64 or aarch64: https://app.warp.dev/download?package=deb_arm64 +- Fedora/RHEL/CentOS/openSUSE with amd64 or x86_64: https://app.warp.dev/download?package=rpm +- Fedora/RHEL/CentOS/openSUSE with arm64 or aarch64: https://app.warp.dev/download?package=rpm_arm64 +- Arch with amd64 or x86_64: https://app.warp.dev/download?package=pacman +- Arch with arm64 or aarch64: https://app.warp.dev/download?package=pacman_arm64 +- If no native package path is available, use the AppImage fallback: + - amd64 or x86_64: https://app.warp.dev/download?package=appimage + - arm64 or aarch64: https://app.warp.dev/download?package=appimage_arm64 + +Before launch: +- Create a flow-specific artifact directory such as `~/warp-onboarding-logged-out` or `~/warp-onboarding-logged-in`. +- Ensure the run starts from a fresh Warp first-run state by removing only Warp-specific config/data/cache/state directories for the test user, such as `~/.config/warp-terminal`, `~/.local/share/warp-terminal`, `~/.local/state/warp-terminal`, and `~/.cache/warp-terminal` if they exist. +- Do not delete unrelated user files or system directories. + +Screenshot workflow: +- Take the first screenshot before interacting with the first visible Warp window. +- Take one screenshot before every user action. +- Take another screenshot after each action if the UI changes. +- Use sequential filenames with a flow prefix, such as `01-logged-out-initial-window.png` or `01-logged-in-initial-window.png`. +- If anything looks wrong, take an additional issue-focused screenshot that captures the problematic state as clearly as possible. +- Maintain a manifest file in the artifact directory with, for each screenshot: + - filename + - timestamp + - what was visible + - what action was about to happen or just happened +- For issue-focused screenshots, add the suspected issue category and the screen or step where it appeared. +- Do not include secret values, refresh tokens, ID tokens, auth redirect URLs, or Authorization headers in the manifest, logs, shell history, screenshots, or final report. + +Onboarding behavior: +- Baseline children choose the default or most conservative option at each step unless the flow-specific prompt says otherwise, while recording branch points that deserve separate follow-up coverage. +- Follow-up children take the specifically assigned alternate branch, then use the default or most conservative option for unrelated decisions unless the branch assignment says otherwise. +- If telemetry, shell, theme, editor-import, or agent integration choices appear, use the default path and document the choice in the manifest. +- Continue until a normal terminal prompt is visible and usable. + +UI quality review: +- Watch for screens that are visually broken, obviously unfinished, misaligned, truncated, clipped, crowded, low-contrast, unexpectedly blank, stuck loading, or inconsistent with adjacent steps. +- Watch for actionable errors or validation states that appear during normal flow exploration, including auth failures, failed button transitions, controls that do not respond, duplicated overlays, missing images, or broken post-selection states. +- For every suspicious state: + - Capture a screenshot. + - Record the screen, the action that led to it, what looked wrong, and whether it blocked progress. + - Describe the issue factually. If expected behavior is uncertain, say it appears suspicious rather than claiming a confirmed bug. + +Terminal verification: +- Once a terminal session is visible, run a harmless flow-specific command: + - logged-out flow: `echo warp-onboarding-logged-out-ready` + - logged-in flow: `echo warp-onboarding-logged-in-ready` +- Capture a final screenshot showing the usable terminal and command output. + +Report back: +- Whether you were a baseline explorer or a follow-up branch explorer. +- Which flow you ran: logged-out or logged-in. +- OS and distro detected. +- CPU architecture detected. +- Package URL and install method used. +- Launch command used. +- Whether the walkthrough reached a usable terminal session. +- Ordered screenshot list with short descriptions. +- Artifact directory path. +- Any built-in artifact IDs or attachment names if the harness supports artifact upload. +- Any visual polish concern, suspected bug, error state, or unpolished/misaligned screen, including: + - screenshot filename + - screen or step + - action taken immediately before it appeared + - concise observed behavior + - whether it blocked progress +- Any blocker, crash, missing dependency, display problem, auth failure, or step that required judgment. +- For baseline explorers, include a `Follow-up coverage plan` section with zero or more proposed child-agent branches. Each proposal must include: + - suggested agent name + - logged-out or logged-in flow + - onboarding screen or decision point where the alternate branch begins + - exact alternate choice or action sequence to explore + - why it is materially distinct from the baseline path + - what user-visible state, setup outcome, or failure mode it could reveal + - any secret, auth, or environment dependency + - priority: high, medium, or low +- For follow-up explorers, include whether the assigned branch was reachable and completed. If a new branch point appears while following the assigned path, record it as a later-run suggestion instead of recursively expanding the run yourself. + +Do not upload screenshots or logs to public external services. If the harness provides a built-in artifact or screenshot attachment mechanism, use that. Otherwise, leave the files in the artifact directory and report their paths. +``` + +## Logged-out flow prompt + +Append this prompt to the shared child prompt for the logged-out child: + +```text +You own the logged-out onboarding flow. + +Flow-specific goal: +- Do not create an account, log in, or use a real user identity. +- Continue only through login-free or account-free paths until Warp reaches a usable terminal session. +- Stop and report a blocker if the flow requires login or account creation with no skip/continue-without-account option. + +Flow-specific onboarding behavior: +- If there is a skip, "continue without account", "not now", "login later", or equivalent option, use it. +- Do not enter an email address, connect OAuth, paste an auth token, or create credentials. +- Be especially alert for logged-out branch points around choosing terminal-only versus agentic experiences, customization/layout options, third-party integration toggles, and terminal theme selection. If they appear, propose follow-up branches that exercise materially different choices rather than trying all alternates inline. +- Use the artifact directory `~/warp-onboarding-logged-out`. +``` + +## Logged-in flow prompt + +Append this prompt to the shared child prompt for the logged-in child: + +```text +You own the logged-in onboarding flow. + +Flow-specific goal: +- Use the managed secret environment variable `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` to authenticate as the dedicated non-employee, non-`warp.dev` FTUE test user. +- Exercise onboarding screens that are available to an already-authenticated user. +- Continue through the authenticated onboarding path until Warp reaches a usable terminal session. + +Secret handling requirements: +- Before doing auth work, verify that `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` exists and is non-empty without printing it. +- Never echo, log, screenshot, upload, or report the secret value. +- Avoid shell tracing (`set -x`) and avoid writing commands that place the raw token in shell history or process lists. +- Treat every auth redirect URL containing the refresh token as secret-bearing material, even after URL-encoding. +- Do not pass a token-bearing redirect URL to a shell command, desktop URI handler, browser address bar, process argument, log, artifact, or report. In particular, do not use commands such as `xdg-open`, `gio open`, `open`, or equivalent with the redirect URL. +- If you need to construct an auth redirect URL, keep it only in a clipboard value or a private temporary file with user-only permissions, paste it through Warp's visible Paste Auth Token flow, then delete the temporary file immediately after use. + +Secure Paste Auth Token process: +1. Verify `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` exists and is non-empty without printing it. +2. Start Warp's normal login flow and derive the current-run `state` from Warp's generated login URL. +3. Normalize the managed secret privately: + - Trim surrounding whitespace and one pair of surrounding single or double quotes if present. + - If the secret parses as a URL with a `refresh_token` query parameter, extract that `refresh_token` value and ignore any stale `state` in the secret. + - Otherwise, treat the trimmed secret as the raw refresh token. +4. URL-encode the extracted refresh token and current-run `state` separately as query parameter values. +5. Construct the redirect URL only in a clipboard value or private temporary file with user-only permissions. +6. Return to Warp and use the visible Paste Auth Token path: + - Click the `Click here to paste your token from the browser` link, `Paste Auth Token` button, or equivalent pasted-token control shown by Warp. + - Focus the auth token text input that appears. + - Paste the prepared redirect URL into that input and submit it through Warp's UI so Warp parses and validates it. +7. Delete any private temporary files immediately after use and clear the clipboard if the environment supports doing so safely. +8. If the Paste Auth Token UI cannot be reached or automated safely, stop and report an auth blocker instead of parsing the redirect in place of Warp, using a desktop URI handler, browser address bar, or shell command with the token-bearing URL. + +Preferred authenticated path: +- Launch Warp in a fresh first-run state and choose the login/sign-in path from onboarding. +- Use Warp's built-in Paste Auth Token flow rather than visiting real OAuth providers, invoking a desktop URI handler, or asking the agent to parse/validate the redirect URI itself. +- Derive `` from the login URL generated by Warp if the UI exposes a copied login URL or opens the browser. If the UI does not expose the state after reasonable effort, report that as an auth blocker rather than bypassing state validation. +- Do not preflight the token with Firebase Secure Token before handing it to Warp. Warp's desktop redirect handler only requires `refresh_token` and `state`; `user_uid` is optional, and `deleted_anonymous_user=true` handles the anonymous-user override case. +- Treat `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` as either of these secret shapes: + - a raw Firebase refresh token, or + - a complete Warp desktop auth redirect URL containing a `refresh_token` query parameter. +- Normalize the secret into a current-run redirect URL without printing it: + - Trim surrounding whitespace and one pair of surrounding single or double quotes if present. + - If the secret parses as a URL with a `refresh_token` query parameter, extract that `refresh_token` value and ignore any stale `state` in the secret. + - Otherwise, treat the trimmed secret as the raw refresh token. + - URL-encode the extracted refresh token and the current-run `state` separately as query parameter values. + - Build `warp://auth/desktop_redirect?refresh_token=&deleted_anonymous_user=true&state=`. + - Do not include `user_uid` unless it is already present in a provided desktop redirect URL; it is not required for this flow. +- Construct the normalized redirect URL in a clipboard value or private temporary file only, then hand it to Warp through the Paste Auth Token UI. Do not parse, validate, or route the redirect outside of Warp. +- If the Paste Auth Token flow cannot be reached or automated safely, stop and report an auth blocker instead of using a desktop URI handler or any shell command that contains the token-bearing URL. + +Fallback authenticated path: +- If Warp rejects the normalized redirect, report the non-sensitive user-visible error and classify whether the secret appeared to be a raw token or a desktop redirect URL, without reporting any token contents. +- If the Paste Auth Token flow is blocked by UI automation issues, report the blocker and include the exact non-sensitive step where automation failed. +- Do not switch to a logged-out path for this child. + +Flow-specific onboarding behavior: +- Choose login/sign-in rather than skip/login-later when presented with an auth choice. +- After auth succeeds, continue through the remaining onboarding screens with default or conservative options. +- Be especially alert for logged-in branch points around model selection, account-aware onboarding screens, AI/agent setup, workspace or project setup, and any decision that changes available product capability. If they appear, propose follow-up branches that exercise materially different choices rather than trying all alternates inline. +- After the terminal verification succeeds, click the upper-right avatar/account control, open Settings from that menu, and capture an additional screenshot that clearly shows the logged-in user's email address in Warp settings or account/profile settings. +- Include the account/settings email screenshot in the manifest and final report. The email address itself may be visible in the screenshot, but do not copy the email into logs, shell output, or the final text report unless the user explicitly asks for it. +- Use the artifact directory `~/warp-onboarding-logged-in`. +``` + +## Follow-up flow prompt + +Append this prompt to the shared child prompt for every second-wave child, followed by the matching logged-out or logged-in flow prompt and one branch assignment synthesized from the baseline reports: + +```text +You own one follow-up onboarding branch selected by the parent orchestrator from an earlier baseline exploration report. + +Follow-up branch behavior: +- Start from a fresh first-run Warp state and install the same latest stable Linux build using the shared instructions. +- Respect the assigned auth state: remain logged out for logged-out assignments, or use the managed authenticated flow for logged-in assignments. +- Follow the exact alternate onboarding choice or action sequence in the branch assignment. +- Capture screenshots before and after each assigned branch decision, then continue to a usable terminal session if the path allows it. +- Apply the same UI quality review standard as the baseline explorers and call out anything that looks broken, rough, misaligned, confusing, or unexpectedly error-prone. +- If the assigned branch is not reachable, capture the closest relevant screen, report why it was unreachable, and do not silently substitute a different branch. +- If the assigned branch reveals another interesting alternate path, record it as a later-run suggestion rather than recursively launching more agents yourself. + +Final report additions: +- Repeat the exact branch assignment you attempted in concise non-sensitive terms. +- State whether it was reachable, completed, blocked, or not applicable. +- Compare the branch outcome against the likely baseline behavior when that comparison is visible from the UI. +``` + +## Success criteria + +The run is successful when: + +- Warp stable was installed from an official Linux package or AppImage for the detected architecture. +- Screenshots were captured for each onboarding screen and the final usable terminal. +- The logged-out child reached a usable terminal without login, account creation, or a real user identity. +- The logged-in child authenticated using `ONBOARDING_AGENT_FTUE_REFRESH_TOKEN` and reached a usable terminal in the authenticated FTUE path. +- The logged-in child captured an additional post-login screenshot from the avatar/settings flow showing the logged-in user's email address. +- Each terminal session was usable enough to run its flow-specific `echo` command. +- Both baseline explorers returned either concrete follow-up coverage proposals or an explicit explanation that they did not observe meaningful additional branch points. +- The parent orchestrator launched targeted second-wave agents for the highest-value concrete branch proposals, unless there were no such proposals or a prerequisite blocker made them infeasible. +- Every reported visual polish concern, suspected bug, or error state includes a screenshot reference whenever the issue was visible on screen. + +## Common failure handling + +- If the package manager prompts for confirmation, use the non-interactive confirmation flag supported by that package manager. +- If launching `warp-terminal` fails because of display setup, inspect the cloud environment's display variables and try launching from the desktop/app launcher if computer use provides one. +- If the logged-out flow blocks on login with no skip path, stop at that screen, capture a screenshot, and report that as the terminal point for the logged-out flow. +- If the logged-in flow cannot authenticate because the secret is missing, invalid, expired, revoked, or cannot be routed through Warp's auth redirect flow, stop at that screen, capture a screenshot, and report the non-sensitive blocker. +- If the native package cannot be installed because dependencies are unavailable, fall back to the matching AppImage and clearly report the fallback. diff --git a/.agents/skills/promote-feature/SKILL.md b/.agents/skills/promote-feature/SKILL.md index c4fa60da..5f85f753 100644 --- a/.agents/skills/promote-feature/SKILL.md +++ b/.agents/skills/promote-feature/SKILL.md @@ -81,7 +81,7 @@ pub const PREVIEW_FLAGS: &[FeatureFlag] = &[ ### Validate ```bash -cargo fmt +./script/format cargo clippy --workspace --all-targets --all-features --tests -- -D warnings ``` diff --git a/.agents/skills/remove-feature-flag/SKILL.md b/.agents/skills/remove-feature-flag/SKILL.md index f90a0b5d..c83ba8f7 100644 --- a/.agents/skills/remove-feature-flag/SKILL.md +++ b/.agents/skills/remove-feature-flag/SKILL.md @@ -116,7 +116,7 @@ After removing the flag: ```bash # Format and lint -cargo fmt +./script/format cargo clippy --workspace --all-targets --all-features --tests -- -D warnings # Run tests diff --git a/.agents/skills/reproduce-bug-report-local/SKILL.md b/.agents/skills/reproduce-bug-report-local/SKILL.md new file mode 100644 index 00000000..d02fd603 --- /dev/null +++ b/.agents/skills/reproduce-bug-report-local/SKILL.md @@ -0,0 +1,58 @@ +--- +name: reproduce-bug-report-local +specializes: reproduce-bug-report +specializes_source: warpdotdev/common-skills:.agents/skills/reproduce-bug-report +description: Repo-specific bug reproduction guidance for Warp. Specializes the core reproduce-bug-report skill for logged-out Warp UI repros, exact reporter-version installs, and login-free onboarding. +--- + +# Repo-specific bug reproduction guidance for `warp` + +## Prerequisite: install the parent skill + +This skill specializes the core `reproduce-bug-report` skill (named in the `specializes` frontmatter field) and is not functional on its own. Before applying its guidance, confirm the parent skill is installed and resolvable at `.agents/skills/reproduce-bug-report/SKILL.md`. If it is missing, install it first by copying the skill directory from the source declared in the `specializes_source` frontmatter field (`warpdotdev/common-skills:.agents/skills/reproduce-bug-report`). Then continue with the guidance below. + +This file is a companion to the core `reproduce-bug-report` skill. It does not redefine the shared Oz computer-use orchestration, artifact handling, safety rules, or reporting format. It specializes scope and setup for Warp bug reports. + +## Scope + +- Use this workflow only for Warp bugs that can be exercised while the app remains logged out. +- Apply it to UI-visible Warp bugs, interaction bugs, rendering/layout bugs, logged-out onboarding bugs, settings bugs, editor/display bugs, terminal-display bugs, and other visual or interactive issues where screenshots or recordings would help. +- Do not use it for authenticated-user flows, account-specific state, cloud-synced state, logged-in onboarding, or AI behaviors that require login. +- If a report requires authentication, account state, cloud sync, or another logged-in-only capability, do not launch a repro agent with this local specialization; report that it is out of scope for the current logged-out Warp workflow. + +## Warp version and install strategy + +- Prefer reproducing against the exact Warp version/build and channel reported by the user. +- Do not build Warp from source by default. Install the matching Linux package or binary release for the reporter's version/channel instead. +- If the bug report names a macOS or Windows build, use the corresponding Linux build from the same version/channel when a matching Linux artifact exists, and state that this is a Linux proxy for the reporter's platform. +- Use the repository's or Warp release tooling/docs available in the environment to find and install the exact versioned Linux artifact. Do not silently substitute the latest stable build when an exact matching version can be installed. +- If the exact version/build cannot be found or installed, report that clearly, explain what was attempted, and use the closest justified fallback only when it is useful for continuing the investigation. +- Record the requested reporter Warp version, the installed Linux version, the source of the installed artifact, and any fallback decision in the manifest and final report. + +## Logged-out Warp baseline + +- Keep Warp logged out for the entire repro attempt. Do not create an account, sign in, paste auth tokens, or use real user credentials. +- Launch Warp and complete the login-free / continue-without-account onboarding path until a normal logged-out terminal session is usable. +- Capture a post-onboarding baseline screenshot before attempting the bug-specific reproduction. +- If the assigned bug cannot be exercised after entering a normal logged-out Warp session, stop and report the blocker instead of improvising an authenticated flow. + +## Local prompt additions + +When applying the core skill to Warp, ensure the parent prompt and child prompts include: + +- Reporter Warp version/build/channel: the exact value from the report, or `unknown`. +- Build/app target: the exact versioned Linux Warp package/binary to install, or the justified fallback if an exact artifact is unavailable. +- Assigned Warp state: first-run logged-out state, completed logged-out onboarding, terminal/session/layout/settings state, or the targeted code-path hypothesis. +- A reminder that Warp must remain logged out and that logged-in-only reports are blocked for this specialization. + +## Local reproduction priorities + +- Match the reporter's Warp version/build/channel before broadening the search space. +- Follow the issue's exact steps first, then test at most two targeted variations supported by the issue or by a code-path hypothesis. +- Prefer targeted hypotheses derived from Warp UI strings, settings names, feature names, telemetry names, route names, and relevant components over broad exploratory clicking. +- In the final report, include: + - the reporter-requested Warp version/build/channel + - the installed Linux Warp version/build/channel + - the package or binary source + - whether a fallback was used + - whether the test was a Linux proxy for a macOS or Windows report diff --git a/.agents/skills/resolve-merge-conflicts/SKILL.md b/.agents/skills/resolve-merge-conflicts/SKILL.md deleted file mode 100644 index 659a951a..00000000 --- a/.agents/skills/resolve-merge-conflicts/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: resolve-merge-conflicts -description: Resolve Git merge conflicts by extracting only unresolved paths, conflict hunks, and compact diffs instead of loading whole files into context. Use when a merge, rebase, cherry-pick, or stash pop stops on conflicts, when `git status` shows unmerged paths, or when files contain conflict markers. ---- - -# Resolve Merge Conflicts - -## Overview - -Resolve conflicts without opening full files unless the compact view is insufficient. Start with a summary, then inspect one conflicted file at a time. - -## Workflow - -1. Start with a summary. - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py -``` - -Use the summary to identify which files are unresolved, which index stages exist, and how many text hunks each file contains. - -2. Drill into one file. - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py --file path/to/file -``` - -Prefer this over reading the whole file. The script prints only nearby context, the `ours` / `base` / `theirs` sections for each hunk, and a compact unified diff between `ours` and `theirs`. - -3. Resolve the file. - -- Take one side wholesale with `git checkout --ours -- path/to/file` or `git checkout --theirs -- path/to/file` when appropriate. -- Otherwise edit the file directly and remove the conflict markers. -- Read more of the file only if the compact output is not enough to decide the correct merge. - -4. Re-check unresolved files. - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py -git diff --name-only --diff-filter=U -``` - -5. Validate the resolution. - -- Ensure no unmerged paths remain. -- Ensure no `<<<<<<<`, `=======`, or `>>>>>>>` markers remain in the resolved files. -- Run targeted tests, builds, or linters for the touched area. -- Stage the resolved files. - -## Commands - -### Summary only - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py -``` - -### Detailed view for one file - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py --file path/to/file -``` - -### Detailed view for all conflicted files - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py --all -``` - -### JSON output - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py --file path/to/file --json -``` - -### Tune output size - -```bash -python3 .agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py \ - --file path/to/file \ - --context 3 \ - --max-lines 60 -``` - -## Notes - -- Use the script before opening conflicted files directly. -- Resolve one file at a time to keep context small. -- Expect marker-based text conflicts and index-only conflicts such as add/add or modify/delete. The script summarizes both, and it falls back to index-stage previews when the worktree file has no conflict markers. diff --git a/.agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py b/.agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py deleted file mode 100755 index 684b6bf7..00000000 --- a/.agents/skills/resolve-merge-conflicts/scripts/extract_conflict_context.py +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env python3 -"""Summarize and extract compact merge-conflict context from a Git repository.""" - -from __future__ import annotations - -import argparse -import difflib -import json -import re -import subprocess -import sys -from pathlib import Path - - -START_RE = re.compile(r"^<<<<<<<(?: (.*))?$") -BASE_RE = re.compile(r"^\|\|\|\|\|\|\|(?: (.*))?$") -END_RE = re.compile(r"^>>>>>>>(?: (.*))?$") - - -def run_git(repo_root: Path, *args: str) -> str: - result = subprocess.run( - ["git", "-C", str(repo_root), *args], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - message = result.stderr.strip() or result.stdout.strip() or "unknown git error" - raise RuntimeError(f"git {' '.join(args)} failed: {message}") - return result.stdout - - -def find_repo_root(start: Path) -> Path: - result = subprocess.run( - ["git", "-C", str(start), "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - message = result.stderr.strip() or result.stdout.strip() or "not a git repository" - raise RuntimeError(message) - return Path(result.stdout.strip()).resolve() - - -def get_unmerged_entries(repo_root: Path) -> dict[str, dict[int, dict[str, str]]]: - entries: dict[str, dict[int, dict[str, str]]] = {} - output = run_git(repo_root, "ls-files", "-u", "-z") - for record in output.split("\0"): - if not record: - continue - metadata, path = record.split("\t", 1) - mode, object_id, stage_text = metadata.split() - file_entry = entries.setdefault(path, {}) - file_entry[int(stage_text)] = {"mode": mode, "object_id": object_id} - return entries - - -def read_text_file(path: Path) -> list[str] | None: - if not path.exists() or path.is_dir(): - return None - try: - text = path.read_text(encoding="utf-8", errors="replace") - except OSError: - return None - if "\x00" in text: - return None - return text.splitlines() - - -def read_stage_text(repo_root: Path, path: str, stage: int) -> list[str] | None: - result = subprocess.run( - ["git", "-C", str(repo_root), "show", f":{stage}:{path}"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - return None - if "\x00" in result.stdout: - return None - return result.stdout.splitlines() - - -def truncate_lines(lines: list[str], max_lines: int) -> list[str]: - if len(lines) <= max_lines: - return lines - omitted = len(lines) - max_lines - return [*lines[:max_lines], f"... ({omitted} more lines omitted)"] - - -def build_diff( - left_lines: list[str], - right_lines: list[str], - left_label: str, - right_label: str, - max_lines: int, -) -> list[str]: - diff = list( - difflib.unified_diff( - left_lines, - right_lines, - fromfile=left_label, - tofile=right_label, - lineterm="", - ) - ) - if not diff: - diff = ["(no textual diff)"] - return truncate_lines(diff, max_lines) - - -def classify_conflict(stages: list[int], marker_hunks: int) -> str: - if marker_hunks: - return "text" - stage_set = set(stages) - if stage_set == {2, 3}: - return "add/add" - if stage_set == {1, 2}: - return "deleted-by-them" - if stage_set == {1, 3}: - return "deleted-by-us" - if stage_set == {1, 2, 3}: - return "index-only" - return "unmerged" - - -def normalize_requested_path(repo_root: Path, raw_path: str) -> str: - path = Path(raw_path) - candidate = path.resolve() if path.is_absolute() else (repo_root / path).resolve() - try: - return str(candidate.relative_to(repo_root)) - except ValueError as error: - raise RuntimeError(f"path is outside repository: {raw_path}") from error - - -def parse_conflict_hunks(lines: list[str], context: int) -> tuple[list[dict[str, object]], str | None]: - hunks: list[dict[str, object]] = [] - index = 0 - while index < len(lines): - start_match = START_RE.match(lines[index]) - if not start_match: - index += 1 - continue - - start_index = index - ours_label = start_match.group(1) or "ours" - index += 1 - ours: list[str] = [] - base: list[str] = [] - theirs: list[str] = [] - base_label: str | None = None - theirs_label = "theirs" - - while index < len(lines): - base_match = BASE_RE.match(lines[index]) - if base_match: - base_label = base_match.group(1) or "base" - index += 1 - while index < len(lines) and lines[index] != "=======": - base.append(lines[index]) - index += 1 - break - if lines[index] == "=======": - break - ours.append(lines[index]) - index += 1 - - if index >= len(lines) or lines[index] != "=======": - return hunks, f"unterminated conflict starting at line {start_index + 1}" - - index += 1 - end_index = index - while index < len(lines): - end_match = END_RE.match(lines[index]) - if end_match: - theirs_label = end_match.group(1) or "theirs" - end_index = index - index += 1 - break - theirs.append(lines[index]) - index += 1 - else: - return hunks, f"unterminated conflict starting at line {start_index + 1}" - - hunks.append( - { - "start_line": start_index + 1, - "end_line": end_index + 1, - "before_context": lines[max(0, start_index - context):start_index], - "ours": ours, - "ours_label": ours_label, - "base": base or None, - "base_label": base_label, - "theirs": theirs, - "theirs_label": theirs_label, - "after_context": lines[index:index + context], - } - ) - - return hunks, None - - -def build_summary_report(repo_root: Path, path: str, stage_entries: dict[int, dict[str, str]], context: int) -> dict[str, object]: - worktree_lines = read_text_file(repo_root / path) - hunks: list[dict[str, object]] = [] - parse_error = None - if worktree_lines is not None: - hunks, parse_error = parse_conflict_hunks(worktree_lines, context) - stages = sorted(stage_entries) - return { - "path": path, - "stages": stages, - "conflict_type": classify_conflict(stages, len(hunks)), - "marker_hunks": len(hunks), - "parse_error": parse_error, - "worktree_present": worktree_lines is not None, - "hunks": hunks, - } - - -def build_index_preview(repo_root: Path, report: dict[str, object], max_lines: int) -> dict[str, object]: - path = str(report["path"]) - ours = read_stage_text(repo_root, path, 2) - theirs = read_stage_text(repo_root, path, 3) - base = read_stage_text(repo_root, path, 1) - preview: dict[str, object] = { - "ours": truncate_lines(ours, max_lines) if ours else None, - "theirs": truncate_lines(theirs, max_lines) if theirs else None, - "base": truncate_lines(base, max_lines) if base else None, - } - if ours and theirs: - preview["ours_vs_theirs_diff"] = build_diff(ours, theirs, "ours", "theirs", max_lines) - return preview - - -def section_lines(title: str, lines: list[str] | None) -> list[str]: - if lines is None: - return [f"{title}:", " (not present)"] - if not lines: - return [f"{title}:", " (empty)"] - return [f"{title}:", *[f" {line}" for line in lines]] - - -def render_summary_text(repo_root: Path, reports: list[dict[str, object]]) -> str: - lines = [f"repo: {repo_root}", f"conflicted files: {len(reports)}"] - for report in reports: - stages = ",".join(str(stage) for stage in report["stages"]) - lines.append( - f"- {report['path']} | type={report['conflict_type']} | stages={stages} | hunks={report['marker_hunks']}" - ) - if report["parse_error"]: - lines.append(f" parse-error: {report['parse_error']}") - lines.append("use --file for compact hunk details or --all for every file") - return "\n".join(lines) - - -def render_detail_text( - repo_root: Path, - report: dict[str, object], - max_lines: int, -) -> str: - lines = [ - f"== {report['path']} ==", - f"type: {report['conflict_type']}", - f"stages: {', '.join(str(stage) for stage in report['stages'])}", - ] - parse_error = report["parse_error"] - if parse_error: - lines.append(f"parse-error: {parse_error}") - - hunks = report["hunks"] - if hunks: - lines.append(f"hunks: {len(hunks)}") - for index, hunk in enumerate(hunks, start=1): - ours = list(hunk["ours"]) - theirs = list(hunk["theirs"]) - diff = build_diff( - ours, - theirs, - str(hunk["ours_label"]), - str(hunk["theirs_label"]), - max_lines, - ) - lines.extend( - [ - "", - f"[hunk {index}] current lines {hunk['start_line']}-{hunk['end_line']}", - *section_lines("before", truncate_lines(list(hunk["before_context"]), max_lines)), - *section_lines( - f"ours ({hunk['ours_label']})", - truncate_lines(ours, max_lines), - ), - ] - ) - if hunk["base"] is not None: - lines.extend( - section_lines( - f"base ({hunk['base_label'] or 'base'})", - truncate_lines(list(hunk["base"]), max_lines), - ) - ) - lines.extend( - [ - *section_lines( - f"theirs ({hunk['theirs_label']})", - truncate_lines(theirs, max_lines), - ), - *section_lines("ours vs theirs diff", diff), - *section_lines("after", truncate_lines(list(hunk["after_context"]), max_lines)), - ] - ) - return "\n".join(lines) - - preview = build_index_preview(repo_root, report, max_lines) - lines.append("hunks: 0") - lines.append("index preview:") - lines.extend(section_lines("ours", preview["ours"])) - lines.extend(section_lines("base", preview["base"])) - lines.extend(section_lines("theirs", preview["theirs"])) - if "ours_vs_theirs_diff" in preview: - lines.extend(section_lines("ours vs theirs diff", preview["ours_vs_theirs_diff"])) - return "\n".join(lines) - - -def render_json( - repo_root: Path, - reports: list[dict[str, object]], - include_details: bool, - max_lines: int, -) -> str: - files: list[dict[str, object]] = [] - for report in reports: - file_entry: dict[str, object] = { - "path": report["path"], - "conflict_type": report["conflict_type"], - "stages": report["stages"], - "marker_hunks": report["marker_hunks"], - "parse_error": report["parse_error"], - } - if include_details: - if report["hunks"]: - file_entry["hunks"] = [ - { - "start_line": hunk["start_line"], - "end_line": hunk["end_line"], - "before_context": truncate_lines(list(hunk["before_context"]), max_lines), - "ours_label": hunk["ours_label"], - "ours": truncate_lines(list(hunk["ours"]), max_lines), - "base_label": hunk["base_label"], - "base": truncate_lines(list(hunk["base"]), max_lines) if hunk["base"] else None, - "theirs_label": hunk["theirs_label"], - "theirs": truncate_lines(list(hunk["theirs"]), max_lines), - "after_context": truncate_lines(list(hunk["after_context"]), max_lines), - "ours_vs_theirs_diff": build_diff( - list(hunk["ours"]), - list(hunk["theirs"]), - str(hunk["ours_label"]), - str(hunk["theirs_label"]), - max_lines, - ), - } - for hunk in report["hunks"] - ] - else: - file_entry["index_preview"] = build_index_preview(repo_root, report, max_lines) - files.append(file_entry) - - return json.dumps( - { - "repo_root": str(repo_root), - "conflicted_files": files, - }, - indent=2, - ) - - -def main() -> int: - parser = argparse.ArgumentParser( - description="Summarize and extract compact merge-conflict context." - ) - parser.add_argument("--repo", default=".", help="Path inside the target repository.") - parser.add_argument( - "--file", - action="append", - default=[], - help="Conflicted file to inspect in detail. Repeat to inspect multiple files.", - ) - parser.add_argument( - "--all", - action="store_true", - help="Print detailed output for every conflicted file.", - ) - parser.add_argument( - "--json", - action="store_true", - help="Emit JSON instead of text.", - ) - parser.add_argument( - "--context", - type=int, - default=2, - help="Lines of surrounding context to include around each conflict hunk.", - ) - parser.add_argument( - "--max-lines", - type=int, - default=40, - help="Maximum lines to print for each section before truncating.", - ) - args = parser.parse_args() - - if args.all and args.file: - parser.error("--all cannot be combined with --file") - if args.context < 0: - parser.error("--context must be non-negative") - if args.max_lines <= 0: - parser.error("--max-lines must be positive") - - try: - repo_root = find_repo_root(Path(args.repo).resolve()) - entries = get_unmerged_entries(repo_root) - except RuntimeError as error: - print(f"error: {error}", file=sys.stderr) - return 2 - - reports = [ - build_summary_report(repo_root, path, entries[path], args.context) - for path in sorted(entries) - ] - - if not reports: - message = json.dumps({"repo_root": str(repo_root), "conflicted_files": []}, indent=2) if args.json else f"repo: {repo_root}\nconflicted files: 0" - print(message) - return 0 - - if args.all: - selected_reports = reports - elif args.file: - try: - requested_paths = {normalize_requested_path(repo_root, path) for path in args.file} - except RuntimeError as error: - print(f"error: {error}", file=sys.stderr) - return 2 - known_paths = {str(report["path"]) for report in reports} - missing = sorted(requested_paths - known_paths) - if missing: - for path in missing: - print(f"error: conflicted file not found: {path}", file=sys.stderr) - return 2 - selected_reports = [report for report in reports if report["path"] in requested_paths] - else: - selected_reports = [] - - if args.json: - print(render_json(repo_root, selected_reports or reports, bool(selected_reports), args.max_lines)) - return 0 - - if not selected_reports: - print(render_summary_text(repo_root, reports)) - return 0 - - print("\n\n".join(render_detail_text(repo_root, report, args.max_lines) for report in selected_reports)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.agents/skills/review-pr-local/SKILL.md b/.agents/skills/review-pr-local/SKILL.md index f5016b81..d36a964d 100644 --- a/.agents/skills/review-pr-local/SKILL.md +++ b/.agents/skills/review-pr-local/SKILL.md @@ -1,10 +1,15 @@ --- name: review-pr-local specializes: review-pr -description: Repo-specific review guidance for warp-external. Only the categories declared overridable by the core review-pr skill may be specialized here. +specializes_source: warpdotdev/common-skills:.agents/skills/review-pr +description: Repo-specific review guidance for warp. Only the categories declared overridable by the core review-pr skill may be specialized here. --- -# Repo-specific review guidance for `warp-external` +# Repo-specific review guidance for `warp` + +## Prerequisite: install the parent skill + +This skill specializes the core `review-pr` skill (named in the `specializes` frontmatter field) and is not functional on its own. Before applying its guidance, confirm the parent skill is installed and resolvable at `.agents/skills/review-pr/SKILL.md`. If it is missing, install it first by copying the skill directory from the source declared in the `specializes_source` frontmatter field (`warpdotdev/common-skills:.agents/skills/review-pr`). Then continue with the guidance below. This file is a companion to the core `review-pr` skill. It does not redefine the review output schema, severity labels, safety rules, or @@ -15,13 +20,25 @@ skill marks as overridable. - Do not suggest adding test cases that only vary constructor inputs or struct fields when an existing test already covers the meaningful behavior. Only suggest new tests when they exercise a distinct code path or edge case. - When a PR is clearly a V0 or initial implementation, frame robustness suggestions such as timeouts, retries, and lifecycle management as optional future work rather than blocking concerns, unless they risk correctness, security, data loss, or a persistent UI hang. -- For Rust changes, apply the repository conventions from `WARP.md`: avoid unnecessary type annotations, prefer imports over long path qualifiers, name context parameters `ctx` and place them last, remove unused parameters instead of prefixing them with `_`, and prefer inline format arguments in macros. +- For Rust changes, apply the repository conventions from `AGENTS.md`: avoid unnecessary type annotations, prefer imports over long path qualifiers, name context parameters `ctx` and place them last, remove unused parameters instead of prefixing them with `_`, and prefer inline format arguments in macros. - Avoid wildcard `_` match arms when an enum can reasonably be matched exhaustively; exhaustive matches are preferred so future variants are surfaced during review. - For new or changed feature flags, prefer high-level runtime checks with `FeatureFlag::YourFlag.is_enabled()` over `#[cfg(...)]` unless the code cannot compile without a compile-time gate. - Flag nested or redundant `TerminalModel` locking when the call stack may already hold the model lock. Prefer passing locked references down the stack and keeping lock scopes short. - In WarpUI code, flag inline `MouseStateHandle::default()` usage during render or event handling. Mouse state handles should be created during construction and then cloned/referenced where needed. - For user-facing UI changes, mention missing validation only when it is tied to a concrete risk or when the PR changes behavior that should be verified visually. +## Behavioral or UI-impacting changes require visual evidence + +- If the PR changes anything user-visible (UI components, layout, styling, copy in surfaces users see, terminal/Warp app visuals, or other behavior a user can perceive), analyze both `pr_description.txt` and any PR comments available in the workflow context for attached screenshots, GIFs, or videos demonstrating the change end to end. + - Treat markdown image/video embeds (`![...](...)`, ``, `