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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ squads doctor # Check tools and readiness

# Execute
squads run <squad/agent> # Run an agent or full squad
squads autopilot # Autonomous scheduling with budget control
squads autonomous start # Start the scheduling daemon (reads SQUAD.md routines)
squads autonomous stop # Stop the daemon
squads autonomous status # Show daemon status, running agents, next runs

# Squad lifecycle
squads pause <squad> # Pause a squad (run/org/cron will refuse until resumed)
squads resume <squad> # Resume a paused squad

# Monitor
squads status [squad] # Overview of all squads
Expand Down Expand Up @@ -54,6 +60,42 @@ squads exec list # Own execution history
squads kpi record <squad> <kpi> <value> # Record a metric
```

## Pause and Resume

Pause a squad to prevent it from being dispatched by `squads run`, org cycles,
and the autonomous daemon. The squad definition is preserved — only execution
is blocked.

```bash
# Pause a squad (optionally with a reason)
squads pause engineering
squads pause engineering --reason "waiting for design sign-off"

# Resume a paused squad
squads resume engineering

# Force-run a paused squad (bypasses the pause guard)
squads run engineering --force
```

**Options**

| Command | Option | Description |
|---------|--------|-------------|
| `pause` | `-r, --reason <text>` | Record why the squad is paused |
| `pause` | `-j, --json` | Machine-readable output |
| `resume` | `-j, --json` | Machine-readable output |

**Behavior when a squad is paused**

- `squads run <squad>` exits with an error and shows the pause reason
- `squads run --org` silently skips the squad
- The autonomous daemon skips scheduled routines for the squad
- `squads status` marks the squad as `paused`
- All state, memory, and goals are preserved

Resume with `squads resume <squad>` to restore normal dispatch.

Everything above works locally — no login, no cloud, no API.
Every command supports `--json` for machine consumption.

Expand Down
26 changes: 19 additions & 7 deletions docs/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,25 @@ multi-agent synergy happens.
squads run research --parallel
```

**Autonomous dispatch** — let Squads decide what to run, when, and in
what order. Autopilot reads goals and feedback, respects phase
ordering, and manages budget constraints. This is the hands-off mode for
continuous operations.
**Autonomous dispatch** — a long-lived daemon reads cron schedules defined in
each squad's `SQUAD.md` and spawns agents automatically. Paused squads are
skipped. The daemon auto-pauses after repeated spawn failures (e.g., quota
exhausted) and resumes when you clear the pause.

```bash
squads autopilot --interval 30 --budget 50
squads autonomous start # Start the scheduling daemon
squads autonomous stop # Stop the daemon
squads autonomous status # Show daemon status + next runs
squads autonomous pause "quota hit" # Pause manually (daemon stays running)
squads autonomous resume # Resume after a pause
```

For timed one-off cycles rather than a persistent daemon, use `squads run`
interval flags:

```bash
squads run -i 30 --budget 50 # Autopilot: 30-minute cycles, $50/day cap
squads run --once --dry-run # Preview one autopilot cycle
```

## Local execution
Expand All @@ -53,8 +65,8 @@ explicitly pushes to GitHub or another service you've configured.
| 8–12 | 32 GB+ RAM, 10+ cores (M-series Mac / desktop) |

*Actual capacity depends on your CPU, memory, and which providers you
use. `squads autopilot --max-parallel 3` controls concurrent executions.
Monitor with `squads sessions`.*
use. `SQUADS_MAX_CONCURRENT=3` controls concurrent executions for the
autonomous daemon. Monitor with `squads sessions`.*

### Cloud scaling

