Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/scripts/digest-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
You summarise changes to pstack, Lauren Tan's plugin of agent skills, agents and playbooks for Cursor. The reader is garyj. He installs pstack into Claude Code and Codex through a fork of Michael Ramos's mirror, and he reads these summaries to learn what Lauren is doing and how she works.

The user message is a JSON list of the merged pull requests in this update, oldest first. Each has a number, URL, title, author, description and the pstack files it changed.

Write the body of a GitHub issue in Markdown:

1. Open with two or three sentences on the changes that matter most and why.
2. Group the changes under `##` headings by theme, such as new skills, changes to existing skills, principles, playbooks and scripts, agents, fixes, and docs. Leave out empty groups. Each bullet says what changed and why, and ends with the PR link as `[#123](url)`. Combine PRs that changed the same thing into one bullet.
3. Add a `## How she did it` section with the methods the descriptions report: evals and their results, measurements, token or word cuts, and community PRs she reworked and credited. Leave the section out if the descriptions report none.
4. Add a `## Cursor-only` section for changes that only affect Cursor, such as Cursor manifests, Cursor or Grok Bot behaviour, or the Origin forge, so they don't change anything in Claude Code or Codex. Leave it out if there are none.

Rules:

- Use only facts from the descriptions and file lists. When a description gives no reason, don't invent one.
- Name skills as `/name` and principles by their folder name.
- Use plain words and short sentences. No em dashes, no hype, no closing summary.
- When an author other than poteto wrote a PR, say so in its bullet.
98 changes: 98 additions & 0 deletions .github/scripts/digest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# /// script
# requires-python = ">=3.12"
# dependencies = ["anthropic>=1.15"]
# ///
"""Summarise the cursor/plugins PRs that changed pstack between two commits."""

import base64
import json
import re
import subprocess
import sys
from pathlib import Path

import anthropic

REPO = 'cursor/plugins'
MODEL = 'claude-sonnet-5-5'
PROMPT = Path(__file__).with_name('digest-prompt.md').read_text()


def gh(path: str) -> list | dict:
out = subprocess.run(['gh', 'api', '--paginate', '--slurp', path], capture_output=True, text=True, check=True)
pages = json.loads(out.stdout)
return [item for page in pages for item in page] if isinstance(pages[0], list) else pages[0]


def version(sha: str) -> str:
manifest = gh(f'repos/{REPO}/contents/pstack/.cursor-plugin/plugin.json?ref={sha}')
return json.loads(base64.b64decode(manifest['content']))['version']


def commits_between(old: str, new: str) -> list[str]:
shas = []
for commit in gh(f'repos/{REPO}/commits?path=pstack&sha={new}&per_page=100'):
if commit['sha'].startswith(old):
Comment on lines +34 to +35
return shas[::-1]
shas.append(commit['sha'])
sys.exit(f'{old} is not an ancestor of {new} in {REPO}')


def clean(body: str | None) -> str:
body = re.sub(r'<!-- CURSOR_SUMMARY -->.*?<!-- /CURSOR_SUMMARY -->', '', body or '', flags=re.DOTALL)
body = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL)
return re.sub(r'<div><a href=\"https://cursor\.com/agents.*', '', body).strip()


def pull_requests(shas: list[str]) -> list[dict]:
prs = {}
for sha in shas:
for pr in gh(f'repos/{REPO}/commits/{sha}/pulls'):
if pr['merged_at'] and not pr['title'].startswith('chore(pstack): bump'):
prs[pr['number']] = pr
return [
{
'number': pr['number'],
'url': pr['html_url'],
'title': pr['title'],
'author': pr['user']['login'],
'description': clean(pr['body']),
'files': [
f"{f['filename']} (+{f['additions']} -{f['deletions']})"
for f in gh(f"repos/{REPO}/pulls/{pr['number']}/files?per_page=100")
if f['filename'].startswith('pstack/')
],
}
for pr in sorted(prs.values(), key=lambda pr: pr['merged_at'])
]


