chore: add skills from agent-orchestrator (#204)
* chore: add skills from agent-orchestrator Copy the skills directory from AgentWrapper/agent-orchestrator into <repo_root>/skills/, preserving structure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: format with prettier [skip ci] --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
b0b732fe8a
commit
1dbeeccfd1
|
|
@ -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/<name>/`
|
||||
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
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
---
|
||||
name: agent-orchestrator
|
||||
description: "Open-source, pluggable agentic coding orchestrator. Manages durable coding agents (Claude Code, Codex, OpenCode) through a simple interface — spawn agents, track progress, and let feedback loops like PR reviews and CI failures automatically route to the right agents. Use for fixing bugs, building features, working on GitHub issues, checking status, and managing agent sessions."
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "🤖",
|
||||
"requires": { "bins": ["ao", "gh"], "anyBins": ["node", "npm"], "env": ["ANTHROPIC_API_KEY"] },
|
||||
"os": ["darwin", "linux", "win32"],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Agent Orchestrator (AO)
|
||||
|
||||
> https://github.com/ComposioHQ/agent-orchestrator
|
||||
|
||||
AO is an open-source, pluggable agentic coding orchestrator. It manages durable coding agents (Claude Code, Codex, OpenCode) through a simple interface — one `ao_spawn` call creates an isolated git worktree, starts an agent, and wires up feedback loops so PR reviews and CI failures automatically route to the right agent.
|
||||
|
||||
## Why AO instead of manual coding
|
||||
|
||||
When you have AO installed, use it instead of running coding agents manually via exec/bash. Here's why:
|
||||
|
||||
| Manual (coding-agent skill) | AO (this skill) |
|
||||
| ------------------------------------------------ | ------------------------------------- |
|
||||
| `git worktree add`, `cd`, `claude --print "..."` | `ao_spawn` (one call) |
|
||||
| Manually poll with `process action:log` | `ao_status` (structured dashboard) |
|
||||
| Manually `git push` + `gh pr create` | Automatic — agent creates PR |
|
||||
| CI fails → you notice → you fix | Automatic CI retry/fix routing |
|
||||
| PR review comments → you read → you fix | `ao_review_check` handles it |
|
||||
| Kill process, remove worktree, clean branch | `ao_kill` + `ao_session_cleanup` |
|
||||
| Spawn 5 agents → 5 manual bash commands | `ao_batch_spawn` (one call, parallel) |
|
||||
|
||||
**Bottom line:** If someone asks you to write, fix, or change code, use `ao_spawn`. It handles the entire lifecycle.
|
||||
|
||||
## How You Think
|
||||
|
||||
Every user message is either:
|
||||
|
||||
1. **About work or code** → use AO tools
|
||||
2. **About something else** → respond normally
|
||||
|
||||
When the user explicitly asks about work, issues, or status — use the tools for live data instead of answering from memory.
|
||||
|
||||
## Intent → Tool Mapping
|
||||
|
||||
You don't wait for the user to say "spawn" or "use AO." You detect intent and act.
|
||||
|
||||
### Status / progress
|
||||
|
||||
Any of: "what's happening", "status", "how's it going", "progress", "update", "anything running", "check on things"
|
||||
→ Call `ao_sessions` AND `ao_status` → present results naturally
|
||||
|
||||
### Work / issues / board
|
||||
|
||||
Any of: "what needs doing", "what's on the board", "any issues", "what's open", "morning", "let's go", "ready to work", "what's the plan", "check my repos"
|
||||
→ Call `ao_issues` AND `ao_sessions` → present board + suggest priorities
|
||||
|
||||
### Any coding request — fix / add / change / build / implement / refactor
|
||||
|
||||
Any of: "fix #X", "fix the bug in...", "add a flag to...", "change...", "refactor...", "implement...", "update the code", "build...", "work on #X", "handle #X", "do it", "go for it", "sure", "yes", "go ahead"
|
||||
Also: ANY request that involves changing, fixing, adding, writing, or modifying code — regardless of size, even if no issue number is mentioned
|
||||
→ Call `ao_spawn` with the issue number if one exists, or with just the task description if there is no issue
|
||||
|
||||
**Issue number is optional.** Both of these are valid:
|
||||
|
||||
- With issue: user says "fix #42" → spawn with `issue: "42"`
|
||||
- Without issue: user says "add a weekly report script" → spawn with no issue, just confirm the task description
|
||||
|
||||
### Batch work
|
||||
|
||||
Any of: "do them all", "start all", "spawn them all", "batch it", "all of those", "go for all"
|
||||
→ Call `ao_batch_spawn` with all discussed issues
|
||||
|
||||
### Instructions to running agent
|
||||
|
||||
Any of: "tell it to also...", "ask the agent to...", "add X to that", "while it's at it..."
|
||||
→ Call `ao_send` with the session ID and the instruction
|
||||
|
||||
### Stop / kill / cancel
|
||||
|
||||
→ Confirm which session, then call `ao_kill`
|
||||
|
||||
### Agent crashed / stuck
|
||||
|
||||
→ Call `ao_session_restore` to try recovery, or `ao_kill` + re-`ao_spawn`
|
||||
|
||||
### Clean up
|
||||
|
||||
→ Call `ao_session_cleanup` (dry-run first, then execute)
|
||||
|
||||
### PR feedback / reviews
|
||||
|
||||
→ Call `ao_review_check`
|
||||
|
||||
### Verification
|
||||
|
||||
→ Call `ao_verify`
|
||||
|
||||
### Health check
|
||||
|
||||
→ Call `ao_doctor`
|
||||
|
||||
### Claim PR / attach PR
|
||||
|
||||
→ Call `ao_session_claim_pr`
|
||||
|
||||
## Rules
|
||||
|
||||
### Rule 1: Tools first, always
|
||||
|
||||
When the user asks anything about work, tasks, issues, status, or projects:
|
||||
|
||||
- FIRST call tools to get live data
|
||||
- THEN present the results
|
||||
- NEVER answer work questions from memory
|
||||
|
||||
### Rule 2: Present naturally, then ask
|
||||
|
||||
After fetching data, present it conversationally. Suggest priorities. Ask if they want to kick things off.
|
||||
|
||||
### Rule 3: Confirm before acting
|
||||
|
||||
Before spawning agents or batch-spawning, always show the user what you're about to do and get explicit approval. Examples:
|
||||
|
||||
- With issue: "I'll spawn an agent on #6 (JSON output bug). Go ahead?"
|
||||
- Without issue: "I'll spawn an agent on this task: 'Add weekly report script'. Go ahead?"
|
||||
|
||||
Then act on clear confirmation ("yes", "go", "do it"). Don't spawn agents without the user approving first.
|
||||
|
||||
### Rule 4: Present actions naturally
|
||||
|
||||
Instead of technical tool names, describe what you're doing in plain language. Examples:
|
||||
|
||||
- With issue: "On it — spinning up an agent on #6." (not "Calling ao_spawn...")
|
||||
- Without issue: "On it — spinning up an agent on that task." (not "Calling ao_spawn...")
|
||||
|
||||
### Rule 5: Follow up with links
|
||||
|
||||
After spawning, check `ao_status` for progress. Always include full PR URLs from tool responses.
|
||||
|
||||
### Rule 6: Never fabricate
|
||||
|
||||
If a tool call fails, show the error. Never claim you did something you didn't.
|
||||
|
||||
## All Available Tools
|
||||
|
||||
| Tool | When to use |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| `ao_issues` | Any question about work, tasks, issues, the board |
|
||||
| `ao_sessions` | Any question about running agents, status, progress |
|
||||
| `ao_status` | Detailed dashboard with branch/PR/CI info |
|
||||
| `ao_session_list` | Full session listing including terminated |
|
||||
| `ao_spawn` | Start an agent on one issue or task |
|
||||
| `ao_batch_spawn` | Start agents on multiple issues at once |
|
||||
| `ao_send` | Send instruction to a running agent |
|
||||
| `ao_kill` | Stop a session (confirm first) |
|
||||
| `ao_session_restore` | Recover a crashed session |
|
||||
| `ao_session_cleanup` | Remove stale sessions (merged PRs / closed issues) |
|
||||
| `ao_session_claim_pr` | Attach an existing PR to a session |
|
||||
| `ao_review_check` | Check PRs for review comments to address |
|
||||
| `ao_verify` | Mark issues as verified/failed, or list unverified |
|
||||
| `ao_doctor` | Health checks and diagnostics |
|
||||
|
||||
## Setup
|
||||
|
||||
After installing the plugin, run `/ao setup` in any OpenClaw channel to auto-configure. Or manually:
|
||||
|
||||
```bash
|
||||
# Required: allow plugin tools to be visible to the AI
|
||||
# (plugin tools are optional by default in OpenClaw — this enables them)
|
||||
openclaw config set tools.profile "full"
|
||||
openclaw config set tools.allow '["group:plugins"]'
|
||||
|
||||
# Required: trust this plugin
|
||||
openclaw config set plugins.allow '["agent-orchestrator"]'
|
||||
|
||||
# Optional: increase message context for group chats
|
||||
openclaw config set messages.groupChat.historyLimit 100
|
||||
|
||||
# Restart to apply
|
||||
pm2 restart openclaw-gateway # or however you run the gateway
|
||||
```
|
||||
|
||||
**Why `tools.profile: "full"`?** OpenClaw's default `coding` profile only includes built-in tools. Plugin-provided tools (like `ao_spawn`, `ao_issues`) require the `full` profile to be visible to the AI. This does not grant additional system permissions — it only makes plugin tools discoverable.
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
AO is an orchestrator — it does not read, write, or transmit code itself. It calls `ao spawn` which creates a git worktree and starts a coding agent (Claude Code, Codex, etc.). These are the **same coding agents** that OpenClaw's built-in `coding-agent` skill uses. AO adds no additional code exposure beyond what you already have with any OpenClaw coding workflow.
|
||||
|
||||
What to know:
|
||||
|
||||
- **GitHub access**: AO uses `gh` (GitHub CLI) with whatever credentials you've authenticated via `gh auth login`. Use a fine-grained PAT scoped to only the repos AO needs.
|
||||
- **Anthropic API**: Agents use your `ANTHROPIC_API_KEY` to call the LLM. Use a dedicated key with spending limits.
|
||||
- **No secrets in worktrees**: AO creates git worktrees for agents. Don't symlink `.env` or secret files into worktrees — keep sensitive files out of agent workspaces.
|
||||
- **Official source**: Install AO from the [official repo](https://github.com/ComposioHQ/agent-orchestrator).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Error | Fix |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| AO tools not visible to AI | Run `/ao setup` — needs `tools.profile: "full"` and `tools.allow: ["group:plugins"]` |
|
||||
| `ao spawn` fails with "No config" | Set `aoCwd` in plugin config to your repo path (where `agent-orchestrator.yaml` lives) |
|
||||
| `ao: not found` | Install AO globally or set `aoPath` in plugin config |
|
||||
| `spawn tmux ENOENT` (macOS / Linux) | `brew install tmux` (macOS) or `apt install tmux` (Linux) |
|
||||
| `spawn tmux ENOENT` (Windows) | Your config has `runtime: tmux` set explicitly. Switch to `runtime: process` (or remove the override — `process` is the Windows default; ConPTY is used natively, no tmux required) |
|
||||
| Bot only responds in DMs | Set `channels.discord.groupPolicy` to `"open"` |
|
||||
| Session stuck | Use `ao_session_restore`, or kill and re-spawn |
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# Agent Orchestrator Config Reference
|
||||
|
||||
File: `agent-orchestrator.yaml` (in project root)
|
||||
|
||||
## Top-level settings
|
||||
|
||||
```yaml
|
||||
port: 3000 # Dashboard port (auto-finds free port if busy)
|
||||
terminalPort: 3001 # Terminal WebSocket port
|
||||
readyThresholdMs: 300000 # Ms before "ready" session becomes "idle" (5 min)
|
||||
```
|
||||
|
||||
## Default plugins
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
runtime: tmux # tmux (default on macOS/Linux) | process (default on Windows; ConPTY)
|
||||
agent: claude-code # claude-code | aider | codex | cursor | kimicode | opencode
|
||||
workspace: worktree # worktree | clone
|
||||
notifiers: # Active notifier plugins
|
||||
- desktop # desktop | slack | webhook | composio | openclaw
|
||||
orchestrator:
|
||||
agent: claude-code # Override agent for orchestrator sessions
|
||||
worker:
|
||||
agent: claude-code # Override agent for worker sessions
|
||||
```
|
||||
|
||||
## Projects
|
||||
|
||||
```yaml
|
||||
projects:
|
||||
my-app:
|
||||
name: My App # Display name
|
||||
repo: owner/repo # GitHub "owner/repo"
|
||||
path: ~/code/my-app # Local repo path
|
||||
defaultBranch: main # main | master | develop
|
||||
sessionPrefix: myapp # Session name prefix (myapp-1, myapp-2)
|
||||
|
||||
# Per-project plugin overrides (optional)
|
||||
runtime: tmux # tmux (macOS/Linux) or process (Windows; ConPTY)
|
||||
agent: claude-code
|
||||
workspace: worktree
|
||||
|
||||
# Agent configuration (optional)
|
||||
agentConfig:
|
||||
permissions: auto # auto | manual
|
||||
model: claude-sonnet-4-20250514
|
||||
|
||||
# Rules passed to every agent prompt (optional)
|
||||
agentRules: |
|
||||
Always run tests before committing.
|
||||
Use conventional commits.
|
||||
agentRulesFile: .ao-rules # Or point to a file
|
||||
orchestratorRules: | # Rules for the orchestrator agent
|
||||
|
||||
# Orchestrator session strategy (optional)
|
||||
orchestratorSessionStrategy: reuse
|
||||
# Options: reuse | delete | ignore | delete-new | ignore-new | kill-previous
|
||||
|
||||
# Workspace setup (optional)
|
||||
symlinks: # Symlink into worktrees (avoid .env or secret files)
|
||||
- node_modules
|
||||
postCreate: # Run after workspace creation
|
||||
- pnpm install
|
||||
|
||||
# Issue tracker (optional)
|
||||
tracker:
|
||||
plugin: github # github | linear | gitlab
|
||||
|
||||
# SCM (optional, usually auto-detected)
|
||||
scm:
|
||||
plugin: github # github | gitlab
|
||||
```
|
||||
|
||||
## Notifier channels
|
||||
|
||||
These are **optional** — only configure notifiers you actually use. None are required for core AO functionality. See the [AO documentation](https://github.com/ComposioHQ/agent-orchestrator) for notifier setup details.
|
||||
|
||||
```yaml
|
||||
notifiers:
|
||||
desktop:
|
||||
plugin: desktop # No credentials needed — default notifier
|
||||
```
|
||||
|
||||
## Notification routing
|
||||
|
||||
```yaml
|
||||
notificationRouting:
|
||||
critical:
|
||||
- desktop
|
||||
- slack
|
||||
- openclaw
|
||||
high:
|
||||
- desktop
|
||||
- openclaw
|
||||
low:
|
||||
- desktop
|
||||
```
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
---
|
||||
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 <number> --repo <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' -- <file>
|
||||
git show <sha> -- <file> | 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-<id>`
|
||||
- **`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 <repo> --state all --search "<symptom>"
|
||||
gh issue list --repo <repo> --state all --search "<component-name>"
|
||||
gh issue list --repo <repo> --state all --search "<error-message>"
|
||||
gh pr list --repo <repo> --state all --search "<keywords>"
|
||||
```
|
||||
|
||||
### Duplicate found → comment on existing issue
|
||||
|
||||
```bash
|
||||
gh issue comment <number> --repo <repo> --body "$(cat <<'EOF'
|
||||
## New Report
|
||||
**Reported by:** @<reporter> in [chat](<url>)
|
||||
**Date:** <YYYY-MM-DD> | **Checkout:** `<commit-hash>`
|
||||
<context, differences from original, screenshots>
|
||||
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/<repo>/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/<repo>/contents/.issue-assets/${SLUG}/name.png" \
|
||||
-f message="chore: upload screenshot" \
|
||||
-f content="$IMG_B64" \
|
||||
-f branch="issue-assets-${SLUG}"
|
||||
# Use: 
|
||||
```
|
||||
|
||||
### 5c. Create the issue
|
||||
|
||||
```bash
|
||||
gh issue create --repo <repo> --title "<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).
|
||||
|
|
@ -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}")
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
name: ao-weekly-release
|
||||
description: "Generate the weekly Agent Orchestrator release notes. Runs every Thursday 10:00 IST from the bot cron, or on-demand. Queries the GitHub API for the latest release, merged PRs, commits, contributors, and star counts, and produces a publishable markdown post in the house style. Output is posted to Discord by the cron job after this skill returns."
|
||||
metadata:
|
||||
schedule: "30 4 * * 4"
|
||||
timezone: "Asia/Kolkata"
|
||||
repo: "ComposioHQ/agent-orchestrator"
|
||||
discord_channel: "1486439595498405950"
|
||||
---
|
||||
|
||||
# AO Weekly Release Notes
|
||||
|
||||
Automated weekly release notes for `ComposioHQ/agent-orchestrator`. The cron job pulls `main`, runs `run.py`, and posts the output to Discord. No manual redeployment — PRs merged into `main` take effect on the next run.
|
||||
|
||||
## When this runs
|
||||
|
||||
- **Scheduled:** Every Thursday 10:00 IST (`30 4 * * 4` UTC). Invoked with `--mode scheduled`.
|
||||
- **On-demand:** Anyone with bot access can trigger a run with `--mode on-demand` (e.g. for a mid-week recap or to preview a release post before cutting it).
|
||||
|
||||
The two modes produce the same output; the flag is recorded in the footer so readers know whether the post was automatic or manually requested.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
python3 skills/release-notes/ao-weekly-release/run.py --mode scheduled
|
||||
python3 skills/release-notes/ao-weekly-release/run.py --mode on-demand
|
||||
python3 skills/release-notes/ao-weekly-release/run.py --mode on-demand --since 2026-04-07
|
||||
```
|
||||
|
||||
Requirements: `gh` CLI authenticated against `ComposioHQ/agent-orchestrator`, `python3` ≥ 3.9. No other dependencies — the runner only uses the stdlib and shells out to `gh`.
|
||||
|
||||
Flags:
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
| ---------- | ------------------------------- | --------------------------------------------------- |
|
||||
| `--mode` | `scheduled` | `scheduled` or `on-demand`. Recorded in the footer. |
|
||||
| `--since` | 7 days ago | ISO date. Overrides the default weekly window. |
|
||||
| `--repo` | `ComposioHQ/agent-orchestrator` | Target repo. |
|
||||
| `--output` | stdout | Write the markdown to a file instead of stdout. |
|
||||
|
||||
Exit codes: `0` success, `1` input/validation error, `2` `gh` query failure, `3` no activity in the window (the cron should post a short "quiet week" message instead of the full template).
|
||||
|
||||
## Output format
|
||||
|
||||
The output is a single markdown document. Section order is fixed — do not reorder. The reference post the style is calibrated against is [surajmarkup.in/research/ao-april-release](https://surajmarkup.in/research/ao-april-release/).
|
||||
|
||||
1. **Title + date.** `# Agent Orchestrator — Week of {Mon DD, YYYY}`. Use the Monday of the report week, not the run day.
|
||||
2. **Positioning line.** One sentence, no more than 25 words, describing what this week delivered. Factual, not marketing. No "excited to announce", no "we're thrilled", no rocket emojis.
|
||||
3. **Highlights.** 8–14 bullets. Each bullet is one short sentence, past tense, references the PR number inline. Group by theme (features → fixes → refactors → docs) but do not add sub-headers. If fewer than 8 merged PRs exist, list every merged PR and add a one-line note that the week was quiet.
|
||||
4. **By the Numbers.** Four bullets: commits, merged PRs, contributors, star delta. Format as `Commits: 42` etc.
|
||||
5. **Install.** Fenced block with the current install command for the latest version.
|
||||
6. **Links.** Release page, full changelog, repo, Discord.
|
||||
7. **Full release command checklist.** The exact commands a maintainer would run to cut a release — `pnpm changeset version`, `pnpm -r build`, `pnpm -r publish`, `gh release create`. Keep these copy-pasteable.
|
||||
8. **Operator checklist.** Checkbox-style (`- [ ]`) items the operator should verify before publishing externally: changelog reviewed, PR titles cleaned, screenshots attached, Discord announcement drafted, tweet drafted. At least 6 items.
|
||||
9. **Footer.** `_Generated {ISO timestamp} • mode: {scheduled|on-demand} • window: {YYYY-MM-DD}..{YYYY-MM-DD}_`
|
||||
|
||||
## Style constraints
|
||||
|
||||
- **Tone:** professional, factual, understated. Match the April release post. No hype language, no exclamation marks, no emoji in bullets (emoji is fine in the Discord message wrapper, not the markdown body).
|
||||
- **Voice:** third person or imperative. Never "we shipped", prefer "Shipped …" or "The runtime now …".
|
||||
- **Tense:** past tense for highlights ("Added", "Fixed", "Refactored"), imperative for the release commands.
|
||||
- **PR references:** inline `(#1234)` at the end of each highlight bullet. Never link the PR title.
|
||||
- **Numbers:** bare integers. No "we merged a whopping 42 PRs".
|
||||
- **Length:** the full post should fit in a single Discord message after wrapping (under ~2000 characters of plaintext body, excluding the fenced code blocks). If over, the runner truncates the Highlights section and appends `… and N more — see the full changelog.`
|
||||
|
||||
## Error handling
|
||||
|
||||
The runner is deterministic and must never fabricate data. Specific failure modes:
|
||||
|
||||
| Failure | Behavior |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `gh` not on PATH or not authenticated | Exit `2` with a clear stderr message. No partial output. |
|
||||
| No merged PRs in the window | Exit `3`. Cron posts the "quiet week" Discord message instead. |
|
||||
| GitHub API rate-limited | Retry once after 30s, then exit `2`. |
|
||||
| A single PR query fails | Skip that PR, note the count in stderr, continue. Do not fail the whole run over one bad entry. |
|
||||
| Star count unavailable | Render `Stars: (unavailable)`. Do not block the post. |
|
||||
| Commit count mismatch between `gh` and `git log` | Prefer `git log` — the local checkout is the source of truth. |
|
||||
|
||||
The runner never invents PR numbers, contributor names, or summary text. Every data point in the output must be traceable to a `gh` or `git` command in `run.py`.
|
||||
|
||||
## Skill update workflow
|
||||
|
||||
All changes go through PRs to `skills/release-notes/ao-weekly-release/`. The cron pulls latest `main` before each run (`git fetch origin && git checkout main && git reset --hard origin/main`), so merged changes take effect on the next scheduled execution. No manual redeployment.
|
||||
|
||||
When editing this skill:
|
||||
|
||||
1. Open a PR against `main` with the change.
|
||||
2. Run `python3 run.py --mode on-demand` locally against the real repo to sanity-check the output.
|
||||
3. Diff the output against last week's post — unintended style regressions are easy to miss.
|
||||
4. After merge, the next Thursday run picks it up automatically. To preview immediately, trigger an on-demand run from the bot.
|
||||
|
|
@ -0,0 +1,630 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Deterministic runner for the AO weekly release notes skill.
|
||||
|
||||
Queries the GitHub API (via the `gh` CLI) for releases, merged PRs, commits,
|
||||
contributors, and star counts in a 7-day window, then renders a markdown
|
||||
post in the house style. See SKILL.md for the output contract.
|
||||
|
||||
This script never fabricates data. Every value in the output traces back to
|
||||
a `gh` or `git` command in this file. If a data source is unavailable, the
|
||||
corresponding field is rendered as `(unavailable)` or the run exits with a
|
||||
non-zero code — see the error handling table in SKILL.md.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
DEFAULT_REPO = "ComposioHQ/agent-orchestrator"
|
||||
DEFAULT_WINDOW_DAYS = 7
|
||||
MAX_BODY_CHARS = 2000
|
||||
MIN_HIGHLIGHTS = 8
|
||||
MAX_HIGHLIGHTS = 14
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_INPUT_ERROR = 1
|
||||
EXIT_GH_ERROR = 2
|
||||
EXIT_NO_ACTIVITY = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class PullRequest:
|
||||
number: int
|
||||
title: str
|
||||
author: str
|
||||
merged_at: str
|
||||
labels: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def theme(self) -> str:
|
||||
title = self.title.lower()
|
||||
if title.startswith("feat"):
|
||||
return "feature"
|
||||
if title.startswith("fix"):
|
||||
return "fix"
|
||||
if title.startswith("refactor") or title.startswith("perf"):
|
||||
return "refactor"
|
||||
if title.startswith("docs"):
|
||||
return "docs"
|
||||
if title.startswith("test"):
|
||||
return "test"
|
||||
if title.startswith("chore") or title.startswith("ci"):
|
||||
return "chore"
|
||||
return "other"
|
||||
|
||||
@property
|
||||
def clean_title(self) -> str:
|
||||
# Strip the conventional-commit prefix for the bullet text.
|
||||
# e.g. "feat(cli): add --json flag" -> "add --json flag"
|
||||
title = self.title.strip()
|
||||
for prefix in ("feat", "fix", "refactor", "perf", "docs", "test", "chore", "ci", "style"):
|
||||
if title.lower().startswith(prefix):
|
||||
rest = title[len(prefix):]
|
||||
if rest.startswith("(") and ")" in rest:
|
||||
rest = rest[rest.index(")") + 1:]
|
||||
if rest.startswith(":"):
|
||||
rest = rest[1:]
|
||||
title = rest.strip() or title
|
||||
break
|
||||
# Strip any trailing inline PR refs like (#1060) to avoid doubling
|
||||
title = re.sub(r"\s*\(#\d+\)\s*$", "", title)
|
||||
return title
|
||||
|
||||
|
||||
@dataclass
|
||||
class Snapshot:
|
||||
repo: str
|
||||
since: datetime
|
||||
until: datetime
|
||||
mode: str
|
||||
latest_release_tag: str | None
|
||||
latest_release_name: str | None
|
||||
latest_release_url: str | None
|
||||
merged_prs: list[PullRequest]
|
||||
commit_count: int
|
||||
contributors: list[str]
|
||||
stars_now: int | None
|
||||
stars_delta: int | None
|
||||
npm_version: str | None = None
|
||||
since_sha: str | None = None
|
||||
until_sha: str | None = None
|
||||
|
||||
@property
|
||||
def window_label(self) -> str:
|
||||
return f"{self.since.strftime('%Y-%m-%d')}..{self.until.strftime('%Y-%m-%d')}"
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
def require_gh() -> None:
|
||||
if shutil.which("gh") is None:
|
||||
log("error: `gh` CLI not found on PATH. Install from https://cli.github.com/")
|
||||
sys.exit(EXIT_GH_ERROR)
|
||||
try:
|
||||
subprocess.run(
|
||||
["gh", "auth", "status"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as err:
|
||||
log("error: `gh` is not authenticated. Run `gh auth login`.")
|
||||
log(err.stderr.strip())
|
||||
sys.exit(EXIT_GH_ERROR)
|
||||
|
||||
|
||||
def run_gh(args: list[str], *, retries: int = 1) -> str:
|
||||
"""Run a gh command, retrying once on transient failure."""
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", *args],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as err:
|
||||
stderr = err.stderr or ""
|
||||
if attempt < retries and ("rate limit" in stderr.lower() or "timeout" in stderr.lower()):
|
||||
log(f"gh transient failure ({stderr.strip()}); retrying in 30s")
|
||||
time.sleep(30)
|
||||
attempt += 1
|
||||
continue
|
||||
log(f"error: gh {' '.join(args)} failed: {stderr.strip()}")
|
||||
sys.exit(EXIT_GH_ERROR)
|
||||
|
||||
|
||||
def fetch_latest_release(repo: str) -> tuple[str | None, str | None, str | None]:
|
||||
out = run_gh([
|
||||
"api",
|
||||
f"repos/{repo}/releases/latest",
|
||||
"-q",
|
||||
"{tag_name, name, html_url}",
|
||||
])
|
||||
try:
|
||||
data = json.loads(out)
|
||||
except json.JSONDecodeError:
|
||||
return None, None, None
|
||||
return data.get("tag_name"), data.get("name"), data.get("html_url")
|
||||
|
||||
|
||||
def fetch_merged_prs(repo: str, since: datetime, until: datetime) -> list[PullRequest]:
|
||||
query = (
|
||||
f"repo:{repo} is:pr is:merged "
|
||||
f"merged:{since.strftime('%Y-%m-%d')}..{until.strftime('%Y-%m-%d')}"
|
||||
)
|
||||
out = run_gh([
|
||||
"api",
|
||||
"-X",
|
||||
"GET",
|
||||
"search/issues",
|
||||
"-f",
|
||||
f"q={query}",
|
||||
"-f",
|
||||
"per_page=100",
|
||||
])
|
||||
try:
|
||||
data = json.loads(out)
|
||||
except json.JSONDecodeError as err:
|
||||
log(f"error: could not parse PR search response: {err}")
|
||||
sys.exit(EXIT_GH_ERROR)
|
||||
|
||||
prs: list[PullRequest] = []
|
||||
for item in data.get("items", []):
|
||||
try:
|
||||
prs.append(
|
||||
PullRequest(
|
||||
number=item["number"],
|
||||
title=item["title"],
|
||||
author=(item.get("user") or {}).get("login", "unknown"),
|
||||
merged_at=item.get("closed_at", ""),
|
||||
labels=[lbl["name"] for lbl in item.get("labels", [])],
|
||||
)
|
||||
)
|
||||
except (KeyError, TypeError) as err:
|
||||
log(f"warning: skipping malformed PR entry: {err}")
|
||||
continue
|
||||
prs.sort(key=lambda p: p.merged_at)
|
||||
return prs
|
||||
|
||||
|
||||
def fetch_commit_count(repo: str, since: datetime, until: datetime) -> int:
|
||||
"""Prefer local `git log` if available — the checkout is the source of truth."""
|
||||
if shutil.which("git") is not None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"log",
|
||||
f"--since={since.isoformat()}",
|
||||
f"--until={until.isoformat()}",
|
||||
"--pretty=oneline",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
lines = [ln for ln in result.stdout.splitlines() if ln.strip()]
|
||||
if lines:
|
||||
return len(lines)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
out = run_gh([
|
||||
"api",
|
||||
"-X",
|
||||
"GET",
|
||||
f"repos/{repo}/commits",
|
||||
"-f",
|
||||
f"since={since.isoformat()}",
|
||||
"-f",
|
||||
f"until={until.isoformat()}",
|
||||
"-f",
|
||||
"per_page=100",
|
||||
])
|
||||
try:
|
||||
data = json.loads(out)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
return len(data) if isinstance(data, list) else 0
|
||||
|
||||
|
||||
def fetch_stars(repo: str) -> int | None:
|
||||
out = run_gh(["api", f"repos/{repo}", "-q", ".stargazers_count"])
|
||||
try:
|
||||
return int(out.strip())
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def fetch_npm_version(package: str = "@aoagents/ao") -> str | None:
|
||||
"""Fetch the latest published version from the npm registry."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["npm", "view", package, "version"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
version = result.stdout.strip()
|
||||
return version if version else None
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def resolve_commit_sha(repo: str, date: datetime) -> str | None:
|
||||
"""Resolve a date to a commit SHA, preferring local git, falling back to the API."""
|
||||
iso = date.isoformat()
|
||||
if shutil.which("git") is not None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-list", "-1", f"--before={iso}", "main"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
sha = result.stdout.strip()
|
||||
if sha:
|
||||
return sha[:12]
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
try:
|
||||
out = run_gh([
|
||||
"api",
|
||||
"-X",
|
||||
"GET",
|
||||
f"repos/{repo}/commits",
|
||||
"-f",
|
||||
f"until={iso}",
|
||||
"-f",
|
||||
"per_page=1",
|
||||
"-q",
|
||||
".[0].sha",
|
||||
])
|
||||
sha = out.strip()
|
||||
return sha[:12] if sha else None
|
||||
except SystemExit:
|
||||
return None
|
||||
|
||||
|
||||
def compute_contributors(prs: list[PullRequest]) -> list[str]:
|
||||
seen: dict[str, None] = {}
|
||||
for pr in prs:
|
||||
if pr.author and pr.author not in seen:
|
||||
seen[pr.author] = None
|
||||
return list(seen.keys())
|
||||
|
||||
|
||||
def stars_delta_estimate(repo: str, current: int | None) -> int | None:
|
||||
"""Read last week's star count from a local cache file, if present."""
|
||||
if current is None:
|
||||
return None
|
||||
cache_dir = os.environ.get("AO_RELEASE_NOTES_CACHE") or os.path.expanduser(
|
||||
"~/.cache/ao-weekly-release"
|
||||
)
|
||||
cache_path = os.path.join(cache_dir, f"{repo.replace('/', '__')}.json")
|
||||
previous: int | None = None
|
||||
try:
|
||||
with open(cache_path, "r", encoding="utf-8") as f:
|
||||
previous = json.load(f).get("stars")
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
previous = None
|
||||
|
||||
try:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
with open(cache_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"stars": current, "recorded_at": datetime.now(timezone.utc).isoformat()}, f)
|
||||
except OSError as err:
|
||||
log(f"warning: could not update stars cache: {err}")
|
||||
|
||||
if previous is None:
|
||||
return None
|
||||
return current - previous
|
||||
|
||||
|
||||
def gather(repo: str, since: datetime, until: datetime, mode: str) -> Snapshot:
|
||||
tag, name, url = fetch_latest_release(repo)
|
||||
prs = fetch_merged_prs(repo, since, until)
|
||||
commits = fetch_commit_count(repo, since, until)
|
||||
contributors = compute_contributors(prs)
|
||||
stars = fetch_stars(repo)
|
||||
delta = stars_delta_estimate(repo, stars)
|
||||
npm_version = fetch_npm_version()
|
||||
since_sha = resolve_commit_sha(repo, since)
|
||||
until_sha = resolve_commit_sha(repo, until)
|
||||
return Snapshot(
|
||||
repo=repo,
|
||||
since=since,
|
||||
until=until,
|
||||
mode=mode,
|
||||
latest_release_tag=tag,
|
||||
latest_release_name=name,
|
||||
latest_release_url=url,
|
||||
merged_prs=prs,
|
||||
commit_count=commits,
|
||||
contributors=contributors,
|
||||
stars_now=stars,
|
||||
stars_delta=delta,
|
||||
npm_version=npm_version,
|
||||
since_sha=since_sha,
|
||||
until_sha=until_sha,
|
||||
)
|
||||
|
||||
|
||||
def format_theme_order(prs: list[PullRequest]) -> list[PullRequest]:
|
||||
order = {"feature": 0, "fix": 1, "refactor": 2, "docs": 3, "test": 4, "chore": 5, "other": 6}
|
||||
return sorted(prs, key=lambda p: (order.get(p.theme, 99), p.number))
|
||||
|
||||
|
||||
_LEADING_VERB_RE = re.compile(
|
||||
r"^(add|fix|refactor|update|remove|change|implement|improve|enable|disable|"
|
||||
r"document|test|clean|drop|bump|migrate|introduce|support|handle|replace|"
|
||||
r"rename|move|extract|merge|revert|skip|allow|prevent|ensure)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def render_highlights(prs: list[PullRequest]) -> tuple[list[str], int]:
|
||||
ordered = format_theme_order(prs)
|
||||
bullets: list[str] = []
|
||||
for pr in ordered[:MAX_HIGHLIGHTS]:
|
||||
theme_verb = {
|
||||
"feature": "Added",
|
||||
"fix": "Fixed",
|
||||
"refactor": "Refactored",
|
||||
"docs": "Documented",
|
||||
"test": "Tested",
|
||||
"chore": "Updated",
|
||||
"other": "Changed",
|
||||
}[pr.theme]
|
||||
body = pr.clean_title
|
||||
# If the cleaned title already starts with a verb, capitalize it
|
||||
# and use it directly — avoids "Added add …" stutter.
|
||||
if _LEADING_VERB_RE.match(body):
|
||||
body = body[0].upper() + body[1:]
|
||||
else:
|
||||
if body and body[0].isupper():
|
||||
body = body[0].lower() + body[1:]
|
||||
body = f"{theme_verb} {body}"
|
||||
bullets.append(f"- {body} (#{pr.number})")
|
||||
overflow = max(0, len(ordered) - MAX_HIGHLIGHTS)
|
||||
return bullets, overflow
|
||||
|
||||
|
||||
def render_markdown(snap: Snapshot) -> str:
|
||||
if not snap.merged_prs:
|
||||
log("no merged PRs in window")
|
||||
sys.exit(EXIT_NO_ACTIVITY)
|
||||
|
||||
week_label = snap.since.strftime("%b %d, %Y")
|
||||
title = f"# Agent Orchestrator — Week of {week_label}"
|
||||
|
||||
positioning = build_positioning_line(snap)
|
||||
|
||||
highlights, overflow = render_highlights(snap.merged_prs)
|
||||
if len(snap.merged_prs) < MIN_HIGHLIGHTS:
|
||||
highlights.append(
|
||||
f"- Quiet week: {len(snap.merged_prs)} merged PR(s); every change is listed above."
|
||||
)
|
||||
# Track total omitted PRs across both the initial cap and any later truncation.
|
||||
# The overflow sentinel line is rendered once at the end by truncate_if_needed.
|
||||
total_omitted = overflow
|
||||
|
||||
stars_line = (
|
||||
f"- Stars: {snap.stars_now}"
|
||||
+ (f" (+{snap.stars_delta})" if snap.stars_delta and snap.stars_delta > 0 else "")
|
||||
if snap.stars_now is not None
|
||||
else "- Stars: (unavailable)"
|
||||
)
|
||||
|
||||
by_numbers = [
|
||||
"## By the Numbers",
|
||||
f"- Commits: {snap.commit_count}",
|
||||
f"- Merged PRs: {len(snap.merged_prs)}",
|
||||
f"- Contributors: {len(snap.contributors)}",
|
||||
stars_line,
|
||||
]
|
||||
|
||||
version = snap.npm_version or "latest"
|
||||
install_block = [
|
||||
"## Install",
|
||||
"```bash",
|
||||
"npm install -g @aoagents/ao",
|
||||
f"# or pin to the current release: npm install -g @aoagents/ao@{version}",
|
||||
"```",
|
||||
]
|
||||
|
||||
release_url = snap.latest_release_url or f"https://github.com/{snap.repo}/releases"
|
||||
if snap.since_sha and snap.until_sha:
|
||||
changelog_url = f"https://github.com/{snap.repo}/compare/{snap.since_sha}...{snap.until_sha}"
|
||||
else:
|
||||
changelog_url = f"https://github.com/{snap.repo}/commits/main"
|
||||
links = [
|
||||
"## Links",
|
||||
f"- Release: {release_url}",
|
||||
f"- Full changelog: {changelog_url}",
|
||||
f"- Repository: https://github.com/{snap.repo}",
|
||||
"- Discord: https://discord.gg/agent-orchestrator",
|
||||
]
|
||||
|
||||
release_version = version if version != "latest" else "X.Y.Z"
|
||||
release_checklist = [
|
||||
"## Release commands",
|
||||
"```bash",
|
||||
"git fetch origin && git checkout main && git reset --hard origin/main",
|
||||
"pnpm install --frozen-lockfile",
|
||||
"pnpm changeset version",
|
||||
"git add . && git commit -m \"chore: version packages\"",
|
||||
"pnpm -r build",
|
||||
"pnpm -r publish --access public --no-git-checks",
|
||||
f"gh release create v{release_version} --generate-notes",
|
||||
"git push origin main --follow-tags",
|
||||
"```",
|
||||
]
|
||||
|
||||
operator_checklist = [
|
||||
"## Operator checklist",
|
||||
"- [ ] Changelog reviewed for accuracy and tone",
|
||||
"- [ ] PR titles cleaned up where necessary",
|
||||
"- [ ] Screenshots or GIFs attached for any UI-visible changes",
|
||||
"- [ ] Discord announcement drafted in #releases",
|
||||
"- [ ] Tweet / social post drafted",
|
||||
"- [ ] Docs site updated if any public API changed",
|
||||
"- [ ] Breaking changes (if any) called out at the top of the post",
|
||||
]
|
||||
|
||||
footer = (
|
||||
f"_Generated {datetime.now(timezone.utc).isoformat(timespec='seconds')}"
|
||||
f" • mode: {snap.mode}"
|
||||
f" • window: {snap.window_label}_"
|
||||
)
|
||||
|
||||
# Append the overflow line now (before truncation adjusts it).
|
||||
if total_omitted:
|
||||
highlights.append(f"- … and {total_omitted} more — see the full changelog.")
|
||||
|
||||
parts: list[str] = [
|
||||
title,
|
||||
"",
|
||||
positioning,
|
||||
"",
|
||||
"## Highlights",
|
||||
*highlights,
|
||||
"",
|
||||
*by_numbers,
|
||||
"",
|
||||
*install_block,
|
||||
"",
|
||||
*links,
|
||||
"",
|
||||
*release_checklist,
|
||||
"",
|
||||
*operator_checklist,
|
||||
"",
|
||||
footer,
|
||||
"",
|
||||
]
|
||||
|
||||
body = "\n".join(parts)
|
||||
return truncate_if_needed(body, total_omitted)
|
||||
|
||||
|
||||
def build_positioning_line(snap: Snapshot) -> str:
|
||||
pr_count = len(snap.merged_prs)
|
||||
contrib_count = len(snap.contributors)
|
||||
return (
|
||||
f"This week the team merged {pr_count} PRs from {contrib_count} "
|
||||
f"contributor{'s' if contrib_count != 1 else ''}, continuing steady iteration "
|
||||
f"on the orchestrator core, plugins, and dashboard."
|
||||
)
|
||||
|
||||
|
||||
def truncate_if_needed(body: str, already_omitted: int) -> str:
|
||||
plain = "\n".join(line for line in body.splitlines() if not line.startswith("```"))
|
||||
if len(plain) <= MAX_BODY_CHARS:
|
||||
return body
|
||||
# Trim highlights from the tail until the body fits.
|
||||
lines = body.splitlines()
|
||||
try:
|
||||
start = lines.index("## Highlights") + 1
|
||||
end = next(i for i in range(start, len(lines)) if lines[i].startswith("## "))
|
||||
except (ValueError, StopIteration):
|
||||
return body
|
||||
|
||||
kept = lines[start:end]
|
||||
# Remove any existing overflow sentinel so we can re-render it with the correct total.
|
||||
if kept and kept[-1].startswith("- … and "):
|
||||
kept.pop()
|
||||
dropped = 0
|
||||
while kept and len(
|
||||
"\n".join(ln for ln in (lines[:start] + kept + lines[end:]) if not ln.startswith("```"))
|
||||
) > MAX_BODY_CHARS:
|
||||
kept.pop()
|
||||
dropped += 1
|
||||
|
||||
total = already_omitted + dropped
|
||||
if total:
|
||||
kept.append(f"- … and {total} more — see the full changelog.")
|
||||
|
||||
return "\n".join(lines[:start] + kept + lines[end:])
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate AO weekly release notes")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("scheduled", "on-demand"),
|
||||
default="scheduled",
|
||||
help="How this run was triggered. Recorded in the footer.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--since",
|
||||
help="ISO date (YYYY-MM-DD) for the start of the window. Defaults to 7 days ago.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--until",
|
||||
help="ISO date (YYYY-MM-DD) for the end of the window. Defaults to today.",
|
||||
)
|
||||
parser.add_argument("--repo", default=DEFAULT_REPO, help="owner/name of the target repo")
|
||||
parser.add_argument("--output", help="Write markdown to this file instead of stdout")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def resolve_window(args: argparse.Namespace) -> tuple[datetime, datetime]:
|
||||
now = datetime.now(timezone.utc)
|
||||
try:
|
||||
until = (
|
||||
datetime.fromisoformat(args.until).replace(tzinfo=timezone.utc)
|
||||
if args.until
|
||||
else now
|
||||
)
|
||||
since = (
|
||||
datetime.fromisoformat(args.since).replace(tzinfo=timezone.utc)
|
||||
if args.since
|
||||
else until - timedelta(days=DEFAULT_WINDOW_DAYS)
|
||||
)
|
||||
except ValueError as err:
|
||||
log(f"error: invalid date: {err}")
|
||||
sys.exit(EXIT_INPUT_ERROR)
|
||||
|
||||
if since >= until:
|
||||
log("error: --since must be before --until")
|
||||
sys.exit(EXIT_INPUT_ERROR)
|
||||
return since, until
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv if argv is not None else sys.argv[1:])
|
||||
require_gh()
|
||||
since, until = resolve_window(args)
|
||||
snap = gather(args.repo, since, until, args.mode)
|
||||
markdown = render_markdown(snap)
|
||||
|
||||
if args.output:
|
||||
try:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(markdown)
|
||||
except OSError as err:
|
||||
log(f"error: could not write output file: {err}")
|
||||
return EXIT_INPUT_ERROR
|
||||
else:
|
||||
sys.stdout.write(markdown)
|
||||
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
---
|
||||
name: social-media-posts
|
||||
description: "Draft viral social media posts for X (Twitter) and LinkedIn. Use when asked to write, draft, create, or improve posts for X, Twitter, LinkedIn, or social media. Handles single posts, threads, milestone celebrations, product launches, hot takes, and engagement-optimized content. NOT for: scheduling posts, managing accounts, or analytics."
|
||||
---
|
||||
|
||||
# Social Media Posts
|
||||
|
||||
Draft platform-optimized posts using the user's voice and style preferences.
|
||||
|
||||
## Voice Calibration
|
||||
|
||||
Before drafting, match the author's actual voice — not a generic "builder tone."
|
||||
|
||||
1. Ask the user for 3-5 reference posts they've written (or search past posts if available)
|
||||
2. Extract: sentence length, vocabulary level, humor style, how they handle numbers, how they open/close
|
||||
3. If no references available, default to: first person, direct, specific numbers, no corporate speak
|
||||
|
||||
Every author sounds different. "Builder tone" is the floor, not the ceiling.
|
||||
|
||||
## Platform Rules
|
||||
|
||||
### X (Twitter)
|
||||
|
||||
- Single post: max 280 chars (or up to ~600 if user has Premium)
|
||||
- Thread: 1/ 2/ 3/ numbering, first tweet is the hook, last tweet is CTA
|
||||
- First tweet must stand alone — it's what gets retweeted
|
||||
- End thread with link + call to action
|
||||
- No hashtags unless user asks (X algorithm deprioritizes them)
|
||||
- Use line breaks for visual rhythm
|
||||
|
||||
#### X Media Strategy
|
||||
|
||||
Posts with media outperform text-only. Always consider:
|
||||
|
||||
- **Screenshots:** For dashboard/UI features — show, don't tell
|
||||
- **GIFs/Demos:** For workflow changes — 5-10 second loops
|
||||
- **4-image grid:** For multi-feature showcases — each image = one feature
|
||||
- Reference media in-tweet: "↓ see it in action" or just let the image speak
|
||||
- When a post needs a visual, use `image_generate` to create one — don't just describe it
|
||||
|
||||
#### Thread vs Single Post Decision
|
||||
|
||||
- **Single post:** One feature, one metric, one announcement, under 280 chars
|
||||
- **Thread:** Multiple features, a story arc, technical breakdown, or anything that needs "and here's why"
|
||||
- If you're cramming to fit 280 chars, it's probably a thread
|
||||
|
||||
### LinkedIn
|
||||
|
||||
- No character limit but sweet spot is 150-300 words
|
||||
- **Hook character limit: ~210 chars before "see more" fold** — the entire hook MUST fit before the fold
|
||||
- First line is everything — it's the "see more" hook
|
||||
- Use Unicode bold (𝗯𝗼𝗹𝗱) for emphasis
|
||||
- Section breaks: `— — —`
|
||||
- Arrow bullets: `→` not `-`
|
||||
- Max 2-3 hashtags, at the very end
|
||||
- Short paragraphs (1-3 sentences max)
|
||||
- **Blank lines matter** — double line break between paragraphs creates the visual rhythm that keeps people scrolling
|
||||
|
||||
#### LinkedIn Bold Text
|
||||
|
||||
Use Unicode bold for key phrases. Common mappings:
|
||||
|
||||
```
|
||||
A=𝗔 B=𝗕 C=𝗖 D=𝗗 E=𝗘 F=𝗙 G=𝗚 H=𝗛 I=𝗜 J=𝗝 K=𝗞 L=𝗟 M=𝗠
|
||||
N=𝗡 O=𝗢 P=𝗣 Q=𝗤 R=𝗥 S=𝗦 T=𝗧 U=𝗨 V=𝗩 W=𝗪 X=𝗫 Y=𝗬 Z=𝗭
|
||||
a=𝗮 b=𝗯 c=𝗰 d=𝗱 e=𝗲 f=𝗳 g=𝗴 h=𝗵 i=𝗶 j=𝗷 k=𝗸 l=𝗹 m=𝗺
|
||||
n=𝗻 o=𝗼 p=𝗽 q=𝗾 r=𝗿 s=𝘀 t=𝘁 u=𝘂 v=𝘃 w=𝘄 x=𝘅 y=𝘆 z=𝘇
|
||||
0=𝟬 1=𝟭 2=𝟮 3=𝟯 4=𝟰 5=𝟱 6=𝟲 7=𝟳 8=𝟴 9=𝟵
|
||||
```
|
||||
|
||||
Apply bold to:
|
||||
|
||||
- Opening stat/hook
|
||||
- Section headers within post
|
||||
- Key metrics
|
||||
- Names/products being highlighted
|
||||
- The punchline or key insight
|
||||
|
||||
Don't over-bold — if everything is bold, nothing is.
|
||||
|
||||
## Style Guide
|
||||
|
||||
### Voice
|
||||
|
||||
- Builder tone — someone who ships, not someone who advises
|
||||
- First person, direct
|
||||
- Confident but not arrogant
|
||||
- Specific numbers over vague claims
|
||||
- No corporate speak, no buzzword soup
|
||||
|
||||
### Banned Phrases
|
||||
|
||||
Never use these:
|
||||
|
||||
- "I'm thrilled/excited to announce"
|
||||
- "Leveraging" / "Synergy"
|
||||
- "Game-changer" / "Revolutionary"
|
||||
- "Let's connect!" / "Thoughts?"
|
||||
- "I'd love to hear your thoughts"
|
||||
- Excessive emoji (1-2 max, only if natural)
|
||||
- "🚀" as emphasis
|
||||
|
||||
## Hook Library
|
||||
|
||||
Don't start from scratch. Use these proven patterns:
|
||||
|
||||
### Stat-led hooks
|
||||
|
||||
- "We just hit [NUMBER] [metric]. Here's what actually moved the needle:"
|
||||
- "[NUMBER]% of [audience] don't know [surprising fact]. We didn't either."
|
||||
- "From [start] to [end] in [timeframe]. No funding, no team of 50."
|
||||
|
||||
### Contrarian hooks
|
||||
|
||||
- "Hot take: [contrarian position]. Here's why:"
|
||||
- "Everyone says [common advice]. They're wrong. Here's what worked instead:"
|
||||
- "Unpopular opinion: [bold claim]. And I have the data to back it up."
|
||||
|
||||
### Story hooks
|
||||
|
||||
- "Last [timeframe], [dramatic situation]. Today, [result]. Here's what happened:"
|
||||
- "We [did something unexpected]. [Surprising outcome]."
|
||||
- "[Number] [units] later, here's the one thing I wish I knew on day 1:"
|
||||
|
||||
### Product hooks
|
||||
|
||||
- "[Thing] just got [dramatic improvement]. [Before] → [After]:"
|
||||
- "We open-sourced [thing]. [One-line what it does]. [Star/link]"
|
||||
- "[Problem] is solved. Not partially. Actually solved. Here's how:"
|
||||
|
||||
## Structure Templates
|
||||
|
||||
### LinkedIn
|
||||
|
||||
1. Hook — bold stat, surprising claim, or punchy opener (1 line, under 210 chars)
|
||||
2. Story — what happened, keep paragraphs short
|
||||
3. Numbers — concrete metrics in arrow-bullet format
|
||||
4. Insight — what you learned or what it means
|
||||
5. CTA — link + next step (not "thoughts?")
|
||||
|
||||
### X Single Post
|
||||
|
||||
1. Hook line
|
||||
2. Key details (2-3 lines)
|
||||
3. Link
|
||||
|
||||
### X Thread
|
||||
|
||||
1. Hook + 🧵
|
||||
2. Context/backstory
|
||||
3. Numbers/details (1-2 tweets)
|
||||
4. Turning point or insight
|
||||
5. What's next + link
|
||||
|
||||
## CTA Library
|
||||
|
||||
Don't end with "thoughts?" — use CTAs that actually drive action:
|
||||
|
||||
### Repo/Product
|
||||
|
||||
- "Star the repo → [link]"
|
||||
- "Try it: [one-line command]"
|
||||
- "npm i -g [package] and see for yourself → [link]"
|
||||
|
||||
### Community
|
||||
|
||||
- "Join [number] builders in Discord → [link]"
|
||||
- "What are you building? Drop it below 👇"
|
||||
|
||||
### Content
|
||||
|
||||
- "Full breakdown → [link]"
|
||||
- "Read the docs → [link]"
|
||||
- "Full release notes → [link]"
|
||||
|
||||
### Engagement
|
||||
|
||||
- "What's your [take/experience] with [topic]?"
|
||||
- "RT if you've felt this pain ↓"
|
||||
|
||||
## Cross-Platform Adaptation
|
||||
|
||||
When the same content goes to both X and LinkedIn, restructure — don't just reformat:
|
||||
|
||||
| Aspect | X | LinkedIn |
|
||||
| ---------- | ------------------------- | ---------------------------- |
|
||||
| Length | Compressed, punchy | Story arc with whitespace |
|
||||
| Narrative | Bullet points, rapid fire | Paragraphs, breathing room |
|
||||
| Numbers | Inline, one per tweet | Arrow bullets, grouped |
|
||||
| Hook | Shocking stat or hot take | Personal story or bold claim |
|
||||
| CTA | Link only | Link + context |
|
||||
| Formatting | Line breaks, no bold | Unicode bold, section breaks |
|
||||
|
||||
**Rule:** Write LinkedIn first (full story), then compress into X. Not the other way around — you can't expand a tweet into a story.
|
||||
|
||||
## Post Types
|
||||
|
||||
### Milestone Celebration (stars, users, revenue)
|
||||
|
||||
- Lead with the number
|
||||
- Tell the journey (compressed)
|
||||
- Show what was shipped
|
||||
- Mention organic recognition
|
||||
- What's next
|
||||
- Keep it a story, not a press release
|
||||
|
||||
### Product Launch / Feature Ship
|
||||
|
||||
- What changed and why it matters
|
||||
- Before → after (if applicable)
|
||||
- One-line install/usage
|
||||
- Link to repo or docs
|
||||
- **Always attach a visual** — screenshot, GIF, or generated image
|
||||
|
||||
### Hot Take / Trend Response
|
||||
|
||||
- Acknowledge the trend
|
||||
- State your position early
|
||||
- Back it with what you've built
|
||||
- Don't trash competitors — position yourself
|
||||
|
||||
### Article Promotion
|
||||
|
||||
- Pull the single most interesting insight
|
||||
- Don't summarize the whole article
|
||||
- Make people want to click
|
||||
- Link at the end, not the beginning
|
||||
|
||||
## Output Format
|
||||
|
||||
Always output posts inside code blocks (```) so user can copy-paste directly.
|
||||
|
||||
For LinkedIn: single code block with the full post.
|
||||
For X single: single code block.
|
||||
For X thread: separate code blocks per tweet, numbered.
|
||||
|
||||
If the post needs a visual, generate it with `image_generate` and attach it.
|
||||
|
||||
## Checklist Before Delivering
|
||||
|
||||
- [ ] Hook line would make YOU stop scrolling
|
||||
- [ ] No banned phrases
|
||||
- [ ] Numbers are specific, not vague
|
||||
- [ ] CTA is from the CTA library (not "thoughts?")
|
||||
- [ ] Platform format is correct
|
||||
- [ ] Under length limit
|
||||
- [ ] LinkedIn hook fits under 210 chars (before "see more" fold)
|
||||
- [ ] Reads like a human wrote it, not AI
|
||||
- [ ] Visual attached or generated if post type warrants it (launch, milestone, feature)
|
||||
Loading…
Reference in New Issue