Expand Down
29 changes: 28 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ program
.option('--phased', 'Autopilot: use dependency-based phase ordering (from SQUAD.md depends_on)')
.option('--no-eval', 'Skip post-run COO evaluation')
.option('--org', 'Run all squads as a coordinated org cycle (scan → plan → execute → report)')
.option('--force', 'Force re-run squads that already completed today')
.option('--force', 'Force re-run squads that already completed today; bypasses pause enforcement')
.option('--resume', 'Resume org cycle from where quota stopped it')
.option('--wait-for-quota', 'Org cycle: on quota cap, poll until the session window reopens instead of stopping')
.option('-y, --yes', 'Skip the org-run cost confirmation (for deliberate/non-interactive triggers)')
Expand Down Expand Up @@ -361,6 +361,33 @@ program.command('list').description('List squads (alias for: squads status)').ac
return statusCommand();
});

// Pause command - suspend a squad (enforced by runner + org planner + cron)
program
.command('pause <squad>')
.description('Pause a squad — run/org/cron dispatch will refuse until resumed')
.option('-r, --reason <text>', 'Reason for pausing')
.option('-j, --json', 'Output as JSON')
.addHelpText('after', `
Examples:
$ squads pause engineering Pause without reason
$ squads pause engineering --reason "waiting for design sign-off"
$ squads resume engineering Resume a paused squad
`)
.action(async (squad, options) => {
const { pauseCommand } = await import('./commands/pause.js');
return pauseCommand(squad, options);
});

// Resume command - reactivate a paused squad
program
.command('resume <squad>')
.description('Resume a paused squad')
.option('-j, --json', 'Output as JSON')
.action(async (squad, options) => {
const { resumeCommand } = await import('./commands/pause.js');
return resumeCommand(squad, options);
});

// Orchestrate command - lead-coordinated squad execution
registerOrchestrateCommand(program);

