diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5c5def..3d6c5a9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 739761b..3daa51d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2d43da7 --- /dev/null +++ b/CHANGELOG.md @@ -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 ` 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 diff --git a/package-lock.json b/package-lock.json index 9955f02..6cfe6b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "squads-cli", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "squads-cli", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.71.2", diff --git a/package.json b/package.json index 2ffa510..b6b967a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/cli.ts b/src/cli.ts index 036c196..8ffe6a4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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, @@ -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) @@ -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 diff --git a/src/commands/brief.ts b/src/commands/brief.ts new file mode 100644 index 0000000..295d32d --- /dev/null +++ b/src/commands/brief.ts @@ -0,0 +1,173 @@ +import { Command } from 'commander'; +import { 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 { + 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', 'haiku'], { + 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.length > 120 ? task.body.slice(0, 120) + '...' : task.body}${RESET}`); + 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 proc = spawnSync('gh', ['issue', 'create', '-R', repo, '--title', task.title, '--body', task.body], { encoding: 'utf-8' }); + if (proc.status !== 0) throw new Error(proc.stderr); + const url = proc.stdout.trim(); + 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 ', '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, + }); + }); +}