def summarise(prs: list[dict]) -> str:
response = anthropic.Anthropic().beta.messages.create(
model=MODEL,
max_tokens=16000,
betas=['server-side-fallback-2026-07-01'],
fallbacks='default',
system=PROMPT,
messages=[{'role': 'user', 'content': json.dumps(prs, indent=2)}],
)
if response.stop_reason != 'end_turn':
sys.exit(f'Claude stopped with {response.stop_reason} (request {response._request_id})')
return ''.join(block.text for block in response.content if block.type == 'text')


def main(old: str, new: str, out_dir: Path) -> None:
prs = pull_requests(commits_between(old, new))
if not prs:
sys.exit(f'No merged pstack PRs between {old} and {new}')
Comment on lines +86 to +87
old_version, new_version = version(old), version(new)
(out_dir / 'title.txt').write_text(f'pstack {old_version} → {new_version}: what Lauren changed')
(out_dir / 'body.md').write_text(
f'{summarise(prs)}\n\n---\n\n'
f'[Cursor diff {old[:7]}...{new[:7]}](https://github.com/{REPO}/compare/{old}...{new}) · '
f'{len(prs)} PRs · summarised by `{MODEL}`\n'
)


if __name__ == '__main__':
main(sys.argv[1], sys.argv[2], Path(sys.argv[3]))
50 changes: 48 additions & 2 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@ on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
inputs:
from:
description: Cursor commit to summarise from (skips the sync)
required: false
to:
description: Cursor commit to summarise to
required: false

permissions:
contents: write
issues: write

concurrency:
group: sync
cancel-in-progress: false

jobs:
sync:
if: ${{ !inputs.from }}
runs-on: ubuntu-latest
outputs:
from: ${{ steps.range.outputs.from }}
to: ${{ steps.range.outputs.to }}
moved: ${{ steps.upstream.outputs.moved }}
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -26,8 +39,12 @@ jobs:
git remote add michael https://github.com/backnotprop/pstack.git
git fetch michael main upstream

- name: Set upstream to michael/upstream
run: git push --force origin refs/remotes/michael/upstream:refs/heads/upstream
- name: Record the Cursor commits this sync spans
id: range
run: |
cursor_sha() { git log -1 --format=%s "$1" | sed -n 's|^upstream: cursor/plugins/pstack @ ||p'; }
echo "from=$(cursor_sha origin/upstream)" >> "$GITHUB_OUTPUT"
echo "to=$(cursor_sha michael/upstream)" >> "$GITHUB_OUTPUT"

- name: Merge michael/main
run: |
Expand All @@ -48,6 +65,12 @@ jobs:
- name: Push
run: git push origin HEAD:main

- name: Set upstream to michael/upstream
id: upstream
run: |
git push --force origin refs/remotes/michael/upstream:refs/heads/upstream
echo "moved=true" >> "$GITHUB_OUTPUT"
Comment on lines +71 to +72

- name: Check .apm/ covers everything pstack ships
run: |
diff -r agents .apm/agents
Expand All @@ -60,3 +83,26 @@ jobs:
echo "::error::New top-level entries that .apm/ may not cover: $unknown"
exit 1
fi

digest:
needs: sync
if: ${{ always() && (inputs.from || (needs.sync.outputs.moved == 'true' && needs.sync.outputs.from != needs.sync.outputs.to)) }}
runs-on: ubuntu-latest
env:
FROM: ${{ inputs.from || needs.sync.outputs.from }}
TO: ${{ inputs.to || needs.sync.outputs.to }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v7

- uses: astral-sh/setup-uv@v10.2.0
with:
enable-cache: false

- name: Summarise Lauren's changes
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: uv run .github/scripts/digest.py "$FROM" "$TO" "$RUNNER_TEMP"

- name: Open the digest issue
run: gh issue create -R "$GITHUB_REPOSITORY" --title "$(cat "$RUNNER_TEMP/title.txt")" --body-file "$RUNNER_TEMP/body.md"
Loading