diff --git a/AGENTS.md b/AGENTS.md index 58c556242..7b4c07125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,26 @@ Monorepo (pnpm) with packages: `core`, `cli`, `web`, and `plugins/*`. The web da Full guidelines with AO-specific context: see "Working Principles" in CLAUDE.md. +## Skills + +Agents working on this repo should use these checked-in skills: + +### Bug Triage (`skills/bug-triage/`) + +**When to use:** Any time a bug is reported — in chat, issues, or live observation. + +**What it covers:** +- Full triage workflow: gather context → search duplicates → file/update GitHub issues → push fix PRs +- Root cause analysis with `git log -S` archaeology and upstream dependency research +- GitHub API-based file editing (no local checkout needed) via `scripts/push_fix_to_github.py` +- NPM package regression diffing +- Remote code inspection when the repo isn't cloned locally + +**How to load:** Read `skills/bug-triage/SKILL.md` and follow its step-by-step workflow. The `scripts/` directory contains executable tools: +- `push_fix_to_github.py` — Push a single-file fix and create a PR entirely via GitHub API + +**Always pull latest main before triaging.** Stale code = bad triage. No exceptions. + ## Key Files - `packages/core/src/types.ts` — All plugin interfaces (Agent, Runtime, Workspace, etc.) diff --git a/CLAUDE.md b/CLAUDE.md index 3c5738d9b..42c583e49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -363,6 +363,19 @@ All importable from `@aoagents/ao-core` unless noted: | `packages/cli/src/lib/running-state.ts` | RunningState + LastStopState management (register/unregister, last-stop read/write) | | `packages/web/src/components/ProjectSidebar.tsx` | Sidebar — always shows all projects' sessions | +## Skills + +The `skills/` directory contains reusable workflow documents for common tasks. Load them before starting work: + +| Skill | When to load | +|-------|-------------| +| [`skills/bug-triage/SKILL.md`](skills/bug-triage/SKILL.md) | Triage a bug report — investigate, search duplicates, file GitHub issues, push fix PRs | +| [`skills/agent-orchestrator/SKILL.md`](skills/agent-orchestrator/SKILL.md) | Architecture and conventions for working on this codebase | +| [`skills/release-notes/ao-weekly-release/SKILL.md`](skills/release-notes/ao-weekly-release/SKILL.md) | Generate weekly release notes from git history | +| [`skills/social-media/SKILL.md`](skills/social-media/SKILL.md) | Social media post generation | + +See [`skills/README.md`](skills/README.md) for how to install skills into other coding agents (Cursor, Copilot, Codex, etc.). + ## Plugin Standards ### Package Layout diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 000000000..e70ce2c7a --- /dev/null +++ b/skills/README.md @@ -0,0 +1,91 @@ +# Skills + +Reusable skill documents for AI coding agents working on this repository. Each skill is a self-contained `SKILL.md` that teaches an agent how to perform a specific task. + +## Available Skills + +| Skill | Description | +|-------|-------------| +| [`bug-triage/`](bug-triage/SKILL.md) | Triage bugs reported in chat/issues — investigate, search duplicates, file GitHub issues, push fix PRs | +| [`agent-orchestrator/`](agent-orchestrator/SKILL.md) | Architecture and conventions for working on the agent-orchestrator codebase | +| [`release-notes/`](release-notes/ao-weekly-release/SKILL.md) | Generate weekly release notes from git history | +| [`social-media/`](social-media/SKILL.md) | Social media post generation | + +## How to Use + +Copy the skill into your coding agent's skill directory. The destination depends on which agent you're using: + +### Claude Code + +```bash +cp -r skills/bug-triage .claude/skills/bug-triage +``` + +Or add the full path to your `CLAUDE.md`: + +```markdown +See skills/bug-triage/SKILL.md for bug triage workflow. +``` + +### OpenAI Codex CLI + +```bash +cp -r skills/bug-triage .codex/skills/bug-triage +``` + +Or reference in `AGENTS.md`: + +```markdown +See skills/bug-triage/SKILL.md for bug triage workflow. +``` + +### Cursor + +Add to `.cursor/rules/` as a rule file: + +```bash +cp skills/bug-triage/SKILL.md .cursor/rules/bug-triage.mdc +``` + +### Windsurf / Other Codeium-based agents + +Add to `.windsurf/rules/`: + +```bash +cp skills/bug-triage/SKILL.md .windsurf/rules/bug-triage.md +``` + +### GitHub Copilot + +Add to `.github/copilot-instructions.md` or reference in `.github/`: + +```bash +cp skills/bug-triage/SKILL.md .github/skills/bug-triage.md +``` + +### Gemini CLI + +```bash +cp -r skills/bug-triage .gemini/skills/bug-triage +``` + +### Agent Orchestrator (this project) + +Skills in this `skills/` directory are automatically available to agents spawned via `ao spawn`. Reference them in `AGENTS.md` or `CLAUDE.md` so agents load them at the start of a session. + +## Writing a New Skill + +1. Create a directory under `skills//` +2. Add a `SKILL.md` with YAML frontmatter: + +```yaml +--- +name: my-skill +description: One-line description of what the skill does. +trigger: When to activate this skill. +--- +``` + +3. Write the skill body in markdown — numbered steps, code blocks, tables +4. Keep it agent-agnostic: use `gh` CLI, `git`, and standard Unix tools. Avoid tying to a specific agent framework +5. Reference it in `AGENTS.md` so spawned agents discover it diff --git a/skills/bug-triage/SKILL.md b/skills/bug-triage/SKILL.md new file mode 100644 index 000000000..4485e86b8 --- /dev/null +++ b/skills/bug-triage/SKILL.md @@ -0,0 +1,311 @@ +--- +name: bug-triage +description: Triage bugs reported in chat/issues, search for duplicates, file or update GitHub issues with full context, and push fix PRs. +trigger: User reports a bug, or asks to triage/file an issue for a reported problem. +--- + +# Bug Triage Skill + +Triage bugs into well-structured GitHub issues on the correct upstream repo. + +## 1. Pre-flight + +- **Pull latest code:** `git pull origin main`. Stale code = bad triage. +- **Target repo:** Always file on the **upstream org** (`ComposioHQ/agent-orchestrator`), not forks. +- **Record source:** chat URL, reporter name, attachments. + +## 2. Gather Context + +### 2a. Extract the report + +| Source | How to gather | +|--------|---------------| +| **Discord/Slack thread** | Read full thread. Extract: reporter name, original description (the thread starter, not whoever tagged you), screenshots, follow-ups | +| **GitHub issue** | `gh issue view --repo --json body,comments` | +| **Live observation** | Pull live state via observability tools | + +### 2b. Minimum viable report gate + +Before tracing code, verify the report has enough substance: + +**Required (ALL):** what happened, where (page/command/feature), when (after upgrade? first time?) + +**Required (2 of 4):** OS/shell/runtime, AO version (`ao --version`), reproducibility (consistent vs intermittent), reproduction steps + +If insufficient, ask: +> "I'd like to triage this but need more info: (1) **What happened?** (error/behavior), (2) **Where?** (page/command), (3) **When did it start?**, (4) **How to reproduce?**" + +### 2c. Local diagnostics (if bug is on same machine) + +Gather everything yourself before asking the reporter: + +```bash +# Environment +ao --version && node --version && echo $SHELL && uname -a +cat agent-orchestrator.yaml +cat ~/.agent-orchestrator/running.json + +# Process health +pm2 status +tmux list-sessions +lsof -i :3000 + +# AO event log — structured timeline +ao events list --limit 50 # recent events +ao events list --session ao-5 --limit 100 # filter by session +ao events list --log-level error --since 1h # errors only +ao events search "spawn failed" # full-text search +ao events stats # counts by kind/source + +# Session state files +cat ~/.agent-orchestrator/projects/*/sessions/*.json | python3 -m json.tool +``` + +Event kinds: `session.spawned`, `session.spawn_failed`, `session.killed`, `lifecycle.transition`, `ci.failing`, `review.pending`, `runtime.probe_failed`, `agent.process_probe_failed`, `reaction.escalated`, `lifecycle.poll_failed`. Sources: `lifecycle`, `session-manager`, `api`, `runtime`, `agent`, `reaction`. + +**Try the reproduction steps.** Running the actual command is worth 100 lines of code tracing. + +## 3. Investigate + +### 3a. Trace the code path + +**Always trace the actual code** — don't surface-level diagnose. [#1129](https://github.com/ComposioHQ/agent-orchestrator/issues/1129) looked like a simple `ao stop` issue but was actually a session lineage/cascade problem. + +```bash +git fetch origin main && git log --oneline origin/main -5 # current HEAD +# Record the commit hash you're analyzing against +``` + +**Git archaeology** — find which commits introduced/removed specific code: +```bash +git log --oneline -S 'exact-string' -- +git show -- | grep -B 5 -A 10 'pattern' +``` +Example: [#1391](https://github.com/ComposioHQ/agent-orchestrator/issues/1391) traced a mobile layout break to a `display: flex` → `display: grid` change. + +**Research upstream dependencies** (xterm, node-pty, React, etc.) — check installed vs latest version, search their GitHub issues, check changelogs. Root cause is often upstream. + +### 3b. Cross-platform check + +AO runs on **Windows, macOS, Linux** as first-class targets. If env info indicates Windows (or is unknown), check for these patterns: + +- **Path separators** — hardcoded `/` or `\` breaks cross-platform +- **Shell syntax** — PowerShell lacks `&&`, `$VAR`, `$(cat ...)`, `/dev/null`, here-docs +- **`process.platform === "win32"` inline** — must use `isWindows()` from `@aoagents/ao-core` +- **`process.kill(-pid)`** — POSIX-only; use `killProcessTree()` +- **Named pipes vs Unix sockets** — Windows uses `\\.\pipe\ao-pty-` +- **`localhost`** — Windows resolves to `::1` first, causing ~21s stalls on IPv4-only servers +- **NTFS case-insensitivity** — use `pathsEqual()`, not `===` +- **ConPTY orphans** — can trigger WER dialogs if pty-host not shut down cooperatively +- **`.cmd` shim resolution** — needs `shell: true` for `PATHEXT` lookup + +Key files: `packages/core/src/platform.ts`, `docs/CROSS_PLATFORM.md`, `packages/plugins/runtime-process/`, `packages/cli/src/lib/path-equality.ts` + +### 3c. Stop-and-ask triggers + +Stop and ask for more info if: +- **3 failed hypotheses** — traced 3 code paths, none explain it +- **Root cause is upstream** — file with upstream reference, don't guess a local fix +- **UI-only bug** and you can't screenshot — ask reporter to describe +- **Can't reproduce** — ask for different config/sequence + +## 4. Search for Duplicates + +Search with multiple strategies, always using `--state all` (closed bugs regress): + +```bash +gh issue list --repo --state all --search "" +gh issue list --repo --state all --search "" +gh issue list --repo --state all --search "" +gh pr list --repo --state all --search "" +``` + +### Duplicate found → comment on existing issue + +```bash +gh issue comment --repo --body "$(cat <<'EOF' +## New Report +**Reported by:** @ in [chat]() +**Date:** | **Checkout:** `` + +EOF +)" +``` + +### No duplicate → file new issue (next section) + +## 5. File New Issue + +### 5a. Pre-submission checklist + +- [ ] Reporter attribution correct (original reporter, not who tagged you) +- [ ] Commit hash recorded +- [ ] AO version recorded +- [ ] Root cause confidence scored (see 5c) +- [ ] Related issues cross-linked +- [ ] Reproduction steps are concrete +- [ ] Screenshots uploaded with real URLs (see 5b) + +### 5b. Upload screenshots + +**⛔ NEVER use placeholder URLs.** Upload BEFORE creating the issue. ([#1151](https://github.com/ComposioHQ/agent-orchestrator/issues/1151) RCA on this pattern.) + +```bash +SLUG="descriptive-slug" +# Create asset branch +gh api -X POST repos//git/refs \ + -f ref="refs/heads/issue-assets-${SLUG}" \ + -f sha=$(git rev-parse origin/main) + +# Upload (portable base64) +IMG_B64=$(base64 < /path/to/screenshot.png | tr -d '\n') +gh api -X PUT "repos//contents/.issue-assets/${SLUG}/name.png" \ + -f message="chore: upload screenshot" \ + -f content="$IMG_B64" \ + -f branch="issue-assets-${SLUG}" +# Use: ![screenshot](https://raw.githubusercontent.com//issue-assets-/.issue-assets/) +``` + +### 5c. Create the issue + +```bash +gh issue create --repo --title "" --body "$(cat <<'EOF' +## Bug +<summary> + +**Source:** <url> | **Reported by:** @<reporter> | **Analyzed against:** `<hash>` +**Confidence:** High/Medium/Low + +## Reproduction +1. <step> + +## Root Cause +<file paths, line numbers, explanation> + +## Fix +<suggested approach> + +## Impact +- <effects> +EOF +)" +``` + +### 5d. Label and prioritize + +```bash +gh issue edit <number> --repo <repo> --add-label "bug" +``` + +| Priority label | Criteria | +|----------------|----------| +| `priority: critical` | Data loss, security, system down | +| `priority: high` | Core feature broken, no workaround | +| `priority: medium` | Feature degraded, workaround exists | +| `priority: low` | Cosmetic, edge case | + +**Confidence scoring** (include in issue body): + +| Level | Meaning | Extra labels | +|-------|---------|-------------| +| **High** | Traced exact code path, specific lines, mechanism explained | `bug` only | +| **Medium** | Strong hypothesis but unconfirmed | `bug`, `to-explore` | +| **Low** | Can't trace, multiple conflicting theories | `bug`, `to-reproduce` | + +Example: [PR #1608](https://github.com/ComposioHQ/agent-orchestrator/pull/1608) was diagnosed High as xterm v6 issue — real cause was a `=` prefix on tmux `set-option`. Should have been Medium. + +**All available labels:** `priority: critical/high/medium/low`, `bug`, `enhancement`, `good-first-issue`, `to-reproduce`, `to-explore`. No others (no `p0`, `p1`, etc.). + +### 5e. Cross-link related issues + +Search by subsystem and add a `## Related` section to the issue body: +``` +## Related +- [#1020](url) — stale session blocking ao start (same subsystem) +- [#1035](url) — same race condition +``` + +### 5f. Push a fix PR (always attempt) + +- **Trivial fix:** Push immediately. +- **Complex fix:** Note in issue, suggest spawning an agent. +- **Unclear fix:** Don't push a guess. Document and flag. + +```bash +OLD_STRING='<old>' NEW_STRING='<new>' \ +python3 skills/bug-triage/scripts/push_fix_to_github.py \ + <owner/repo> fix/slug path/to/file.tsx \ + "fix(scope): commit msg" "fix(scope): PR title" \ + "Fixes #<n> + +## Summary +<what changed> + +## Test +<how to verify>" +``` + +The script reads from GitHub API, applies one replacement, pushes, opens PR — no local checkout needed. **Verify `OLD_STRING` matches GitHub first:** `gh api repos/<repo>/contents/<path>?ref=main -q '.content' | python3 -c "import base64,sys; sys.stdout.buffer.write(base64.b64decode(sys.stdin.read()))"` + +**Multiple edits to same file:** The push script does one replacement per run. For multiple changes, use a Python script to read from the branch (`gh api` + `base64.b64decode`), apply all replacements, then push via `gh api -X PUT` with the updated SHA. + +### 5g. Report back + +Issue URL, PR URL (if created), labels, root cause summary, whether fix agent was suggested. + +--- + +## Appendix + +### A. Subsystem Quick Reference + +| Subsystem | Collect | Key files | +|-----------|---------|-----------| +| **CLI** (`ao start/stop/spawn`) | Config YAML, install method, version, OS | `packages/cli/src/commands/` | +| **Web UI** | Screenshot, browser, viewport | `packages/web/src/components/`, `globals.css` | +| **Terminal** | Runtime type, tmux version, shell | `DirectTerminal.tsx`, `useXtermTerminal.ts` | +| **Lifecycle** | State transitions, session IDs | `core/src/lifecycle-manager.ts`, `core/src/lifecycle-state.ts` | +| **Sessions** | Session ID, spawn config, runtime | `core/src/session-manager.ts` | +| **Plugins** | Plugin name, agent version | `packages/plugins/<agent>/` | +| **Config** | YAML contents, project path | `packages/core/src/config.ts` | + +**Misrouting patterns:** +- Terminal bugs → tmux (runtime-tmux) vs xterm (web) vs PTY (runtime-process/Windows). Trace where bytes flow. +- "Session stuck" → lifecycle state machine vs agent process vs runtime connection. +- "Config not saving" → config loading (c12) vs project registration (running-state.ts) vs YAML write (permissions). + +### B. Remote Code Inspection (no local clone) + +```bash +gh api repos/{owner}/{repo}/git/trees/main?recursive=1 --jq '.tree[].path' # list files +gh api repos/{owner}/{repo}/contents/{path} --jq '.content' | python3 -c "import base64,sys; sys.stdout.buffer.write(base64.b64decode(sys.stdin.read()))" # read file +gh search code "term" --repo {owner}/{repo} --json path --jq '.[].path' # search code +gh api "repos/{owner}/{repo}/commits?path={path}&per_page=10" --jq '.[] | "\(.sha[0:8]) \(.commit.message | split("\n")[0])"' # file history +``` + +### C. NPM Package Regression Diffing + +Diff **published** packages (not local builds) when regression follows an upgrade: + +```bash +mkdir -p /tmp/ao-diff/{v1,v2} +curl -sL https://registry.npmjs.org/@scope/pkg/-/pkg-OLD.tgz | tar xz -C /tmp/ao-diff/v1 +curl -sL https://registry.npmjs.org/@scope/pkg/-/pkg-NEW.tgz | tar xz -C /tmp/ao-diff/v2 +diff -rq /tmp/ao-diff/v1/package/ /tmp/ao-diff/v2/package/ +``` + +Example: [PR #1608](https://github.com/ComposioHQ/agent-orchestrator/pull/1608) — source analysis led to wrong theories, npm diff showed the only change was a `=` prefix on tmux `set-option`. + +## Formatting Rules + +- **Linkify all issue/PR refs:** `[#123](https://github.com/ComposioHQ/agent-orchestrator/issues/123)`, `[PR #456](url)`. Never bare `#123`. + +## Pitfalls + +- **Reporter ≠ person who tagged you.** Always attribute to the original reporter. +- **Record the commit hash** you analyzed — code changes fast. +- **GitHub issue is mandatory** — every triaged bug gets one, even if fix is trivial. +- **`gh api --jq .content` truncates large files** (>~100KB). Use local git instead. +- **Push script arg limits** — long commit messages hit `OSError: Argument list too long`. Use a Python script with JSON payloads instead. +- **`OLD_STRING` must match GitHub byte-for-byte** — local code may differ from `origin/main`. +- **New fields on shared TS interfaces MUST be optional** (`field?: Type`). Downstream `Partial<X>` spreads break on required fields. Example: [PR #1523](https://github.com/ComposioHQ/agent-orchestrator/pull/1523). diff --git a/skills/bug-triage/scripts/push_fix_to_github.py b/skills/bug-triage/scripts/push_fix_to_github.py new file mode 100644 index 000000000..83ce23dff --- /dev/null +++ b/skills/bug-triage/scripts/push_fix_to_github.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Push a file fix to GitHub via API and create a PR. + +Usage: python3 push_fix_to_github.py <repo> <branch-name> <file-path> <commit-message> <pr-title> <pr-body> + +Reads the original file content from GitHub (default branch), applies a sed-like +replacement using OLD_STRING / NEW_STRING env vars, and pushes via GitHub API. + +Environment variables: + OLD_STRING - The exact string to find in the file (required) + NEW_STRING - The replacement string (set to empty string to delete) (required as env var) + BASE_SHA - Override the base commit SHA (optional, defaults to default branch HEAD) + BASE_BRANCH - Override the base branch name (optional, defaults to "main") + +Example: + OLD_STRING='<td className="foo">{bar}</td>' \ + NEW_STRING='<td className="foo"><a href="#">{bar}</a></td>' \ + python3 push_fix_to_github.py ComposioHQ/agent-orchestrator fix/branch packages/web/src/File.tsx \ + "fix: description" "fix: title" "Fixes #123" +""" +import sys, os, json, subprocess, base64 + + +def run_gh(args, check=True): + """Run a gh API command and return parsed JSON.""" + cmd = ["gh", "api"] + args + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if check and result.returncode != 0: + print(f"ERROR: gh api failed: {result.stderr}", file=sys.stderr) + sys.exit(1) + try: + return json.loads(result.stdout) if result.stdout.strip() else {} + except json.JSONDecodeError: + if check: + print(f"ERROR: Invalid JSON: {result.stdout[:300]}", file=sys.stderr) + sys.exit(1) + return {} + + +if __name__ == "__main__": + if len(sys.argv) < 7: + print("Usage: push_fix_to_github.py <repo> <branch> <file-path> <commit-msg> <pr-title> <pr-body>", file=sys.stderr) + sys.exit(1) + + repo = sys.argv[1] + branch = sys.argv[2] + file_path = sys.argv[3] + commit_msg = sys.argv[4] + pr_title = sys.argv[5] + pr_body = sys.argv[6] + base_branch = os.environ.get("BASE_BRANCH", "main") + + old_string = os.environ.get("OLD_STRING", "") + new_string = os.environ.get("NEW_STRING") + + if not old_string: + print("ERROR: OLD_STRING env var is required", file=sys.stderr) + sys.exit(1) + if new_string is None: + print("ERROR: NEW_STRING env var is required (set to empty string to delete)", file=sys.stderr) + sys.exit(1) + + # 1. Get base HEAD SHA first + base_sha = os.environ.get("BASE_SHA") + if not base_sha: + ref_data = run_gh([f"repos/{repo}/git/ref/heads/{base_branch}"]) + base_sha = ref_data["object"]["sha"] + + # 2. Create branch from base SHA (before fetching file to avoid SHA race) + print(f"Creating branch {branch} from {base_branch} ({base_sha[:8]})...") + run_gh([ + "-X", "POST", f"repos/{repo}/git/refs", + "-f", f"ref=refs/heads/{branch}", + "-f", f"sha={base_sha}" + ], check=False) + + # 3. Fetch file content from the new branch (avoids SHA mismatch) + print(f"Fetching {file_path} from {repo} (branch: {branch})...") + file_data = run_gh([f"repos/{repo}/contents/{file_path}?ref={branch}"]) + file_sha = file_data["sha"] + decoded_content = base64.b64decode(file_data["content"]).decode("utf-8") + + print(f"File SHA: {file_sha}") + + # 4. Apply replacement + if old_string not in decoded_content: + print(f"ERROR: OLD_STRING not found in file!", file=sys.stderr) + print(f"Looking for:\n{old_string}", file=sys.stderr) + sys.exit(1) + + match_count = decoded_content.count(old_string) + if match_count > 1: + print(f"WARNING: OLD_STRING found {match_count} times — replacing only the first occurrence.", file=sys.stderr) + + new_content = decoded_content.replace(old_string, new_string, 1) + encoded = base64.b64encode(new_content.encode("utf-8")).decode("ascii") + + # 5. Push file to branch + print(f"Pushing updated file...") + result = run_gh([ + "-X", "PUT", f"repos/{repo}/contents/{file_path}", + "-f", f"message={commit_msg}", + "-f", f"content={encoded}", + "-f", f"sha={file_sha}", + "-f", f"branch={branch}" + ]) + + # 6. Create PR + print(f"Creating PR...") + pr_result = run_gh([ + "-X", "POST", f"repos/{repo}/pulls", + "-f", f"title={pr_title}", + "-f", f"body={pr_body}", + "-f", f"head={branch}", + "-f", f"base={base_branch}" + ]) + + pr_url = pr_result.get("html_url", "unknown") + pr_number = pr_result.get("number", "?") + print(f"\n✅ PR #{pr_number}: {pr_url}")