From 051f78aee75f73efdf0435bbf6458e68a8818a45 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:05:44 -0400 Subject: [PATCH 01/12] chore(ci): split dev quality checks from release automation --- .github/workflows/ci.yml | 6 -- .github/workflows/push-ci.yml | 134 ++++++++++++++++++++++++++++++++++ .husky/pre-push | 4 +- package.json | 6 +- scripts/ci-parity-gate.sh | 8 +- scripts/run-test-bucket.js | 101 +++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/push-ci.yml create mode 100644 scripts/run-test-bucket.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b5bf26a..3dba111b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,8 +50,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh @@ -84,8 +82,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh @@ -127,8 +123,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh diff --git a/.github/workflows/push-ci.yml b/.github/workflows/push-ci.yml new file mode 100644 index 00000000..2aef1166 --- /dev/null +++ b/.github/workflows/push-ci.yml @@ -0,0 +1,134 @@ +name: Push CI + +on: + push: + branches: [dev] + +concurrency: + group: push-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: [self-hosted, linux, x64] + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + strategy: + fail-fast: false + matrix: + check: + - { name: typecheck, cmd: 'bun run typecheck' } + - { name: lint, cmd: 'bun run lint' } + - { name: format, cmd: 'bun run format:check' } + name: ${{ matrix.check.name }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Run ${{ matrix.check.name }} + run: ${{ matrix.check.cmd }} + + build: + runs-on: [self-hosted, linux, x64] + name: build + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Build + run: bun run build:all + + - name: Upload dist artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 1 + if-no-files-found: error + + test: + runs-on: [self-hosted, linux, x64] + name: test + needs: [build] + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Test + run: bun run test:all + + - name: Test CLI e2e + env: + CCS_E2E_SKIP_BUILD: '1' + run: bun run test:e2e diff --git a/.husky/pre-push b/.husky/pre-push index 07a426a0..b47b1dd4 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -45,9 +45,9 @@ echo " branch: $CURRENT_BRANCH" echo " base: $BASE_BRANCH" bun run typecheck -bun run lint:fix +bun run lint bun run format:check -bun run build:all +bun run test:fast git fetch origin "$BASE_BRANCH" --quiet || true DIFF_RANGE="HEAD" diff --git a/package.json b/package.json index d758342e..9ef64f27 100644 --- a/package.json +++ b/package.json @@ -70,12 +70,14 @@ "lint:fix": "eslint src/ --fix", "format": "prettier --write src/", "format:check": "prettier --check src/", - "validate": "bun run typecheck && bun run lint:fix && bun run format:check && bun run test:all", + "validate": "bun run typecheck && bun run lint && bun run format:check && bun run test:fast", "validate:ci-parity": "bash scripts/ci-parity-gate.sh", "verify:bundle": "node scripts/verify-bundle.js", "test": "bun run build && bun run test:all", "test:ci": "bun run test:all", - "test:all": "bun test --max-concurrency=1 tests/unit tests/integration tests/npm", + "test:fast": "node scripts/run-test-bucket.js fast", + "test:slow": "node scripts/run-test-bucket.js slow", + "test:all": "bun run test:fast && bun run test:slow", "test:unit": "bun test tests/unit", "test:npm": "bun test tests/npm/", "test:native": "bash tests/native/unix/edge-cases.sh", diff --git a/scripts/ci-parity-gate.sh b/scripts/ci-parity-gate.sh index 5e1c7a1f..c9b31a53 100755 --- a/scripts/ci-parity-gate.sh +++ b/scripts/ci-parity-gate.sh @@ -55,8 +55,12 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then fi fi -echo "[i] Running CI-equivalent local checks..." +echo "[i] Running CI-parity local checks..." +bun run typecheck +bun run lint +bun run format:check bun run build:all -bun run validate +bun run test:all +CCS_E2E_SKIP_BUILD=1 bun run test:e2e echo "[OK] CI parity gate passed." diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js new file mode 100644 index 00000000..f17ed2f7 --- /dev/null +++ b/scripts/run-test-bucket.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const bucket = process.argv[2]; +const rootDir = path.resolve(__dirname, '..'); +const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm']; +const slowTests = [ + 'tests/integration/cursor-daemon-lifecycle.test.ts', + 'tests/integration/proxy/daemon-lifecycle.test.ts', + 'tests/unit/commands/persist-command-handler.test.ts', + 'tests/unit/hooks/ccs-browser-mcp-server.test.ts', + 'tests/unit/targets/codex-runtime-integration.test.ts', + 'tests/unit/targets/codex-settings-bridge-launch.test.ts', + 'tests/unit/targets/droid-command-routing-integration.test.ts', + 'tests/unit/targets/droid-config-manager.test.ts', + 'tests/unit/targets/settings-profile-browser-launch.test.ts', + 'tests/unit/targets/settings-profile-image-analysis-launch.test.ts', + 'tests/unit/targets/settings-profile-websearch-launch.test.ts', + 'tests/unit/web-server/cursor-routes.test.ts', + 'tests/unit/web-server/websearch-routes.test.ts', +]; + +if (bucket !== 'fast' && bucket !== 'slow') { + console.error('[X] Usage: node scripts/run-test-bucket.js '); + process.exit(1); +} + +const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; + +function collectFiles(dir, files = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectFiles(fullPath, files); + continue; + } + + if (filePattern.test(entry.name)) { + files.push(path.relative(rootDir, fullPath).split(path.sep).join('/')); + } + } + + return files; +} + +function readsBuiltDist(relativePath) { + const source = fs.readFileSync(path.join(rootDir, relativePath), 'utf8'); + return source.includes('dist/'); +} + +const discovered = candidateRoots + .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) + .sort(); +const forceSlow = discovered.filter((file) => { + if (file.startsWith('tests/npm/')) { + return true; + } + + if (/\.(c|m)?js$/.test(file)) { + return true; + } + + return readsBuiltDist(file); +}); +const slowSet = new Set([...slowTests, ...forceSlow]); +const selected = + bucket === 'slow' + ? [...slowSet].sort() + : discovered.filter((file) => !slowSet.has(file)); + +if (selected.length === 0) { + console.error(`[X] No tests matched the '${bucket}' bucket.`); + process.exit(1); +} + +if (bucket === 'slow' && !fs.existsSync(path.join(rootDir, 'dist', 'ccs.js'))) { + const build = spawnSync('bun', ['run', 'build'], { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + + if (build.status !== 0) { + process.exit(build.status ?? 1); + } +} + +const result = spawnSync( + 'bun', + ['test', '--max-concurrency=1', ...selected], + { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }, +); + +process.exit(result.status ?? 1); From b9f5783facddb2decb702e378e558c5faac2fc80 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:06:55 -0400 Subject: [PATCH 02/12] test(ci): cover push quality workflow --- .../scripts/github/push-ci-workflow.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/unit/scripts/github/push-ci-workflow.test.ts diff --git a/tests/unit/scripts/github/push-ci-workflow.test.ts b/tests/unit/scripts/github/push-ci-workflow.test.ts new file mode 100644 index 00000000..f498e014 --- /dev/null +++ b/tests/unit/scripts/github/push-ci-workflow.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +function resolvePath(relativePath: string) { + return path.resolve(import.meta.dir, relativePath); +} + +describe('push ci workflow', () => { + test('keeps dev push quality checks separate from release automation', () => { + const workflowPath = resolvePath('../../../../.github/workflows/push-ci.yml'); + + expect(fs.existsSync(workflowPath)).toBe(true); + + const workflow = fs.readFileSync(workflowPath, 'utf8'); + + expect(workflow).toContain('name: Push CI'); + expect(workflow).toContain('push:'); + expect(workflow).toContain('branches: [dev]'); + expect(workflow).toContain('group: push-ci-${{ github.ref }}'); + expect(workflow).toContain('cancel-in-progress: true'); + expect(workflow).toContain('runs-on: [self-hosted, linux, x64]'); + expect(workflow).toContain("key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}"); + expect(workflow).not.toContain('restore-keys:'); + expect(workflow).toContain("name: ${{ matrix.check.name }}"); + expect(workflow).toContain("cmd: 'bun run typecheck'"); + expect(workflow).toContain("cmd: 'bun run lint'"); + expect(workflow).toContain("cmd: 'bun run format:check'"); + expect(workflow).toContain('run: bun run build:all'); + expect(workflow).toContain('run: bun run test:all'); + expect(workflow).toContain("CCS_E2E_SKIP_BUILD: '1'"); + expect(workflow).toContain('run: bun run test:e2e'); + }); +}); From c9eaae17c184a91addc2df85733db3d6339bb720 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:07:12 -0400 Subject: [PATCH 03/12] docs(contrib): clarify CI lanes and parity gates --- .github/pull_request_template.md | 6 ++- CLAUDE.md | 67 ++++++++++++++------------------ CONTRIBUTING.md | 51 ++++++++++++++++++++++-- README.md | 3 ++ 4 files changed, 85 insertions(+), 42 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3ed3a780..9002ba3f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,8 +6,9 @@ Use what applies. If you skipped something, add a short note instead of forcing it. -- [ ] `bun run validate` -- [ ] `bun run validate:ci-parity` +- [ ] `bun run format && bun run validate` +- [ ] `bun run validate:ci-parity` before requesting review +- [ ] `bun run test:e2e` if this PR touches command routing, proxy flows, or workflow/release logic - [ ] `cd ui && bun run validate` if UI changed - [ ] Not run @@ -20,6 +21,7 @@ Check what applies. Not every item is relevant for every PR. - [ ] Relevant `--help` output updated if CLI behavior changed - [ ] Tests added or updated if behavior changed - [ ] README or local docs updated if user-facing behavior changed +- [ ] If a check failed, the PR body explains what failed and what changed to fix it - [ ] No secrets, tokens, or private config data are included ## Docs Impact diff --git a/CLAUDE.md b/CLAUDE.md index 11175260..ad01f074 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,20 @@ gh pr checks ### Absolute rule AI MUST NOT declare a task done, close a session, or move to the next task while CI is red or still running. Leaving a PR red and moving on is the primary failure mode this protocol prevents. +### Self-Hosted Runner Awareness + +- If `gh pr checks` or `gh run watch` stays queued for more than 10 minutes, assume the self-hosted runner is offline. +- Confirm on the maintainer host with `ssh docker "systemctl status actions-runner"`. +- Treat runner outages as infrastructure issues, not code failures. Do not blindly rerun local commands and hope the queue clears. + +### Dev Release vs Push CI + +- `CI` is the pull-request quality gate for contributor branches. +- `Push CI` is the post-merge quality signal for `dev`. +- `Dev Release` publishes the `@dev` package after `dev` changes land. +- A red `Dev Release` does **not** automatically mean contributor code failed. Check `Push CI` first. +- `dev-release.yml` currently pushes with `PAT_TOKEN` because `dev` is protected by required status checks (`typecheck`, `lint`, `format`, `build`, `test`). Do not switch it back to `github.token` unless branch protection changes with it. + ## Core Function Multi-provider profile and runtime manager for Claude Code, Factory Droid, @@ -87,7 +101,7 @@ broader topic. | Mistake | Consequence | Correct Action | |---------|-------------|----------------| | Running `validate` without `format` first | format:check fails | Run `bun run format` BEFORE validate | -| Assuming maintainability check is always strict | PR/feature branches run warning mode by default | Use `bun run maintainability:check:strict` before merge when touching debt-sensitive code | +| Treating `Dev Release` as the contributor quality signal | Publish failures on `dev` look like broken code | Check PR `CI` on the branch and `Push CI` on `dev` first | | Using `chore:` for dev→main PR | No npm release triggered | Use `feat:` or `fix:` prefix | | Committing directly to `main` or `dev` | Bypasses CI/review | Always use PRs | | Manual version bump or git tag | Conflicts with semantic-release | Let CI handle versioning | @@ -208,8 +222,8 @@ Quality gates MUST pass before pushing. **Both projects have identical workflow. # Main project (from repo root) bun run format # Step 1: Fix formatting bun run lint:fix # Step 2: Fix lint issues -bun run validate # Step 3: Full gate (typecheck + lint + format + maintainability + tests) -bun run validate:ci-parity # Step 4: full CI parity gate (build + validate + base branch check) +bun run validate # Step 3: Fast gate (typecheck + lint + format + test:fast) +bun run validate:ci-parity # Step 4: PR-CI parity gate (branch check + build + full tests + e2e) # UI project (if UI changed) cd ui @@ -221,17 +235,17 @@ bun run validate # Step 3: Final check (must pass) **WHY THIS ORDER:** - `validate` runs `format:check` which only VERIFIES—won't fix - If format:check fails, you skipped step 1 -- CI runs `validate` only (no auto-fix)—local must be clean +- `validate` now uses read-only `lint`, so autofix still belongs in step 2 +- PR CI and `validate:ci-parity` both run non-mutating checks only -### What Validate Runs +### What Each Gate Runs | Project | Command | Runs | |---------|---------|------| -| Main | `bun run validate` | typecheck + lint:fix + format:check + test:all | +| Main | `bun run validate` | typecheck + lint + format:check + test:fast | +| Main | `bun run validate:ci-parity` | base branch check + typecheck + lint + format:check + build:all + test:all + test:e2e | | UI | `bun run validate` | typecheck + lint:fix + format:check | -**Note:** `maintainability:check` is a SEPARATE gate — not part of `validate`. Run it explicitly via `bun run maintainability:check[:strict|:warn]` when touching debt-sensitive code or before merging to protected branches. - ### ESLint Rules (ALL errors) | Rule | Level | Notes | @@ -255,33 +269,13 @@ bun run validate # Step 3: Final check (must pass) ### Automatic Enforcement -- `prepublishOnly` / `prepack` runs `build:all` + `validate` + `sync-version.js` -- CI/CD runs `bun run validate` on every PR (maintainability is warning mode on PR events) +- `prepack` runs `build:all` +- PR `CI` runs `typecheck`, `lint`, `format`, `build`, `test:all`, and `test:e2e` +- `Push CI` runs the same quality suite on `dev` after merge, separate from release publishing +- `Dev Release` still runs build + validate + tests before publishing and still requires `PAT_TOKEN` to push back to protected `dev` - husky `pre-commit` runs quick lint/type/format checks - husky `pre-push` runs the full `bun run validate:ci-parity` gate on `main`/`dev`/hotfix branches -- husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint:fix` + `format:check` + targeted checks based on changed files) before GitHub CI handles the full matrix - -### Maintainability Baseline Gate - -- Baseline file: `docs/metrics/maintainability-baseline.json` -- Metric collector/check script: `scripts/maintainability-baseline.js` -- Branch-aware gate wrapper: `scripts/maintainability-check.js` -- Enforcement path: `bun run maintainability:check` (run separately — NOT part of `bun run validate`; invoked by `validate:ci-parity` on protected branches) -- Gate modes: - - `strict`: protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`) and equivalent CI refs - - `warn`: pull request CI and non-protected local branches (non-blocking for parallel PR workflow) - - override commands: - - `bun run maintainability:check:strict` - - `bun run maintainability:check:warn` -- Gated metrics (must not increase vs baseline): - - `processExitReferenceCount` - - `synchronousFsApiReferenceCount` -- Informational metrics (collected but not gated): - - `largeFileCountOver350Loc` -- Baseline update policy: - 1. Prefer reducing the metric and keeping the baseline unchanged. - 2. On protected-branch integration (strict mode), if increase is intentional and accepted, run `bun run maintainability:baseline`. - 3. Commit both the code change and `docs/metrics/maintainability-baseline.json`, and state reason in PR description. +- husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint` + `format:check` + `test:fast`) plus targeted checks based on changed files ## Critical Constraints (NEVER VIOLATE) @@ -532,17 +526,16 @@ rm -rf ~/.ccs # Clean environment Optimized for iterative push-then-review workflow. Do NOT run the full gate on every push — CI is the safety net. Run the full gate once before asking for review / merge. ### Tier 1 — Iterative push (feature branch) -Husky `pre-push` auto-runs: `typecheck + lint:fix + format:check + build:all` plus targeted tests based on changed files. AI does **nothing extra** at push time. +Husky `pre-push` auto-runs: `typecheck + lint + format:check + test:fast` plus targeted checks based on changed files. AI does **nothing extra** at push time. **After push (MANDATORY):** follow the [CI-First Protocol](#ci-first-protocol-mandatory) — watch CI until green. Do not move on while CI is red. ### Tier 2 — Before requesting review / merge Run ONCE, not per push: -- [ ] `bun run validate:ci-parity` — full build + validate matches CI +- [ ] `bun run validate:ci-parity` — branch freshness + build + full non-e2e tests + e2e - [ ] `gh pr checks ` — all checks green -- [ ] If touching debt-sensitive code: `bun run maintainability:check:strict` -- [ ] If strict mode fails and increase is intentional: `bun run maintainability:baseline` and commit `docs/metrics/maintainability-baseline.json` - [ ] If UI changed: `cd ui && bun run format && bun run validate` +- [ ] If touching command routing, proxy flows, workflows, or release logic: `bun run test:e2e` ### Code / Docs / Standards (verify before merge) - [ ] Conventional commit format (`feat:`, `fix:`, etc.) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c283c060..a40efd91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,22 @@ Rules: - Treat `hotfix/*` as maintainer-only emergency flow from `main`. - Delete your branch after merge. +## CI and Release Flow + +CCS now uses three separate automation lanes: + +- `CI` runs on pull requests to `dev` and `main`. This is the review gate for contributor branches. +- `Push CI` runs after a merge lands on `dev`. This is the code-quality signal for the shared `dev` branch. +- `Dev Release` publishes the `@dev` package after `dev` changes land. It is release automation, not the primary contributor quality signal. + +If `Dev Release` is red but your PR checks were green, check `Push CI` before assuming the merged code is broken. + +If `CI` or `Push CI` stays queued for more than 10 minutes, the self-hosted runner is probably offline. That is a maintainer issue, not a contributor mistake. + +## AI Agent Rules + +`CONTRIBUTING.md` is the human entry point. For AI agents working in this repo, the authoritative automation and workflow rules live in [CLAUDE.md](./CLAUDE.md). + ## AI Review Lane CCS PR review no longer depends on `anthropics/claude-code-action`. The repository review lane is self-hosted PR-Agent: @@ -129,16 +145,35 @@ Use `bun run dev` from the repo root when working on the local dashboard experie ## Validation -If you can, run these before you open or update a PR: +Run this fast local gate before you open or update a PR: ```bash bun run format bun run lint:fix bun run validate +``` + +`bun run validate` is the day-to-day contributor gate. It runs: + +- `typecheck` +- `lint` +- `format:check` +- `test:fast` + +Before you ask for review, or whenever you want the closest local equivalent to PR CI, run: + +```bash bun run validate:ci-parity ``` -If you changed the dashboard: +`bun run validate:ci-parity` adds: + +- branch freshness check against `origin/dev` or `origin/main` +- `build:all` +- full non-e2e test suite via `test:all` +- `test:e2e` with `CCS_E2E_SKIP_BUILD=1` + +If you changed the dashboard, run the UI gate too: ```bash cd ui @@ -155,7 +190,17 @@ bun run test:native bun run test:e2e ``` -`bun run validate` is the main gate. It covers typechecking, linting, format checks, maintainability checks, and automated tests for the main project. +Use `bun run test:e2e` locally before review if you touch command routing, proxy flows, release automation, or workflow wiring and want to reproduce the same CLI e2e lane that PR CI runs. + +### Why Did CI Fail? + +| Symptom | Likely cause | Fix | +| --- | --- | --- | +| `format` fails in PR CI | `bun run format` was skipped locally | Run `bun run format`, recommit, push again | +| `lint` fails in PR CI | `validate` now uses read-only `lint` | Run `bun run lint:fix`, then rerun `bun run validate` | +| `test` fails in PR CI but `validate` passed | The failure is in `test:slow` or `test:e2e` | Run `bun run validate:ci-parity` locally | +| Checks stay queued for >10 min | Self-hosted runner is offline | Wait for maintainer intervention; rerunning usually does not help | +| `Dev Release` is red on `dev` after merge | Release-only failure or publish problem | Check `Push CI` first to confirm code quality | If you cannot run the full suite, that is still fine for early or docs-only PRs. Just say what you did run, or what blocked you, in the PR. diff --git a/README.md b/README.md index 4aab06f4..ea2c2617 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,9 @@ ccs ollama "summarize these logs" ## Contribute And Report Safely - Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md) +- Daily local gate: `bun run format && bun run validate` +- Before review or merge confidence: `bun run validate:ci-parity` +- If PR checks stay queued for more than 10 minutes, assume the self-hosted runner is offline and notify a maintainer instead of retrying blindly - Starter work: [good first issue](https://github.com/kaitranntt/ccs/labels/good%20first%20issue), [help wanted](https://github.com/kaitranntt/ccs/labels/help%20wanted) From b314cf353ed38ea16919ba4fa0029dfc5cf66c0b Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:12:07 -0400 Subject: [PATCH 04/12] fix(ci): keep release workflows on full test coverage --- .github/workflows/dev-release.yml | 5 ++- .github/workflows/release.yml | 5 ++- package.json | 2 +- scripts/run-test-bucket.js | 74 ++++++++++++++++++++++--------- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 55e23f25..d3fb654c 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -47,9 +47,12 @@ jobs: - name: Build run: bun run build:all - - name: Validate (typecheck + lint + format + tests) + - name: Validate fast gate run: bun run validate + - name: Test slow bucket + run: bun run test:slow + - name: Test CLI e2e env: CCS_E2E_SKIP_BUILD: '1' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 466394ec..db140f7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,9 +42,12 @@ jobs: - name: Build package run: bun run build:all - - name: Validate (typecheck + lint + format + tests) + - name: Validate fast gate run: bun run validate + - name: Test slow bucket + run: bun run test:slow + - name: Test CLI e2e env: CCS_E2E_SKIP_BUILD: '1' diff --git a/package.json b/package.json index 9ef64f27..c0a95eab 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "test:ci": "bun run test:all", "test:fast": "node scripts/run-test-bucket.js fast", "test:slow": "node scripts/run-test-bucket.js slow", - "test:all": "bun run test:fast && bun run test:slow", + "test:all": "node scripts/run-test-bucket.js all", "test:unit": "bun test tests/unit", "test:npm": "bun test tests/npm/", "test:native": "bash tests/native/unix/edge-cases.sh", diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index f17ed2f7..5c02ec75 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -23,8 +23,8 @@ const slowTests = [ 'tests/unit/web-server/websearch-routes.test.ts', ]; -if (bucket !== 'fast' && bucket !== 'slow') { - console.error('[X] Usage: node scripts/run-test-bucket.js '); +if (!['fast', 'slow', 'all'].includes(bucket)) { + console.error('[X] Usage: node scripts/run-test-bucket.js '); process.exit(1); } @@ -66,36 +66,66 @@ const forceSlow = discovered.filter((file) => { return readsBuiltDist(file); }); const slowSet = new Set([...slowTests, ...forceSlow]); -const selected = - bucket === 'slow' + +function selectBucket(name) { + return name === 'slow' ? [...slowSet].sort() : discovered.filter((file) => !slowSet.has(file)); - -if (selected.length === 0) { - console.error(`[X] No tests matched the '${bucket}' bucket.`); - process.exit(1); } -if (bucket === 'slow' && !fs.existsSync(path.join(rootDir, 'dist', 'ccs.js'))) { +function ensureBuildForSlowBucket() { + if (fs.existsSync(path.join(rootDir, 'dist', 'ccs.js'))) { + return 0; + } + const build = spawnSync('bun', ['run', 'build'], { cwd: rootDir, stdio: 'inherit', shell: process.platform === 'win32', }); - if (build.status !== 0) { - process.exit(build.status ?? 1); - } + return build.status ?? 1; } -const result = spawnSync( - 'bun', - ['test', '--max-concurrency=1', ...selected], - { - cwd: rootDir, - stdio: 'inherit', - shell: process.platform === 'win32', - }, -); +function runBucket(name) { + const selected = selectBucket(name); -process.exit(result.status ?? 1); + if (selected.length === 0) { + console.error(`[X] No tests matched the '${name}' bucket.`); + return 1; + } + + if (name === 'slow') { + const buildStatus = ensureBuildForSlowBucket(); + if (buildStatus !== 0) { + return buildStatus; + } + } + + const result = spawnSync( + 'bun', + ['test', '--max-concurrency=1', ...selected], + { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }, + ); + + return result.status ?? 1; +} + +if (bucket === 'all') { + let exitCode = 0; + + for (const name of ['fast', 'slow']) { + const status = runBucket(name); + if (status !== 0) { + exitCode = status; + } + } + + process.exit(exitCode); +} + +process.exit(runBucket(bucket)); From ddfbcb63f44eb6f379765bf5e44f42189d9012f3 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:12:24 -0400 Subject: [PATCH 05/12] docs(contrib): align local gate shortcuts --- .github/pull_request_template.md | 2 +- CLAUDE.md | 2 +- README.md | 2 +- docs/code-standards.md | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9002ba3f..c3c6ce9d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ Use what applies. If you skipped something, add a short note instead of forcing it. -- [ ] `bun run format && bun run validate` +- [ ] `bun run format && bun run lint:fix && bun run validate` - [ ] `bun run validate:ci-parity` before requesting review - [ ] `bun run test:e2e` if this PR touches command routing, proxy flows, or workflow/release logic - [ ] `cd ui && bun run validate` if UI changed diff --git a/CLAUDE.md b/CLAUDE.md index ad01f074..f55cc489 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,7 +272,7 @@ bun run validate # Step 3: Final check (must pass) - `prepack` runs `build:all` - PR `CI` runs `typecheck`, `lint`, `format`, `build`, `test:all`, and `test:e2e` - `Push CI` runs the same quality suite on `dev` after merge, separate from release publishing -- `Dev Release` still runs build + validate + tests before publishing and still requires `PAT_TOKEN` to push back to protected `dev` +- `Dev Release` still runs build + fast validation + slow tests + e2e before publishing and still requires `PAT_TOKEN` to push back to protected `dev` - husky `pre-commit` runs quick lint/type/format checks - husky `pre-push` runs the full `bun run validate:ci-parity` gate on `main`/`dev`/hotfix branches - husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint` + `format:check` + `test:fast`) plus targeted checks based on changed files diff --git a/README.md b/README.md index ea2c2617..5a131860 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ ccs ollama "summarize these logs" ## Contribute And Report Safely - Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md) -- Daily local gate: `bun run format && bun run validate` +- Daily local gate: `bun run format && bun run lint:fix && bun run validate` - Before review or merge confidence: `bun run validate:ci-parity` - If PR checks stay queued for more than 10 minutes, assume the self-hosted runner is offline and notify a maintainer instead of retrying blindly - Starter work: diff --git a/docs/code-standards.md b/docs/code-standards.md index 3c3b35cb..de84eebe 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -599,6 +599,7 @@ return ( bun run format bun run lint:fix bun run validate +bun run validate:ci-parity # UI project (if changed) cd ui @@ -611,7 +612,7 @@ bun run validate | Project | Command | Checks | |---------|---------|--------| -| Main | `bun run validate` | typecheck + lint + format:check + test | +| Main | `bun run validate` | typecheck + lint + format:check + test:fast | | UI | `bun run validate` | typecheck + lint + format:check | --- From cf5fc96b7d25b55100045f3b3595aa2eb982445b Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:23:00 -0400 Subject: [PATCH 06/12] test(ci): cover pr workflow coverage --- tests/unit/scripts/github/ci-workflow.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/unit/scripts/github/ci-workflow.test.ts diff --git a/tests/unit/scripts/github/ci-workflow.test.ts b/tests/unit/scripts/github/ci-workflow.test.ts new file mode 100644 index 00000000..a5319626 --- /dev/null +++ b/tests/unit/scripts/github/ci-workflow.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +function resolvePath(relativePath: string) { + return path.resolve(import.meta.dir, relativePath); +} + +describe('pr ci workflow', () => { + test('keeps full coverage on pull requests', () => { + const workflowPath = resolvePath('../../../../.github/workflows/ci.yml'); + + expect(fs.existsSync(workflowPath)).toBe(true); + + const workflow = fs.readFileSync(workflowPath, 'utf8'); + + expect(workflow).toContain('name: CI'); + expect(workflow).toContain('pull_request:'); + expect(workflow).toContain('branches: [main, dev]'); + expect(workflow).toContain('group: ci-${{ github.ref }}'); + expect(workflow).toContain('cancel-in-progress: true'); + expect(workflow).toContain('fail-fast: false'); + expect(workflow).toContain('runs-on: [self-hosted, linux, x64]'); + expect(workflow).toContain("cmd: 'bun run typecheck'"); + expect(workflow).toContain("cmd: 'bun run lint'"); + expect(workflow).toContain("cmd: 'bun run format:check'"); + expect(workflow).toContain("key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}"); + expect(workflow).not.toContain('restore-keys:'); + expect(workflow).toContain('name: dist'); + expect(workflow).toContain('path: dist/'); + expect(workflow).toContain('needs: [build]'); + expect(workflow).toContain('run: bun run test:all'); + expect(workflow).toContain("CCS_E2E_SKIP_BUILD: '1'"); + expect(workflow).toContain('run: bun run test:e2e'); + }); +}); From 572b18422076f03d2d8ccbfce802740148a5fe36 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:41:35 -0400 Subject: [PATCH 07/12] fix(ci): address reviewer follow-up findings --- CLAUDE.md | 9 ++++++++- docs/project-roadmap.md | 25 +++++-------------------- scripts/run-test-bucket.js | 4 ---- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f55cc489..c7ce2d33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,8 @@ AI MUST NOT declare a task done, close a session, or move to the next task while - `Push CI` is the post-merge quality signal for `dev`. - `Dev Release` publishes the `@dev` package after `dev` changes land. - A red `Dev Release` does **not** automatically mean contributor code failed. Check `Push CI` first. -- `dev-release.yml` currently pushes with `PAT_TOKEN` because `dev` is protected by required status checks (`typecheck`, `lint`, `format`, `build`, `test`). Do not switch it back to `github.token` unless branch protection changes with it. +- Verified on `2026-04-22` via `gh api repos/kaitranntt/ccs/branches/dev/protection`: `dev` currently requires `typecheck`, `lint`, `format`, `build`, and `test`, has no branch restrictions, and has no required PR-review gate. +- `dev-release.yml` currently pushes with `PAT_TOKEN` because `dev` is protected by those required status checks. Do not switch it back to `github.token` unless branch protection changes with it. ## Core Function @@ -277,6 +278,12 @@ bun run validate # Step 3: Final check (must pass) - husky `pre-push` runs the full `bun run validate:ci-parity` gate on `main`/`dev`/hotfix branches - husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint` + `format:check` + `test:fast`) plus targeted checks based on changed files +### Maintainability Gate Status + +- The historical maintainability baseline gate is retired from the active CCS workflow. +- `validate`, `validate:ci-parity`, PR `CI`, `Push CI`, and release workflows do **not** invoke `maintainability:check`. +- Older roadmap references to `maintainability:baseline` / `maintainability:check` are historical context, not current repo commands. + ## Critical Constraints (NEVER VIOLATE) 1. **NO EMOJIS in CLI output** - Terminal output uses ASCII only: [OK], [!], [X], [i] diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index 0cb616b3..6b76df05 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -242,28 +242,13 @@ All criteria achieved: - [x] Clear domain boundaries - [x] Consistent naming conventions -## Maintainability Gate (Issue #539 Foundation) +## Historical Maintainability Gate (Retired) -- Baseline metrics artifact: `docs/metrics/maintainability-baseline.json` -- Branch-aware gate wrapper: `scripts/maintainability-check.js` -- Generate or refresh baseline: - - `bun run maintainability:baseline` - - `npm run maintainability:baseline` -- Run regression check gate: - - `bun run maintainability:check` - - `npm run maintainability:check` - - `bun run maintainability:check:strict` (force strict locally) +This section is preserved as historical context from the original Issue `#539` work. -The baseline/check scripts enumerate git-tracked files under `src` for deterministic results and fail fast if git file listing is unavailable. - -Default gate behavior: -- strict mode on protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`) -- warning mode on PR CI and non-protected branches (parallel PR friendly) - -The check mode supports a maintainability regression gate that blocks increases in: -- `process.exit` references -- synchronous fs API references -- TypeScript files over 350 LOC +- The maintainability baseline gate is no longer part of the active CCS workflow. +- Current contributor and CI gates are documented in `CLAUDE.md`, `CONTRIBUTING.md`, and the GitHub workflow files. +- Do not assume `maintainability:baseline` or `maintainability:check` exist unless they are reintroduced in a future follow-up. --- diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index 5c02ec75..0f3c0212 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -59,10 +59,6 @@ const forceSlow = discovered.filter((file) => { return true; } - if (/\.(c|m)?js$/.test(file)) { - return true; - } - return readsBuiltDist(file); }); const slowSet = new Set([...slowTests, ...forceSlow]); From 5af639ddf76ba7f84552d7b841c027c235cace1d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 15:54:07 -0400 Subject: [PATCH 08/12] fix(ci): tighten slow bucket guardrails --- README.md | 2 +- scripts/ci-parity-gate.sh | 2 + scripts/run-test-bucket.js | 73 +++++++++++++++------- tests/unit/scripts/run-test-bucket.test.js | 24 +++++++ 4 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 tests/unit/scripts/run-test-bucket.test.js diff --git a/README.md b/README.md index 5a131860..a4b71e6a 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ ccs ollama "summarize these logs" ## Contribute And Report Safely - Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md) -- Daily local gate: `bun run format && bun run lint:fix && bun run validate` +- Daily local gate: `bun run format && bun run lint:fix && bun run validate` (`validate` is the fast path only) - Before review or merge confidence: `bun run validate:ci-parity` - If PR checks stay queued for more than 10 minutes, assume the self-hosted runner is offline and notify a maintainer instead of retrying blindly - Starter work: diff --git a/scripts/ci-parity-gate.sh b/scripts/ci-parity-gate.sh index c9b31a53..36b66b28 100755 --- a/scripts/ci-parity-gate.sh +++ b/scripts/ci-parity-gate.sh @@ -56,6 +56,8 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then fi echo "[i] Running CI-parity local checks..." +# `set -euo pipefail` above makes every step fail fast. Keep these commands +# explicit so parity drift is visible when CI changes. bun run typecheck bun run lint bun run format:check diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index 0f3c0212..da2cdc68 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -4,9 +4,11 @@ const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); -const bucket = process.argv[2]; const rootDir = path.resolve(__dirname, '..'); const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm']; +// Keep this list in sync with any newly added dist-dependent or long-running +// tests. `tests/unit/scripts/run-test-bucket.test.js` verifies every path here +// exists so bucket drift fails loudly instead of silently slowing `test:fast`. const slowTests = [ 'tests/integration/cursor-daemon-lifecycle.test.ts', 'tests/integration/proxy/daemon-lifecycle.test.ts', @@ -23,11 +25,6 @@ const slowTests = [ 'tests/unit/web-server/websearch-routes.test.ts', ]; -if (!['fast', 'slow', 'all'].includes(bucket)) { - console.error('[X] Usage: node scripts/run-test-bucket.js '); - process.exit(1); -} - const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; function collectFiles(dir, files = []) { @@ -51,19 +48,30 @@ function readsBuiltDist(relativePath) { return source.includes('dist/'); } -const discovered = candidateRoots - .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) - .sort(); -const forceSlow = discovered.filter((file) => { +function getDiscoveredTests() { + return candidateRoots + .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) + .sort(); +} + +function shouldForceSlow(file) { if (file.startsWith('tests/npm/')) { return true; } return readsBuiltDist(file); -}); -const slowSet = new Set([...slowTests, ...forceSlow]); +} + +function getSlowSet() { + const discovered = getDiscoveredTests(); + const forceSlow = discovered.filter((file) => shouldForceSlow(file)); + return new Set([...slowTests, ...forceSlow]); +} function selectBucket(name) { + const discovered = getDiscoveredTests(); + const slowSet = getSlowSet(); + return name === 'slow' ? [...slowSet].sort() : discovered.filter((file) => !slowSet.has(file)); @@ -111,17 +119,40 @@ function runBucket(name) { return result.status ?? 1; } -if (bucket === 'all') { - let exitCode = 0; +function main(args = process.argv.slice(2)) { + const bucket = args[0]; - for (const name of ['fast', 'slow']) { - const status = runBucket(name); - if (status !== 0) { - exitCode = status; - } + if (!['fast', 'slow', 'all'].includes(bucket)) { + console.error('[X] Usage: node scripts/run-test-bucket.js '); + return 1; } - process.exit(exitCode); + if (bucket === 'all') { + let exitCode = 0; + + for (const name of ['fast', 'slow']) { + const status = runBucket(name); + if (status !== 0) { + exitCode = status; + } + } + + return exitCode; + } + + return runBucket(bucket); } -process.exit(runBucket(bucket)); +if (require.main === module) { + process.exit(main()); +} + +module.exports = { + slowTests, + readsBuiltDist, + shouldForceSlow, + getDiscoveredTests, + getSlowSet, + selectBucket, + main, +}; diff --git a/tests/unit/scripts/run-test-bucket.test.js b/tests/unit/scripts/run-test-bucket.test.js new file mode 100644 index 00000000..b4a1213d --- /dev/null +++ b/tests/unit/scripts/run-test-bucket.test.js @@ -0,0 +1,24 @@ +const { describe, expect, test } = require('bun:test'); +const path = require('node:path'); +const bucket = require('../../../scripts/run-test-bucket.js'); + +describe('run-test-bucket', () => { + test('all declared slow tests still exist on disk', () => { + for (const relativePath of bucket.slowTests) { + const absolutePath = path.resolve(__dirname, '../../../', relativePath); + expect(Bun.file(absolutePath).exists()).resolves.toBe(true); + } + }); + + test('forces npm tests into the slow bucket', () => { + expect(bucket.shouldForceSlow('tests/npm/cli.test.js')).toBe(true); + }); + + test('keeps dist-independent javascript tests in the fast bucket', () => { + expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false); + }); + + test('still forces dist-dependent tests into the slow bucket', () => { + expect(bucket.shouldForceSlow('tests/unit/config-dir-override.test.js')).toBe(true); + }); +}); From 6d4dcc1174f4c6382233e12cfa9a211ddde367db Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 16:00:39 -0400 Subject: [PATCH 09/12] fix(ci): keep fast bucket stable for commonjs tests --- scripts/run-test-bucket.js | 11 +++++++++++ tests/unit/scripts/run-test-bucket.test.js | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index da2cdc68..4cd682d6 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -24,6 +24,12 @@ const slowTests = [ 'tests/unit/web-server/cursor-routes.test.ts', 'tests/unit/web-server/websearch-routes.test.ts', ]; +// CommonJS-heavy JS suites stay slow by default because many of them mutate +// module cache or process state. Opt them into `test:fast` only after they are +// proven stable in the mixed fast bucket. +const fastJsTests = new Set([ + 'tests/unit/flag-parsing-simple.test.js', +]); const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; @@ -59,6 +65,10 @@ function shouldForceSlow(file) { return true; } + if (/\.(c|m)?js$/.test(file) && !fastJsTests.has(file)) { + return true; + } + return readsBuiltDist(file); } @@ -149,6 +159,7 @@ if (require.main === module) { module.exports = { slowTests, + fastJsTests, readsBuiltDist, shouldForceSlow, getDiscoveredTests, diff --git a/tests/unit/scripts/run-test-bucket.test.js b/tests/unit/scripts/run-test-bucket.test.js index b4a1213d..d9c79ef2 100644 --- a/tests/unit/scripts/run-test-bucket.test.js +++ b/tests/unit/scripts/run-test-bucket.test.js @@ -18,6 +18,10 @@ describe('run-test-bucket', () => { expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false); }); + test('keeps non-allowlisted javascript tests in the slow bucket', () => { + expect(bucket.shouldForceSlow('tests/unit/commands/persist-command.test.js')).toBe(true); + }); + test('still forces dist-dependent tests into the slow bucket', () => { expect(bucket.shouldForceSlow('tests/unit/config-dir-override.test.js')).toBe(true); }); From a3f5aff45d06c3c200e568d92217a56525ec3dbf Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 16:20:35 -0400 Subject: [PATCH 10/12] docs: remove maintainer-only runner info from contributor docs Self-hosted runner troubleshooting (`ssh docker`, `systemctl status actions-runner`, `cliproxy` runner label) is maintainer infra. External contributors have no access to that host and the guidance was noise. - CLAUDE.md: drop "Self-Hosted Runner Awareness" subsection. - CONTRIBUTING.md: rewrite queued-runner note as generic "maintainer infrastructure issue; leave a comment". Remove `cliproxy` runner label mentions from AI Review Lane. Equivalent maintainer runbook now lives in the monorepo root CLAUDE.md, where it's actually actionable. --- CLAUDE.md | 6 ------ CONTRIBUTING.md | 5 ++--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7ce2d33..05003c5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,12 +41,6 @@ gh pr checks ### Absolute rule AI MUST NOT declare a task done, close a session, or move to the next task while CI is red or still running. Leaving a PR red and moving on is the primary failure mode this protocol prevents. -### Self-Hosted Runner Awareness - -- If `gh pr checks` or `gh run watch` stays queued for more than 10 minutes, assume the self-hosted runner is offline. -- Confirm on the maintainer host with `ssh docker "systemctl status actions-runner"`. -- Treat runner outages as infrastructure issues, not code failures. Do not blindly rerun local commands and hope the queue clears. - ### Dev Release vs Push CI - `CI` is the pull-request quality gate for contributor branches. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a40efd91..b72b2b00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,7 +86,7 @@ CCS now uses three separate automation lanes: If `Dev Release` is red but your PR checks were green, check `Push CI` before assuming the merged code is broken. -If `CI` or `Push CI` stays queued for more than 10 minutes, the self-hosted runner is probably offline. That is a maintainer issue, not a contributor mistake. +If `CI` or `Push CI` stays queued for a long time, it is a maintainer infrastructure issue, not a contributor mistake. Leave a comment on your PR and a maintainer will address it. ## AI Agent Rules @@ -97,9 +97,8 @@ If `CI` or `Push CI` stays queued for more than 10 minutes, the self-hosted runn CCS PR review no longer depends on `anthropics/claude-code-action`. The repository review lane is self-hosted PR-Agent: - The retained `.github/workflows/ai-review.yml` runs PR-Agent in GitHub Actions. -- PR-Agent reviews run on the existing self-hosted `cliproxy` runner. - Use `/review` on the PR when you need a fresh pass after follow-up commits. -- Only the trusted `/review` comment path is enabled on the privileged self-hosted runner. +- Only the trusted `/review` comment path is enabled. - Keep repository-level reviewer instructions in the root `.pr_agent.toml`. - Keep runtime wiring and defaults in `ai-review.yml`, which still maps the existing `AI_REVIEW_BASE_URL`, `AI_REVIEW_MODEL`, and `AI_REVIEW_API_KEY` integrations onto PR-Agent's `OPENAI.*` and `config.*` settings. - If you change review defaults, update the workflow or `.pr_agent.toml` alongside the contributor or architecture docs in the same PR. From eab2b48f2c9a536453411d9bde2c8c5867f57162 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 16:31:29 -0400 Subject: [PATCH 11/12] docs(test-bucket): document slowTests addition criteria Reviewer flagged that the manual `slowTests` array silently degrades `test:fast` if new subprocess/server/timer tests are added without being declared slow (no literal `dist/` means auto-detection misses them). Interim fix: explicit 5-rule criteria block in the script so contributors know when to add a test. Enforcement (automated perf-budget warning in push-ci.yml) tracked in issue #1071. --- scripts/run-test-bucket.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index 4cd682d6..52981bac 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -6,9 +6,18 @@ const { spawnSync } = require('node:child_process'); const rootDir = path.resolve(__dirname, '..'); const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm']; -// Keep this list in sync with any newly added dist-dependent or long-running -// tests. `tests/unit/scripts/run-test-bucket.test.js` verifies every path here -// exists so bucket drift fails loudly instead of silently slowing `test:fast`. +// Add a `.ts` test to `slowTests` when ANY of these apply: +// 1. It spawns a child process (CLI, bun test, node, gh, etc.). +// 2. It binds a port, starts a server, or talks to localhost. +// 3. It reads a real file from `dist/` or the repo root at runtime. +// 4. It waits on a timer > 500ms or a filesystem watcher. +// 5. A single run consistently takes > 1500ms on reference hardware. +// Tests that literally reference `dist/` in source are auto-forced slow by +// `readsBuiltDist`. This list is the manual catch-all for `.ts` tests that +// meet the criteria above without the literal `dist/` string. +// `tests/unit/scripts/run-test-bucket.test.js` verifies every path here exists +// (catches deletion drift) but CANNOT detect new undeclared slow tests. +// Automated perf-budget enforcement tracked in issue #1071. const slowTests = [ 'tests/integration/cursor-daemon-lifecycle.test.ts', 'tests/integration/proxy/daemon-lifecycle.test.ts', From 6b5c74aa4baaac2b43da73250e9e8e30f68aafa5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 16:42:45 -0400 Subject: [PATCH 12/12] perf(test-bucket): only serialize slow bucket, parallelize fast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer noted `--max-concurrency=1` was applied to both buckets. Slow bucket needs it (subprocesses, ports, shared state → flaky in parallel) but fast bucket was unnecessarily capped. Remove the flag for fast; keep for slow. Verified: test:fast still green (2510 pass, 14.4s locally). --- scripts/run-test-bucket.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js index 52981bac..adca8e60 100644 --- a/scripts/run-test-bucket.js +++ b/scripts/run-test-bucket.js @@ -125,15 +125,18 @@ function runBucket(name) { } } - const result = spawnSync( - 'bun', - ['test', '--max-concurrency=1', ...selected], - { - cwd: rootDir, - stdio: 'inherit', - shell: process.platform === 'win32', - }, - ); + // Slow bucket forces sequential execution because it spawns subprocesses, + // binds ports, and touches shared state — parallelism causes flakes. + // Fast bucket keeps bun's default parallelism for speed. + const bunArgs = name === 'slow' + ? ['test', '--max-concurrency=1', ...selected] + : ['test', ...selected]; + + const result = spawnSync('bun', bunArgs, { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }); return result.status ?? 1; }