Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .changeset/apm-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"squad": minor
---

feat: APM integration — squad skill publish/install + apm.yml in init

Implements #824. Adds `squad skill publish/install/list` commands and generates `apm.yml` on `squad init`.
5 changes: 5 additions & 0 deletions .changeset/deprecate-tunnel-rc-repl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bradygaster/squad-cli": patch
---

Add deprecation warnings for tunnel, rc, and REPL commands. The interactive shell (no-args), `squad start`, `squad start --tunnel`, `squad rc`, and `squad rc-tunnel` now emit yellow deprecation notices pointing users to the GitHub Copilot CLI. No behavior changes — all commands still work.
7 changes: 7 additions & 0 deletions .changeset/readiness-file-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
---

ci: add file list with line stats to PR readiness comment

The PR readiness bot now shows changed files with per-file addition/deletion
counts, scope classification (Product/Infrastructure/Mixed), and totals.
5 changes: 5 additions & 0 deletions .changeset/review-findings-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bradygaster/squad-cli': patch
---

fix: address post-merge review findings — YAML escaping, type safety, deprecation messages
8 changes: 8 additions & 0 deletions .changeset/scope-boundary-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

ci: scope boundary enforcement for repo-health PRs

New CI check that fails repo-health PRs if they modify product source
code under packages/*/src/. Enforces separation between infrastructure
and product changes.
8 changes: 8 additions & 0 deletions .changeset/smart-pr-nudge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

ci: add smart PR nudge for stale PRs

New workflow that runs on weekdays and posts actionable diagnoses on PRs
stale for 7+ days. Checks CI status, unresolved threads, missing reviews,
outdated branches, and draft status. Won't nudge the same PR twice per week.
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ Any PR that modifies files under `packages/squad-cli/src/` or `packages/squad-sd
- The `changelog-gate` CI check will fail without this
- Escape hatch: add the `skip-changelog` label (use sparingly)

## Automated PR Nudge

The **PR Nudge** workflow (`.github/workflows/squad-pr-nudge.yml`) runs on weekdays at 2pm UTC and posts actionable comments on open PRs that have been stale for 7+ days. It diagnoses specific blockers — failing CI checks, unresolved review threads, missing approvals, outdated branches, and draft status — so PR authors know exactly what to do next. Draft PRs get a 14-day grace period. The workflow won't nudge the same PR more than once per week.

## Decisions

If you make a decision that affects other team members, write it to:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/squad-insider-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,38 @@ jobs:
- name: Build packages
run: npm -w packages/squad-sdk run build && npm -w packages/squad-cli run build

- name: Pin CLI SDK dependency to exact workspace version
run: |
SDK_VERSION=$(node -p "require('./packages/squad-sdk/package.json').version")
echo "Pinning CLI SDK dep to exact version: $SDK_VERSION"
cd packages/squad-cli
node -e "
const pkg = require('./package.json');
pkg.dependencies['@bradygaster/squad-sdk'] = '$SDK_VERSION';
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "Verified: $(node -p "require('./package.json').dependencies['@bradygaster/squad-sdk']")"

- name: Publish squad-sdk with insider tag
run: npm -w packages/squad-sdk publish --tag insider --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Verify SDK is available on registry
run: |
SDK_VERSION=$(node -p "require('./packages/squad-sdk/package.json').version")
echo "Waiting for @bradygaster/squad-sdk@$SDK_VERSION on npm..."
for i in 1 2 3 4 5; do
if npm view "@bradygaster/squad-sdk@$SDK_VERSION" version 2>/dev/null; then
echo "✅ SDK $SDK_VERSION is live on npm"
exit 0
fi
echo "Attempt $i/5 — not yet visible, waiting 10s..."
sleep 10
done
echo "::error::SDK $SDK_VERSION not visible on npm after 50s"
exit 1

- name: Publish squad-cli with insider tag
run: npm -w packages/squad-cli publish --tag insider --access public
env:
Expand Down
184 changes: 184 additions & 0 deletions .github/workflows/squad-pr-nudge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: PR Nudge
on:
schedule:
- cron: '0 14 * * 1-5' # 2pm UTC weekdays (morning US Pacific)
workflow_dispatch: {} # manual trigger for testing

permissions:
contents: read
pull-requests: write
checks: read
issues: read

jobs:
nudge-stale-prs:
name: "Nudge Stale PRs"
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const STALE_DAYS = 7;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - STALE_DAYS);

// Get all open PRs, oldest-updated first
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 50
});

for (const pr of prs.data) {
// Skip PRs updated recently
const lastPush = new Date(pr.updated_at);
if (lastPush > cutoff) continue;

// Give draft PRs 14 days grace period instead of 7
if (pr.draft) {
const created = new Date(pr.created_at);
const draftCutoff = new Date();
draftCutoff.setDate(draftCutoff.getDate() - 14);
if (created > draftCutoff) continue;
}

// Don't nudge the same PR more than once per week
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 5,
sort: 'created',
direction: 'desc'
});
const recentNudge = comments.data.find(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes('<!-- pr-nudge -->') &&
new Date(c.created_at) > cutoff
);
if (recentNudge) continue;

// Build the diagnosis — collect actionable items
const actions = [];
const daysSinceUpdate = Math.floor((Date.now() - lastPush) / (1000 * 60 * 60 * 24));

// 1. Check if still in draft
if (pr.draft) {
actions.push('📝 **Still in draft** — mark as "Ready for review" when you\'re done, or close if abandoned.');
}

// 2. Check CI status for failures
const checks = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha,
per_page: 50
});
const failedChecks = checks.data.check_runs.filter(c =>
c.conclusion === 'failure'
).map(c => c.name);
if (failedChecks.length > 0) {
actions.push(`🔴 **${failedChecks.length} CI check(s) failing:** ${failedChecks.join(', ')}. Fix these first.`);
}

// 3. Check for unresolved review threads (Copilot vs human)
const threads = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 50) {
nodes {
isResolved
isOutdated
comments(first: 1) {
nodes { author { login } }
}
}
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
number: pr.number
});
const unresolvedThreads = threads.repository.pullRequest.reviewThreads.nodes
.filter(t => !t.isResolved && !t.isOutdated);
if (unresolvedThreads.length > 0) {
const copilotThreads = unresolvedThreads.filter(t =>
t.comments.nodes[0]?.author?.login?.includes('copilot')
);
const humanThreads = unresolvedThreads.length - copilotThreads.length;
const parts = [];
if (copilotThreads.length > 0) parts.push(`${copilotThreads.length} from Copilot`);
if (humanThreads > 0) parts.push(`${humanThreads} from reviewers`);
actions.push(`💬 **${unresolvedThreads.length} unresolved review thread(s)** (${parts.join(', ')}). Address and resolve them.`);
}

// 4. Check review state (changes requested vs approved vs none)
const reviews = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
const latestByUser = {};
for (const r of reviews.data) {
if (r.state === 'COMMENTED') continue;
latestByUser[r.user.login] = r;
}
const changesRequested = Object.values(latestByUser).filter(r => r.state === 'CHANGES_REQUESTED');
const approvals = Object.values(latestByUser).filter(r => r.state === 'APPROVED');
if (changesRequested.length > 0) {
const reviewers = changesRequested.map(r => `@${r.user.login}`).join(', ');
actions.push(`🔄 **Changes requested** by ${reviewers}. Address their feedback and request re-review.`);
} else if (approvals.length === 0 && !pr.draft) {
actions.push('👀 **No approving reviews yet.** Request a review from a teammate.');
}

// 5. Check if branch is behind base
const comparison = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: pr.head.sha,
head: pr.base.ref
});
if (comparison.data.ahead_by > 10) {
actions.push(`⬇️ **${comparison.data.ahead_by} commits behind ${pr.base.ref}.** Rebase to pick up latest changes.`);
}

// 6. If everything looks good and approved — it's ready to merge
if (actions.length === 0 && approvals.length > 0) {
actions.push('✅ **Looks ready to merge!** All checks pass, approved — just needs someone to click merge.');
}

// Fallback if no specific blockers found
if (actions.length === 0) {
actions.push('🤔 **No obvious blockers found** — but this PR has been quiet. Is it still active?');
}

// Post the nudge comment
const body = [
'<!-- pr-nudge -->',
`👋 **Friendly nudge** — this PR has had no activity for **${daysSinceUpdate} days**.`,
'',
'**What needs attention:**',
...actions.map(a => `- ${a}`),
'',
'---',
'*If this PR is abandoned, please close it. If it\'s blocked on something external, leave a comment so the team knows.*',
'*This is an automated check that runs on weekdays. It won\'t nudge the same PR more than once per week.*'
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: body
});

core.info(`Nudged PR #${pr.number}: ${pr.title} (${daysSinceUpdate} days stale, ${actions.length} action items)`);
}
4 changes: 2 additions & 2 deletions .github/workflows/squad-repo-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:

# ─── Architectural Review (INFORMATIONAL) ───────────────────────────
architectural-review:
name: Architectural Review
name: Architectural Review — Structure & Design Rules
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
Expand Down Expand Up @@ -150,7 +150,7 @@ jobs:

# ─── Security Review (INFORMATIONAL) ────────────────────────────────
security-review:
name: Security Review
name: Security Review — Permissions & Secrets
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/squad-scope-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Scope Check
on:
pull_request:
types: [opened, synchronize, reopened, labeled]

permissions:
pull-requests: read
contents: read

jobs:
scope-boundary:
name: "Scope Boundary"
runs-on: ubuntu-latest
if: >-
startsWith(github.head_ref, 'repo-health/') ||
contains(github.event.pull_request.labels.*.name, 'repo-health')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for product code in repo-health PR
run: |
PRODUCT_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'packages/squad-cli/src/' 'packages/squad-sdk/src/')
if [ -n "$PRODUCT_FILES" ]; then
echo "::error::Repo-health PRs must not modify product source code."
echo "::error::The following product files were changed:"
echo "$PRODUCT_FILES" | while read -r f; do echo "::error:: - $f"; done
echo "::error::Move product changes to a separate PR."
exit 1
fi
echo "✅ No product source files in this repo-health PR."
3 changes: 2 additions & 1 deletion .squad-templates/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ If the user says "I need a designer" or "add someone for DevOps":
4. **Update `.squad/casting/registry.json`** with the new agent entry.
5. Add to team.md roster.
6. Add routing entries to routing.md.
7. Say: *"✅ {CastName} joined the team as {Role}."*
7. **Wire enforcement (if applicable).** If the new member's role involves gating other agents' work (reviewer, design approver, quality gate), add a numbered enforcement rule to `routing.md` → Rules section. A routing table entry (step 6) only handles explicit requests — enforcement rules are required for automatic gates. Read `.squad/templates/workflow-wiring-guide.md` for the full wiring process, including walkthroughs for common role types (code reviewer, documenter). Check `.squad/templates/issue-lifecycle.md` for lifecycle integration if the project uses PR-gated workflows.
8. Say: *"✅ {CastName} joined the team as {Role}."*

### Removing Team Members

Expand Down
Loading