Expand Down
9 changes: 8 additions & 1 deletion src/commands/autonomous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { join } from "path";
import { homedir } from "os";
import { spawn, execSync } from "child_process";
import { findSquadsDir, listSquads, Routine } from "../lib/squad-parser.js";
import { findSquadsDir, listSquads, loadSquad, Routine } from "../lib/squad-parser.js";
import {
cronMatches,
getNextCronRun,
Expand Down Expand Up @@ -417,6 +417,13 @@ async function daemonLoop(): Promise<void> {
for (const routine of routines) {
if (!cronMatches(routine.schedule, now)) continue;

// Skip paused squads — do not dispatch any agents for them
const routineSquad = loadSquad(routine.squad);
if (routineSquad?.status === 'paused') {
daemonLog(`SKIP: ${routine.squad} is paused — skipping scheduled routine "${routine.name}"`);
continue;
}

for (const agentName of routine.agents) {
const key = `${routine.squad}/${agentName}`;

Expand Down
19 changes: 16 additions & 3 deletions src/commands/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ interface SquadMetrics {
mission: string;
goals: Goal[];
lastActivity: string;
status: 'active' | 'stale' | 'needs-goal';
status: 'active' | 'stale' | 'needs-goal' | 'paused';
github: SquadGitHubStats | null;
goalProgress: number; // 0-100
pausedSince?: string;
pausedReason?: string;
}

function getLastActivityDate(squadName: string): string {
Expand Down Expand Up @@ -135,7 +137,9 @@ function collectSquadMetrics(

let status: SquadMetrics['status'] = 'active';
const activeGoals = squad.goals.filter(g => !g.completed);
if (activeGoals.length === 0) {
if (squad.status === 'paused') {
status = 'paused';
} else if (activeGoals.length === 0) {
status = 'needs-goal';
} else if (lastActivity.includes('w') || lastActivity === '—') {
status = 'stale';
Expand Down Expand Up @@ -178,6 +182,8 @@ function collectSquadMetrics(
status,
github: githubStats,
goalProgress,
pausedSince: squad.paused_since,
pausedReason: squad.paused_reason,
});
}

Expand Down Expand Up @@ -293,12 +299,19 @@ function renderSquadsTable(squadData: SquadMetrics[]): void {
const completedCount = squad.goals.filter(g => g.completed).length;
const totalCount = squad.goals.length;

const isPaused = squad.status === 'paused';
const nameColor = isPaused ? colors.yellow : colors.cyan;
const commitColor = commits > 10 ? colors.green : commits > 0 ? colors.cyan : colors.dim;
const prColor = prs > 0 ? colors.green : colors.dim;
const issueColor = issuesClosed > 0 ? colors.green : colors.dim;

const pausedSuffix = isPaused ? `${colors.yellow}⏸${RESET}` : '';
const nameField = isPaused
? `${nameColor}${padEnd(squad.name, w.name - 1)}${RESET}${pausedSuffix}`
: `${nameColor}${padEnd(squad.name, w.name)}${RESET}`;

writeLine(` ${colors.purple}${box.vertical}${RESET} ` +
`${colors.cyan}${padEnd(squad.name, w.name)}${RESET}` +
nameField +
`${commitColor}${padEnd(String(commits), w.commits)}${RESET}` +
`${prColor}${padEnd(String(prs), w.prs)}${RESET}` +
`${issueColor}${padEnd(`${issuesClosed}/${issuesOpen}`, w.issues)}${RESET}` +
Expand Down
155 changes: 155 additions & 0 deletions src/commands/pause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { findSquadsDir, loadSquad, setSquadPauseState, findSimilarSquads, listSquads } from '../lib/squad-parser.js';
import { colors, bold, RESET, gradient, icons, writeLine } from '../lib/terminal.js';
import { track, Events } from '../lib/telemetry.js';

interface PauseOptions {
reason?: string;
json?: boolean;
}

interface ResumeOptions {
json?: boolean;
}

export async function pauseCommand(
squadName: string,
options: PauseOptions = {}
): Promise<void> {
await track(Events.CLI_STATUS, { squad: squadName, action: 'pause' });

const squadsDir = findSquadsDir();
if (!squadsDir) {
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'pause', error: 'No .agents/squads directory found' }, null, 2));
} else {
writeLine(` ${colors.red}No .agents/squads directory found${RESET}`);
writeLine(` ${colors.dim}Run \`squads init\` to create one.${RESET}`);
}
process.exit(1);
}

const squad = loadSquad(squadName);
if (!squad) {
const similar = findSimilarSquads(squadName, listSquads(squadsDir));
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'pause', error: `Squad "${squadName}" not found` }, null, 2));
} else {
writeLine(` ${colors.red}Squad "${squadName}" not found.${RESET}`);
if (similar.length > 0) {
writeLine(` ${colors.dim}Did you mean: ${similar.join(', ')}?${RESET}`);
}
}
process.exit(1);
}

if (squad.status === 'paused') {
const since = squad.paused_since ? ` since ${squad.paused_since}` : '';
const reason = squad.paused_reason ? ` (${squad.paused_reason})` : '';
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'pause', error: `Squad "${squadName}" is already paused${since}${reason}` }, null, 2));
} else {
writeLine(` ${colors.yellow}${icons.warning} Squad "${squadName}" is already paused${since}${reason}.${RESET}`);
}
process.exit(1);
}

const ok = setSquadPauseState(squadName, true, options.reason);
if (!ok) {
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'pause', error: 'Failed to write SQUAD.md' }, null, 2));
} else {
writeLine(` ${colors.red}Failed to write SQUAD.md for "${squadName}".${RESET}`);
}
process.exit(1);
}

if (options.json) {
console.log(JSON.stringify({
ok: true,
command: 'pause',
data: { squad: squadName, reason: options.reason || null, paused_since: new Date().toISOString() },
}, null, 2));
return;
}

writeLine();
writeLine(` ${gradient('squads')} ${colors.dim}pause${RESET} ${colors.cyan}${squadName}${RESET}`);
writeLine();
writeLine(` ${colors.yellow}${icons.warning} ${bold}${squadName}${RESET} is now ${colors.yellow}paused${RESET}.`);
if (options.reason) {
writeLine(` ${colors.dim}Reason: ${options.reason}${RESET}`);
}
writeLine();
writeLine(` ${colors.dim}Enforcement:${RESET}`);
writeLine(` ${colors.dim}• \`squads run ${squadName}\` will refuse until resumed${RESET}`);
writeLine(` ${colors.dim}• \`squads run --org\` will skip this squad${RESET}`);
writeLine(` ${colors.dim}• Scheduled dispatch will skip this squad${RESET}`);
writeLine();
writeLine(` ${colors.dim}To resume: squads resume ${squadName}${RESET}`);
writeLine(` ${colors.dim}To force-run anyway: squads run ${squadName} --force${RESET}`);
writeLine();
}

