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
13 changes: 13 additions & 0 deletions .changeset/quiet-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@bradygaster/squad-cli": minor
---

Add `--notify-level` to control watch round reporting noise (#803)

- `--notify-level important` (default): only print rounds with actual work items
- `--notify-level all`: print every round including empty (old behavior)
- `--notify-level none`: suppress all round output
- Add machine name (`os.hostname()`) and repo name to round headers for attribution
(shown in round headers when the board has items, and in "Board is clear" message)
- Configurable via `.squad/config.json` watch section: `"notifyLevel": "important"`
- Empty rounds silenced by default — no more "Round 160, Round 161" spam
Comment on lines +7 to +13

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changeset states machine/repo attribution is added to "round headers", but in notifyLevel: 'all' mode empty rounds currently print only the clear-board message without any (machine · repo) context. Either update the implementation so empty-round output also includes attribution, or tweak the changeset wording so it doesn’t overstate the attribution coverage.

Copilot uses AI. Check for mistakes.
158 changes: 158 additions & 0 deletions docs/src/content/blog/030-v092-whats-coming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
title: "What's Coming in v0.9.2 — 10 Features, Zero Breaking Changes"
date: 2026-04-04
author: "Tamir"
tags: [squad, release, features, watch, cleanup, fact-checker, external-state, self-upgrade, fleet, verbose, skills, scratch-dir, triage, notifications]
status: draft
hero: "The next Squad release ships 10 new features, 4 bug fixes, and 60+ tests — all backward-compatible. Here's what to expect."
---

# What's Coming in v0.9.2 — 10 Features, Zero Breaking Changes

> ⚠️ **Experimental** — Squad is alpha software. APIs, commands, and behavior may change between releases.

> _The next Squad release ships 10 new features, 4 bug fixes, and 60+ tests — all backward-compatible. Here's what to expect._

## The Noise Problem

If you've been running `squad watch` in production with output forwarded to Teams or Slack, you've probably noticed this:

```
Squad Monitor Round 158
Squad Monitor Round 159
Squad Monitor Round 160
Squad Monitor Round 161
```

Hundreds of messages. No useful information. No way to tell which machine or repo they came from.

This release fixes that — and ships 9 other features while we're at it.

## New Features

### 1. Quiet Notifications (`--notify-level`)

The biggest quality-of-life improvement: watch rounds are now **silent by default when the board is empty**. Only rounds with actual work produce output.

```bash
squad watch --notify-level important # default — only meaningful rounds
squad watch --notify-level all # old behavior (every round)
squad watch --notify-level none # fully silent
```

Round headers now include **machine name and repo** for attribution:

```
🔄 Ralph — Round 5 (DEVBOX-01 · my-project)
```

Persistent config: `{ "watch": { "notifyLevel": "important" } }` in `.squad/config.json`.

### 2. Fleet Hybrid Dispatch (`--dispatch-mode`)

Parallel issue processing via Copilot CLI `/fleet`. Benchmarked at **2.9x faster** for read-heavy workloads.

```bash
squad watch --execute --dispatch-mode hybrid
```

Issues are auto-classified as read (research, reviews → fleet) or write (implementations, fixes → local). Fleet batches reads in parallel while writes execute sequentially.

### 3. Verbose Debugging (`--verbose`)

When watch seems stuck on "Board is clear" but you know there's work, `--verbose` shows you exactly what's happening:

```bash
squad watch --verbose
```

Prints: issue counts, label matches, auth status, PR review states, round timing, capability execution paths.

### 4. Fact-Checker Agent Role (🔍)

New built-in role for output verification. Validates claims, detects hallucinations, runs counter-hypotheses.

```
squad new agent fact-checker
```

Confidence ratings: ✅ Verified, ⚠️ Unverified, ❌ Contradicted. Routes automatically on "fact-check", "verify", "audit" keywords.

### 5. 8 Built-in Skills

`squad init` and `squad upgrade` now ship 8 curated skills:

| Skill | What it teaches |
|-------|-----------------|
| squad-conventions | Core patterns and file layout |
| error-recovery | Graceful failure handling |
| secret-handling | Credential safety |
| git-workflow | Branch and commit conventions |
| session-recovery | Checkpoint and resume |
| reviewer-protocol | Code review gates |
| test-discipline | Test-first discipline |
| agent-collaboration | Multi-agent handoffs |

### 6. Scratch Directory (`.squad/.scratch/`)

Agents no longer dump temp files in the repo root. The new `scratchDir()` and `scratchFile()` SDK APIs route all ephemeral files to `.squad/.scratch/` — gitignored and auto-cleaned.

### 7. Cleanup Watch Capability

Automated housekeeping during `squad watch`:
- Clears `.squad/.scratch/` every 10 rounds
- Archives orchestration-log and session-log entries older than 30 days
- Warns about stale decision inbox files (>7 days)

### 8. External State Storage

Move `.squad/` state outside the working tree so it survives branch switches:

```bash
squad externalize # state moves to ~/.squad/projects/{repo}/
squad internalize # move it back
```

### 9. Self-Upgrade (`squad upgrade --self`)

Update the CLI itself from within squad:

```bash
squad upgrade --self # latest stable
squad upgrade --self --insider # latest prerelease
```

Auto-detects npm/pnpm/yarn. After CLI upgrade, automatically runs repo upgrade to apply new templates.

### 10. Triage Label Slug Fix

Multi-word agent names (like "Steve Rogers") now correctly generate `squad:steve-rogers` labels instead of `squad:steve rogers` (which GitHub rejects). Labels are pre-created at watch startup.

## Bug Fixes

- **PR contamination** — Scribe now stages only `.squad/` files, not `git add .` (#783)
- **Outdated review threads** — PR readiness check ignores threads where code changed (#780)
- **Filename uniqueness** — `scratchFile()` uses monotonic counter, not just `Date.now()`
- **Cross-platform paths** — `deriveProjectKey()` handles Windows paths on Linux CI

## By the Numbers

- **10 features** across SDK, CLI, and watch capabilities
- **4 bug fixes** (1 critical — PR contamination)
- **60+ new tests** (scratch: 8, cleanup: 12, fact-checker: 8, skills: 5, external: 12, self-upgrade: 4, notify: 5, triage: 12)
- **4 new feature docs** + 2 updated + README command table (15→17)
- **0 breaking changes** — all opt-in, all backward-compatible
- **1 behavioral change** — notify default flipped from `all` to `important` (use `--notify-level all` to restore)

## Upgrade

```bash
squad upgrade --self # get the latest CLI
squad upgrade # apply new templates to your repo
```

Or if you're on insider:

```bash
squad upgrade --self --insider
```
89 changes: 89 additions & 0 deletions docs/src/content/docs/features/notification-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Notification Level

> ⚠️ **Experimental** — Squad is alpha software. APIs, commands, and behavior may change between releases.

**Try this to silence empty rounds:**
```
squad watch --notify-level important
```

**Try this to see everything (debugging):**
```
squad watch --notify-level all
```

**Try this in config for persistent setting:**
```json
{ "watch": { "notifyLevel": "important" } }
```

When `squad watch` runs continuously, it prints a board report after every round. In production setups where output is forwarded to Teams, Slack, or email, this creates noise — hundreds of "Round N" messages with no useful content when the board is clear.

---

## Notify Levels

| Level | Behavior | When to use |
|-------|----------|-------------|
| `important` | Only print rounds with actual work items | **Default.** Best for production/Teams channels |
| `all` | Print every round, including empty ones | Debugging. Old behavior before this feature |
| `none` | Suppress all round output | Headless/CI — only errors matter |

## Machine and Repo Attribution

Every round header now includes the machine hostname and repo name:

```
🔄 Ralph — Round 5 (DEVBOX-01 · my-project)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 Untriaged: 2
🟢 Ready to merge: 1
```

This tells you **where** the message came from — which machine and which repo — so when multiple watch instances report to the same Teams channel, you can distinguish them.

## Configuration

### CLI flag (per-run)

```bash
squad watch --notify-level important # default
squad watch --notify-level all # old behavior
squad watch --notify-level none # silent
```

### Config file (persistent)

In `.squad/config.json`:

```json
{
"watch": {
"notifyLevel": "important",
"interval": 10,
"execute": true
}
}
```

Config file settings are overridden by CLI flags when both are present.

## What Counts as "Important"

A round is considered important (and reported) when **any** board counter is non-zero:

- Untriaged issues
- Assigned but unstarted work
- Draft PRs
- Changes requested on PRs
- CI failures
- PRs needing review
- PRs ready to merge
- Issues executed this round

If all counters are zero, the round is silent in `important` mode.

## See Also

- [Ralph — Work Monitor](/docs/features/ralph) — full Ralph documentation
- [Watch capabilities](/docs/features/ralph#watch-mode) — how squad watch works
2 changes: 2 additions & 0 deletions docs/src/content/docs/features/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,10 @@ All new features are **opt-in** and disabled by default. Existing `squad watch`

| Flag | Description | Example |
|------|-------------|---------|
| `--notify-level LEVEL` | Control round reporting noise: `important` (default), `all`, `none` | `squad watch --notify-level important` |
| `--retro` | Enforce retrospective checks (Fridays or when missed >7 days) | `squad watch --retro` |
| `--decision-hygiene` | Auto-merge decision inbox when >5 files | `squad watch --decision-hygiene` |
| `--cleanup` | Auto-clear scratch files, archive old logs (every 10 rounds) | `squad watch --cleanup` |
| `--channel-routing` | Route notifications to specific Teams channels (requires `.squad/teams-channels.json`) | `squad watch --channel-routing` |

### Common Workflows
Expand Down
9 changes: 9 additions & 0 deletions packages/squad-cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ async function main(): Promise<void> {
: { projectNumber: parseInt(args[boardProjectIdx + 1]!, 10) };
}

// Notify level: --notify-level all|important|none (default: important)
const notifyLevelIdx = args.indexOf('--notify-level');
const notifyLevelArg = (notifyLevelIdx !== -1 && args[notifyLevelIdx + 1]) ? args[notifyLevelIdx + 1] : undefined;
const notifyLevel = (notifyLevelArg === 'all' || notifyLevelArg === 'important' || notifyLevelArg === 'none')
? notifyLevelArg : undefined;

// verbose flag parsing is in PR #782 (--verbose)

// Load config: .squad/config.json merged with CLI overrides
const config = loadWatchConfig(process.cwd(), {
interval,
Expand All @@ -394,6 +402,7 @@ async function main(): Promise<void> {
timeout,
copilotFlags,
agentCmd,
notifyLevel,
capabilities: Object.keys(capabilities).length > 0 ? capabilities : undefined,
});

Expand Down
18 changes: 17 additions & 1 deletion packages/squad-cli/src/cli/commands/watch/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ export interface WatchConfig {
agentCmd?: string;
/** Per-capability config: `true` / `false` / object with sub-options. */
capabilities: Record<string, boolean | Record<string, unknown>>;
/**
* Controls how verbose watch round reporting is.
* - 'all' — print every round (current behavior)
* - 'important' — only print rounds with actual work (default)
* - 'none' — suppress all round reporting
*/
notifyLevel?: 'all' | 'important' | 'none';
/** Verbose diagnostics (--verbose flag). */
verbose?: boolean;
}
Comment on lines +23 to 32

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WatchConfig introduces a verbose option and documents it as coming from a --verbose flag, but there is no CLI parsing for --verbose and the value is not referenced anywhere in the watch implementation. This is misleading documentation and effectively a dead config knob. Either remove verbose from WatchConfig/normalize logic for now, or implement the flag parsing and wire config.verbose into actual diagnostics output.

Copilot uses AI. Check for mistakes.

const DEFAULTS: WatchConfig = {
interval: 10,
execute: false,
maxConcurrent: 1,
timeout: 30,
notifyLevel: 'important',
capabilities: {},
};

Expand Down Expand Up @@ -63,6 +73,8 @@ export function loadWatchConfig(
timeout: cliOverrides.timeout ?? fileConfig.timeout ?? DEFAULTS.timeout,
copilotFlags: cliOverrides.copilotFlags ?? fileConfig.copilotFlags ?? DEFAULTS.copilotFlags,
agentCmd: cliOverrides.agentCmd ?? fileConfig.agentCmd ?? DEFAULTS.agentCmd,
notifyLevel: cliOverrides.notifyLevel ?? fileConfig.notifyLevel ?? DEFAULTS.notifyLevel,
verbose: cliOverrides.verbose ?? fileConfig.verbose ?? false,
capabilities: {
...DEFAULTS.capabilities,
...(fileConfig.capabilities ?? {}),
Expand All @@ -83,10 +95,14 @@ function normalizeFileConfig(raw: Record<string, unknown>): Partial<WatchConfig>
if (typeof raw['timeout'] === 'number') result.timeout = raw['timeout'];
if (typeof raw['copilotFlags'] === 'string') result.copilotFlags = raw['copilotFlags'];
if (typeof raw['agentCmd'] === 'string') result.agentCmd = raw['agentCmd'];
if (raw['notifyLevel'] === 'all' || raw['notifyLevel'] === 'important' || raw['notifyLevel'] === 'none') {
result.notifyLevel = raw['notifyLevel'];
}
if (typeof raw['verbose'] === 'boolean') result.verbose = raw['verbose'];

// Everything else is a capability key
const caps: Record<string, boolean | Record<string, unknown>> = {};
const reserved = new Set(['interval', 'execute', 'maxConcurrent', 'timeout', 'copilotFlags', 'agentCmd']);
const reserved = new Set(['interval', 'execute', 'maxConcurrent', 'timeout', 'copilotFlags', 'agentCmd', 'notifyLevel', 'verbose']);
for (const [key, value] of Object.entries(raw)) {
if (reserved.has(key)) continue;
if (typeof value === 'boolean' || (typeof value === 'object' && value !== null && !Array.isArray(value))) {
Expand Down
Loading
Loading