diff --git a/.gh-resolved b/.gh-resolved new file mode 100644 index 000000000..23f49df04 --- /dev/null +++ b/.gh-resolved @@ -0,0 +1 @@ +diberry/squad diff --git a/.github/workflows/squad-pr-target-guard.yml b/.github/workflows/squad-pr-target-guard.yml new file mode 100644 index 000000000..6816f70f8 --- /dev/null +++ b/.github/workflows/squad-pr-target-guard.yml @@ -0,0 +1,56 @@ +name: PR Target Guard + +on: + pull_request_target: + branches: [dev, main, preview, insider] + types: [opened, edited, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + target-repo-check: + name: "🔒 PR target repo check" + runs-on: ubuntu-latest + steps: + - name: Verify PR targets this fork (not upstream) + env: + EXPECTED_REPO: diberry/squad + PR_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD: ${{ github.event.pull_request.head.repo.full_name }} + PR_BASE: ${{ github.event.pull_request.base.repo.full_name }} + run: | + echo "PR #$PR_NUMBER" + echo " Head: $PR_HEAD" + echo " Base: $PR_BASE" + echo " Expected base repo: $EXPECTED_REPO" + + if [ "$PR_BASE" != "$EXPECTED_REPO" ]; then + echo "::error::PR targets '$PR_BASE' instead of '$EXPECTED_REPO'." + echo "::error::This is a fork — PRs should target diberry/squad, not upstream." + echo "" + echo "To fix: close this PR and re-create with:" + echo " gh pr create --base dev --repo diberry/squad" + exit 1 + fi + + echo "✅ PR correctly targets $EXPECTED_REPO" + + review-requirement-check: + name: "🔒 Review requirement check" + runs-on: ubuntu-latest + if: github.event.action == 'opened' || github.event.action == 'reopened' + steps: + - name: Remind about review requirement + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "⚠️ REVIEW POLICY: All PRs to dev/main require at least 1 approving review." + echo "" + echo "This check is informational — branch protection rules enforce the requirement." + echo "If branch protection is not yet configured, see:" + echo " docs/branch-protection-setup.md in this repo" + echo "" + echo "✅ Review reminder posted for PR #$PR_NUMBER" diff --git a/docs/src/content/docs/guide/building-resilient-agents.md b/docs/src/content/docs/guide/building-resilient-agents.md index 9306d08a5..cfedcd6ba 100644 --- a/docs/src/content/docs/guide/building-resilient-agents.md +++ b/docs/src/content/docs/guide/building-resilient-agents.md @@ -38,7 +38,7 @@ Each failed probe attempt resets the backoff. This gives flaky services time to ## Configuration -Squad includes sensible defaults — most agents won't need to change these. Configure the circuit breaker in your agent's `squad.json` or initialization code only if you want to customize: +Squad includes sensible defaults — most agents won't need to change these. Configure the circuit breaker in your agent's `squad.config.ts` or initialization code only if you want to customize: ```json { @@ -57,7 +57,7 @@ Squad includes sensible defaults — most agents won't need to change these. Con | Parameter | Default | Meaning | |-----------|---------|---------| | `failureThreshold` | 5 | Open circuit after this many failures | -| `successThreshold` | 2 | Close circuit after this many successes in half-open | +| `successThreshold` | 2 | Number of consecutive successes during half-open state before closing circuit | | `timeWindow` | 60s | Count failures within this window | | `initialBackoff` | 2m | Start backoff at this duration | | `maxBackoff` | 30m | Cap backoff at this duration | @@ -72,30 +72,37 @@ The circuit breaker persists its state to disk. If an agent restarts while the c ## How to apply to custom agents -When building a custom agent, wrap your external calls with circuit breaker protection: +When building a custom agent, wrap your external calls with circuit breaker protection. This example shows the conceptual pattern (note: a `@squad/resilience` module is planned but not yet available): ```typescript -import { CircuitBreaker } from '@squad/resilience'; - -const breaker = new CircuitBreaker({ - failureThreshold: 5, - timeWindow: 60000, -}); +// Pseudocode: Circuit breaker pattern for external calls +async function callDownstreamAPI(breaker) { + try { + // Check circuit state before making the call + if (breaker.state === 'OPEN') { + throw new Error('Circuit is open; retrying later'); + } -async function callDownstreamAPI() { - return breaker.execute(async () => { const response = await fetch('https://api.example.com/data'); - if (!response.ok) throw new Error(`API error: ${response.status}`); + if (!response.ok) { + breaker.recordFailure(); + throw new Error(`API error: ${response.status}`); + } + + breaker.recordSuccess(); return response.json(); - }); + } catch (err) { + breaker.recordFailure(); + throw err; + } } // Circuit breaker automatically handles state transitions -// and exponential backoff — just call it. +// and exponential backoff. try { - const data = await callDownstreamAPI(); + const data = await callDownstreamAPI(circuitBreaker); } catch (err) { - if (err.code === 'CIRCUIT_OPEN') { + if (err.message.includes('Circuit is open')) { console.log('Circuit is open; retrying later'); } else { console.error('Request failed:', err); @@ -103,7 +110,7 @@ try { } ``` -When the circuit opens, `execute()` throws a `CIRCUIT_OPEN` error. Your agent can catch this and backoff gracefully, or fail-fast to upstream callers. +The circuit breaker automatically transitions between states and applies exponential backoff. Failures increment a counter within the time window; successes during half-open close the circuit. ---