export async function resumeCommand(
squadName: string,
options: ResumeOptions = {}
): Promise<void> {
await track(Events.CLI_STATUS, { squad: squadName, action: 'resume' });

const squadsDir = findSquadsDir();
if (!squadsDir) {
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'resume', error: 'No .agents/squads directory found' }, null, 2));
} else {
writeLine(` ${colors.red}No .agents/squads directory found${RESET}`);
writeLine(` ${colors.dim}Run \`squads init\` to create one.${RESET}`);
}
process.exit(1);
}

const squad = loadSquad(squadName);
if (!squad) {
const similar = findSimilarSquads(squadName, listSquads(squadsDir));
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'resume', error: `Squad "${squadName}" not found` }, null, 2));
} else {
writeLine(` ${colors.red}Squad "${squadName}" not found.${RESET}`);
if (similar.length > 0) {
writeLine(` ${colors.dim}Did you mean: ${similar.join(', ')}?${RESET}`);
}
}
process.exit(1);
}

if (squad.status !== 'paused') {
if (options.json) {
console.log(JSON.stringify({ ok: true, command: 'resume', action: 'noop', message: `Squad "${squadName}" is already active` }, null, 2));
} else {
writeLine(` ${colors.yellow}${icons.warning} Squad "${squadName}" is not paused — nothing to do.${RESET}`);
}
return;
}

const ok = setSquadPauseState(squadName, false);
if (!ok) {
if (options.json) {
console.log(JSON.stringify({ ok: false, command: 'resume', error: 'Failed to write SQUAD.md' }, null, 2));
} else {
writeLine(` ${colors.red}Failed to write SQUAD.md for "${squadName}".${RESET}`);
}
process.exit(1);
}

if (options.json) {
console.log(JSON.stringify({ ok: true, command: 'resume', data: { squad: squadName } }, null, 2));
return;
}

writeLine();
writeLine(` ${gradient('squads')} ${colors.dim}resume${RESET} ${colors.cyan}${squadName}${RESET}`);
writeLine();
writeLine(` ${colors.green}${icons.success} ${bold}${squadName}${RESET} is now ${colors.green}active${RESET}.`);
writeLine();
writeLine(` ${colors.dim}Run: squads run ${squadName}${RESET}`);
writeLine();
}
16 changes: 16 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,22 @@ export async function runCommand(
// Check if target is a squad or an agent
const squad = loadSquad(squadName);

// Paused-squad enforcement: refuse to run a paused squad unless --force is set
if (squad && squad.status === 'paused' && !options.force) {
const since = squad.paused_since ? ` (paused ${new Date(squad.paused_since).toLocaleDateString()})` : '';
const reason = squad.paused_reason ? `: ${squad.paused_reason}` : '';
writeLine();
writeLine(` ${colors.yellow}⏸ Squad "${squadName}" is paused${since}${reason}.${RESET}`);
writeLine(` ${colors.dim}To run anyway: squads run ${squadName} --force${RESET}`);
writeLine(` ${colors.dim}To resume: squads resume ${squadName}${RESET}`);
writeLine();
process.exit(1);
}

if (squad && squad.status === 'paused' && options.force) {
writeLine(` ${colors.yellow}⏸ Warning: running paused squad "${squadName}" (--force override).${RESET}`);
}

// Pre-flight executor check: verify CLI and auth before attempting execution
// Only runs when we're actually going to execute (not dry-run)
if (options.execute && !options.dryRun) {
Expand Down
Loading
Loading