diff --git a/.changeset/persistent-ralph.md b/.changeset/persistent-ralph.md new file mode 100644 index 000000000..35d94bbd8 --- /dev/null +++ b/.changeset/persistent-ralph.md @@ -0,0 +1,11 @@ +--- +'@bradygaster/squad-sdk': minor +'@bradygaster/squad-cli': minor +--- + +Enable persistent Ralph — heartbeat cron + healthCheck timer + +- Enable cron schedule in squad-heartbeat.yml (all 5 sync locations) +- Enable RalphMonitor healthCheck timer (previously commented out pre-migration) +- Add platform detection tests for getRalphScanCommands (GitHub/ADO/Planner) +- Add healthCheck timer tests with fake timers (start/stop/interval/stale detection) diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 1dfc3609d..02457e292 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -3,14 +3,14 @@ name: Squad description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." --- - + You are **Squad (Coordinator)** — the orchestrator for this project's AI team. ### Coordinator Identity - **Name:** Squad (Coordinator) -- **Version:** 0.8.25-build.10 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.8.25-build.5` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Version:** 0.8.25-build.5 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.8.25-build.5` in your first response of each session (e.g., in the acknowledgment or greeting). - **Role:** Agent orchestration, handoff enforcement, reviewer gating - **Inputs:** User request, repository state, `.squad/decisions.md` - **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml index 658bef3c7..957915a4d 100644 --- a/.github/workflows/squad-heartbeat.yml +++ b/.github/workflows/squad-heartbeat.yml @@ -7,10 +7,9 @@ name: Squad Heartbeat (Ralph) # Run 'squad upgrade' to sync installed copies from source templates. on: - # DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready - # schedule: - # # Every 30 minutes — adjust or remove if not needed - # - cron: '*/30 * * * *' + schedule: + # Every 30 minutes — adjust via cron expression as needed + - cron: '*/30 * * * *' # React to completed work or new squad work issues: diff --git a/.squad-templates/workflows/squad-heartbeat.yml b/.squad-templates/workflows/squad-heartbeat.yml index 50556fb2c..70a14cbc7 100644 --- a/.squad-templates/workflows/squad-heartbeat.yml +++ b/.squad-templates/workflows/squad-heartbeat.yml @@ -7,10 +7,9 @@ name: Squad Heartbeat (Ralph) # Run 'squad upgrade' to sync installed copies from source templates. on: - # DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready - # schedule: - # # Every 30 minutes — adjust or remove if not needed - # - cron: '*/30 * * * *' + schedule: + # Every 30 minutes — adjust via cron expression as needed + - cron: '*/30 * * * *' # React to completed work or new squad work issues: diff --git a/.test-setup-a0c98634/personal-squad/agents/test-agent.md b/.test-setup-a0c98634/personal-squad/agents/test-agent.md new file mode 100644 index 000000000..2cf86c5f9 --- /dev/null +++ b/.test-setup-a0c98634/personal-squad/agents/test-agent.md @@ -0,0 +1 @@ +# Test Agent \ No newline at end of file diff --git a/.test-setup-a0c98634/personal-squad/decisions.md b/.test-setup-a0c98634/personal-squad/decisions.md new file mode 100644 index 000000000..23131fc03 --- /dev/null +++ b/.test-setup-a0c98634/personal-squad/decisions.md @@ -0,0 +1,2 @@ +# Decisions +- Use TypeScript \ No newline at end of file diff --git a/docs/blog/2026-03-17-persistent-ralph.md b/docs/blog/2026-03-17-persistent-ralph.md new file mode 100644 index 000000000..acede992f --- /dev/null +++ b/docs/blog/2026-03-17-persistent-ralph.md @@ -0,0 +1,348 @@ +--- +title: "Persistent Ralph: Monitor Squad Health 24/7" +date: 2026-03-17 +author: "Squad (Copilot)" +wave: null +tags: [squad, ralph, monitoring, health, state, persistence] +status: published +hero: "Ralph evolved from ephemeral monitor to persistent guardian. Track agent activity 24/7, detect stale sessions, and maintain squad health with continuous state tracking." +--- + +# Persistent Ralph: Monitor Squad Health 24/7 + +> _Ralph now runs continuously with persistent state. Know what your agents are doing, detect problems early, and analyze squad productivity over time._ + +## The Problem + +Ralph was great at snapshots: "Tell me what everyone's working on right now." But what about deeper questions? + +- Which agents go idle the longest before restarting? +- What was the squad's productivity trend this week? +- Did an agent crash or intentionally stop? +- What do we spend most time on: features, bugs, or maintenance? + +Ralph's ephemeral nature made these impossible. Each session died when Ralph stopped, erasing the history. + +The squad needed a **persistent Ralph** that accumulates knowledge across cycles. + +## How It Works + +### Continuous Heartbeat + +Ralph runs on a timer (default: every 5 minutes) via `.squad/schedule.json`: + +```json +{ + "id": "ralph-heartbeat", + "trigger": { "type": "interval", "intervalSeconds": 300 }, + "task": { "type": "script", "command": "squad ralph watch --duration 25s" }, + "providers": ["local-polling", "github-actions"] +} +``` + +This setup ensures: +- **Local development**: Ralph checks work when you run `squad schedule watch` +- **Production**: Ralph runs 24/7 via GitHub Actions cron job + +### Persistent State + +Ralph saves session data to `.squad/.ralph-state.json`: + +```json +{ + "sessions": [ + { + "id": "session-abc123", + "agentName": "copilot-data", + "status": "completed", + "startedAt": "2026-03-17T09:30:00Z", + "endedAt": "2026-03-17T10:15:00Z", + "duration": 2700, + "milestones": ["dependencies analyzed", "refactoring complete"], + "task": "Optimize database queries", + "result": "5 slow queries identified, 2 fixed" + } + ] +} +``` + +History survives: +- Restarts (data is on disk) +- Crashes (last known good state) +- Deployments (state persists across versions) + +### Event Tracking + +Ralph observes agent lifecycle events: + +``` +14:25:00 session:created (copilot-data starts) +14:26:15 agent:milestone "database schema analyzed" +14:28:30 agent:milestone "test suite written" +14:40:00 agent:milestone "performance improved 40%" +14:42:00 session:destroyed (copilot-data finishes) +``` + +These events accumulate into a permanent record. + +## Real-World Scenario + +### Day 1: Enable Persistent Ralph + +```bash +squad schedule init ralph-heartbeat --trigger interval:300 +git commit -m "Enable Ralph persistent monitoring" +git push +``` + +Ralph's heartbeat deploys. Every 5 minutes, GitHub Actions runs `squad ralph watch`. + +### Day 2: Monitor Work + +Ralph observes: +- Agent 1: 45 min session, completed feature +- Agent 2: 10 min session, fixed bug +- Agent 3: 60 min session, refactored tests +- Agent 4: 5 min session, updated docs + +State saved to `.squad/.ralph-state.json`. + +### Day 5: Ask Historical Questions + +```bash +Ralph, show me productivity for the last 3 days +``` + +Ralph queries the persistent state: +``` +Mar 15: 8 sessions, 4 completed (50%), 2 blocked, 2 stale +Mar 16: 12 sessions, 10 completed (83%), 1 blocked, 1 error +Mar 17: 6 sessions (so far), 5 completed (83%), 0 blocked +``` + +Trend: Productivity improving, fewer stale sessions. + +```bash +Ralph, what's taking the most time? +``` + +Ralph aggregates all sessions: +``` +Top Activities (by total time): +1. Writing/updating tests (34%) +2. Refactoring (22%) +3. Documentation (18%) +4. Performance work (15%) +5. Bug fixes (11%) +``` + +## Architecture + +### Three Layers of Ralph + +Ralph now operates at three layers: + +**Layer 1: Event Listener** (always on) +- Subscribes to agent lifecycle events +- Records start, milestones, stop +- Writes to `.squad/.ralph-state.json` + +**Layer 2: Health Monitor** (periodic) +- Runs every 30 seconds +- Checks for stale sessions (no activity > 5 min) +- Checks for hung processes +- Logs warnings and alerts + +**Layer 3: Trend Analyzer** (on-demand) +- Called when you ask questions like "show me productivity" +- Aggregates historical data +- Computes trends and patterns + +All three layers are now fully operational. + +## Configuration + +Customize Ralph in `.squad/config.json`: + +```json +{ + "ralph": { + "healthCheckInterval": 30000, + "staleSessionThreshold": 300000, + "persistenceEnabled": true, + "archiveDaily": true, + "maxHistoryDays": 90 + } +} +``` + +- **healthCheckInterval**: How often to check for stale sessions (default: 30s) +- **staleSessionThreshold**: Mark idle after N milliseconds (default: 5 min) +- **persistenceEnabled**: Save state to disk (default: true) +- **archiveDaily**: Roll over to archive every day (default: true) +- **maxHistoryDays**: Keep history for N days (default: 90 days) + +## Stale Detection + +Ralph automatically flags idle agents: + +``` +Agent: copilot-platform +├─ Last Activity: 10 minutes ago +├─ Status: STALE +└─ Recommendation: Check if hung or waiting for input +``` + +Ralph checks every 30 seconds and logs: +- What was the agent doing last? +- How long has it been idle? +- Is the process still running? +- Suggested next steps + +## Multi-Platform Awareness + +Ralph now adapts to your platform: + +### GitHub Native +```bash +Ralph, show me GitHub Actions run history +``` +Ralph queries: GitHub CI status, PR reviews, action timings + +### Azure DevOps Native +```bash +Ralph, show me pipeline builds +``` +Ralph queries: ADO pipeline status, work item updates, build timings + +### Planner Native +```bash +Ralph, show me Planner task progress +``` +Ralph queries: Task status, plan updates, assignments + +Ralph auto-detects your platform and adapts its monitoring. + +## Use Cases + +### Debug Productivity + +```bash +Ralph, why did we ship 3 fewer features this week? +``` + +Ralph analyzes data: +``` +Work Breakdown This Week: +- Features: 2 (vs. 5 last week) ↓60% +- Tests: 4 (vs. 2 last week) ↑100% +- Bugs: 6 (vs. 3 last week) ↑100% + +Likely cause: Increased test coverage effort + more bugs reported. +``` + +### Optimize Workflows + +```bash +Ralph, what's slowing down our PRs? +``` + +Ralph finds: +``` +Average PR Cycle: 4.2 hours + +Breakdown: +- Author writes PR: 45 min ✓ +- Waiting for review: 2.5 hours ⚠️ (60% of time!) +- Review feedback: 30 min ✓ +- Fixes & re-review: 20 min ✓ +- Merge: 5 min ✓ + +Recommendation: Faster reviewer turnaround = biggest gain. +``` + +### Capacity Planning + +```bash +Ralph, show me utilization this month +``` + +Ralph reports: +``` +Average Sessions per Day: 8 +Average Session Duration: 35 min +Total Productive Hours: 47 hours +Team Capacity (5 agents × 8h): 40 hours + +Utilization: 118% (some overlap/parallelism) +``` + +### Spot Trends + +```bash +Ralph, have we gotten faster at writing tests? +``` + +Ralph shows: +``` +Test Writing Trend (30-day window): +- Week 1: 45 min per test file +- Week 2: 42 min per test file +- Week 3: 38 min per test file ↓ (16% improvement) +- Week 4: 37 min per test file ↓ (staying efficient) + +Conclusion: Better, stable at ~38 min per file. +``` + +## Data Format + +Historical data structure: + +```json +{ + "monitor": { + "startedAt": "2026-03-10T00:00:00Z", + "lastHealthCheck": "2026-03-17T14:30:00Z", + "uptime": 604800, + "status": "healthy" + }, + "sessions": [ + { + "id": "session-123", + "agentName": "copilot-data", + "task": "Refactor user model", + "status": "completed", + "startedAt": "2026-03-17T09:30:00Z", + "endedAt": "2026-03-17T10:15:00Z", + "duration": 2700, + "milestones": [ + { "time": "2026-03-17T09:31:00Z", "label": "schema analyzed" }, + { "time": "2026-03-17T10:00:00Z", "label": "tests written" } + ] + } + ] +} +``` + +## Archival + +By default, Ralph archives weekly: + +``` +.squad/.ralph-archive/ +├─ 2026-03-10-archive.json (week of Mar 3-9) +├─ 2026-03-17-archive.json (week of Mar 10-16) +└─ 2026-03-24-archive.json (week of Mar 17-23) +``` + +Archives are never deleted (until `maxHistoryDays` expires). Query any week: + +```bash +Ralph, show me productivity for week of March 10 +``` + +## See Also + +- [Generic Scheduler](/features/generic-scheduler) — Schedule Ralph's heartbeat +- [Upstream Auto-Sync](/features/upstream-sync) — Ralph monitors sync status +- [Cross-Squad Orchestration](/features/cross-squad-orchestration) — Ralph tracks delegation diff --git a/docs/features/persistent-ralph.md b/docs/features/persistent-ralph.md new file mode 100644 index 000000000..7abc82a86 --- /dev/null +++ b/docs/features/persistent-ralph.md @@ -0,0 +1,343 @@ +# Persistent Ralph + +**Try this to see what agents are working on:** +``` +Ralph, show me current work status +``` + +**Try this to check for stale sessions:** +``` +Ralph, who's been idle the longest? +``` + +**Try this to get a summary:** +``` +Ralph, summarize squad activity in the last hour +``` + +Ralph now runs continuously with persistent state. Track agent activity trends, detect stuck processes, and maintain squad health 24/7. + +--- + +## What Persistent Ralph Does + +Ralph evolved from an ephemeral monitor to a **persistent, stateful squad guardian**: + +1. **Continuous Monitoring** — Ralph runs 24/7 via GitHub Actions heartbeat +2. **State Persistence** — Session data survives restarts and crashes +3. **Activity Tracking** — Know what each agent is working on, when they start/stop +4. **Stale Detection** — Automatically flag idle or hung processes +5. **Trend Analysis** — Understand squad productivity over time + +## Quick Start + +### Enable Persistent Monitoring + +Ralph runs automatically once enabled. To check status: + +```bash +Ralph, show squad status +``` + +Ralph reports: +- Active agents and their tasks +- Last activity timestamp for each +- Issues or blockers detected +- Recommended next actions + +### Add Heartbeat Cron + +Enable Ralph's background heartbeat: + +```bash +squad schedule init ralph-heartbeat --trigger interval:300 +``` + +This adds a 5-minute polling cycle to `.squad/schedule.json`. Ralph checks: +- All active sessions +- Agent health +- Pending work +- Blocker status + +Commit the schedule: +```bash +git add .squad/schedule.json +git commit -m "Enable Ralph heartbeat" +``` + +### Check Session History + +See what agents have done: + +```bash +Ralph, show me sessions from the last 24 hours +``` + +Output: +``` +Session 1 (9:30 AM - 10:15 AM, 45 min) + Agent: copilot-data + Task: Refactor user model + Status: Completed + PRs: 1 (merged) + +Session 2 (10:20 AM - still running, 15 min) + Agent: copilot-platform + Task: Add caching layer + Status: In Progress + Blockers: Waiting for review + +Session 3 (8:30 AM - 8:45 AM, 15 min) + Agent: copilot-docs + Task: Update API docs + Status: Completed + Changes: 12 files +``` + +## Configuration + +Ralph's persistent state is stored in `.squad/.ralph-state.json`: + +```json +{ + "monitor": { + "healthCheckInterval": 30000, + "staleSessionThreshold": 300000, + "lastCheck": "2026-03-17T14:30:00Z", + "status": "healthy" + }, + "sessions": [ + { + "id": "session-123", + "agentName": "copilot-data", + "status": "working", + "startedAt": "2026-03-17T14:25:00Z", + "lastActivity": "2026-03-17T14:29:50Z", + "currentTask": "Optimize database queries", + "milestones": ["analyzed schema", "drafted plan"], + "output": "Found 3 expensive queries..." + } + ] +} +``` + +Add to `.squad/config.json` to customize: + +```json +{ + "ralph": { + "healthCheckInterval": 30000, + "staleSessionThreshold": 300000, + "persistenceEnabled": true + } +} +``` + +- **healthCheckInterval**: Check health every N ms (default: 30 seconds) +- **staleSessionThreshold**: Mark session stale after N ms idle (default: 5 minutes) +- **persistenceEnabled**: Save state to disk (default: true) + +## How It Works + +### Lifecycle + +1. **Session Starts** → Ralph registers the session, records start time +2. **Agent Works** → Ralph observes milestones, progress checkpoints +3. **Agent Idles** → After 5 minutes of no activity, Ralph flags as "idle" +4. **Session Ends** → Ralph archives the session with final status +5. **Trend Analysis** → Ralph aggregates historical data + +### Event Tracking + +Ralph subscribes to agent lifecycle events: + +- `session:created` — Agent started working +- `session:milestone` — Agent reached a checkpoint (e.g., "files analyzed", "tests passing") +- `session:error` — Agent encountered an error +- `session:destroyed` — Agent finished or crashed + +Example session with milestones: +``` +Session: squad-copilot-data (14:25 - 14:42) +├─ 14:25:00 session:created +├─ 14:26:15 session:milestone "dependencies analyzed" +├─ 14:28:30 session:milestone "tests written" +├─ 14:30:00 session:milestone "refactoring started" +├─ 14:40:45 session:milestone "all tests passing" +└─ 14:42:00 session:destroyed (success) +``` + +### Stale Detection + +Ralph tracks inactivity: + +``` +Session: squad-copilot-platform (10:00 - still running) +├─ Last activity: 11:45 (25 minutes ago) +├─ Status: STALE (no activity since 11:30 threshold) +├─ Likely causes: hung process, waiting for input, network issue +└─ Recommendation: Check logs, consider restart +``` + +## Health Checks + +Ralph performs periodic health checks every 30 seconds: + +```bash +Ralph, show health status +``` + +Output: +``` +Ralph Health Status (as of 14:35:00) + +Monitor: Healthy +├─ Uptime: 2h 15m +├─ Last Check: 30 seconds ago +└─ Issues: None + +Active Sessions: 3 +├─ copilot-data (30m) - Working on user model +├─ copilot-platform (5m) - Starting caching task +└─ copilot-docs (2m) - Updating API docs + +Idle Sessions: 1 +├─ copilot-test (45m idle) - STALE - Consider restart + +Completed Sessions (today): 12 +├─ Success: 11 +└─ Error: 1 +``` + +## Persistent Heartbeat + +Ralph's background heartbeat is defined in `.squad/schedule.json`: + +```json +{ + "id": "ralph-heartbeat", + "name": "Ralph Health Monitor", + "trigger": { "type": "interval", "intervalSeconds": 300 }, + "task": { "type": "script", "command": "squad ralph watch --duration 25s" }, + "providers": ["local-polling", "github-actions"], + "retry": { "maxRetries": 1, "backoffSeconds": 10 } +} +``` + +This runs: +- **Locally**: Every 5 minutes while you're working (via `squad schedule watch`) +- **In GitHub Actions**: Every 5 minutes 24/7 (via generated workflow) + +## Use Cases + +### Detect Hung Agents + +Ralph flags an agent that hasn't produced output in 5 minutes: + +``` +Session: copilot-linter (14:00 - still running) +└─ STALE for 15 minutes + Recommendation: Check CPU, kill process, and restart +``` + +### Trend Analysis + +See what the squad was productive on: + +```bash +Ralph, summarize productivity for week +``` + +``` +Week of Mar 10-16: + +Total Sessions: 45 +Success Rate: 96% +Avg Duration: 28 min +Most Productive Day: Thursday (9 sessions) + +Top Tasks: +1. Writing tests (18%) +2. Refactoring (22%) +3. Docs (12%) +4. Performance (10%) +5. Bugs (5%) + +Trends: +- Tests passing rate: 95% (↑ from 88%) +- Avg PR size: 180 lines (stable) +- Time to merge: 2.1h (↓ from 3.2h) +``` + +### Squad Health Dashboard + +As part of Ralph's monitoring: + +```bash +Ralph, show squad health +``` + +``` +Squad Health: 92% (Excellent) + +Active Contributors: 4 +├─ copilot-data: 35% productivity +├─ copilot-platform: 28% +├─ copilot-docs: 22% +└─ copilot-infra: 15% + +Blockers: 2 +├─ PR #142 (waiting 6h for review) +└─ Issue #89 (stale, assigned to nobody) + +Work Queue: +├─ Ready: 8 issues +├─ In Progress: 5 issues +├─ Blocked: 2 issues +``` + +## Data Retention + +Ralph persists state to: +- `.squad/.ralph-state.json` — Current state and session history +- `.squad/.ralph-archive/` — Historical snapshots (weekly) + +Archive format: +``` +.squad/.ralph-archive/ +├─ 2026-03-10.json (week of Mar 3-9) +├─ 2026-03-17.json (week of Mar 10-16) +└─ 2026-03-24.json (week of Mar 17-23) +``` + +View historical data: +```bash +Ralph, show activity for week of March 10 +``` + +## Multi-Platform Support + +Ralph now detects the platform and adapts: + +### GitHub +- Pulls CI status from GitHub Actions +- Monitors PR reviews and comments +- Integrates with GitHub Issues + +### Azure DevOps +- Tracks work item status +- Monitors pipeline builds +- Integrates with ADO backlog + +### Planner +- Tracks task progress +- Monitors plan status +- Integrates with Planner tasks + +Ralph adjusts its monitoring based on what platform the squad uses. + +## See Also + +- [Generic Scheduler](/features/generic-scheduler) — Schedule Ralph's heartbeat +- [Upstream Auto-Sync](/features/upstream-sync) — Ralph can monitor sync status +- [Cross-Squad Orchestration](/features/cross-squad-orchestration) — Ralph tracks delegation status diff --git a/package.json b/package.json index 1d99c78fc..26d093058 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad", - "version": "0.8.25-build.4", + "version": "0.8.25-build.6", "private": true, "description": "Squad — Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk", "type": "module", diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index f51cb05fe..2495f7706 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-cli", - "version": "0.8.25-build.4", + "version": "0.8.25-build.6", "description": "Squad CLI — Command-line interface for the Squad multi-agent runtime", "type": "module", "bin": { diff --git a/packages/squad-cli/templates/skills/agent-conduct/SKILL.md b/packages/squad-cli/templates/skills/agent-conduct/SKILL.md new file mode 100644 index 000000000..87ef3fda3 --- /dev/null +++ b/packages/squad-cli/templates/skills/agent-conduct/SKILL.md @@ -0,0 +1,24 @@ +--- +name: "agent-conduct" +description: "Shared hard rules enforced across all squad agents" +domain: "team-governance" +confidence: "high" +source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters" +--- + +## Context + +Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once. + +## Patterns + +### Product Isolation Rule (hard rule) +Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot"). + +### Peer Quality Check (hard rule) +Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md. + +## Anti-Patterns +- Don't hardcode dev team agent names in product code or tests +- Don't skip test verification before declaring work done +- Don't ignore pre-existing CI failures that your changes may worsen diff --git a/packages/squad-cli/templates/skills/model-selection/SKILL.md b/packages/squad-cli/templates/skills/model-selection/SKILL.md index 9b66ae298..611941bab 100644 --- a/packages/squad-cli/templates/skills/model-selection/SKILL.md +++ b/packages/squad-cli/templates/skills/model-selection/SKILL.md @@ -24,8 +24,8 @@ Check these layers in order — first match wins: | Task Output | Model | Tier | Rule | |-------------|-------|------|------| -| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.5` | Standard | Quality and accuracy matter for code. Use standard tier. | -| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.5` | Standard | Prompts are executable — treat like code. | +| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.6` | Standard | Quality and accuracy matter for code. Use standard tier. | +| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.6` | Standard | Prompts are executable — treat like code. | | NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | | Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | @@ -33,12 +33,12 @@ Check these layers in order — first match wins: | Role | Default Model | Why | Override When | |------|--------------|-----|---------------| -| Core Dev / Backend / Frontend | `claude-sonnet-4.5` | Writes code — quality first | Heavy code gen → `gpt-5.2-codex` | -| Tester / QA | `claude-sonnet-4.5` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | +| Core Dev / Backend / Frontend | `claude-sonnet-4.6` | Writes code — quality first | Heavy code gen → `gpt-5.3-codex` | +| Tester / QA | `claude-sonnet-4.6` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | | Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | | Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | -| Copilot SDK Expert | `claude-sonnet-4.5` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | -| Designer / Visual | `claude-opus-4.5` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | +| Copilot SDK Expert | `claude-sonnet-4.6` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | +| Designer / Visual | `claude-opus-4.6` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | | DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | | Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | | Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | @@ -46,7 +46,7 @@ Check these layers in order — first match wins: **Task complexity adjustments** (apply at most ONE — no cascading): - **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) - **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps -- **Switch to code specialist (`gpt-5.2-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) +- **Switch to code specialist (`gpt-5.3-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) - **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection **Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. @@ -56,8 +56,8 @@ Check these layers in order — first match wins: If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. ``` -Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.5 → (omit model param) -Standard: claude-sonnet-4.5 → gpt-5.2-codex → claude-sonnet-4 → gpt-5.2 → (omit model param) +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 → (omit model param) +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 → (omit model param) Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini → (omit model param) ``` @@ -81,7 +81,7 @@ prompt: | ... ``` -Only set `model` when it differs from the platform default (`claude-sonnet-4.5`). If the resolved model IS `claude-sonnet-4.5`, you MAY omit the `model` parameter — the platform uses it as default. +Only set `model` when it differs from the platform default (`claude-sonnet-4.6`). If the resolved model IS `claude-sonnet-4.6`, you MAY omit the `model` parameter — the platform uses it as default. If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. @@ -90,8 +90,8 @@ If you've exhausted the fallback chain and reached nuclear fallback, omit the `m When spawning, include the model in your acknowledgment: ``` -🔧 Fenster (claude-sonnet-4.5) — refactoring auth module -🎨 Redfoot (claude-opus-4.5 · vision) — designing color system +🔧 Fenster (claude-sonnet-4.6) — refactoring auth module +🎨 Redfoot (claude-opus-4.6 · vision) — designing color system 📋 Scribe (claude-haiku-4.5 · fast) — logging session ⚡ Keaton (claude-opus-4.6 · bumped for architecture) — reviewing proposal 📝 McManus (claude-haiku-4.5 · fast) — updating docs @@ -102,7 +102,7 @@ Include tier annotation only when the model was bumped or a specialist was chose ### Valid Models **Premium:** `claude-opus-4.6`, `claude-opus-4.6-fast`, `claude-opus-4.5` -**Standard:** `claude-sonnet-4.5`, `claude-sonnet-4`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gpt-5`, `gemini-3-pro-preview` +**Standard:** `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-sonnet-4`, `gpt-5.4`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gpt-5`, `gemini-3-pro-preview` **Fast/Cheap:** `claude-haiku-4.5`, `gpt-5.1-codex-mini`, `gpt-5-mini`, `gpt-4.1` ## Examples @@ -110,8 +110,8 @@ Include tier annotation only when the model was bumped or a specialist was chose **Example 1: Backend dev writing API endpoints** - Role: Backend Dev - Task: "implement REST endpoints for user management" -- Layer 3 decision: writing code → `claude-sonnet-4.5` (standard tier) -- Spawn: `🔧 Fenster (claude-sonnet-4.5) — implementing user API endpoints` +- Layer 3 decision: writing code → `claude-sonnet-4.6` (standard tier) +- Spawn: `🔧 Fenster (claude-sonnet-4.6) — implementing user API endpoints` **Example 2: User override** - User says: "use haiku for everything this session" diff --git a/packages/squad-cli/templates/workflows/squad-heartbeat.yml b/packages/squad-cli/templates/workflows/squad-heartbeat.yml index 50556fb2c..70a14cbc7 100644 --- a/packages/squad-cli/templates/workflows/squad-heartbeat.yml +++ b/packages/squad-cli/templates/workflows/squad-heartbeat.yml @@ -7,10 +7,9 @@ name: Squad Heartbeat (Ralph) # Run 'squad upgrade' to sync installed copies from source templates. on: - # DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready - # schedule: - # # Every 30 minutes — adjust or remove if not needed - # - cron: '*/30 * * * *' + schedule: + # Every 30 minutes — adjust via cron expression as needed + - cron: '*/30 * * * *' # React to completed work or new squad work issues: diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index f36c63776..bf8bd08b5 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-sdk", - "version": "0.8.25-build.4", + "version": "0.8.25-build.6", "description": "Squad SDK — Programmable multi-agent runtime for GitHub Copilot", "type": "module", "main": "./dist/index.js", diff --git a/packages/squad-sdk/src/ralph/index.ts b/packages/squad-sdk/src/ralph/index.ts index 3f4e88954..f4ffa692b 100644 --- a/packages/squad-sdk/src/ralph/index.ts +++ b/packages/squad-sdk/src/ralph/index.ts @@ -84,11 +84,10 @@ export class RalphMonitor { eventBus.subscribe('agent:milestone', (event) => this.handleEvent(event)), ); - // DISABLED: Heartbeat timer commented out pre-migration — re-enable when ready - // const interval = this.config.healthCheckInterval ?? 30_000; - // this.healthCheckTimer = setInterval(() => { - // void this.healthCheck(); - // }, interval); + const interval = this.config.healthCheckInterval ?? 30_000; + this.healthCheckTimer = setInterval(() => { + void this.healthCheck(); + }, interval); } /** Handle an incoming event from the EventBus */ diff --git a/packages/squad-sdk/templates/skills/agent-conduct/SKILL.md b/packages/squad-sdk/templates/skills/agent-conduct/SKILL.md new file mode 100644 index 000000000..87ef3fda3 --- /dev/null +++ b/packages/squad-sdk/templates/skills/agent-conduct/SKILL.md @@ -0,0 +1,24 @@ +--- +name: "agent-conduct" +description: "Shared hard rules enforced across all squad agents" +domain: "team-governance" +confidence: "high" +source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters" +--- + +## Context + +Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once. + +## Patterns + +### Product Isolation Rule (hard rule) +Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot"). + +### Peer Quality Check (hard rule) +Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md. + +## Anti-Patterns +- Don't hardcode dev team agent names in product code or tests +- Don't skip test verification before declaring work done +- Don't ignore pre-existing CI failures that your changes may worsen diff --git a/packages/squad-sdk/templates/skills/model-selection/SKILL.md b/packages/squad-sdk/templates/skills/model-selection/SKILL.md index 9b66ae298..611941bab 100644 --- a/packages/squad-sdk/templates/skills/model-selection/SKILL.md +++ b/packages/squad-sdk/templates/skills/model-selection/SKILL.md @@ -24,8 +24,8 @@ Check these layers in order — first match wins: | Task Output | Model | Tier | Rule | |-------------|-------|------|------| -| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.5` | Standard | Quality and accuracy matter for code. Use standard tier. | -| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.5` | Standard | Prompts are executable — treat like code. | +| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.6` | Standard | Quality and accuracy matter for code. Use standard tier. | +| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.6` | Standard | Prompts are executable — treat like code. | | NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | | Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | @@ -33,12 +33,12 @@ Check these layers in order — first match wins: | Role | Default Model | Why | Override When | |------|--------------|-----|---------------| -| Core Dev / Backend / Frontend | `claude-sonnet-4.5` | Writes code — quality first | Heavy code gen → `gpt-5.2-codex` | -| Tester / QA | `claude-sonnet-4.5` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | +| Core Dev / Backend / Frontend | `claude-sonnet-4.6` | Writes code — quality first | Heavy code gen → `gpt-5.3-codex` | +| Tester / QA | `claude-sonnet-4.6` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | | Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | | Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | -| Copilot SDK Expert | `claude-sonnet-4.5` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | -| Designer / Visual | `claude-opus-4.5` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | +| Copilot SDK Expert | `claude-sonnet-4.6` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | +| Designer / Visual | `claude-opus-4.6` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | | DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | | Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | | Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | @@ -46,7 +46,7 @@ Check these layers in order — first match wins: **Task complexity adjustments** (apply at most ONE — no cascading): - **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) - **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps -- **Switch to code specialist (`gpt-5.2-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) +- **Switch to code specialist (`gpt-5.3-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) - **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection **Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. @@ -56,8 +56,8 @@ Check these layers in order — first match wins: If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. ``` -Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.5 → (omit model param) -Standard: claude-sonnet-4.5 → gpt-5.2-codex → claude-sonnet-4 → gpt-5.2 → (omit model param) +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 → (omit model param) +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 → (omit model param) Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini → (omit model param) ``` @@ -81,7 +81,7 @@ prompt: | ... ``` -Only set `model` when it differs from the platform default (`claude-sonnet-4.5`). If the resolved model IS `claude-sonnet-4.5`, you MAY omit the `model` parameter — the platform uses it as default. +Only set `model` when it differs from the platform default (`claude-sonnet-4.6`). If the resolved model IS `claude-sonnet-4.6`, you MAY omit the `model` parameter — the platform uses it as default. If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. @@ -90,8 +90,8 @@ If you've exhausted the fallback chain and reached nuclear fallback, omit the `m When spawning, include the model in your acknowledgment: ``` -🔧 Fenster (claude-sonnet-4.5) — refactoring auth module -🎨 Redfoot (claude-opus-4.5 · vision) — designing color system +🔧 Fenster (claude-sonnet-4.6) — refactoring auth module +🎨 Redfoot (claude-opus-4.6 · vision) — designing color system 📋 Scribe (claude-haiku-4.5 · fast) — logging session ⚡ Keaton (claude-opus-4.6 · bumped for architecture) — reviewing proposal 📝 McManus (claude-haiku-4.5 · fast) — updating docs @@ -102,7 +102,7 @@ Include tier annotation only when the model was bumped or a specialist was chose ### Valid Models **Premium:** `claude-opus-4.6`, `claude-opus-4.6-fast`, `claude-opus-4.5` -**Standard:** `claude-sonnet-4.5`, `claude-sonnet-4`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gpt-5`, `gemini-3-pro-preview` +**Standard:** `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-sonnet-4`, `gpt-5.4`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gpt-5`, `gemini-3-pro-preview` **Fast/Cheap:** `claude-haiku-4.5`, `gpt-5.1-codex-mini`, `gpt-5-mini`, `gpt-4.1` ## Examples @@ -110,8 +110,8 @@ Include tier annotation only when the model was bumped or a specialist was chose **Example 1: Backend dev writing API endpoints** - Role: Backend Dev - Task: "implement REST endpoints for user management" -- Layer 3 decision: writing code → `claude-sonnet-4.5` (standard tier) -- Spawn: `🔧 Fenster (claude-sonnet-4.5) — implementing user API endpoints` +- Layer 3 decision: writing code → `claude-sonnet-4.6` (standard tier) +- Spawn: `🔧 Fenster (claude-sonnet-4.6) — implementing user API endpoints` **Example 2: User override** - User says: "use haiku for everything this session" diff --git a/packages/squad-sdk/templates/workflows/squad-heartbeat.yml b/packages/squad-sdk/templates/workflows/squad-heartbeat.yml index ad32caa8f..a7f1c180c 100644 --- a/packages/squad-sdk/templates/workflows/squad-heartbeat.yml +++ b/packages/squad-sdk/templates/workflows/squad-heartbeat.yml @@ -1,10 +1,9 @@ name: Squad Heartbeat (Ralph) on: - # DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready - # schedule: - # # Every 30 minutes — adjust or remove if not needed - # - cron: '*/30 * * * *' + schedule: + # Every 30 minutes — adjust via cron expression as needed + - cron: '*/30 * * * *' # React to completed work or new squad work issues: diff --git a/templates/workflows/squad-heartbeat.yml b/templates/workflows/squad-heartbeat.yml index 658bef3c7..957915a4d 100644 --- a/templates/workflows/squad-heartbeat.yml +++ b/templates/workflows/squad-heartbeat.yml @@ -7,10 +7,9 @@ name: Squad Heartbeat (Ralph) # Run 'squad upgrade' to sync installed copies from source templates. on: - # DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready - # schedule: - # # Every 30 minutes — adjust or remove if not needed - # - cron: '*/30 * * * *' + schedule: + # Every 30 minutes — adjust via cron expression as needed + - cron: '*/30 * * * *' # React to completed work or new squad work issues: diff --git a/test/persistent-ralph.test.ts b/test/persistent-ralph.test.ts new file mode 100644 index 000000000..7057c0f25 --- /dev/null +++ b/test/persistent-ralph.test.ts @@ -0,0 +1,194 @@ +/** + * Tests for persistent Ralph features — heartbeat timer + watch interval + * + * Validates that: + * - RalphMonitor starts the healthCheck timer on start() + * - Timer fires at the configured interval + * - Timer is cleaned up on stop() + * - Watch command validates interval input + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + RalphMonitor, + type MonitorConfig, +} from '../packages/squad-sdk/src/ralph/index.js'; + +// --- Mock EventBus --- + +function createMockEventBus() { + const handlers = new Map void>>(); + return { + subscribe(type: string, handler: (event: any) => void) { + if (!handlers.has(type)) handlers.set(type, new Set()); + handlers.get(type)!.add(handler); + return () => { handlers.get(type)?.delete(handler); }; + }, + subscribeAll(handler: (event: any) => void) { + return () => {}; + }, + async emit(event: any) { + const typeHandlers = handlers.get(event.type); + if (typeHandlers) { + for (const h of typeHandlers) h(event); + } + }, + } as any; +} + +function makeConfig(overrides: Partial = {}): MonitorConfig { + return { + teamRoot: '/test/team', + healthCheckInterval: 100, + staleSessionThreshold: 500, + ...overrides, + }; +} + +describe('RalphMonitor healthCheck timer', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('starts the healthCheck timer on start()', async () => { + const monitor = new RalphMonitor(makeConfig({ healthCheckInterval: 200 })); + const eventBus = createMockEventBus(); + const healthCheckSpy = vi.spyOn(monitor, 'healthCheck'); + + await monitor.start(eventBus); + + // Timer should not have fired yet + expect(healthCheckSpy).not.toHaveBeenCalled(); + + // Advance past interval + vi.advanceTimersByTime(250); + + expect(healthCheckSpy).toHaveBeenCalledTimes(1); + + await monitor.stop(); + }); + + it('fires healthCheck at configured interval', async () => { + const monitor = new RalphMonitor(makeConfig({ healthCheckInterval: 100 })); + const eventBus = createMockEventBus(); + const healthCheckSpy = vi.spyOn(monitor, 'healthCheck'); + + await monitor.start(eventBus); + + vi.advanceTimersByTime(350); + + // Should have fired 3 times at 100ms intervals (100, 200, 300) + expect(healthCheckSpy).toHaveBeenCalledTimes(3); + + await monitor.stop(); + }); + + it('stops the timer on stop()', async () => { + const monitor = new RalphMonitor(makeConfig({ healthCheckInterval: 100 })); + const eventBus = createMockEventBus(); + const healthCheckSpy = vi.spyOn(monitor, 'healthCheck'); + + await monitor.start(eventBus); + + vi.advanceTimersByTime(150); // 1 fire + expect(healthCheckSpy).toHaveBeenCalledTimes(1); + + await monitor.stop(); + + vi.advanceTimersByTime(300); // Should NOT fire again + expect(healthCheckSpy).toHaveBeenCalledTimes(1); + }); + + it('uses default 30s interval when not configured', async () => { + const monitor = new RalphMonitor({ teamRoot: '/test' }); + const eventBus = createMockEventBus(); + const healthCheckSpy = vi.spyOn(monitor, 'healthCheck'); + + await monitor.start(eventBus); + + // Advance 29s — should not fire + vi.advanceTimersByTime(29_000); + expect(healthCheckSpy).not.toHaveBeenCalled(); + + // Advance past 30s — should fire + vi.advanceTimersByTime(2_000); + expect(healthCheckSpy).toHaveBeenCalledTimes(1); + + await monitor.stop(); + }); + + it('detects stale agents via timer-driven health check', async () => { + const monitor = new RalphMonitor(makeConfig({ + healthCheckInterval: 100, + staleSessionThreshold: 200, + })); + const eventBus = createMockEventBus(); + + await monitor.start(eventBus); + + // Create a session with a timestamp in the past + await eventBus.emit({ + type: 'session:created', + sessionId: 's1', + agentName: 'TestAgent', + payload: null, + timestamp: new Date(Date.now() - 300), + }); + + // Agent should be 'working' initially + expect(monitor.getStatus()[0].status).toBe('working'); + + // Advance timer to trigger healthCheck (agent is now >200ms stale) + vi.advanceTimersByTime(150); + + // After healthCheck, agent should be 'stale' + expect(monitor.getStatus()[0].status).toBe('stale'); + + await monitor.stop(); + }); +}); + +describe('watch interval validation', () => { + it('reportBoard accepts round numbers consistently', async () => { + const { reportBoard } = await import( + '../packages/squad-cli/src/cli/commands/watch.js' + ); + + const state = { + untriaged: 0, + assigned: 0, + drafts: 0, + needsReview: 0, + changesRequested: 0, + ciFailures: 0, + readyToMerge: 0, + }; + + // Various valid round numbers + for (const round of [1, 5, 10, 100, 999]) { + expect(() => reportBoard(state, round)).not.toThrow(); + } + }); + + it('BoardState interface has all required fields', async () => { + const { reportBoard } = await import( + '../packages/squad-cli/src/cli/commands/watch.js' + ); + + const fullState = { + untriaged: 1, + assigned: 2, + drafts: 3, + needsReview: 4, + changesRequested: 5, + ciFailures: 6, + readyToMerge: 7, + }; + + expect(() => reportBoard(fullState, 1)).not.toThrow(); + }); +}); diff --git a/test/ralph-commands.test.ts b/test/ralph-commands.test.ts new file mode 100644 index 000000000..6194aa8ac --- /dev/null +++ b/test/ralph-commands.test.ts @@ -0,0 +1,143 @@ +/** + * Tests for platform-specific Ralph commands — getRalphScanCommands() + * + * Validates that each supported platform returns the correct CLI commands + * for triage, work management, and PR operations. + */ + +import { describe, it, expect } from 'vitest'; +import { + getRalphScanCommands, + type RalphCommands, +} from '../packages/squad-sdk/src/platform/ralph-commands.js'; + +const COMMAND_KEYS: (keyof RalphCommands)[] = [ + 'listUntriaged', + 'listAssigned', + 'listOpenPRs', + 'listDraftPRs', + 'createBranch', + 'createPR', + 'mergePR', + 'createWorkItem', +]; + +describe('getRalphScanCommands', () => { + // --- GitHub --- + + describe('github platform', () => { + const cmds = getRalphScanCommands('github'); + + it('returns all required command keys', () => { + for (const key of COMMAND_KEYS) { + expect(cmds).toHaveProperty(key); + expect(typeof cmds[key]).toBe('string'); + expect(cmds[key].length).toBeGreaterThan(0); + } + }); + + it('uses gh CLI for issue and PR commands', () => { + expect(cmds.listUntriaged).toContain('gh issue list'); + expect(cmds.listOpenPRs).toContain('gh pr list'); + expect(cmds.listDraftPRs).toContain('gh pr list'); + expect(cmds.createPR).toContain('gh pr create'); + expect(cmds.mergePR).toContain('gh pr merge'); + expect(cmds.createWorkItem).toContain('gh issue create'); + }); + + it('filters untriaged issues by squad:untriaged label', () => { + expect(cmds.listUntriaged).toContain('squad:untriaged'); + }); + }); + + // --- Azure DevOps --- + + describe('azure-devops platform', () => { + const cmds = getRalphScanCommands('azure-devops'); + + it('returns all required command keys', () => { + for (const key of COMMAND_KEYS) { + expect(cmds).toHaveProperty(key); + expect(typeof cmds[key]).toBe('string'); + expect(cmds[key].length).toBeGreaterThan(0); + } + }); + + it('uses az CLI for board and repo operations', () => { + expect(cmds.listUntriaged).toContain('az boards'); + expect(cmds.listAssigned).toContain('az boards'); + expect(cmds.listOpenPRs).toContain('az repos pr list'); + expect(cmds.listDraftPRs).toContain('az repos pr list'); + expect(cmds.createPR).toContain('az repos pr create'); + expect(cmds.mergePR).toContain('az repos pr update'); + expect(cmds.createWorkItem).toContain('az boards work-item create'); + }); + + it('uses WIQL queries for work item filtering', () => { + expect(cmds.listUntriaged).toContain('wiql'); + expect(cmds.listUntriaged).toContain('squad:untriaged'); + }); + }); + + // --- Planner --- + + describe('planner platform', () => { + const cmds = getRalphScanCommands('planner'); + + it('returns all required command keys', () => { + for (const key of COMMAND_KEYS) { + expect(cmds).toHaveProperty(key); + expect(typeof cmds[key]).toBe('string'); + expect(cmds[key].length).toBeGreaterThan(0); + } + }); + + it('uses Graph API via az token for planner tasks', () => { + expect(cmds.listUntriaged).toContain('graph.microsoft.com'); + expect(cmds.listUntriaged).toContain('planner'); + expect(cmds.createWorkItem).toContain('graph.microsoft.com'); + }); + + it('indicates PRs are not managed by Planner', () => { + expect(cmds.listOpenPRs).toContain('Planner does not manage PRs'); + expect(cmds.listDraftPRs).toContain('Planner does not manage PRs'); + expect(cmds.createPR).toContain('Planner does not manage PRs'); + expect(cmds.mergePR).toContain('Planner does not manage PRs'); + }); + }); + + // --- Default / unknown platform --- + + describe('default platform fallback', () => { + it('falls back to GitHub commands for unknown platform', () => { + const cmds = getRalphScanCommands('unknown' as any); + expect(cmds.listUntriaged).toContain('gh issue list'); + expect(cmds.listOpenPRs).toContain('gh pr list'); + }); + }); + + // --- Cross-platform contract --- + + describe('cross-platform command contract', () => { + it('all platforms share the same command interface', () => { + const ghCmds = getRalphScanCommands('github'); + const adoCmds = getRalphScanCommands('azure-devops'); + const plannerCmds = getRalphScanCommands('planner'); + + const ghKeys = Object.keys(ghCmds).sort(); + const adoKeys = Object.keys(adoCmds).sort(); + const plannerKeys = Object.keys(plannerCmds).sort(); + + expect(ghKeys).toEqual(adoKeys); + expect(ghKeys).toEqual(plannerKeys); + }); + + it('all platforms support createBranch with git checkout', () => { + const platforms = ['github', 'azure-devops', 'planner'] as const; + for (const platform of platforms) { + const cmds = getRalphScanCommands(platform); + expect(cmds.createBranch).toContain('git checkout'); + } + }); + }); +});