Skip to content

github-gstack-intelligence-agent #187

github-gstack-intelligence-agent

github-gstack-intelligence-agent #187

# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ GitHub GStack Intelligence — Agent Workflow (v1.0.5) ║
# ║ ║
# ║ A Githubification of garrytan/gstack — Garry Tan's (CEO, Y Combinator) ║
# ║ collection of 39 AI engineering skills. This workflow adapts 26 of those ║
# ║ skills to run autonomously on GitHub Actions, using Issues as a ║
# ║ conversational UI, Git as persistent memory, and Actions as the compute ║
# ║ layer. No external servers, databases, or CLI tools required. ║
# ║ ║
# ║ PROVENANCE: ║
# ║ Skill prompts : garrytan/gstack (upstream, refreshable via dispatch) ║
# ║ Agent runtime : pi-mono by Earendil (@earendil-works/pi-coding-agent) ║
# ║ Architecture : japer-technology/githubification methodology ║
# ║ Family member : one of 10+ github-*-intelligence insertable AI agents ║
# ║ in the japer-technology ecosystem ║
# ║ ║
# ║ QUICK START — copy this file into any repo, four steps to a live agent: ║
# ║ ║
# ║ 1. Copy this file → .github/workflows/ in your repository. ║
# ║ 2. Add an LLM API key as a repository secret ║
# ║ (Settings → Secrets and variables → Actions). ║
# ║ At minimum, add ONE of: ║
# ║ • OPENAI_API_KEY (OpenAI — default, uses GPT-5.4) ║
# ║ • ANTHROPIC_API_KEY (Anthropic Claude) ║
# ║ • GEMINI_API_KEY (Google Gemini) ║
# ║ • XAI_API_KEY (xAI Grok) ║
# ║ • OPENROUTER_API_KEY (OpenRouter / DeepSeek) ║
# ║ • MISTRAL_API_KEY (Mistral) ║
# ║ • GROQ_API_KEY (Groq) ║
# ║ 3. Run the workflow manually: ║
# ║ Actions → github-gstack-intelligence-agent → Run workflow. ║
# ║ This installs the agent folder into your repo (or upgrades it). ║
# ║ 4. Open an issue — the agent reads your message and replies. ║
# ║ ║
# ║ HOW IT WORKS: ║
# ║ • Every issue is a persistent conversation. Comment again to continue. ║
# ║ • Session transcripts (JSONL) are committed to git — fully auditable. ║
# ║ • Only repo collaborators with write/maintain/admin access can trigger ║
# ║ the agent. Unauthorized users receive a 👎 reaction — no silent fail. ║
# ║ • Pull requests auto-trigger /review. Labels gate /cso and /design-review.║
# ║ • Scheduled crons run /retro (Fridays) and /benchmark (daily) if enabled. ║
# ║ • Releases trigger /document-release. Deployments trigger /canary. ║
# ║ ║
# ║ WHAT THIS WORKFLOW CONTAINS (four jobs): ║
# ║ run-install — Self-installer/upgrader from template repo. ║
# ║ run-refresh-gstack — Pulls latest skills from garrytan/gstack upstream. ║
# ║ run-agent — Core AI agent: routes events → skills → LLM → Git. ║
# ║ run-gitpages — Publishes public-site directory to GitHub Pages. ║
# ║ ║
# ║ COST DISCLOSURE: ║
# ║ Every agent run consumes LLM API credits. The default model (GPT-5.4 ║
# ║ with high thinking) costs roughly $0.50–$2.00 per invocation depending ║
# ║ on context size. On an active repo (20+ PRs/day, slash commands, ║
# ║ scheduled skills), monthly costs can reach $500–$2000+. Cost controls ║
# ║ (model tiering, rate limiting, diff-based filtering) are planned but ║
# ║ NOT YET IMPLEMENTED as of v1.0.5. Monitor your LLM provider dashboard. ║
# ║ ║
# ║ DATA DISCLOSURE: ║
# ║ The agent sends issue/PR content, code diffs, and repository files to ║
# ║ your configured LLM provider's API. All session data is committed to ║
# ║ your repo's git history (visible to anyone with repo access). No data ║
# ║ is sent to japer-technology or any third party beyond the LLM provider. ║
# ║ ║
# ║ LIMITATIONS (v1.0.5): ║
# ║ • No per-skill model tiering — all skills use the same model/settings. ║
# ║ • No rate limiting — concurrent issues each trigger separate LLM calls. ║
# ║ • 4-hour timeout per agent run — complex pipelines may be cut short. ║
# ║ • Memory (state/memory.log) is append-only with no automatic pruning. ║
# ║ • 13 of gstack's 39 skills are not adapted (require local-only features). ║
# ║ ║
# ║ Source: https://github.com/japer-technology/github-gstack-intelligence ║
# ║ Docs : .github-gstack-intelligence/README.md (installed after step 3) ║
# ║ Skills: .github-gstack-intelligence/docs/SKILLS.md ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
name: github-gstack-intelligence-agent
# ──────────────────────────────────────────────────────────────────────────────
# TRIGGERS
# This workflow listens for eight classes of GitHub event. Each event is routed
# to a specific skill by router.ts (TypeScript, not YAML conditionals). The
# `if:` guards on each job filter at the workflow level; fine-grained routing
# (slash commands, labels, config-driven skill enablement) happens in code.
#
# Event flow: GitHub event → workflow trigger → job `if:` guard → router.ts
# → skill prompt → pi-coding-agent (LLM) → comment + git commit.
# ──────────────────────────────────────────────────────────────────────────────
on:
# 1. A new issue is opened → the agent reads it and posts an AI response.
issues:
types: [opened]
# 2. A comment is added to an existing issue → the agent continues the
# conversation, loading the full session history from git.
issue_comment:
types: [created]
# 3. A pull request is opened or updated → the agent runs a /review skill
# (or /cso if the security-audit label is present).
pull_request:
types: [opened, synchronize]
# 4. Code is pushed to main → triggers a GitHub Pages deployment so the
# agent's public-site stays up to date.
# paths-ignore ensures that editing this workflow file alone does NOT
# trigger a redundant Pages deploy.
push:
branches: ["main"]
paths-ignore:
- ".github/workflows/**"
# 5. Manual "Run workflow" button → installs the agent folder into your
# repository, or upgrades it when a newer version is available.
# Safe to re-run; it installs, upgrades, or skips as appropriate.
workflow_dispatch:
inputs:
function:
description: "Workflow function to run"
required: false
default: run-install
type: choice
options:
- run-install
- run-refresh-gstack
# 6. Scheduled triggers for automated skills.
# IMPORTANT: these skills are DISABLED by default in config.json.
# Enable them explicitly before relying on scheduled output.
# Each cron run consumes one LLM invocation worth of API credits.
schedule:
- cron: '0 17 * * 5' # Every Friday at 5 PM UTC → /retro (weekly retrospective, if enabled).
- cron: '0 6 * * *' # Every day at 6 AM UTC → /benchmark (daily performance check, if enabled).
# 7. A release is published → /document-release skill updates documentation.
release:
types: [published]
# 8. A deployment succeeds → /canary skill runs post-deployment monitoring.
deployment_status:
# ──────────────────────────────────────────────────────────────────────────────
# PERMISSIONS
# Least-privilege set. Each permission is required for a specific capability;
# removing any one will break the corresponding feature. These permissions
# apply to the GITHUB_TOKEN generated for this workflow run — they do NOT
# grant access to other repositories or external systems.
# ──────────────────────────────────────────────────────────────────────────────
permissions:
contents: write # Read repo files, commit session state, push agent edits and installed files.
issues: write # Post AI replies as issue comments, add/remove reaction indicators (🚀/👍/👎).
pull-requests: write # Post review/CSO/design-review findings as PR comments, add reactions.
actions: write # Required so run-install commits can trigger subsequent workflow runs.
pages: write # Upload and deploy the public-site directory to GitHub Pages.
id-token: write # OIDC token for actions/deploy-pages — required by GitHub's Pages deployment.
# ══════════════════════════════════════════════════════════════════════════════
# JOBS
# ══════════════════════════════════════════════════════════════════════════════
jobs:
# ────────────────────────────────────────────────────────────────────────────
# JOB 1 — run-install
#
# Purpose : Self-installer and upgrader. Downloads the agent folder from the
# template repository (japer-technology/github-gstack-intelligence)
# and commits it into YOUR repo. Compares semver VERSION files to
# decide: install (no folder), upgrade (remote > local), or skip.
# Trigger : workflow_dispatch with function=run-install (manual button only).
# Safe : Idempotent. Re-running never damages existing state.
# Upgrade : File-by-file with three categories:
# ALWAYS overwrite — lifecycle code, skills, package.json, VERSION
# NEVER overwrite — config.json, AGENTS.md, .pi/settings.json,
# .pi/APPEND_SYSTEM.md, .pi/BOOTSTRAP.md, state/
# DEFAULT — copy only if file is missing locally
# User customizations in the NEVER category survive every upgrade.
# ────────────────────────────────────────────────────────────────────────────
run-install:
runs-on: ubuntu-latest
timeout-minutes: 10 # Prevent runaway downloads or git operations from burning CI minutes.
# Only run when triggered manually via the Actions UI.
# Skip when running inside the template repository itself — the run-install job
# downloads FROM that repo, so running it there would be self-referential.
if: >-
github.event_name == 'workflow_dispatch'
&& github.event.inputs.function == 'run-install'
&& github.repository != 'japer-technology/github-gstack-intelligence'
steps:
# 1. Check out the repository so we can inspect and modify its contents.
- name: Checkout
uses: actions/checkout@v6
with:
# Always operate on the default branch (usually "main").
ref: ${{ github.event.repository.default_branch }}
# 2. Determine whether to install, upgrade, or skip.
# • No folder → action=install
# • Folder present, local VERSION < template VERSION → action=upgrade
# • Folder present, local VERSION >= template VERSION → action=skip
- name: Check for .github-gstack-intelligence
id: check-folder
run: |
if [ ! -d ".github-gstack-intelligence" ]; then
echo "action=install" >> "$GITHUB_OUTPUT"
echo "📦 .github-gstack-intelligence not found — will install."
else
LOCAL_VERSION="0.0.0"
if [ -f ".github-gstack-intelligence/VERSION" ]; then
LOCAL_VERSION=$(tr -d '[:space:]' < .github-gstack-intelligence/VERSION)
fi
# Fetch only the VERSION file from the template repository.
REMOTE_VERSION=$(curl -fsSL "https://raw.githubusercontent.com/japer-technology/github-gstack-intelligence/main/.github-gstack-intelligence/VERSION" | tr -d '[:space:]' || true)
if [ -z "$REMOTE_VERSION" ]; then
echo "::warning::Could not fetch remote VERSION — skipping upgrade check."
echo "action=skip" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Local VERSION: $LOCAL_VERSION"
echo "Remote VERSION: $REMOTE_VERSION"
# Validate that both versions look like semver (digits and dots).
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+$'
if ! [[ "$LOCAL_VERSION" =~ $SEMVER_RE ]] || ! [[ "$REMOTE_VERSION" =~ $SEMVER_RE ]]; then
echo "::warning::VERSION format is not valid semver — skipping upgrade check."
echo "action=skip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Compare semver components (major.minor.patch).
IFS='.' read -r L_MAJOR L_MINOR L_PATCH <<< "$LOCAL_VERSION"
IFS='.' read -r R_MAJOR R_MINOR R_PATCH <<< "$REMOTE_VERSION"
NEEDS_UPGRADE=false
if [ "$R_MAJOR" -gt "$L_MAJOR" ]; then
NEEDS_UPGRADE=true
elif [ "$R_MAJOR" -eq "$L_MAJOR" ] && [ "$R_MINOR" -gt "$L_MINOR" ]; then
NEEDS_UPGRADE=true
elif [ "$R_MAJOR" -eq "$L_MAJOR" ] && [ "$R_MINOR" -eq "$L_MINOR" ] && [ "$R_PATCH" -gt "$L_PATCH" ]; then
NEEDS_UPGRADE=true
fi
if [ "$NEEDS_UPGRADE" = true ]; then
echo "action=upgrade" >> "$GITHUB_OUTPUT"
echo "⬆️ Upgrade available: $LOCAL_VERSION → $REMOTE_VERSION"
else
echo "action=skip" >> "$GITHUB_OUTPUT"
echo "✅ Local version ($LOCAL_VERSION) >= remote version ($REMOTE_VERSION) — nothing to do."
fi
fi
# 3. Download the template repository as a zip, extract it, and apply
# install or upgrade rules.
# On fresh install: copies the agent folder and initialises defaults.
# On upgrade: walks the template file-by-file using three categories:
# ALWAYS overwrite — framework code (lifecycle/, skills/, etc.)
# NEVER overwrite — user-customisable files (config, state, etc.)
# DEFAULT — copy only if the file is new locally
# This ensures user additions and modifications are never lost, even
# for files outside the explicit preserve list.
- name: Download and install from template
if: steps.check-folder.outputs.action != 'skip'
env:
INSTALL_ACTION: ${{ steps.check-folder.outputs.action }}
run: |
set -euo pipefail
ACTION="$INSTALL_ACTION"
TARGET=".github-gstack-intelligence"
# Download the latest template from the main branch.
curl -fsSL "https://github.com/japer-technology/github-gstack-intelligence/archive/refs/heads/main.zip" \
-o /tmp/template.zip
unzip -q /tmp/template.zip -d /tmp/template
EXTRACTED=$(ls -d /tmp/template/github-gstack-intelligence-*)
# Remove items from the extracted template that must not be copied
# into the user's repo (heavy dependencies and internal analysis).
rm -rf "$EXTRACTED/$TARGET/node_modules"
rm -rf "$EXTRACTED/$TARGET/docs/analysis"
rm -rf "$EXTRACTED/$TARGET/public-site"
if [ "$ACTION" = "upgrade" ]; then
# ── File-by-file upgrade ────────────────────────────────────
#
# Instead of deleting everything and restoring backups, we walk
# through the template file-by-file and apply upgrade rules.
# Files that exist locally but are NOT in the ALWAYS-overwrite
# list are left untouched — the user may have modified them on
# purpose, or added new files since the last install.
# ────────────────────────────────────────────────────────────
# Remove source repo's session state from the template so it is
# never copied — the "never" category alone is not enough because
# it copies template files when they don't exist locally.
rm -rf "$EXTRACTED/$TARGET/state"
echo "── Upgrade: processing template files ──"
while IFS= read -r relpath; do
relpath="${relpath#./}"
SRC="$EXTRACTED/$TARGET/$relpath"
DST="$TARGET/$relpath"
# ── Classify the file ──
# "always" takes precedence: checked first, then "never" only
# applies if the file was not already classified as "always".
CATEGORY="default"
# Always overwrite: framework code that must stay current.
case "$relpath" in
lifecycle/*) CATEGORY="always" ;;
skills/*) CATEGORY="always" ;;
package.json) CATEGORY="always" ;;
bun.lock) CATEGORY="always" ;;
VERSION) CATEGORY="always" ;;
esac
# Never overwrite: user customisations that must be preserved.
if [ "$CATEGORY" = "default" ]; then
case "$relpath" in
config.json) CATEGORY="never" ;;
AGENTS.md) CATEGORY="never" ;;
.pi/settings.json) CATEGORY="never" ;;
.pi/APPEND_SYSTEM.md) CATEGORY="never" ;;
.pi/BOOTSTRAP.md) CATEGORY="never" ;;
state/*) CATEGORY="never" ;;
esac
fi
# ── Apply the rule ──
case "$CATEGORY" in
always)
mkdir -p "$(dirname "$DST")"
cp "$SRC" "$DST"
echo " ↻ overwrite $relpath"
;;
never)
if [ -f "$DST" ]; then
echo " ✓ preserve $relpath"
else
mkdir -p "$(dirname "$DST")"
cp "$SRC" "$DST"
echo " + new $relpath (default from template)"
fi
;;
default)
if [ -f "$DST" ]; then
echo " · skip $relpath (exists locally — may be customised)"
else
mkdir -p "$(dirname "$DST")"
cp "$SRC" "$DST"
echo " + new $relpath"
fi
;;
esac
done < <( cd "$EXTRACTED/$TARGET" && find . -type f | sort )
echo "── Upgrade complete ──"
else
# Fresh install.
cp -R "$EXTRACTED/$TARGET" "$TARGET"
# Remove the source repo's session state — each repo starts fresh.
rm -rf "$TARGET/state"
# Initialise defaults for a fresh install:
# • AGENTS.md — the agent's identity file (editable by the user).
# • settings.json — default LLM provider and model configuration.
cp "$TARGET/install/GSTACK-INTELLIGENCE-AGENTS.md" "$TARGET/AGENTS.md"
mkdir -p "$TARGET/.pi"
cp "$TARGET/install/settings.json" "$TARGET/.pi/settings.json"
fi
# 4. Ensure common ignore patterns are present in .gitignore so that
# node_modules and OS junk files never get committed.
- name: Ensure .gitignore entries
if: steps.check-folder.outputs.action != 'skip'
run: |
touch .gitignore
for entry in "node_modules/" ".github-gstack-intelligence/node_modules/" ".DS_Store"; do
grep -qxF "$entry" .gitignore || echo "$entry" >> .gitignore
done
# 4b. Ensure required Git attributes are present in .gitattributes so
# that the append-only memory log merges correctly across parallel
# agent runs (union merge driver).
- name: Ensure .gitattributes entries
if: steps.check-folder.outputs.action != 'skip'
run: |
touch .gitattributes
for entry in "memory.log merge=union"; do
grep -qxF "$entry" .gitattributes || echo "$entry" >> .gitattributes
done
# 5. Commit and push. Uses the appropriate message for install vs upgrade.
# If nothing changed (edge case), the step is a harmless no-op.
- name: Commit and push
if: steps.check-folder.outputs.action != 'skip'
env:
INSTALL_ACTION: ${{ steps.check-folder.outputs.action }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .github-gstack-intelligence/ .gitignore .gitattributes
if [ "$INSTALL_ACTION" = "upgrade" ]; then
COMMIT_MSG="chore: upgrade .github-gstack-intelligence from template"
else
COMMIT_MSG="chore: install .github-gstack-intelligence from template"
fi
if git diff --cached --quiet; then
echo "No changes to commit."
else
git commit -m "$COMMIT_MSG"
git push
fi
# ────────────────────────────────────────────────────────────────────────────
# JOB 2 — run-refresh-gstack
#
# Purpose : Pull the latest skill prompts and reference files from upstream
# garrytan/gstack. This is the supply chain for AI prompts —
# skills are vendored like dependencies, tracked by commit SHA,
# and diffable in git history.
# Source : garrytan/gstack @ main (commit SHA recorded in skills/source.json)
# Process : refresh.ts fetches raw files → removes AskUserQuestion (local-only
# interactive prompt) → replaces browse daemon refs with Playwright
# → stamps generated-file marker → writes to skills/ directory.
# Verify : Python validation ensures every output file exists, has content
# ≥50 bytes, contains the generated marker, and the do-not-edit
# warning. Fails the job if any check fails.
# Trigger : workflow_dispatch with function=run-refresh-gstack.
# ────────────────────────────────────────────────────────────────────────────
run-refresh-gstack:
runs-on: ubuntu-latest
timeout-minutes: 10 # Prevent hung refresh operations from burning CI minutes.
if: >-
github.event_name == 'workflow_dispatch'
&& github.event.inputs.function == 'run-refresh-gstack'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.repository.default_branch }}
- name: Check for .github-gstack-intelligence
id: check-folder
run: |
if [ -d ".github-gstack-intelligence" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "::error::.github-gstack-intelligence folder not found."
exit 1
fi
- name: Setup Bun
if: steps.check-folder.outputs.exists == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2"
- name: Run refresh
if: steps.check-folder.outputs.exists == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bun .github-gstack-intelligence/lifecycle/refresh.ts
- name: Verify refreshed gstack files
if: steps.check-folder.outputs.exists == 'true'
run: |
python3 - <<'PY'
import json
import pathlib
import sys
root = pathlib.Path(".github-gstack-intelligence")
metadata_path = root / "skills" / "source.json"
if not metadata_path.exists():
print("::error::Missing .github-gstack-intelligence/skills/source.json after refresh.")
sys.exit(1)
metadata = json.loads(metadata_path.read_text())
# Validate source metadata.
source = metadata.get("source", {})
if not source.get("repo"):
print("::error::source.json is missing a valid 'source.repo' field.")
sys.exit(1)
if not source.get("ref"):
print("::error::source.json is missing a valid 'source.ref' field.")
sys.exit(1)
if not source.get("commit"):
print("::warning::source.json has no resolved commit SHA — using ref only.")
inputs = metadata.get("inputs", [])
outputs = metadata.get("outputs", [])
if not inputs:
print("::error::Refresh metadata does not list any checked gstack source files.")
sys.exit(1)
if not outputs:
print("::error::Refresh metadata does not list any copied files.")
sys.exit(1)
GENERATED_MARKER = "<!-- GSTACK-INTELLIGENCE: GENERATED FILE -->"
DO_NOT_TOUCH = "Do not touch"
missing_outputs = []
unmarked_outputs = []
unwarned_outputs = []
empty_outputs = []
for relative_path in outputs:
output_path = root / relative_path
if not output_path.exists():
missing_outputs.append(relative_path)
continue
content = output_path.read_text()
if len(content) < 50:
empty_outputs.append(relative_path)
continue
if output_path.suffix == ".md":
if GENERATED_MARKER not in content:
unmarked_outputs.append(relative_path)
if DO_NOT_TOUCH not in content:
unwarned_outputs.append(relative_path)
if missing_outputs:
print(f"::error::Refresh is missing copied gstack files: {', '.join(missing_outputs)}")
sys.exit(1)
if empty_outputs:
print(f"::error::Refreshed gstack files have no meaningful content: {', '.join(empty_outputs)}")
sys.exit(1)
if unmarked_outputs:
print(f"::error::Generated gstack files are missing the upgrade warning marker: {', '.join(unmarked_outputs)}")
sys.exit(1)
if unwarned_outputs:
print(f"::error::Generated gstack files are missing the 'Do not touch' warning: {', '.join(unwarned_outputs)}")
sys.exit(1)
print(f"✅ Validated {len(inputs)} upstream gstack files → {len(outputs)} copied outputs.")
print(f" Source: {source.get('repo')} @ {source.get('commit', source.get('ref'))}")
print(f" All outputs exist, have content, the generated marker, and the 'Do not touch' warning.")
PY
- name: Commit and push refreshed resources
if: steps.check-folder.outputs.exists == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .github-gstack-intelligence/
if git diff --cached --quiet; then
echo "No refresh changes to commit."
else
git commit -m "chore: refresh gstack resources"
git push
fi
# ────────────────────────────────────────────────────────────────────────────
# JOB 3 — run-agent
#
# Purpose : The core AI agent. Routes GitHub events to one of 26 specialist
# skills authored by Garry Tan (garrytan/gstack), executes the
# skill via pi-coding-agent (LLM call), posts the response as a
# comment, and commits session state to git.
#
# Pipeline: Event → router.ts (skill selection) → agent.ts (prompt assembly,
# context injection, session resume) → pi-coding-agent (LLM) →
# JSONL extraction → comment post → git commit+push with retry.
#
# Skills : /review (auto on PR), /cso (PR + label), /design-review (PR +
# label), /qa (slash cmd + URL), /qa-only, /investigate (label),
# /office-hours (label), /design-consultation (label), /ship,
# /autoplan (chains CEO→design→eng→DX), /plan-ceo-review,
# /plan-eng-review, /plan-design-review, /plan-devex-review,
# /retro (cron), /benchmark (cron), /document-release (release),
# /canary (deploy), /careful, /design-html, /design-shotgun,
# /devex-review, /guard, /health, /land-and-deploy, /learn
#
# Trigger : issues.opened, issue_comment.created, pull_request, schedule,
# release.published, deployment_status (success only).
# Security: Collaborators with write/maintain/admin only. Schedule, release,
# and deployment events bypass actor checks (no human actor).
# Cost : Each run is one LLM invocation. /autoplan chains four.
# Timeout : 30 minutes. Complex pipelines or large codebases may hit this.
# ────────────────────────────────────────────────────────────────────────────
run-agent:
runs-on: ubuntu-latest
timeout-minutes: 240 # LLM calls with high-thinking can take 5-10 min per skill. 4 hour cap
# prevents runaway billing but may truncate /autoplan (chains 4 skills).
# Concurrency: one agent run per target (issue or PR) at a time.
# For issues: the second comment waits for the first to finish (cancel-in-progress: false).
# For PRs: a new push cancels the previous review (cancel-in-progress via expression).
# For schedule/release/deployment_status: one run per event type at a time.
concurrency:
group: >-
github-gstack-intelligence-${{ github.repository }}-${{
github.event_name == 'pull_request'
&& format('pr-{0}', github.event.pull_request.number)
|| github.event_name == 'schedule'
&& format('schedule-{0}', github.event.schedule)
|| github.event_name == 'release'
&& format('release-{0}', github.event.release.tag_name)
|| github.event_name == 'deployment_status'
&& format('deploy-{0}', github.event.deployment.id)
|| format('issue-{0}', github.event.issue.number)
}}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Trigger guard:
# • Run on new issues.
# • Run on issue comments, BUT skip comments posted by bots (the agent
# itself) to avoid infinite loops, AND skip comments on pull requests
# (issue_comment fires for both issues and PRs).
# • Run on pull_request events (opened, synchronize), skipping bot-authored PRs.
# • Run on schedule events (no actor check — runs as github-actions).
# • Run on release events (published).
# • Run on deployment_status events (only successful deployments).
# • Reject the github-actions actor (both 'github-actions' and
# 'github-actions[bot]') for non-scheduled/release/deployment events
# to prevent bot-loop escalation.
if: >-
(github.event_name == 'schedule')
|| (github.event_name == 'release')
|| (github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success')
|| (
github.actor != 'github-actions'
&& github.actor != 'github-actions[bot]'
&& (
(github.event_name == 'issues')
|| (github.event_name == 'issue_comment' && !github.event.issue.pull_request && !endsWith(github.event.comment.user.login, '[bot]'))
|| (github.event_name == 'pull_request' && !endsWith(github.event.pull_request.user.login, '[bot]'))
)
)
steps:
# 1. AUTHORIZATION — verify the actor has write-level (or higher) access
# to the repository. This prevents random users on public repos from
# consuming your LLM credits. On success a 🚀 reaction is added to
# signal the agent is working; the reaction state is saved to a temp
# file so the agent can later replace it with 👍 on completion.
- name: Authorize
id: authorize
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── Pass GitHub context through environment variables ──────────
# Avoids direct ${{ }} interpolation in shell code, which is an
# expression-injection vector when values are attacker-controlled.
# These specific values are GitHub-controlled (not user input), but
# using env vars is a defence-in-depth best practice.
EVENT_NAME: ${{ github.event_name }}
ACTOR: ${{ github.actor }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Schedule, release, and deployment_status events have no human actor —
# the workflow runs as github-actions. Skip authorization for these.
if [[ "$EVENT_NAME" == "schedule" || "$EVENT_NAME" == "release" || "$EVENT_NAME" == "deployment_status" ]]; then
echo "::notice::${EVENT_NAME} trigger — no actor authorization needed"
exit 0
fi
# Query the GitHub API for the actor's permission level on this repo.
PERM=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.permission' 2>/dev/null || echo "none")
echo "Actor: ${ACTOR}, Permission: $PERM"
# Reject anyone below write access.
if [[ "$PERM" != "admin" && "$PERM" != "maintain" && "$PERM" != "write" ]]; then
echo "::error::Unauthorized: ${ACTOR} has '$PERM' permission"
exit 1
fi
# Add a 🚀 "rocket" reaction to the comment, issue, or PR as a visual
# indicator that the agent has started processing. Save the reaction
# ID so the agent can swap it for 👍 when it finishes.
# Use jq for safe JSON construction — handles empty/null values without
# producing malformed output.
if [[ "$EVENT_NAME" == "issue_comment" ]]; then
REACTION_ID=$(gh api "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content=rocket --jq '.id' 2>/dev/null || echo "")
jq -n \
--arg rid "$REACTION_ID" \
--arg cid "$COMMENT_ID" \
--arg inum "$ISSUE_NUMBER" \
--arg repo "$REPO" \
'{reactionId: (if $rid == "" then null else $rid end), reactionTarget: "comment", commentId: ($cid | tonumber), issueNumber: ($inum | tonumber), repo: $repo}' \
> /tmp/reaction-state.json
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
# PRs are issues in GitHub — use the issues reactions API with the PR number.
REACTION_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/reactions" -f content=rocket --jq '.id' 2>/dev/null || echo "")
jq -n \
--arg rid "$REACTION_ID" \
--arg prnum "$PR_NUMBER" \
--arg repo "$REPO" \
'{reactionId: (if $rid == "" then null else $rid end), reactionTarget: "issue", commentId: null, issueNumber: ($prnum | tonumber), repo: $repo}' \
> /tmp/reaction-state.json
else
REACTION_ID=$(gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/reactions" -f content=rocket --jq '.id' 2>/dev/null || echo "")
jq -n \
--arg rid "$REACTION_ID" \
--arg inum "$ISSUE_NUMBER" \
--arg repo "$REPO" \
'{reactionId: (if $rid == "" then null else $rid end), reactionTarget: "issue", commentId: null, issueNumber: ($inum | tonumber), repo: $repo}' \
> /tmp/reaction-state.json
fi
# 2. REJECTION FEEDBACK — if authorization failed, add a 👎 reaction so
# the user gets immediate visual feedback that their request was denied.
- name: Reject
if: failure() && steps.authorize.outcome == 'failure'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ "$EVENT_NAME" == "issue_comment" ]]; then
gh api "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content=-1
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
gh api "repos/${REPO}/issues/${PR_NUMBER}/reactions" -f content=-1
else
gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/reactions" -f content=-1
fi
# 3. CHECKOUT — clone the full repository (fetch-depth: 0) so the agent
# can read prior session history, project files, and commit new state.
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0 # Full history needed — the agent reads and commits session state.
# 4. SAFETY CHECK — ensure the agent folder exists. If the user hasn't
# run the run-install job yet, skip gracefully instead of crashing.
- name: Check for .github-gstack-intelligence
id: check-folder
run: |
if [ -d ".github-gstack-intelligence" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "::notice::.github-gstack-intelligence folder not found, skipping."
fi
# 5. RUNTIME — install Bun, a fast JavaScript/TypeScript runtime. The
# agent code (lifecycle/agent.ts) runs directly under Bun without a
# separate compile step.
- name: Setup Bun
if: steps.check-folder.outputs.exists == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2" # Pinned for reproducible builds across runs.
# 6. CACHE — restore node_modules from a prior run when bun.lock hasn't
# changed. Shaves ~5-10 seconds off the typical cold-start install.
- name: Cache dependencies
if: steps.check-folder.outputs.exists == 'true'
uses: actions/cache@v5
with:
path: .github-gstack-intelligence/node_modules
key: gsi-deps-${{ hashFiles('.github-gstack-intelligence/bun.lock') }}
# 7. INSTALL — install (or verify) the agent's npm dependencies.
# --frozen-lockfile ensures the lockfile is never modified, so builds
# are deterministic.
- name: Install dependencies
if: steps.check-folder.outputs.exists == 'true'
run: cd .github-gstack-intelligence && bun install --frozen-lockfile
# 8. PLAYWRIGHT (CONDITIONAL) — install the Chromium browser only when
# a browser-based skill is being invoked (/qa, /qa-only, /design-review,
# /canary, /devex-review, /design-html, /design-shotgun, /land-and-deploy).
# Adds ~30s and ~200MB to the runner, so we skip it for all
# other skills to keep runs fast.
# IMPORTANT: the playwright version is read from package.json to stay
# in sync with the installed playwright-core dependency. A version
# mismatch would download an incompatible Chromium binary.
- name: Install Playwright (conditional)
if: >-
steps.check-folder.outputs.exists == 'true'
&& (
(github.event_name == 'issue_comment' && (
startsWith(github.event.comment.body, '/qa ') || github.event.comment.body == '/qa'
|| startsWith(github.event.comment.body, '/qa-only ') || github.event.comment.body == '/qa-only'
|| startsWith(github.event.comment.body, '/design-review')
|| startsWith(github.event.comment.body, '/canary')
|| startsWith(github.event.comment.body, '/devex-review')
|| startsWith(github.event.comment.body, '/design-html')
|| startsWith(github.event.comment.body, '/design-shotgun')
|| startsWith(github.event.comment.body, '/land-and-deploy')
))
|| github.event_name == 'deployment_status'
)
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
run: |
# Read the playwright-core version from package.json so the CLI
# downloads the exact Chromium build that matches the library.
PKG=".github-gstack-intelligence/package.json"
if [ ! -f "$PKG" ]; then
echo "::error::${PKG} not found — cannot determine playwright-core version."
exit 1
fi
PW_VERSION=$(node -e "
const deps = require('./${PKG}').dependencies || {};
const v = deps['playwright-core'];
if (!v) { console.error('playwright-core not found in ${PKG}'); process.exit(1); }
console.log(v);
")
echo "Installing Playwright CLI @ ${PW_VERSION} (matching playwright-core dependency)"
npx "playwright@${PW_VERSION}" install chromium --with-deps
# 9. RUN THE AGENT — execute the core agent script. It reads the
# triggering issue/comment, loads the matching conversation session,
# calls the configured LLM, posts the reply, and commits state.
#
# The `--route` flag enables the event router: slash commands, label
# routing, and skill-aware execution. Without it, all events go
# through general conversation.
#
# All supported LLM provider keys are passed as environment variables.
# Only the key for your chosen provider needs to be set as a secret;
# the others will simply be empty and are safely ignored.
- name: Run
if: steps.check-folder.outputs.exists == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
run: bun .github-gstack-intelligence/lifecycle/agent.ts --route
# ────────────────────────────────────────────────────────────────────────────
# JOB 4 — run-gitpages
#
# Purpose : Publish the agent's public-site directory as a GitHub Pages
# site. Provides a live web presence powered by the agent's
# public output — no separate hosting needed.
# Trigger : Runs after all other jobs complete (needs: clause). On push
# events the upstream jobs are skipped, so this starts immediately.
# Auto : Attempts to enable GitHub Pages via API on first run. If the
# API call fails (org policy restrictions), a warning guides the
# user to enable it manually — the job does not fail.
# Note : This job uses always() && !cancelled() so it runs even when
# upstream jobs are skipped (e.g. push events skip run-agent).
# ────────────────────────────────────────────────────────────────────────────
run-gitpages:
# Run after agent/install jobs finish. On push events those jobs are
# skipped so run-gitpages starts immediately.
needs: [run-agent, run-install, run-refresh-gstack]
# always() ensures the job runs even when upstream jobs are skipped;
# !cancelled() still respects manual cancellation.
if: always() && !cancelled()
runs-on: ubuntu-latest
timeout-minutes: 30 # Pages uploads are small; 30 min cap prevents stuck deployments.
# Concurrency: only one Pages deployment at a time across the entire repo.
concurrency:
group: "pages"
cancel-in-progress: false
# Declare the GitHub Pages deployment environment so the workflow run UI
# shows a direct link to the deployed site.
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
# 1. Check out the repo so we can read the public-site directory.
# ref: main ensures we pick up any commits the agent just pushed.
- name: Checkout
uses: actions/checkout@v6
with:
ref: main
# 2. Verify the public-site directory exists. If the agent has not
# been installed yet (e.g. on a fresh template repo) there is nothing
# to deploy, so we abort early with a clear warning instead of
# failing in a later step.
- name: Check public-site exists
id: check-folder
run: |
if [ -d ".github-gstack-intelligence/public-site" ]; then
echo "folder_exists=true" >> "$GITHUB_OUTPUT"
echo "✅ public-site directory found."
else
echo "folder_exists=false" >> "$GITHUB_OUTPUT"
echo "::warning::Directory .github-gstack-intelligence/public-site not found. Skipping Pages deployment. Run the installer first (Actions → Run workflow)."
fi
# 3. AUTO-ENABLE Pages — attempt to enable GitHub Pages via the API.
# This is a convenience so users don't have to visit Settings manually.
# If the API call fails (e.g. insufficient org permissions), a warning
# is surfaced and the remaining deploy steps are skipped gracefully.
- name: Enable Pages
if: steps.check-folder.outputs.folder_exists == 'true'
id: enable-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
# Check if Pages is already active.
if gh api "repos/${REPO}/pages" --silent 2>/dev/null; then
echo "pages_active=true" >> "$GITHUB_OUTPUT"
echo "✅ GitHub Pages is already enabled."
# If not, try to enable it with the "workflow" build type.
elif gh api "repos/${REPO}/pages" \
-X POST -f build_type=workflow --silent 2>/dev/null; then
echo "pages_active=true" >> "$GITHUB_OUTPUT"
echo "✅ GitHub Pages has been enabled."
else
echo "pages_active=false" >> "$GITHUB_OUTPUT"
echo "::warning::Could not enable GitHub Pages automatically. Please enable it manually in repository Settings → Pages → Source → GitHub Actions."
fi
# 4. Configure Pages — sets up the required Pages metadata.
- name: Setup Pages
if: steps.check-folder.outputs.folder_exists == 'true' && steps.enable-pages.outputs.pages_active == 'true'
uses: actions/configure-pages@v5
# 5. Upload the public-site directory as a Pages artifact.
- name: Upload artifact
if: steps.check-folder.outputs.folder_exists == 'true' && steps.enable-pages.outputs.pages_active == 'true'
uses: actions/upload-pages-artifact@v4
with:
path: '.github-gstack-intelligence/public-site'
# 6. Deploy the uploaded artifact to GitHub Pages.
- name: Deploy to GitHub Pages
if: steps.check-folder.outputs.folder_exists == 'true' && steps.enable-pages.outputs.pages_active == 'true'
id: deployment
uses: actions/deploy-pages@v4