Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
24 changes: 20 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,29 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
registry-url: 'https://registry.npmjs.org'

# Upgrade npm to a version that supports OIDC trusted publishing (>= 11.5.1)
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest

- run: npm ci

- run: npm run build

- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Determine npm dist-tag from package.json
id: dist_tag
run: |
VERSION=$(node -p "require('./package.json').version")
if [[ "$VERSION" == *-* ]]; then
echo "npm_tag=next" >> "$GITHUB_OUTPUT"
echo "Publishing pre-release $VERSION to @next"
else
echo "npm_tag=latest" >> "$GITHUB_OUTPUT"
echo "Publishing $VERSION to @latest"
fi

# Authentication via OIDC trusted publisher (configured on npmjs.com).
# No NPM_TOKEN needed.
- run: npm publish --provenance --access public --tag ${{ steps.dist_tag.outputs.npm_tag }}
10 changes: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'

# Upgrade npm to a version that supports OIDC trusted publishing (>= 11.5.1)
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest

- name: Install dependencies
run: npm ci

Expand All @@ -93,10 +97,10 @@ jobs:
echo "Publishing $TAG to @latest"
fi

# Authentication via OIDC trusted publisher (configured on npmjs.com).
# No NPM_TOKEN needed.
- name: Publish to npm
run: npm publish --provenance --access public --tag ${{ steps.dist_tag.outputs.npm_tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Changelog

All notable changes to `squads-cli` are documented here.

This project follows [Semantic Versioning](https://semver.org/).
Releases are also published as [GitHub Releases](https://github.com/agents-squads/squads-cli/releases).

## [0.3.1] — 2026-04-24

First stable v0.3.x release on `@latest`. Same code as `0.3.0-rc.1` (burned in on `@next`).

> Note: `0.3.0` was skipped because that version slot is reserved by a deprecated historical pre-release (Jan 2026) and npm enforces version immutability.

### Added
- **Conversation protocol** — agents talk to each other and use tools mid-conversation. `squads run <squad>` now drives a lead → scan → work → review → verify cycle.
- **Org cycle** — `squads run` with no target runs all squads in waves, with smart-skip for converged work.
- **New commands** — `review`, `credentials`, `goals`, `log`, plus minor refinements to `init`, `status`, and `run`.
- **Project config system** — `.squads/config.yml` for per-project settings (`agent_timeout_minutes`, `token_budget`, `cost_ceiling`, `company_name`, `compose_file`, `telemetry`). Resolution: env var > config file > defaults.
- **PreToolUse guardrail hooks** — agent sessions can be gated by user-defined safety hooks.
- **Demo agent scaffold** — `squads init` now includes starter agents and "what's next" guidance.
- **Growth squad template** — added to `squads init` seed templates.
- **Tier 2 documentation** — guides for local-services mode (Postgres, Redis, API, Bridge).

### Changed
- **Run engine rewrite** — decomposed into smaller modules (`conversation.ts`, `workflow.ts`, context helpers). Foundation for future cloud execution.
- **Role-based timeouts** — workers, reviewers, and leads have appropriate per-role timeouts (replaces hardcoded 8-minute ceiling).
- **Anti-collision rules** — multiple squads no longer race to create the same release PR or duplicate issues.
- **Prompts extracted** — lead briefings, planning instructions, and orchestrator prompts moved from TypeScript into `templates/prompts/*.md`.
- **Services command** — agnostic compose-file discovery (no hardcoded internal paths).
- **OIDC trusted publishing** — `release.yml` and `publish.yml` now publish via GitHub OIDC instead of `NPM_TOKEN`. No long-lived secret to rotate.
- **Audit remediation** — removed hardcoded values, parameterized company name, extracted internal prompts.

### Fixed
- **Telemetry write-only key** — restored after being incorrectly removed in March (telemetry has been silent since 2026-03-14).
- **First-run UX** — prerequisites check, helpful empty-state for `squads list` with no squads, schedule hint after first run.

### Infrastructure
- `@next` dist-tag channel — pre-release tags (`v0.3.0-rc.1`, `v0.4.0-beta.1`, etc.) auto-publish to `@next` for burn-in. Clean semver tags publish to `@latest`.
- npm install via `npm i -g squads-cli@next` for early access.

## [0.2.2] — 2026-03-28

- IDP (Internal Developer Platform), observability infrastructure, tiered architecture, org cycle scaffolding.

## [0.2.1] — 2026-03-13

- First-run experience reset.

## [0.2.0] and earlier

See [GitHub Releases](https://github.com/agents-squads/squads-cli/releases) for the full history.

> Versions `0.3.0`, `0.4.0`–`0.4.13`, `0.5.0`–`0.5.1`, `0.6.0`–`0.6.2`, and `0.7.0` were experimental pre-releases published in early 2026 and have been **deprecated** on npm. Do not install them. Start at `0.2.2` or `0.3.1+`.

[0.3.1]: https://github.com/agents-squads/squads-cli/releases/tag/v0.3.1
[0.2.2]: https://github.com/agents-squads/squads-cli/releases/tag/v0.2.2
[0.2.1]: https://github.com/agents-squads/squads-cli/releases/tag/v0.2.1
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "squads-cli",
"version": "0.3.0",
"version": "0.3.1",
"description": "Your AI workforce. Every user gets an AI manager that runs their team — finance, marketing, engineering, operations — for the cost of API calls.",
"type": "module",
"bin": {
Expand Down
8 changes: 8 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { registerServicesCommands } from './commands/services.js';
import { registerGoalsCommand } from './commands/goals.js';
import { registerCredentialsCommand } from './commands/credentials.js';
import { registerReviewCommand } from './commands/review.js';
import { registerBriefCommand } from './commands/brief.js';

// All other command handlers are lazy-loaded via dynamic import() inside
// action handlers. Only the invoked command's dependencies are loaded,
Expand Down Expand Up @@ -189,6 +190,12 @@ program
.name('squads')
.description('Your AI workforce — business operating system for AI managers')
.version(version)
.addHelpText('after', `
Resources:
Changelog https://github.com/agents-squads/squads-cli/blob/main/CHANGELOG.md
Releases https://github.com/agents-squads/squads-cli/releases
Issues https://github.com/agents-squads/squads-cli/issues
`)
// Enable typo suggestions (Commander.js built-in feature)
.showSuggestionAfterError(true)
// Configure help to exit with code 0 (Unix convention)
Expand Down Expand Up @@ -1089,6 +1096,7 @@ registerServicesCommands(program);
registerGoalsCommand(program);
registerCredentialsCommand(program);
registerReviewCommand(program);
registerBriefCommand(program);

// Providers command - show LLM CLI availability for multi-LLM support
program
Expand Down
176 changes: 176 additions & 0 deletions src/commands/brief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Command } from 'commander';
import { execSync, spawnSync } from 'child_process';
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
import { join } from 'path';
import { findProjectRoot } from '../lib/squad-parser.js';
import { getSquadRepos } from '../lib/squad-loop.js';
import { bold, colors, dim, RESET, writeLine } from '../lib/terminal.js';

interface BriefTask {
squad: string;
repo?: string;
title: string;
body: string;
}

interface BriefResult {
focus: string;
tasks: BriefTask[];
}

const EXTRACTION_PROMPT = `You are reading recent Claude Code session transcripts between a founder and their AI cofounder.

Extract actionable GitHub issues from these sessions. Look for:
- Things the founder explicitly asked for ("build X", "fix Y", "we need Z")
- Frustrations about current system behavior
- Decisions made that require implementation work
- Features discussed and approved

For each task identify the owning squad (intelligence, engineering, cli, product, finance, etc.), a clear imperative title under 72 chars, and a body with context + acceptance criteria (3-5 sentences).

Respond with ONLY valid JSON — no markdown, no explanation:
{
"focus": "one sentence: what is the founder currently most focused on?",
"tasks": [
{
"squad": "intelligence",
"title": "Build buyer shortlist from GPS and MP databases",
"body": "Query GPS (610 companies) and MP tender database to identify Chilean companies with AI budget and buying intent. Profile two buyer types: solo founders needing retainer support, and industrial CTOs in mining/water/energy. Cross-reference MP tender spend with GPS decision makers. Output: ranked shortlist with company, decision maker, budget signals, and reason to buy. Research only — no outreach."
}
]
}

Only include clearly actionable tasks. Skip vague intentions. Max 8 tasks.`;

async function briefCommand(options: { sessions: number; dryRun: boolean; coo: boolean }): Promise<void> {
const projectRoot = findProjectRoot();
if (!projectRoot) {
writeLine(` ${colors.red}Not in a squads project.${RESET} Run from a directory with .agents/`);
return;
}

const convDir = join(projectRoot, '.agents', 'conversations', 'cli');
if (!existsSync(convDir)) {
writeLine(` ${colors.yellow}No CLI sessions found${RESET} at ${dim}${convDir}${RESET}`);
return;
}

const files = readdirSync(convDir)
.filter(f => f.endsWith('.md'))
.map(f => ({ name: f, path: join(convDir, f), mtime: statSync(join(convDir, f)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime)
.slice(0, options.sessions);

if (files.length === 0) {
writeLine(` ${colors.yellow}No session files found.${RESET}`);
return;
}

writeLine();
writeLine(` ${bold}squads brief${RESET} ${dim}reading ${files.length} session(s)...${RESET}`);
writeLine();

const transcripts = files
.map(f => `=== Session: ${f.name} ===\n${readFileSync(f.path, 'utf-8')}`)
.join('\n\n');

let result: BriefResult;
try {
const { CLAUDECODE: _cc, ANTHROPIC_API_KEY: _ak, ...cleanEnv } = process.env;
const prompt = `${EXTRACTION_PROMPT}\n\n${transcripts}`;
const proc = spawnSync('claude', ['--print', '--model', 'claude-haiku-4-5-20251001'], {
Comment thread
agents-squads[bot] marked this conversation as resolved.
Outdated
input: prompt,
encoding: 'utf-8',
timeout: 60_000,
env: cleanEnv,
stdio: ['pipe', 'pipe', 'pipe'],
});

if (proc.status !== 0 || !proc.stdout) {
writeLine(` ${colors.red}Extraction failed.${RESET} Is ${dim}claude${RESET} installed and logged in?`);
return;
}

const jsonMatch = proc.stdout.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error('No JSON found in response');
result = JSON.parse(jsonMatch[0]) as BriefResult;
} catch (err) {
writeLine(` ${colors.red}Extraction failed:${RESET} ${err instanceof Error ? err.message : String(err)}`);
return;
}

const squadRepos = getSquadRepos();

writeLine(` ${bold}Founder focus:${RESET} ${result.focus}`);
writeLine();
writeLine(` ${colors.cyan}${result.tasks.length} issue(s) proposed:${RESET}`);
writeLine();

for (const task of result.tasks) {
const repo = task.repo ?? squadRepos[task.squad];
writeLine(` ${bold}[${task.squad}]${RESET} ${task.title}`);
writeLine(` ${dim}${task.body.slice(0, 120)}...${RESET}`);
Comment thread
agents-squads[bot] marked this conversation as resolved.
Outdated
if (repo) writeLine(` ${dim}→ ${repo}${RESET}`);
writeLine();
}

if (options.dryRun) {
writeLine(` ${dim}--dry-run: no issues created.${RESET}`);
return;
}

let created = 0;
let skipped = 0;

for (const task of result.tasks) {
const repo = task.repo ?? squadRepos[task.squad];
if (!repo) {
writeLine(` ${colors.yellow}skip${RESET} [${task.squad}] — no repo: field in SQUAD.md`);
skipped++;
continue;
}

try {
const title = task.title.replace(/"/g, '\\"');
const body = task.body.replace(/"/g, '\\"');
const url = execSync(
`gh issue create -R "${repo}" --title "${title}" --body "${body}" 2>/dev/null`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
).trim();
Comment thread
agents-squads[bot] marked this conversation as resolved.
Outdated
writeLine(` ${colors.green}created${RESET} ${url}`);
created++;
} catch {
writeLine(` ${colors.red}failed${RESET} [${task.squad}] ${task.title}`);
skipped++;
}
}

writeLine();
writeLine(
` ${bold}${created} created${RESET}${skipped > 0 ? ` ${dim}${skipped} skipped${RESET}` : ''}`
);

if (options.coo) {
const focusPath = join(projectRoot, '.agents', 'memory', 'company', 'founder-focus.md');
const date = new Date().toISOString().split('T')[0];
const content = `# Founder Focus — ${date}\n\n${result.focus}\n\n## Issues created\n\n${result.tasks.map(t => `- [${t.squad}] ${t.title}`).join('\n')}\n`;
writeFileSync(focusPath, content);
writeLine(` ${dim}founder-focus.md written → ${focusPath}${RESET}`);
}
}

export function registerBriefCommand(program: Command): void {
program
.command('brief')
.description('Read recent sessions, extract founder intentions, create GitHub issues')
.option('-n, --sessions <n>', 'Number of recent sessions to read', '5')
.option('--dry-run', 'Show proposed issues without creating them')
.option('--coo', 'Write founder-focus.md for COO context')
.action(async (options) => {
await briefCommand({
sessions: parseInt(options.sessions, 10),
dryRun: !!options.dryRun,
coo: !!options.coo,
});
});
}
Loading