Repository: aarondfrancis/counselors Branch: main Commit: ddf0a2476d95 Files: 117 Total size: 520.3 KB Directory structure: gitextract_xut__s4g/ ├── .gitattributes ├── .github/ │ ├── scripts/ │ │ └── parse-changelog.sh │ └── workflows/ │ ├── ci.yml │ ├── release-binaries.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── assets/ │ ├── amp-deep-settings.json │ ├── amp-readonly-settings.json │ └── presets/ │ ├── bughunt.yml │ ├── contracts.yml │ ├── hotspots.yml │ ├── invariants.yml │ ├── regression.yml │ └── security.yml ├── biome.json ├── install.sh ├── package.json ├── scripts/ │ └── build-binaries.ts ├── src/ │ ├── adapters/ │ │ ├── amp.ts │ │ ├── base.ts │ │ ├── claude.ts │ │ ├── codex.ts │ │ ├── custom.ts │ │ ├── gemini.ts │ │ └── index.ts │ ├── cli.ts │ ├── commands/ │ │ ├── _run-shared.ts │ │ ├── agent.ts │ │ ├── cleanup.ts │ │ ├── config.ts │ │ ├── doctor.ts │ │ ├── groups/ │ │ │ ├── add.ts │ │ │ ├── list.ts │ │ │ └── remove.ts │ │ ├── init.ts │ │ ├── loop.ts │ │ ├── make-dir.ts │ │ ├── run.ts │ │ ├── skill.ts │ │ ├── tools/ │ │ │ ├── add.ts │ │ │ ├── discover.ts │ │ │ ├── list.ts │ │ │ ├── remove.ts │ │ │ ├── rename.ts │ │ │ └── test.ts │ │ └── upgrade.ts │ ├── constants.ts │ ├── core/ │ │ ├── amp-utils.ts │ │ ├── boilerplate.ts │ │ ├── cleanup.ts │ │ ├── config.ts │ │ ├── context.ts │ │ ├── discovery.ts │ │ ├── dispatcher.ts │ │ ├── executor.ts │ │ ├── fs-utils.ts │ │ ├── loop.ts │ │ ├── prompt-builder.ts │ │ ├── prompt-writer.ts │ │ ├── repo-discovery.ts │ │ ├── synthesis.ts │ │ ├── text-utils.ts │ │ └── upgrade.ts │ ├── presets/ │ │ ├── index.ts │ │ └── types.ts │ ├── types.ts │ └── ui/ │ ├── agent-reporter.ts │ ├── logger.ts │ ├── output.ts │ ├── prompts.ts │ ├── reporter.ts │ └── terminal-reporter.ts ├── tests/ │ ├── fixtures/ │ │ ├── bin/ │ │ │ ├── fake-amp │ │ │ ├── fake-claude │ │ │ └── fake-codex │ │ └── configs/ │ │ └── valid.json │ ├── integration/ │ │ └── cli.test.ts │ └── unit/ │ ├── adapters/ │ │ ├── amp.test.ts │ │ ├── claude.test.ts │ │ ├── codex.test.ts │ │ ├── custom.test.ts │ │ ├── gemini.test.ts │ │ └── resolve.test.ts │ ├── agent-reporter.test.ts │ ├── amp-utils.test.ts │ ├── cleanup.test.ts │ ├── config.test.ts │ ├── constants.test.ts │ ├── context.test.ts │ ├── discovery.test.ts │ ├── dispatcher.test.ts │ ├── execute-test.test.ts │ ├── executor.test.ts │ ├── fs-utils.test.ts │ ├── logger.test.ts │ ├── loop-command.test.ts │ ├── loop.test.ts │ ├── output.test.ts │ ├── presets.test.ts │ ├── prompt-builder.test.ts │ ├── prompt-writer.test.ts │ ├── prompts.test.ts │ ├── repo-discovery.test.ts │ ├── reporter.test.ts │ ├── run-shared.test.ts │ ├── synthesis.test.ts │ ├── terminal-reporter.test.ts │ ├── text-utils.test.ts │ ├── tools-add-custom-model.test.ts │ ├── upgrade-exec.test.ts │ ├── upgrade-standalone.test.ts │ └── upgrade.test.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.bat text eol=crlf *.cmd text eol=crlf ================================================ FILE: .github/scripts/parse-changelog.sh ================================================ #!/bin/bash set -e # parse-changelog.sh # Parses and updates CHANGELOG.md following Keep a Changelog format # # Usage: # ./parse-changelog.sh validate - Check changelog has unreleased content # ./parse-changelog.sh release - Move unreleased to version section CHANGELOG_FILE="${CHANGELOG_FILE:-CHANGELOG.md}" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color error() { echo -e "${RED}Error: $1${NC}" >&2 exit 1 } success() { echo -e "${GREEN}$1${NC}" } warn() { echo -e "${YELLOW}$1${NC}" } # Check if changelog exists check_changelog_exists() { if [[ ! -f "$CHANGELOG_FILE" ]]; then error "CHANGELOG.md not found" fi } # Validate that [Unreleased] section exists and has content validate() { check_changelog_exists if ! grep -q "## \[Unreleased\]" "$CHANGELOG_FILE"; then error "CHANGELOG.md does not contain an [Unreleased] section" fi # Extract content between [Unreleased] and the next version section local unreleased_content unreleased_content=$(awk ' /^## \[Unreleased\]/ { capture = 1; next } /^## \[/ && capture { exit } capture { print } ' "$CHANGELOG_FILE") # Check if there's any actual content (not just whitespace) local trimmed trimmed=$(echo "$unreleased_content" | grep -v '^[[:space:]]*$' | grep -v '^###' || true) if [[ -z "$trimmed" ]]; then warn "No unreleased changes found. Will use default release notes." else success "Changelog validation passed. Found unreleased changes." fi } # Move unreleased content to a new version section release() { local version="$1" if [[ -z "$version" ]]; then error "Version argument required. Usage: $0 release " fi # Validate version format (X.Y.Z) if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then error "Invalid version format '$version'. Expected X.Y.Z (e.g., 1.2.0)" fi check_changelog_exists validate # If unreleased section is empty, inject default content local unreleased_content unreleased_content=$(awk ' /^## \[Unreleased\]/ { capture = 1; next } /^## \[/ && capture { exit } capture { print } ' "$CHANGELOG_FILE") local trimmed trimmed=$(echo "$unreleased_content" | grep -v '^[[:space:]]*$' | grep -v '^###' || true) if [[ -z "$trimmed" ]]; then if [[ "$(uname)" == "Darwin" ]]; then sed -i '' '/^## \[Unreleased\]/a\ \ ### Fixed\ - Various bug fixes\ ' "$CHANGELOG_FILE" else sed -i '/^## \[Unreleased\]/a\\n### Fixed\n- Various bug fixes\n' "$CHANGELOG_FILE" fi fi local date date=$(date +%Y-%m-%d) # Get repository info for links local repo_url repo_url=$(git config --get remote.origin.url 2>/dev/null | sed 's/\.git$//' | sed 's/git@github.com:/https:\/\/github.com\//') if [[ -z "$repo_url" ]]; then warn "Could not detect repository URL. Links may need manual update." repo_url="https://github.com/OWNER/REPO" fi # Use awk to move content from [Unreleased] to the new version awk -v version="$version" -v date="$date" ' BEGIN { in_unreleased = 0; content = "" } /^## \[Unreleased\]/ { print $0 print "" in_unreleased = 1 next } /^## \[/ && in_unreleased { # Found next version section, output new version with collected content print "## [" version "] - " date print content in_unreleased = 0 } # Stop capturing at link references section (lines starting with [) /^\[/ && in_unreleased { # Output new version with collected content before the links print "## [" version "] - " date print content in_unreleased = 0 } in_unreleased { content = content $0 "\n" next } { print } END { # Handle case where unreleased section goes to end of file if (in_unreleased && content != "") { print "## [" version "] - " date print content } } ' "$CHANGELOG_FILE" > "$CHANGELOG_FILE.tmp" && mv "$CHANGELOG_FILE.tmp" "$CHANGELOG_FILE" # Update the [Unreleased] comparison link if grep -q "\[Unreleased\]:.*compare" "$CHANGELOG_FILE"; then sed -i.bak "s|\[Unreleased\]: \(.*\)/compare/v[0-9.]*\.\.\.HEAD|[Unreleased]: \1/compare/v$version...HEAD|" "$CHANGELOG_FILE" rm -f "$CHANGELOG_FILE.bak" fi # Find previous version for the new link local prev_version prev_version=$(grep -oE '\[[0-9]+\.[0-9]+\.[0-9]+\](?=:)' "$CHANGELOG_FILE" 2>/dev/null | head -1 | tr -d '[]' || true) # Add new version link if [[ -n "$prev_version" ]] && [[ "$prev_version" != "$version" ]]; then # Insert new version link before the previous version link sed -i.bak "/^\[$prev_version\]:/i\\ [$version]: $repo_url/compare/v$prev_version...v$version " "$CHANGELOG_FILE" rm -f "$CHANGELOG_FILE.bak" elif ! grep -q "^\[$version\]:" "$CHANGELOG_FILE"; then # First release - add link at the end echo "[$version]: $repo_url/releases/tag/v$version" >> "$CHANGELOG_FILE" fi success "Updated CHANGELOG.md for version $version" } # Show usage usage() { echo "Usage: $0 [args]" echo "" echo "Commands:" echo " validate Check changelog has unreleased content" echo " release Move unreleased content to version section" echo "" echo "Environment variables:" echo " CHANGELOG_FILE Path to changelog (default: CHANGELOG.md)" } # Main case "${1:-}" in validate) validate ;; release) release "$2" ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: write jobs: autoformat: if: github.event_name == 'push' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci - name: Auto-fix formatting run: npx biome check --write src/ tests/ - name: Commit formatting fixes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A git diff --cached --quiet || git commit -m "style: auto-fix formatting [skip ci]" git push test: runs-on: ${{ matrix.os }} needs: [autoformat] if: always() && (needs.autoformat.result == 'success' || needs.autoformat.result == 'skipped') strategy: matrix: include: - os: ubuntu-latest node-version: 20 test-command: npm run test - os: ubuntu-latest node-version: 22 test-command: npm run test - os: ubuntu-latest node-version: 24 test-command: npm run test - os: windows-latest node-version: 20 test-command: npm run test -- tests/unit - os: windows-latest node-version: 22 test-command: npm run test -- tests/unit - os: windows-latest node-version: 24 test-command: npm run test -- tests/unit steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - name: Install dependencies run: npm ci - name: Typecheck run: npm run typecheck - name: Lint run: npm run lint - name: Build run: npm run build - name: Test run: ${{ matrix.test-command }} ================================================ FILE: .github/workflows/release-binaries.yml ================================================ name: Release Binaries on: workflow_call: inputs: tag: description: 'Tag to build binaries for (e.g., v1.2.3)' required: true type: string workflow_dispatch: inputs: tag: description: 'Tag to build binaries for (e.g., v1.2.3)' required: true type: string permissions: contents: write jobs: build-binaries: runs-on: ubuntu-latest steps: - name: Normalize and validate tag id: tag run: | TAG="${{ inputs.tag }}" TAG="${TAG#refs/tags/}" if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Error: Invalid tag format '$TAG'. Expected vX.Y.Z" exit 1 fi echo "value=$TAG" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4 with: ref: refs/tags/${{ steps.tag.outputs.value }} - uses: oven-sh/setup-bun@v2 with: bun-version: 1.2.4 - name: Install dependencies run: bun install --frozen-lockfile - name: Build binaries run: bun run scripts/build-binaries.ts - name: Generate checksums run: | cd release for f in counselors-darwin-arm64 counselors-darwin-x64 counselors-linux-x64 counselors-linux-arm64; do sha256sum "$f" > "$f.sha256" done - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.tag.outputs.value }} files: | release/counselors-darwin-arm64 release/counselors-darwin-x64 release/counselors-linux-x64 release/counselors-linux-arm64 release/counselors-darwin-arm64.sha256 release/counselors-darwin-x64.sha256 release/counselors-linux-x64.sha256 release/counselors-linux-arm64.sha256 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release run-name: "Release ${{ inputs.version }}" on: workflow_dispatch: inputs: version: description: 'Version to release (e.g., 1.2.0 or v1.2.0)' required: true type: string permissions: contents: write id-token: write jobs: ci: runs-on: ubuntu-latest if: github.actor == 'aarondfrancis' steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci - name: Typecheck run: npm run typecheck - name: Lint run: npm run lint - name: Build run: npm run build - name: Test run: npm run test release: needs: ci runs-on: ubuntu-latest if: github.actor == 'aarondfrancis' outputs: version: ${{ steps.version.outputs.number }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Normalize and validate version id: version run: | VERSION="${{ inputs.version }}" VERSION="${VERSION#v}" if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Error: Invalid version format '$VERSION'. Expected X.Y.Z or vX.Y.Z" exit 1 fi echo "number=$VERSION" >> "$GITHUB_OUTPUT" - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 registry-url: https://registry.npmjs.org cache: npm - name: Upgrade npm for OIDC support run: npm install -g npm@latest - name: Install dependencies run: npm ci - name: Validate changelog run: | chmod +x .github/scripts/parse-changelog.sh .github/scripts/parse-changelog.sh validate - name: Configure git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Bump version in package.json run: npm version ${{ steps.version.outputs.number }} --no-git-tag-version - name: Update changelog run: .github/scripts/parse-changelog.sh release ${{ steps.version.outputs.number }} - name: Commit version bump and changelog run: | git add package.json package-lock.json CHANGELOG.md git commit -m "Release v${{ steps.version.outputs.number }}" - name: Create and push tag run: | git tag "v${{ steps.version.outputs.number }}" git push origin "v${{ steps.version.outputs.number }}" - name: Push to main run: git push - name: Build run: npm run build - name: Pack npm tarball and compute checksum id: tarball run: | set -euo pipefail PACK_OUTPUT_FILE="$(mktemp)" npm pack --json > "$PACK_OUTPUT_FILE" cat "$PACK_OUTPUT_FILE" TARBALL="$(node -e "const fs = require('node:fs'); const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); if (!Array.isArray(data) || !data[0] || !data[0].filename) process.exit(1); process.stdout.write(data[0].filename);" "$PACK_OUTPUT_FILE")" SHA256="$(sha256sum "$TARBALL" | awk '{print $1}')" echo "filename=$TARBALL" >> "$GITHUB_OUTPUT" echo "sha256=$SHA256" >> "$GITHUB_OUTPUT" - name: Publish to npm run: npm publish --provenance --access public "${{ steps.tarball.outputs.filename }}" release-binaries: needs: release if: github.actor == 'aarondfrancis' uses: ./.github/workflows/release-binaries.yml with: tag: v${{ needs.release.outputs.version }} update-homebrew: needs: - release - release-binaries runs-on: ubuntu-latest if: github.actor == 'aarondfrancis' steps: - name: Update Homebrew formula env: VERSION: ${{ needs.release.outputs.version }} COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | set -euo pipefail if [ -z "${COMMITTER_TOKEN:-}" ]; then echo "HOMEBREW_TAP_TOKEN is not configured." exit 1 fi TAP_DIR="$(mktemp -d)" FORMULA_PATH="Formula/counselors.rb" RELEASE_BASE_URL="https://github.com/aarondfrancis/counselors/releases/download/v${VERSION}" read_sha() { local name="$1" local hash="" for attempt in {1..8}; do hash="$(curl -fsSL "${RELEASE_BASE_URL}/${name}.sha256" | awk '{print $1}' | tr -d '\r\n' || true)" if [[ "$hash" =~ ^[a-f0-9]{64}$ ]]; then echo "$hash" return 0 fi if [ "$attempt" -eq 8 ]; then echo "Failed to fetch valid checksum for ${name} after retries." exit 1 fi sleep 10 done } DARWIN_ARM64_SHA="$(read_sha counselors-darwin-arm64)" DARWIN_X64_SHA="$(read_sha counselors-darwin-x64)" LINUX_ARM64_SHA="$(read_sha counselors-linux-arm64)" LINUX_X64_SHA="$(read_sha counselors-linux-x64)" for hash in "$DARWIN_ARM64_SHA" "$DARWIN_X64_SHA" "$LINUX_ARM64_SHA" "$LINUX_X64_SHA"; do if ! [[ "$hash" =~ ^[a-f0-9]{64}$ ]]; then echo "Invalid checksum fetched from release assets: $hash" exit 1 fi done git clone "https://x-access-token:${COMMITTER_TOKEN}@github.com/aarondfrancis/homebrew-tap.git" "$TAP_DIR" cd "$TAP_DIR" cat > "$FORMULA_PATH" < "counselors" end test do assert_match version.to_s, shell_output("#{bin}/counselors --version") end end EOF if git diff --quiet -- "$FORMULA_PATH"; then echo "No Homebrew formula changes to commit." exit 0 fi git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add "$FORMULA_PATH" git commit -m "counselors ${VERSION}" git push origin main smoke-test-installs: needs: - release - release-binaries - update-homebrew runs-on: ${{ matrix.os }} if: github.actor == 'aarondfrancis' strategy: fail-fast: false matrix: include: - method: npm os: macos-latest - method: npm os: ubuntu-latest - method: standalone os: macos-latest - method: standalone os: ubuntu-latest - method: homebrew os: macos-latest env: VERSION: ${{ needs.release.outputs.version }} steps: - name: Smoke test npm install if: matrix.method == 'npm' run: | set -euo pipefail PREFIX="$RUNNER_TEMP/counselors-npm" rm -rf "$PREFIX" for attempt in {1..8}; do if npm install -g "counselors@${VERSION}" --prefix "$PREFIX"; then break fi if [ "$attempt" -eq 8 ]; then echo "npm install failed after retries." exit 1 fi sleep 10 done "$PREFIX/bin/counselors" --help >/dev/null test "$("$PREFIX/bin/counselors" --version)" = "$VERSION" - name: Smoke test standalone install if: matrix.method == 'standalone' run: | set -euo pipefail INSTALL_DIR="$RUNNER_TEMP/counselors-standalone" SCRIPT="$RUNNER_TEMP/counselors-install.sh" for attempt in {1..8}; do if curl -fsSL "https://raw.githubusercontent.com/aarondfrancis/counselors/v${VERSION}/install.sh" -o "$SCRIPT"; then break fi if [ "$attempt" -eq 8 ]; then echo "failed to download install.sh after retries." exit 1 fi sleep 5 done chmod +x "$SCRIPT" rm -rf "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" for attempt in {1..8}; do if INSTALL_DIR="$INSTALL_DIR" COUNSELORS_VERSION="$VERSION" GITHUB_TOKEN="${{ github.token }}" bash "$SCRIPT"; then break fi if [ "$attempt" -eq 8 ]; then echo "standalone install failed after retries." exit 1 fi rm -f "$INSTALL_DIR/counselors" sleep 10 done "$INSTALL_DIR/counselors" --help >/dev/null test "$("$INSTALL_DIR/counselors" --version)" = "$VERSION" - name: Smoke test Homebrew install if: matrix.method == 'homebrew' run: | set -euo pipefail brew tap aarondfrancis/homebrew-tap for attempt in {1..8}; do if brew list --versions counselors >/dev/null 2>&1; then brew upgrade aarondfrancis/homebrew-tap/counselors || true else brew install aarondfrancis/homebrew-tap/counselors || true fi INSTALLED_VERSION="$(brew list --versions counselors 2>/dev/null | awk '{print $2}')" if [ "$INSTALLED_VERSION" = "$VERSION" ]; then break fi if [ "$attempt" -eq 8 ]; then echo "Homebrew install failed after retries." exit 1 fi brew uninstall counselors --force >/dev/null 2>&1 || true sleep 15 done BREW_BIN="$(brew --prefix counselors)/bin/counselors" "$BREW_BIN" --help >/dev/null test "$("$BREW_BIN" --version)" = "$VERSION" ================================================ FILE: .gitignore ================================================ node_modules/ dist/ release/ agents/ *.tgz ogimage.txt ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Changed - Standalone release binaries are now built into `release/` instead of `dist/`, decoupling binary artifacts from npm package contents - Homebrew formula updates now target platform-specific GitHub release binaries directly (macOS/Linux, arm64/x64) instead of the npm tarball ### Fixed - npm package publish size is dramatically reduced by excluding standalone compiled binaries from the published `files` list - Release smoke tests now include Linux (`ubuntu-latest`) for `npm` and `standalone` install paths to catch Linux-only install/runtime breakages - Homebrew checksum resolution now retries when fetching freshly uploaded release asset checksums, reducing transient CDN propagation failures - Generated Homebrew formula install logic now validates binary discovery in the staging directory before install, with clearer failure behavior ## [0.5.2] - 2026-02-27 ### Added - `loop --duration` now prints the configured duration at the start of execution so users know how long the session will run ## [0.5.1] - 2026-02-27 ### Added - Child process PIDs are now surfaced during discovery and prompt-writing phases, giving outer agents visibility to monitor long-running prep steps ## [0.5.0] - 2026-02-26 ### Added - `loop --list-presets` to print built-in presets with defaults and summaries - Presets system for domain-specific multi-round workflows, now with single-word built-ins: `bughunt`, `security`, `invariants`, `regression`, `contracts`, `hotspots` - Custom presets via YAML files — pass `--preset path/to/preset.yml` to use your own preset definitions - Non-TTY heartbeat: emits elapsed time and active PIDs to stderr every 60 seconds, preventing outer-agent timeouts during long-running dispatches - `mkdir` can now be run without a prompt to create only an output directory (no `prompt.md`) - `upgrade` now prints guidance to refresh the skill template after a successful upgrade ### Changed - Skill template now warns orchestrating LLMs that dispatch is long-running (10–20+ min) and suggests background execution with progress monitoring - Built-in presets are now YAML files with schema validation instead of hardcoded TypeScript - Output directory names include a timestamp prefix for uniqueness (e.g. `1740300000-bughunt`) - Output paths are always absolute, consistent across single `run` and `loop` commands - Default tool timeout increased from 9 minutes to 15 minutes - Non-TTY output is purpose-built for agent consumers with structured lifecycle messages (phase started/completed, tool started/completed with PID and duration) - Replaced monolithic `ProgressDisplay` with event-driven Reporter interface — `TerminalReporter` for TTY, `AgentReporter` for non-TTY, `NullReporter` for dry-run - Loop prompt augmentation now uses stronger multi-round guidance: challenge prior findings, use prior findings as leads, and mark overlap status (`confirmed`, `refined`, `invalidated`, `duplicate`) - `loop` now always appends execution boilerplate, and non-preset inline prompts run discovery + prompt-writing enhancement by default (`--no-inline-enhancement` to opt out); prompt files (`-f`) and stdin prompts skip discovery/prompt-writing - Duration-based loop runs are now truly unbounded by round count (removed hidden 999-round cap); reporter shows `Round N` when total rounds are open-ended - Prior-round prompt references are capped to the most recent 8 reports to control prompt growth in long loops - `mkdir --json` now reports `promptSource: "none"` and `promptFilePath: null` when no prompt input is provided - Loop output files renamed: `round-synthesis.md` → `round-notes.md`, `final-synthesis.md` → `final-notes.md` - CLI help text polished for LLM-friendly clarity across commands - Failed tools now show `see /path/to/tool.stderr` instead of the misleading first line of stderr - Between loop rounds, reporter shows elapsed time, remaining time (when `--duration` is set), and a Ctrl+C hint ### Security - Environment variable denylist (`ENV_DENYLIST`) blocks `NODE_OPTIONS`, `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc. from being re-injected via `invocation.env`, bypassing the allowlist - Markdown fence injection in `gatherContext` fixed via `safeFence()` that extends the delimiter until it doesn't conflict with content ### Fixed - Restored `npm run typecheck` health by fixing `runLoop` abort-state narrowing around SIGINT handling - Null stdio early-return no longer leaks child processes — tracked in `activeChildren` immediately after spawn - `generateSlug` returns `untitled` instead of empty string for non-alphanumeric input - `--file` no longer silently ignores `--context` — gathered context is appended to file content when both flags are provided - Group + explicit tool overlap no longer creates duplicates — tools appearing in both `--group` and `--tools` are deduped while preserving intentional duplicates - UTF-8 buffer truncation no longer splits multi-byte characters — `truncateUtf8` walks backwards past continuation bytes - `extractHeadings` uses `report.outputFile` instead of reconstructing the path - `removeToolFromConfig` cleans up groups left empty after tool removal ## [0.4.12] - 2026-02-19 ### Fixed - `run -f` no longer creates duplicate output directories when the prompt file already lives inside the output base directory - Gemini adapter appends a prompt instruction to suppress tool-use narration ("I will read...", "I will list...") that was polluting headless output ## [0.4.11] - 2026-02-19 ### Added - Progress display shows child process PIDs and parent PID, with a timing note that sessions may take 10+ minutes — lets users verify processes are alive via `ps` or `tasklist` ## [0.4.10] - 2026-02-16 ### Changed - `tools add` custom model flow now asks for a model identifier instead of raw CLI flags, and constructs the correct flags automatically per adapter ### Fixed - Gemini CLI with Vertex AI auth now works — `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` are passed to subprocesses (#13) ## [0.4.9] - 2026-02-16 ### Fixed - Various bug fixes ## [0.4.8] - 2026-02-16 ### Added - `counselors doctor` now warns when multiple installations are detected (e.g. npm + Homebrew + standalone) ### Changed - `counselors tools test` now shows verbose failure details: timeout detection, stderr content, and actual tool output ## [0.4.7] - 2026-02-16 ### Added - `counselors config` command — prints the config file path and the full resolved configuration as JSON - `counselors tools test` now prints the exact shell command used for each tool, so users can reproduce tests manually - `counselors doctor` now validates that every group member references a configured tool ### Fixed - `counselors tools add ` now defaults to a model-specific name (e.g. `gemini-3-pro`) instead of just the adapter name (e.g. `gemini`) ## [0.4.6] - 2026-02-16 ### Fixed - Gemini 3 models now use the correct `-preview` suffixed API model IDs (`gemini-3-pro-preview`, `gemini-3-flash-preview`), fixing `ModelNotFoundError` when running tools ## [0.4.5] - 2026-02-16 ### Fixed - Various bug fixes ## [0.4.4] - 2026-02-16 ### Fixed - `install.sh` now supports `COUNSELORS_VERSION` pinning and performs a more resilient latest-tag lookup (with optional `GITHUB_TOKEN` auth) to avoid transient GitHub API failures. - Release standalone smoke test now fetches `install.sh` from the release tag and runs it with `COUNSELORS_VERSION`, eliminating `main` drift and reducing flaky retries. - Release workflow now computes Homebrew's SHA256 from a locally packed npm tarball and publishes that exact tarball, avoiding npm registry propagation 404s during checksum resolution. - Binary discovery now includes `PATH` entries in stage-2 fallback scans, reducing Windows false negatives when `where` lookup times out. ## [0.4.3] - 2026-02-16 ### Changed - Gemini model IDs now use Gemini 3 names (`gemini-3-pro`, `gemini-3-flash`) in adapter config and README group examples. - Release workflow now calls the binaries workflow directly via `workflow_call` instead of relying on tag-push side effects. ### Fixed - Release workflow now passes an explicit tag to Homebrew update logic in manual (`workflow_dispatch`) runs. - Homebrew formula updates now pin the npm tarball SHA256 and replace `sha256 :no_check`, so `brew install` succeeds. - Release workflow now runs parallel smoke tests for npm, standalone installer, and Homebrew installs, validating `--help` and version output. ## [0.4.2] - 2026-02-16 ### Fixed - Various bug fixes ## [0.4.1] - 2026-02-16 ### Fixed - Various bug fixes ## [0.4.0] - 2026-02-16 ### Added - `cleanup` command to delete run output directories older than a configurable age (defaults to 1 day) - Tool groups (`groups` config, `counselors groups ...`, and `counselors run --group`) - `upgrade` command with install-method detection (Homebrew, npm, pnpm, yarn, standalone binary) - Standalone binary releases and `install.sh` curl installer - Support running the same tool multiple times by repeating it in `--tools` (e.g. `--tools opus,opus,opus`) ### Changed - Skill template and docs clarify that output directories are configurable via `defaults.outputDir` and `counselors run -o` - CI runs Windows unit tests on Node 20, 22, and 24 (matching Ubuntu's Node coverage) ### Fixed - Windows: fixed `.cmd/.bat` execution via `cross-spawn` (stdout capture, synthetic ENOENT), and hardened PATH injection + env allowlisting ## [0.3.4] - 2026-02-10 ### Changed - Agentic quickstart rewritten so agents don't refuse it as social engineering — user installs the CLI, agent only runs config commands with explicit purposes - Skill template uses second-precision UNIX timestamps instead of millisecond-precision (macOS `date` doesn't support `%N`) - README adds example prompts and a slash command example to the quickstart ## [0.3.3] - 2026-02-10 ### Changed - Gemini CLI read-only level upgraded from `bestEffort` to `enforced` (tool restrictions are sufficient) - Doctor no longer warns on `bestEffort` read-only level — only `none` triggers a warning ### Fixed - Doctor correctly reports Amp deep mode as `bestEffort` instead of `enforced` ## [0.3.2] - 2026-02-10 ### Fixed - `package.json` bin path and repository URL corrected for npm publishing ## [0.3.1] - 2026-02-10 ### Changed - `agent` command clarifies that `counselors skill` prints a reference template to adapt, not a file to blindly copy ### Fixed - Skill install path in `agent` command now points to `~/.claude/skills/` instead of `~/.claude/commands/` ## [0.3.0] - 2026-02-10 ### Added - Multi-agent parallel dispatch with configurable adapters (Claude, Codex, Gemini, Amp, Custom) - Project-level `.counselors.json` configuration with defaults overrides - Tool management commands: add, remove, test, list, discover - Doctor command for environment diagnostics - Context gathering with file discovery and prompt building - Response synthesis across multiple agent outputs - Amp deep mode support with separate settings file and read-only safety prompt - Model selection during `init` with per-adapter `extraFlags` - Skill template output directory prefixed with timestamp for lexical sorting ### Changed - Simplified `ToolConfig` — removed model concept, unified flags into `extraFlags` ### Security - Sanitize tool IDs before use in filenames to prevent path traversal - Allowlist environment variables passed to child processes - Use `execFileSync` instead of `execSync` in discovery to prevent shell injection - Restrict project config to `defaults` only — cannot inject `tools` - Atomic file writes via temp+rename pattern to prevent partial writes ### Fixed - SIGINT handler properly terminates active child processes - Release workflow: build before test so integration tests find `dist/cli.js` - Release script handles blank changelogs instead of failing - Release workflow accepts leading `v` in version input [Unreleased]: https://github.com/aarondfrancis/counselors/compare/v0.5.2...HEAD [0.3.0]: https://github.com/aarondfrancis/counselors/releases/tag/v0.3.0 [0.3.1]: https://github.com/aarondfrancis/counselors/releases/tag/v0.3.1 [0.3.2]: https://github.com/aarondfrancis/counselors/releases/tag/v0.3.2 [0.3.3]: https://github.com/aarondfrancis/counselors/releases/tag/v0.3.3 [0.3.4]: https://github.com/aarondfrancis/counselors/releases/tag/v0.3.4 [0.4.0]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.0 [0.4.1]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.1 [0.4.2]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.2 [0.4.3]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.3 [0.4.4]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.4 [0.4.5]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.5 [0.4.6]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.6 [0.4.7]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.7 [0.4.8]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.8 [0.4.9]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.9 [0.4.10]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.10 [0.4.11]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.11 [0.4.12]: https://github.com/aarondfrancis/counselors/releases/tag/v0.4.12 [0.5.0]: https://github.com/aarondfrancis/counselors/releases/tag/v0.5.0 [0.5.1]: https://github.com/aarondfrancis/counselors/releases/tag/v0.5.1 [0.5.2]: https://github.com/aarondfrancis/counselors/releases/tag/v0.5.2 ================================================ FILE: README.md ================================================ # counselors By [Aaron Francis](https://aaronfrancis.com), creator of [Faster.dev](https://faster.dev) and [Solo](https://soloterm.com). Fan out prompts to multiple AI coding agents in parallel. `counselors` dispatches the same prompt to Claude, Codex, Gemini, Amp, or custom tools simultaneously, collects their responses, and writes everything to a structured output directory. No MCP servers, no direct API integrations, no complex configuration. It just calls your locally installed CLI tools. ## Will this get me banned from my provider? Counselors only uses providers' **first-party CLI tools**. It does not call provider APIs directly, it does not extract or reuse auth tokens, and it does not do anything "tricky" behind the scenes. It literally runs the official CLI binaries you already installed, the same way you would from your terminal. You are still subject to each provider's terms and rate limits. Counselors is just an orchestrator around the CLIs. ## Agentic quickstart Install the CLI yourself first (pick one): - npm (requires Node 20+): `npm install -g counselors` - Homebrew: `brew install aarondfrancis/homebrew-tap/counselors` - Standalone binary: `curl -fsSL https://github.com/aarondfrancis/counselors/raw/main/install.sh | bash` Then paste this to your AI coding agent: ``` Run `counselors init --auto` to discover and configure installed AI CLIs. Then run `counselors skill` to see how to create a skill for the counselors CLI. ``` Your agent will configure available tools and set up the `/counselors` slash command. ### Updating your skill The recommended skill template changes over time. If you already installed `/counselors` in your agent system, don’t blindly overwrite it. Copy/paste this into your AI coding agent: ``` The counselors CLI has an updated skill template. 1. Run `counselors skill` and capture the full output. 2. Open my existing counselors skill file and compare VERY CAREFULLY for anything that changed. 3. Apply the updates manually; do not blindly overwrite. 4. If you need more context, check the git history for the skill template here: https://github.com/aarondfrancis/counselors/commits/main/src/commands/skill.ts ``` **How it works:** 1. You invoke the Counselors skill with a prompt 2. Your agent gathers context from the codebase 3. Your agent asks which other agents you want to consult 4. Counselors fans out to those agents in parallel for independent research 5. Each agent writes a structured markdown report 6. Your main agent synthesizes and presents the results **Example:** after a big refactor, ask your agents for a second opinion: ``` /counselors We just completed a major refactor of the authentication module. Review the changes for edge cases, test gaps, or regressions we might have missed. ``` Your main agent handles the rest — it gathers relevant code, recent commits, and assembles a detailed prompt before dispatching to the counselors. ## Human quickstart Install the CLI (pick one): - npm (requires Node 20+): `npm install -g counselors` - Homebrew: `brew install aarondfrancis/homebrew-tap/counselors` - Standalone binary: `curl -fsSL https://github.com/aarondfrancis/counselors/raw/main/install.sh | bash` ```bash # Discover installed AI CLIs and create a config counselors init # Send a prompt to all configured tools counselors run "Trace the state management flow in the dashboard and flag any brittleness or stale state bugs" # Send to specific tools only counselors run -t claude,codex "Review src/api/ for security issues and missing edge cases" ``` ## Supported tools | Tool | Adapter | Read-Only | Install | |------|---------|-----------|---------| | Claude Code | `claude` | enforced | [docs](https://docs.anthropic.com/en/docs/claude-code) | | OpenAI Codex | `codex` | enforced | [github](https://github.com/openai/codex) | | Gemini CLI | `gemini` | enforced | [github](https://github.com/google-gemini/gemini-cli) | | Amp CLI | `amp` | enforced | [ampcode.com](https://ampcode.com) | | Custom | user-defined | configurable | — | ## Commands ### `run [prompt]` Dispatch a prompt to configured tools in parallel. ```bash counselors run "Your prompt here" counselors run -f prompt.md # Use a prompt file echo "prompt" | counselors run # Read from stdin counselors run --dry-run "Show plan" # Preview without executing counselors run -t opus,opus,opus "Review this" # Run the same tool multiple times ``` | Flag | Description | |------|-------------| | `-f, --file ` | Use a prompt file (no wrapping) | | `-t, --tools ` | Comma-separated tool IDs | | `-g, --group ` | Comma-separated group name(s) (expands to tool IDs) | | `--context ` | Gather context from paths (comma-separated, or `.` for git diff) | | `--read-only ` | `strict`, `best-effort`, `off` (defaults to config `readOnly`) | | `--dry-run` | Show what would run without executing | | `--json` | Output manifest as JSON | | `-o, --output-dir ` | Base output directory | ### `loop [prompt]` Multi-round dispatch — agents iterate, seeing prior outputs each round. Each round dispatches to all tools in parallel. Starting from round 2, each agent receives the outputs from all prior rounds, so it can build on previous analysis and avoid repeating findings. ```text input: user prompt/focus (e.g.: "focus on the auth module", "look at the sidebar component") | +--> with --preset: | [repo discovery phase] --> [prompt-writing phase] --> execution prompt (includes boilerplate) +--> without --preset: inline arg prompt: default: [repo discovery phase] --> [prompt-writing phase] --> enhanced execution prompt opt-out: --no-inline-enhancement (skip discovery/prompt-writing) file/stdin prompt: used as provided (discovery/prompt-writing skipped) all modes: execution boilerplate is always appended execution prompt | v +------------------------------- loop rounds -------------------------------+ | round 1: dispatch to all selected tools in parallel | | write per-tool outputs + round notes | | | | round N>1: execution prompt + references to prior round outputs | | (new findings, challenge/refine prior findings) | | dispatch in parallel, write outputs + notes | | | | stop when: | | - max rounds reached, or | | - duration expires, or | | - convergence threshold reached, or | | - user aborts (Ctrl+C after current round) | +---------------------------------------------------------------------------+ | v final notes + run manifest ``` ```text Round behavior: round 1 prompt = base execution prompt round N prompt = base execution prompt // Base execution prompt is amended with... + "Prior Round Outputs" section + @refs to recent prior tool outputs + instruction to avoid duplicate findings, challenge/refine prior claims, and expand from prior leads ``` ```bash counselors loop "Find and fix test gaps in src/auth/" --rounds 5 counselors loop --duration 30m "Hunt for edge cases" counselors loop --preset bughunt "src/api" --tools opus,codex counselors loop --preset hotspots "critical request path" --group smart counselors loop --list-presets ``` | Flag | Description | |------|-------------| | `--rounds ` | Number of dispatch rounds (default: 3) | | `--duration