chore: add ESLint, Prettier, CI workflow, and comprehensive CLAUDE.md conventions
- ESLint flat config with typescript-eslint strict rules - Prettier config (double quotes, semicolons, 2-space indent) - GitHub Actions CI: lint + typecheck + test on PRs - Cursor BugBot config (.cursor/BUGBOT.md) - Comprehensive CLAUDE.md with code conventions, security rules, plugin patterns, and common mistakes to avoid - Fix unused param lint error in plugin-registry.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5058c409d5
commit
c8061ce03f
|
|
@ -0,0 +1,29 @@
|
|||
# BugBot Configuration
|
||||
|
||||
## Project Context
|
||||
|
||||
Agent Orchestrator is a TypeScript monorepo for managing parallel AI coding agents. It uses pnpm workspaces with packages under `packages/`.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- TypeScript (strict mode, ESM with `.js` extensions in imports)
|
||||
- Node.js 20+ (use `node:` prefix for built-in modules)
|
||||
- pnpm workspaces
|
||||
- Next.js 15 (App Router) for web dashboard
|
||||
- Commander.js for CLI
|
||||
- vitest for testing
|
||||
|
||||
## Review Focus
|
||||
|
||||
- **Security**: Watch for command injection (especially in shell/tmux/git commands), AppleScript injection, GraphQL injection, unsanitized user input in API routes
|
||||
- **Shell execution**: Prefer `execFile` over `exec` to avoid shell injection. Flag any use of `exec` or string concatenation in shell commands
|
||||
- **Plugin pattern**: Plugins must export `{ manifest, create } satisfies PluginModule<T>` with types from `@agent-orchestrator/core`
|
||||
- **Type safety**: Flag `as unknown as T` casts, unguarded `JSON.parse`, and type re-declarations that should import from core
|
||||
- **Resource leaks**: Check for uncleared intervals/timeouts, uncleaned event listeners, missing `cancel()` on streams
|
||||
- **ESM compliance**: Imports must use `.js` extension for local files, `node:` prefix for builtins
|
||||
|
||||
## Ignore
|
||||
|
||||
- `packages/web/src/lib/mock-data.ts` — temporary mock data, will be replaced
|
||||
- `scripts/` — legacy bash scripts, not part of the TypeScript codebase
|
||||
- `artifacts/` — design documents, not code
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: pnpm test
|
||||
|
|
@ -4,3 +4,6 @@ dist/
|
|||
*.tsbuildinfo
|
||||
.env
|
||||
.env.local
|
||||
coverage/
|
||||
*.patch
|
||||
*-context.md
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
dist/
|
||||
node_modules/
|
||||
.next/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
pnpm-lock.yaml
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
137
CLAUDE.md
137
CLAUDE.md
|
|
@ -8,13 +8,15 @@ An open-source, agent-agnostic system for orchestrating parallel AI coding agent
|
|||
|
||||
## Tech Stack
|
||||
|
||||
- **Language**: TypeScript throughout (ESM, Node 20+)
|
||||
- **Language**: TypeScript throughout (ESM, Node 20+, strict mode)
|
||||
- **Monorepo**: pnpm workspaces
|
||||
- **Web**: Next.js 15 (App Router) + Tailwind CSS
|
||||
- **CLI**: Commander.js
|
||||
- **Config**: YAML + Zod validation
|
||||
- **Real-time**: Server-Sent Events
|
||||
- **State**: Flat metadata files + JSONL event log
|
||||
- **Linting**: ESLint (flat config) + Prettier
|
||||
- **Testing**: vitest
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -58,50 +60,132 @@ packages/
|
|||
terminal-web/ — xterm.js web terminal
|
||||
```
|
||||
|
||||
## Conventions
|
||||
## Code Conventions
|
||||
|
||||
### TypeScript
|
||||
- ESM modules (`"type": "module"`)
|
||||
- Use `.js` extensions in imports (TypeScript ESM requirement): `import { foo } from "./bar.js"`
|
||||
- Strict mode enabled
|
||||
- Use `node:` prefix for Node.js builtins: `import { readFileSync } from "node:fs"`
|
||||
### TypeScript (MUST follow)
|
||||
|
||||
- **ESM modules** — all packages use `"type": "module"`
|
||||
- **`.js` extensions in imports** — required for ESM: `import { foo } from "./bar.js"`
|
||||
- **`node:` prefix for builtins** — `import { readFileSync } from "node:fs"`, never bare `"fs"`
|
||||
- **Strict mode** — `"strict": true` in all tsconfig
|
||||
- **`type` imports** — use `import type { Foo }` for type-only imports (enforced by ESLint)
|
||||
- **No `any`** — use `unknown` and narrow with type guards. `any` is an ESLint error
|
||||
- **No `as unknown as T` casts** — validate data instead of unsafe casting
|
||||
- **Prefer `const`** — `let` only when reassignment is needed, never `var`
|
||||
- **Semicolons** — always use them
|
||||
- **Double quotes** — for strings (enforced by Prettier)
|
||||
- **2-space indentation** — (enforced by Prettier)
|
||||
|
||||
### Plugin Pattern (MUST follow)
|
||||
|
||||
Every plugin exports a `PluginModule` with type-safe `satisfies`:
|
||||
|
||||
### Plugin Structure
|
||||
Every plugin exports a `PluginModule`:
|
||||
```typescript
|
||||
import type { PluginModule, Runtime } from "@agent-orchestrator/core";
|
||||
|
||||
export const manifest = {
|
||||
const manifest = {
|
||||
name: "tmux",
|
||||
slot: "runtime" as const,
|
||||
description: "Runtime plugin: tmux sessions",
|
||||
version: "0.1.0",
|
||||
};
|
||||
|
||||
export function create(): Runtime {
|
||||
function create(): Runtime {
|
||||
return {
|
||||
name: "tmux",
|
||||
// ... implement interface
|
||||
// ... implement interface methods
|
||||
};
|
||||
}
|
||||
|
||||
export default { manifest, create } satisfies PluginModule<Runtime>;
|
||||
```
|
||||
|
||||
### Shell Commands
|
||||
When a plugin needs to run shell commands (git, tmux, gh, etc.), use `child_process.execFile` or `child_process.spawn` from `node:child_process`. Wrap them in async helpers.
|
||||
**Do NOT** use `const plugin = { ... }; export default plugin;` — always use inline `satisfies` for compile-time type checking.
|
||||
|
||||
### Shell Command Execution (MUST follow)
|
||||
|
||||
- **Always use `execFile`** (or `spawn`) from `node:child_process` — NEVER `exec`
|
||||
- `exec` runs through a shell and is vulnerable to injection
|
||||
- `execFile` passes args as an array, bypassing shell interpretation
|
||||
- **Always add timeouts** — `{ timeout: 30_000 }` for external commands
|
||||
- **Escape user-provided values** — never interpolate into command strings
|
||||
- **Shell escaping**: Do NOT use `JSON.stringify` for shell escaping — it is not a shell escaping function
|
||||
|
||||
```typescript
|
||||
// GOOD
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { timeout: 30_000 });
|
||||
|
||||
// BAD — shell injection risk
|
||||
import { exec } from "node:child_process";
|
||||
exec(`git checkout ${branchName}`); // branchName could contain ; rm -rf /
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Throw typed errors, don't return error codes
|
||||
- Plugin methods should throw if they can't do their job
|
||||
- The core services catch and handle plugin errors
|
||||
- **Always wrap `JSON.parse`** in try/catch — corrupted metadata should not crash the system
|
||||
- **Guard external data** — validate types from API/CLI/file inputs before using them
|
||||
|
||||
### Naming
|
||||
|
||||
- Files: `kebab-case.ts`
|
||||
- Types/Interfaces: `PascalCase`
|
||||
- Functions/variables: `camelCase`
|
||||
- Constants: `UPPER_SNAKE_CASE` only for true constants (env vars, regex patterns)
|
||||
- Plugin names: `kebab-case` matching directory name
|
||||
- Test files: `*.test.ts` co-located with source, or in `__tests__/` directory
|
||||
|
||||
### Imports Order
|
||||
|
||||
1. Node builtins (`node:fs`, `node:path`, etc.)
|
||||
2. External packages (`commander`, `chalk`, `yaml`, etc.)
|
||||
3. Workspace packages (`@agent-orchestrator/core`)
|
||||
4. Relative imports (`./foo.js`)
|
||||
|
||||
### Testing
|
||||
|
||||
- Use **vitest** for all tests
|
||||
- Mock external dependencies (`child_process`, `fs`, HTTP calls)
|
||||
- Co-locate test files: `src/foo.test.ts` or `src/__tests__/foo.test.ts`
|
||||
- Test edge cases: corrupted data, missing files, timeout, concurrent access
|
||||
|
||||
## Building & Development
|
||||
|
||||
```bash
|
||||
pnpm install # install all deps
|
||||
pnpm build # build all packages
|
||||
pnpm typecheck # typecheck all packages
|
||||
pnpm lint # ESLint check
|
||||
pnpm lint:fix # ESLint auto-fix
|
||||
pnpm format # Prettier format
|
||||
pnpm format:check # Prettier check (CI)
|
||||
pnpm test # run all tests
|
||||
```
|
||||
|
||||
### Before Committing
|
||||
|
||||
Always run lint and typecheck:
|
||||
```bash
|
||||
pnpm lint && pnpm typecheck
|
||||
```
|
||||
|
||||
Fix any issues before pushing. CI will reject PRs that fail lint or typecheck.
|
||||
|
||||
## Config
|
||||
|
||||
### Config
|
||||
- Config is loaded from `agent-orchestrator.yaml` (see `.yaml.example`)
|
||||
- All paths support `~` expansion
|
||||
- Per-project overrides for plugins and reactions
|
||||
- Validated with Zod schema at load time
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The `scripts/` directory contains the original bash scripts that this TypeScript codebase replaces. Use them as specifications:
|
||||
The `scripts/` directory contains the original bash scripts that this TypeScript codebase replaces. Use them as behavioral specifications:
|
||||
|
||||
| Script | What It Specifies |
|
||||
|--------|------------------|
|
||||
|
|
@ -115,14 +199,6 @@ The `scripts/` directory contains the original bash scripts that this TypeScript
|
|||
| `claude-session-status` | Activity classification (working/idle/blocked) |
|
||||
| `get-claude-session-info` | Agent introspection (session ID, summary extraction) |
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build # build all packages
|
||||
pnpm typecheck # typecheck all packages
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Stateless orchestrator** — no database, just flat metadata files + event log
|
||||
|
|
@ -130,3 +206,16 @@ pnpm typecheck # typecheck all packages
|
|||
3. **Push notifications** — the Notifier is the primary human interface, not the dashboard
|
||||
4. **Two-tier event handling** — auto-handle routine issues (CI, reviews), notify human only when judgment is needed
|
||||
5. **Backwards-compatible metadata** — flat key=value files matching the existing bash script format
|
||||
6. **Security first** — `execFile` not `exec`, validate all external input, escape strings for target context
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
- Using `exec` instead of `execFile` — security vulnerability
|
||||
- Using `JSON.stringify` for shell escaping — does not escape `$`, backticks, `$()`
|
||||
- Missing `.js` extension in local imports — will fail at runtime with ESM
|
||||
- Using bare `"fs"` instead of `"node:fs"` — inconsistent, may break in edge cases
|
||||
- Casting with `as unknown as T` — bypasses type safety, crashes on bad data
|
||||
- `export default plugin` without `satisfies PluginModule<T>` — loses type checking
|
||||
- Interpolating user input into shell commands, AppleScript, or GraphQL queries
|
||||
- Forgetting to clean up setInterval/setTimeout on disconnect/destroy
|
||||
- Using `on("exit")` instead of `once("exit")` for one-time handlers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
|
||||
export default tseslint.config(
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/.next/**",
|
||||
"**/coverage/**",
|
||||
"packages/web/next.config.js",
|
||||
"packages/web/postcss.config.mjs",
|
||||
],
|
||||
},
|
||||
|
||||
// Base JS rules
|
||||
eslint.configs.recommended,
|
||||
|
||||
// TypeScript strict rules
|
||||
...tseslint.configs.strict,
|
||||
|
||||
// Prettier compat (disables formatting rules)
|
||||
eslintConfigPrettier,
|
||||
|
||||
// Project-wide rules
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: {
|
||||
// Security: prevent shell injection patterns
|
||||
"no-eval": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-new-func": "error",
|
||||
|
||||
// Code quality
|
||||
"no-console": "warn",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-template-curly-in-string": "warn",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
"eqeqeq": ["error", "always"],
|
||||
|
||||
// TypeScript
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ prefer: "type-imports" },
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
},
|
||||
},
|
||||
|
||||
// Relaxed rules for test files
|
||||
{
|
||||
files: ["**/*.test.ts", "**/__tests__/**"],
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
},
|
||||
|
||||
// Relaxed rules for Next.js pages/components
|
||||
{
|
||||
files: ["packages/web/**/*.tsx", "packages/web/**/*.ts"],
|
||||
rules: {
|
||||
"no-console": "off", // Next.js uses console for server logs
|
||||
},
|
||||
}
|
||||
);
|
||||
15
package.json
15
package.json
|
|
@ -8,11 +8,24 @@
|
|||
"node": ">=20.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"dev": "pnpm --filter @agent-orchestrator/web dev",
|
||||
"lint": "pnpm -r lint",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test": "pnpm -r --filter '!@agent-orchestrator/web' test",
|
||||
"clean": "pnpm -r clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.2.3",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript-eslint": "^8.55.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export function createPluginRegistry(): PluginRegistry {
|
|||
}
|
||||
},
|
||||
|
||||
async loadFromConfig(config: OrchestratorConfig): Promise<void> {
|
||||
async loadFromConfig(_config: OrchestratorConfig): Promise<void> {
|
||||
// First, load all built-ins
|
||||
await this.loadBuiltins();
|
||||
|
||||
|
|
|
|||
835
pnpm-lock.yaml
835
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue