add skills
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"run_id": "eval-1-with_skill",
|
||||
"expectations": [
|
||||
{"text": "Plan uses git worktree in a sibling directory", "passed": true, "evidence": "Uses ../omo-wt/feat-max-background-agents"},
|
||||
{"text": "Branch is created from origin/dev", "passed": true, "evidence": "git checkout dev && git pull origin dev, then branch"},
|
||||
{"text": "Plan specifies multiple atomic commits for multi-file changes", "passed": true, "evidence": "2 commits: schema+tests, then concurrency+manager"},
|
||||
{"text": "Runs bun run typecheck, bun test, and bun run build before pushing", "passed": true, "evidence": "Explicit pre-push section with all 3 commands"},
|
||||
{"text": "PR is created targeting dev branch", "passed": true, "evidence": "--base dev in gh pr create"},
|
||||
{"text": "Verification loop includes all 3 gates: CI, review-work, and Cubic", "passed": true, "evidence": "Gate A (CI), Gate B (review-work 5 agents), Gate C (Cubic)"},
|
||||
{"text": "Gates are checked in order: CI first, then review-work, then Cubic", "passed": true, "evidence": "Explicit ordering in verify loop pseudocode"},
|
||||
{"text": "Cubic check uses gh api to check cubic-dev-ai[bot] reviews", "passed": true, "evidence": "Mentions cubic-dev-ai[bot] and 'No issues found' signal"},
|
||||
{"text": "Plan includes worktree cleanup after merge", "passed": true, "evidence": "Phase 4: git worktree remove ../omo-wt/feat-max-background-agents"},
|
||||
{"text": "Code changes reference actual files in the codebase", "passed": true, "evidence": "References src/config/schema/background-task.ts, src/features/background-agent/concurrency.ts, manager.ts"}
|
||||
]
|
||||
}
|
||||
+454
@@ -0,0 +1,454 @@
|
||||
# Code Changes: `max_background_agents` Config Option
|
||||
|
||||
## 1. `src/config/schema/background-task.ts` — Add schema field
|
||||
|
||||
```typescript
|
||||
import { z } from "zod"
|
||||
|
||||
export const BackgroundTaskConfigSchema = z.object({
|
||||
defaultConcurrency: z.number().min(1).optional(),
|
||||
providerConcurrency: z.record(z.string(), z.number().min(0)).optional(),
|
||||
modelConcurrency: z.record(z.string(), z.number().min(0)).optional(),
|
||||
maxDepth: z.number().int().min(1).optional(),
|
||||
maxDescendants: z.number().int().min(1).optional(),
|
||||
/** Maximum number of background agents that can run simultaneously across all models/providers (default: 5, minimum: 1) */
|
||||
maxBackgroundAgents: z.number().int().min(1).optional(),
|
||||
/** Stale timeout in milliseconds - interrupt tasks with no activity for this duration (default: 180000 = 3 minutes, minimum: 60000 = 1 minute) */
|
||||
staleTimeoutMs: z.number().min(60000).optional(),
|
||||
/** Timeout for tasks that never received any progress update, falling back to startedAt (default: 1800000 = 30 minutes, minimum: 60000 = 1 minute) */
|
||||
messageStalenessTimeoutMs: z.number().min(60000).optional(),
|
||||
syncPollTimeoutMs: z.number().min(60000).optional(),
|
||||
})
|
||||
|
||||
export type BackgroundTaskConfig = z.infer<typeof BackgroundTaskConfigSchema>
|
||||
```
|
||||
|
||||
**Rationale:** Follows exact same pattern as `maxDepth` and `maxDescendants` — `z.number().int().min(1).optional()`. The field is optional; runtime default of 5 is applied in `ConcurrencyManager`. No barrel export changes needed since `src/config/schema.ts` already does `export * from "./schema/background-task"` and the type is inferred.
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/config/schema/background-task.test.ts` — Add validation tests
|
||||
|
||||
Append after the existing `syncPollTimeoutMs` describe block (before the closing `})`):
|
||||
|
||||
```typescript
|
||||
describe("maxBackgroundAgents", () => {
|
||||
describe("#given valid maxBackgroundAgents (10)", () => {
|
||||
test("#when parsed #then returns correct value", () => {
|
||||
const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 10 })
|
||||
|
||||
expect(result.maxBackgroundAgents).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given maxBackgroundAgents of 1 (minimum)", () => {
|
||||
test("#when parsed #then returns correct value", () => {
|
||||
const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 1 })
|
||||
|
||||
expect(result.maxBackgroundAgents).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given maxBackgroundAgents below minimum (0)", () => {
|
||||
test("#when parsed #then throws ZodError", () => {
|
||||
let thrownError: unknown
|
||||
|
||||
try {
|
||||
BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 0 })
|
||||
} catch (error) {
|
||||
thrownError = error
|
||||
}
|
||||
|
||||
expect(thrownError).toBeInstanceOf(ZodError)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given maxBackgroundAgents not provided", () => {
|
||||
test("#when parsed #then field is undefined", () => {
|
||||
const result = BackgroundTaskConfigSchema.parse({})
|
||||
|
||||
expect(result.maxBackgroundAgents).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#given maxBackgroundAgents is non-integer (2.5)', () => {
|
||||
test("#when parsed #then throws ZodError", () => {
|
||||
let thrownError: unknown
|
||||
|
||||
try {
|
||||
BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 2.5 })
|
||||
} catch (error) {
|
||||
thrownError = error
|
||||
}
|
||||
|
||||
expect(thrownError).toBeInstanceOf(ZodError)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Rationale:** Follows exact test pattern from `maxDepth`, `maxDescendants`, and `syncPollTimeoutMs` tests. Uses `#given`/`#when`/`#then` nested describe style. Tests valid, minimum boundary, below minimum, not provided, and non-integer cases.
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/features/background-agent/concurrency.ts` — Add global agent limit
|
||||
|
||||
```typescript
|
||||
import type { BackgroundTaskConfig } from "../../config/schema"
|
||||
|
||||
const DEFAULT_MAX_BACKGROUND_AGENTS = 5
|
||||
|
||||
/**
|
||||
* Queue entry with settled-flag pattern to prevent double-resolution.
|
||||
*
|
||||
* The settled flag ensures that cancelWaiters() doesn't reject
|
||||
* an entry that was already resolved by release().
|
||||
*/
|
||||
interface QueueEntry {
|
||||
resolve: () => void
|
||||
rawReject: (error: Error) => void
|
||||
settled: boolean
|
||||
}
|
||||
|
||||
export class ConcurrencyManager {
|
||||
private config?: BackgroundTaskConfig
|
||||
private counts: Map<string, number> = new Map()
|
||||
private queues: Map<string, QueueEntry[]> = new Map()
|
||||
private globalRunningCount = 0
|
||||
|
||||
constructor(config?: BackgroundTaskConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
getMaxBackgroundAgents(): number {
|
||||
return this.config?.maxBackgroundAgents ?? DEFAULT_MAX_BACKGROUND_AGENTS
|
||||
}
|
||||
|
||||
getGlobalRunningCount(): number {
|
||||
return this.globalRunningCount
|
||||
}
|
||||
|
||||
canSpawnGlobally(): boolean {
|
||||
return this.globalRunningCount < this.getMaxBackgroundAgents()
|
||||
}
|
||||
|
||||
acquireGlobal(): void {
|
||||
this.globalRunningCount++
|
||||
}
|
||||
|
||||
releaseGlobal(): void {
|
||||
if (this.globalRunningCount > 0) {
|
||||
this.globalRunningCount--
|
||||
}
|
||||
}
|
||||
|
||||
getConcurrencyLimit(model: string): number {
|
||||
// ... existing implementation unchanged ...
|
||||
}
|
||||
|
||||
async acquire(model: string): Promise<void> {
|
||||
// ... existing implementation unchanged ...
|
||||
}
|
||||
|
||||
release(model: string): void {
|
||||
// ... existing implementation unchanged ...
|
||||
}
|
||||
|
||||
cancelWaiters(model: string): void {
|
||||
// ... existing implementation unchanged ...
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const [model] of this.queues) {
|
||||
this.cancelWaiters(model)
|
||||
}
|
||||
this.counts.clear()
|
||||
this.queues.clear()
|
||||
this.globalRunningCount = 0
|
||||
}
|
||||
|
||||
getCount(model: string): number {
|
||||
return this.counts.get(model) ?? 0
|
||||
}
|
||||
|
||||
getQueueLength(model: string): number {
|
||||
return this.queues.get(model)?.length ?? 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
- Add `DEFAULT_MAX_BACKGROUND_AGENTS = 5` constant
|
||||
- Add `globalRunningCount` private field
|
||||
- Add `getMaxBackgroundAgents()`, `getGlobalRunningCount()`, `canSpawnGlobally()`, `acquireGlobal()`, `releaseGlobal()` methods
|
||||
- `clear()` resets `globalRunningCount` to 0
|
||||
- All existing per-model methods remain unchanged
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/features/background-agent/concurrency.test.ts` — Add global limit tests
|
||||
|
||||
Append new describe block:
|
||||
|
||||
```typescript
|
||||
describe("ConcurrencyManager global background agent limit", () => {
|
||||
test("should default max background agents to 5 when no config", () => {
|
||||
// given
|
||||
const manager = new ConcurrencyManager()
|
||||
|
||||
// when
|
||||
const max = manager.getMaxBackgroundAgents()
|
||||
|
||||
// then
|
||||
expect(max).toBe(5)
|
||||
})
|
||||
|
||||
test("should use configured maxBackgroundAgents", () => {
|
||||
// given
|
||||
const config: BackgroundTaskConfig = { maxBackgroundAgents: 10 }
|
||||
const manager = new ConcurrencyManager(config)
|
||||
|
||||
// when
|
||||
const max = manager.getMaxBackgroundAgents()
|
||||
|
||||
// then
|
||||
expect(max).toBe(10)
|
||||
})
|
||||
|
||||
test("should allow spawning when under global limit", () => {
|
||||
// given
|
||||
const config: BackgroundTaskConfig = { maxBackgroundAgents: 2 }
|
||||
const manager = new ConcurrencyManager(config)
|
||||
|
||||
// when
|
||||
manager.acquireGlobal()
|
||||
|
||||
// then
|
||||
expect(manager.canSpawnGlobally()).toBe(true)
|
||||
expect(manager.getGlobalRunningCount()).toBe(1)
|
||||
})
|
||||
|
||||
test("should block spawning when at global limit", () => {
|
||||
// given
|
||||
const config: BackgroundTaskConfig = { maxBackgroundAgents: 2 }
|
||||
const manager = new ConcurrencyManager(config)
|
||||
|
||||
// when
|
||||
manager.acquireGlobal()
|
||||
manager.acquireGlobal()
|
||||
|
||||
// then
|
||||
expect(manager.canSpawnGlobally()).toBe(false)
|
||||
expect(manager.getGlobalRunningCount()).toBe(2)
|
||||
})
|
||||
|
||||
test("should allow spawning again after release", () => {
|
||||
// given
|
||||
const config: BackgroundTaskConfig = { maxBackgroundAgents: 1 }
|
||||
const manager = new ConcurrencyManager(config)
|
||||
manager.acquireGlobal()
|
||||
|
||||
// when
|
||||
manager.releaseGlobal()
|
||||
|
||||
// then
|
||||
expect(manager.canSpawnGlobally()).toBe(true)
|
||||
expect(manager.getGlobalRunningCount()).toBe(0)
|
||||
})
|
||||
|
||||
test("should not go below zero on extra release", () => {
|
||||
// given
|
||||
const manager = new ConcurrencyManager()
|
||||
|
||||
// when
|
||||
manager.releaseGlobal()
|
||||
|
||||
// then
|
||||
expect(manager.getGlobalRunningCount()).toBe(0)
|
||||
})
|
||||
|
||||
test("should reset global count on clear", () => {
|
||||
// given
|
||||
const config: BackgroundTaskConfig = { maxBackgroundAgents: 5 }
|
||||
const manager = new ConcurrencyManager(config)
|
||||
manager.acquireGlobal()
|
||||
manager.acquireGlobal()
|
||||
manager.acquireGlobal()
|
||||
|
||||
// when
|
||||
manager.clear()
|
||||
|
||||
// then
|
||||
expect(manager.getGlobalRunningCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `src/features/background-agent/manager.ts` — Enforce global limit
|
||||
|
||||
### In `launch()` method — add check before task creation (after `reserveSubagentSpawn`):
|
||||
|
||||
```typescript
|
||||
async launch(input: LaunchInput): Promise<BackgroundTask> {
|
||||
// ... existing logging ...
|
||||
|
||||
if (!input.agent || input.agent.trim() === "") {
|
||||
throw new Error("Agent parameter is required")
|
||||
}
|
||||
|
||||
// Check global background agent limit before spawn guard
|
||||
if (!this.concurrencyManager.canSpawnGlobally()) {
|
||||
const max = this.concurrencyManager.getMaxBackgroundAgents()
|
||||
const current = this.concurrencyManager.getGlobalRunningCount()
|
||||
throw new Error(
|
||||
`Background agent spawn blocked: ${current} agents running, max is ${max}. Wait for existing tasks to complete or increase background_task.maxBackgroundAgents.`
|
||||
)
|
||||
}
|
||||
|
||||
const spawnReservation = await this.reserveSubagentSpawn(input.parentSessionID)
|
||||
|
||||
try {
|
||||
// ... existing code ...
|
||||
|
||||
// After task creation, before queueing:
|
||||
this.concurrencyManager.acquireGlobal()
|
||||
|
||||
// ... rest of existing code ...
|
||||
} catch (error) {
|
||||
spawnReservation.rollback()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In `trackTask()` method — add global check:
|
||||
|
||||
```typescript
|
||||
async trackTask(input: { ... }): Promise<BackgroundTask> {
|
||||
const existingTask = this.tasks.get(input.taskId)
|
||||
if (existingTask) {
|
||||
// ... existing re-registration logic unchanged ...
|
||||
return existingTask
|
||||
}
|
||||
|
||||
// Check global limit for new external tasks
|
||||
if (!this.concurrencyManager.canSpawnGlobally()) {
|
||||
const max = this.concurrencyManager.getMaxBackgroundAgents()
|
||||
const current = this.concurrencyManager.getGlobalRunningCount()
|
||||
throw new Error(
|
||||
`Background agent spawn blocked: ${current} agents running, max is ${max}. Wait for existing tasks to complete or increase background_task.maxBackgroundAgents.`
|
||||
)
|
||||
}
|
||||
|
||||
// ... existing task creation ...
|
||||
this.concurrencyManager.acquireGlobal()
|
||||
|
||||
// ... rest unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
### In `tryCompleteTask()` — release global slot:
|
||||
|
||||
```typescript
|
||||
private async tryCompleteTask(task: BackgroundTask, source: string): Promise<boolean> {
|
||||
if (task.status !== "running") {
|
||||
// ... existing guard ...
|
||||
return false
|
||||
}
|
||||
|
||||
task.status = "completed"
|
||||
task.completedAt = new Date()
|
||||
// ... existing history record ...
|
||||
|
||||
removeTaskToastTracking(task.id)
|
||||
|
||||
// Release per-model concurrency
|
||||
if (task.concurrencyKey) {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
task.concurrencyKey = undefined
|
||||
}
|
||||
|
||||
// Release global slot
|
||||
this.concurrencyManager.releaseGlobal()
|
||||
|
||||
// ... rest unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
### In `cancelTask()` — release global slot:
|
||||
|
||||
```typescript
|
||||
async cancelTask(taskId: string, options?: { ... }): Promise<boolean> {
|
||||
// ... existing code up to concurrency release ...
|
||||
|
||||
if (task.concurrencyKey) {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
task.concurrencyKey = undefined
|
||||
}
|
||||
|
||||
// Release global slot (only for running tasks, pending never acquired)
|
||||
if (task.status !== "pending") {
|
||||
this.concurrencyManager.releaseGlobal()
|
||||
}
|
||||
|
||||
// ... rest unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
### In `handleEvent()` session.error handler — release global slot:
|
||||
|
||||
```typescript
|
||||
if (event.type === "session.error") {
|
||||
// ... existing error handling ...
|
||||
|
||||
task.status = "error"
|
||||
// ...
|
||||
|
||||
if (task.concurrencyKey) {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
task.concurrencyKey = undefined
|
||||
}
|
||||
|
||||
// Release global slot
|
||||
this.concurrencyManager.releaseGlobal()
|
||||
|
||||
// ... rest unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
### In prompt error handler inside `startTask()` — release global slot:
|
||||
|
||||
```typescript
|
||||
promptWithModelSuggestionRetry(this.client, { ... }).catch((error) => {
|
||||
// ... existing error handling ...
|
||||
if (existingTask) {
|
||||
existingTask.status = "interrupt"
|
||||
// ...
|
||||
if (existingTask.concurrencyKey) {
|
||||
this.concurrencyManager.release(existingTask.concurrencyKey)
|
||||
existingTask.concurrencyKey = undefined
|
||||
}
|
||||
|
||||
// Release global slot
|
||||
this.concurrencyManager.releaseGlobal()
|
||||
|
||||
// ... rest unchanged ...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
| File | Lines Added | Lines Modified |
|
||||
|------|-------------|----------------|
|
||||
| `src/config/schema/background-task.ts` | 2 | 0 |
|
||||
| `src/config/schema/background-task.test.ts` | ~50 | 0 |
|
||||
| `src/features/background-agent/concurrency.ts` | ~25 | 1 (`clear()`) |
|
||||
| `src/features/background-agent/concurrency.test.ts` | ~70 | 0 |
|
||||
| `src/features/background-agent/manager.ts` | ~20 | 0 |
|
||||
|
||||
Total: ~167 lines added, 1 line modified across 5 files.
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# Execution Plan: `max_background_agents` Config Option
|
||||
|
||||
## Phase 0: Setup — Branch + Worktree
|
||||
|
||||
1. **Create branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull origin dev
|
||||
git checkout -b feat/max-background-agents
|
||||
```
|
||||
|
||||
2. **Create worktree** in sibling directory:
|
||||
```bash
|
||||
mkdir -p ../omo-wt
|
||||
git worktree add ../omo-wt/feat-max-background-agents feat/max-background-agents
|
||||
```
|
||||
|
||||
3. **All subsequent work** happens in `../omo-wt/feat-max-background-agents/`, never in the main worktree.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Implement — Atomic Commits
|
||||
|
||||
### Commit 1: Add `max_background_agents` to config schema
|
||||
|
||||
**Files changed:**
|
||||
- `src/config/schema/background-task.ts` — Add `maxBackgroundAgents` field to `BackgroundTaskConfigSchema`
|
||||
- `src/config/schema/background-task.test.ts` — Add validation tests for the new field
|
||||
|
||||
**What:**
|
||||
- Add `maxBackgroundAgents: z.number().int().min(1).optional()` to `BackgroundTaskConfigSchema`
|
||||
- Default value handled at runtime (5), not in schema (all schema fields are optional per convention)
|
||||
- Add given/when/then tests: valid value, below minimum, not provided, non-number
|
||||
|
||||
### Commit 2: Enforce limit in BackgroundManager + ConcurrencyManager
|
||||
|
||||
**Files changed:**
|
||||
- `src/features/background-agent/concurrency.ts` — Add global agent count tracking + `getGlobalRunningCount()` + `canSpawnGlobally()`
|
||||
- `src/features/background-agent/concurrency.test.ts` — Tests for global limit enforcement
|
||||
- `src/features/background-agent/manager.ts` — Check global limit before `launch()` and `trackTask()`
|
||||
|
||||
**What:**
|
||||
- `ConcurrencyManager` already manages per-model concurrency. Add a separate global counter:
|
||||
- `private globalRunningCount: number = 0`
|
||||
- `private maxBackgroundAgents: number` (from config, default 5)
|
||||
- `acquireGlobal()` / `releaseGlobal()` methods
|
||||
- `getGlobalRunningCount()` for observability
|
||||
- `BackgroundManager.launch()` checks `concurrencyManager.canSpawnGlobally()` before creating task
|
||||
- `BackgroundManager.trackTask()` also checks global limit
|
||||
- On task completion/cancellation/error, call `releaseGlobal()`
|
||||
- Throw descriptive error when limit hit: `"Background agent spawn blocked: ${current} agents running, max is ${max}. Wait for existing tasks to complete or increase background_task.maxBackgroundAgents."`
|
||||
|
||||
### Local Validation
|
||||
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun test src/config/schema/background-task.test.ts
|
||||
bun test src/features/background-agent/concurrency.test.ts
|
||||
bun run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: PR Creation
|
||||
|
||||
1. **Push branch:**
|
||||
```bash
|
||||
git push -u origin feat/max-background-agents
|
||||
```
|
||||
|
||||
2. **Create PR** targeting `dev`:
|
||||
```bash
|
||||
gh pr create \
|
||||
--base dev \
|
||||
--title "feat: add max_background_agents config to limit concurrent background agents" \
|
||||
--body-file /tmp/pull-request-max-background-agents-$(date +%s).md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Verify Loop
|
||||
|
||||
### Gate A: CI
|
||||
- Wait for `ci.yml` workflow to complete
|
||||
- Check: `gh pr checks <PR_NUMBER> --watch`
|
||||
- If fails: read logs, fix, push, re-check
|
||||
|
||||
### Gate B: review-work (5 agents)
|
||||
- Run `/review-work` skill which launches 5 parallel background sub-agents:
|
||||
1. Oracle — goal/constraint verification
|
||||
2. Oracle — code quality
|
||||
3. Oracle — security
|
||||
4. Hephaestus — hands-on QA execution
|
||||
5. Hephaestus — context mining from GitHub/git
|
||||
- All 5 must pass. If any fails, fix and re-push.
|
||||
|
||||
### Gate C: Cubic (cubic-dev-ai[bot])
|
||||
- Wait for Cubic bot review on PR
|
||||
- Must say "No issues found"
|
||||
- If issues found: address feedback, push, re-check
|
||||
|
||||
### Loop
|
||||
```
|
||||
while (!allGatesPass) {
|
||||
if (CI fails) → fix → push → continue
|
||||
if (review-work fails) → fix → push → continue
|
||||
if (Cubic has issues) → fix → push → continue
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Merge + Cleanup
|
||||
|
||||
1. **Squash merge:**
|
||||
```bash
|
||||
gh pr merge <PR_NUMBER> --squash --delete-branch
|
||||
```
|
||||
|
||||
2. **Remove worktree:**
|
||||
```bash
|
||||
git worktree remove ../omo-wt/feat-max-background-agents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Impact Summary
|
||||
|
||||
| File | Change Type |
|
||||
|------|-------------|
|
||||
| `src/config/schema/background-task.ts` | Modified — add schema field |
|
||||
| `src/config/schema/background-task.test.ts` | Modified — add validation tests |
|
||||
| `src/features/background-agent/concurrency.ts` | Modified — add global limit tracking |
|
||||
| `src/features/background-agent/concurrency.test.ts` | Modified — add global limit tests |
|
||||
| `src/features/background-agent/manager.ts` | Modified — enforce global limit in launch/trackTask |
|
||||
|
||||
5 files changed across 2 atomic commits. No new files created (follows existing patterns).
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# PR Description
|
||||
|
||||
**Title:** `feat: add max_background_agents config to limit concurrent background agents`
|
||||
|
||||
**Base:** `dev`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- Add `maxBackgroundAgents` field to `BackgroundTaskConfigSchema` (default: 5, min: 1) to cap total simultaneous background agents across all models/providers
|
||||
- Enforce the global limit in `BackgroundManager.launch()` and `trackTask()` with descriptive error messages when the limit is hit
|
||||
- Release global slots on task completion, cancellation, error, and interrupt to prevent slot leaks
|
||||
|
||||
## Motivation
|
||||
|
||||
The existing concurrency system in `ConcurrencyManager` limits agents **per model/provider** (e.g., 5 concurrent `anthropic/claude-opus-4-6` tasks). However, there is no **global** cap across all models. A user running tasks across multiple providers could spawn an unbounded number of background agents, exhausting system resources.
|
||||
|
||||
`max_background_agents` provides a single knob to limit total concurrent background agents regardless of which model they use.
|
||||
|
||||
## Config Usage
|
||||
|
||||
```jsonc
|
||||
// .opencode/oh-my-opencode.jsonc
|
||||
{
|
||||
"background_task": {
|
||||
"maxBackgroundAgents": 10 // default: 5, min: 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
| File | What |
|
||||
|------|------|
|
||||
| `src/config/schema/background-task.ts` | Add `maxBackgroundAgents` schema field |
|
||||
| `src/config/schema/background-task.test.ts` | Validation tests (valid, boundary, invalid) |
|
||||
| `src/features/background-agent/concurrency.ts` | Global counter + `canSpawnGlobally()` / `acquireGlobal()` / `releaseGlobal()` |
|
||||
| `src/features/background-agent/concurrency.test.ts` | Global limit unit tests |
|
||||
| `src/features/background-agent/manager.ts` | Enforce global limit in `launch()`, `trackTask()`; release in completion/cancel/error paths |
|
||||
|
||||
## Testing
|
||||
|
||||
- `bun test src/config/schema/background-task.test.ts` — schema validation
|
||||
- `bun test src/features/background-agent/concurrency.test.ts` — global limit enforcement
|
||||
- `bun run typecheck` — clean
|
||||
- `bun run build` — clean
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# Verification Strategy
|
||||
|
||||
## Pre-Push Local Validation
|
||||
|
||||
Before every push, run all three checks sequentially:
|
||||
|
||||
```bash
|
||||
bun run typecheck && bun test && bun run build
|
||||
```
|
||||
|
||||
Specific test files to watch:
|
||||
```bash
|
||||
bun test src/config/schema/background-task.test.ts
|
||||
bun test src/features/background-agent/concurrency.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate A: CI (`ci.yml`)
|
||||
|
||||
### What CI runs
|
||||
1. **Tests (split):** mock-heavy tests run in isolation (separate `bun test` processes), rest in batch
|
||||
2. **Typecheck:** `bun run typecheck` (tsc --noEmit)
|
||||
3. **Build:** `bun run build` (ESM + declarations + schema)
|
||||
4. **Schema auto-commit:** if generated schema changed, CI commits it
|
||||
|
||||
### How to monitor
|
||||
```bash
|
||||
gh pr checks <PR_NUMBER> --watch
|
||||
```
|
||||
|
||||
### Common failure scenarios and fixes
|
||||
|
||||
| Failure | Likely Cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| Typecheck error | New field not matching existing type imports | Verify `BackgroundTaskConfig` type is auto-inferred from schema, no manual type updates needed |
|
||||
| Test failure | Test assertion wrong or missing import | Fix test, re-push |
|
||||
| Build failure | Import cycle or missing export | Check barrel exports in `src/config/schema.ts` (already re-exports via `export *`) |
|
||||
| Schema auto-commit | Generated JSON schema changed | Pull the auto-commit, rebase if needed |
|
||||
|
||||
### Recovery
|
||||
```bash
|
||||
# Read CI logs
|
||||
gh run view <RUN_ID> --log-failed
|
||||
|
||||
# Fix, commit, push
|
||||
git add -A && git commit -m "fix: address CI failure" && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate B: review-work (5 parallel agents)
|
||||
|
||||
### What it checks
|
||||
Run `/review-work` which launches 5 background sub-agents:
|
||||
|
||||
| Agent | Role | What it checks for this PR |
|
||||
|-------|------|---------------------------|
|
||||
| Oracle (goal) | Goal/constraint verification | Does `maxBackgroundAgents` actually limit agents? Is default 5? Is min 1? |
|
||||
| Oracle (quality) | Code quality | Follows existing patterns? No catch-all files? Under 200 LOC? given/when/then tests? |
|
||||
| Oracle (security) | Security review | No injection vectors, no unsafe defaults, proper input validation via Zod |
|
||||
| Hephaestus (QA) | Hands-on QA execution | Actually runs tests, checks typecheck, verifies build |
|
||||
| Hephaestus (context) | Context mining | Checks git history, related issues, ensures no duplicate/conflicting PRs |
|
||||
|
||||
### Pass criteria
|
||||
All 5 agents must pass. Any single failure blocks.
|
||||
|
||||
### Common failure scenarios and fixes
|
||||
|
||||
| Agent | Likely Issue | Fix |
|
||||
|-------|-------------|-----|
|
||||
| Oracle (goal) | Global limit not enforced in all exit paths (completion, cancel, error, interrupt) | Audit every status transition in `manager.ts` that should call `releaseGlobal()` |
|
||||
| Oracle (quality) | Test style not matching given/when/then | Restructure tests with `#given`/`#when`/`#then` describe nesting |
|
||||
| Oracle (quality) | File exceeds 200 LOC | `concurrency.ts` is 137 LOC + ~25 new = ~162 LOC, safe. `manager.ts` is already large but we're adding ~20 lines to existing methods, not creating new responsibility |
|
||||
| Oracle (security) | Integer overflow or negative values | Zod `.int().min(1)` handles this at config parse time |
|
||||
| Hephaestus (QA) | Test actually fails when run | Run tests locally first, fix before push |
|
||||
|
||||
### Recovery
|
||||
```bash
|
||||
# Review agent output
|
||||
background_output(task_id="<review-work-task-id>")
|
||||
|
||||
# Fix identified issues
|
||||
# ... edit files ...
|
||||
git add -A && git commit -m "fix: address review-work feedback" && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate C: Cubic (`cubic-dev-ai[bot]`)
|
||||
|
||||
### What it checks
|
||||
Cubic is an automated code review bot that analyzes the PR diff. It must respond with "No issues found" for the gate to pass.
|
||||
|
||||
### Common failure scenarios and fixes
|
||||
|
||||
| Issue | Likely Cause | Fix |
|
||||
|-------|-------------|-----|
|
||||
| "Missing error handling" | `releaseGlobal()` not called in some error path | Add `releaseGlobal()` to the missed path |
|
||||
| "Inconsistent naming" | Field name doesn't match convention | Use `maxBackgroundAgents` (camelCase in schema, `max_background_agents` in JSONC config) |
|
||||
| "Missing documentation" | No JSDoc on new public methods | Add JSDoc comments to `canSpawnGlobally()`, `acquireGlobal()`, `releaseGlobal()`, `getMaxBackgroundAgents()` |
|
||||
| "Test coverage gap" | Missing edge case test | Add the specific test case Cubic identifies |
|
||||
|
||||
### Recovery
|
||||
```bash
|
||||
# Read Cubic's review
|
||||
gh api repos/code-yeongyu/oh-my-openagent/pulls/<PR_NUMBER>/reviews
|
||||
|
||||
# Address each comment
|
||||
# ... edit files ...
|
||||
git add -A && git commit -m "fix: address Cubic review feedback" && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Loop Pseudocode
|
||||
|
||||
```
|
||||
iteration = 0
|
||||
while true:
|
||||
iteration++
|
||||
log("Verification iteration ${iteration}")
|
||||
|
||||
# Gate A: CI (cheapest, check first)
|
||||
push_and_wait_for_ci()
|
||||
if ci_failed:
|
||||
read_ci_logs()
|
||||
fix_and_commit()
|
||||
continue
|
||||
|
||||
# Gate B: review-work (5 agents, more expensive)
|
||||
run_review_work()
|
||||
if any_agent_failed:
|
||||
read_agent_feedback()
|
||||
fix_and_commit()
|
||||
continue
|
||||
|
||||
# Gate C: Cubic (external bot, wait for it)
|
||||
wait_for_cubic_review()
|
||||
if cubic_has_issues:
|
||||
read_cubic_comments()
|
||||
fix_and_commit()
|
||||
continue
|
||||
|
||||
# All gates passed
|
||||
break
|
||||
|
||||
# Merge
|
||||
gh pr merge <PR_NUMBER> --squash --delete-branch
|
||||
```
|
||||
|
||||
No iteration cap. Loop continues until all three gates pass simultaneously in a single iteration.
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Mitigation |
|
||||
|------|------------|------------|
|
||||
| Slot leak (global count never decremented) | Medium | Audit every exit path: `tryCompleteTask`, `cancelTask`, `handleEvent(session.error)`, `startTask` prompt error, `resume` prompt error |
|
||||
| Race condition on global count | Low | `globalRunningCount` is synchronous (single-threaded JS), no async gap between check and increment in `launch()` |
|
||||
| Breaking existing behavior | Low | Default is 5, same as existing per-model default. Users with <5 total agents see no change |
|
||||
| `manager.ts` exceeding 200 LOC | Already exceeded | File is already ~1500 LOC (exempt due to being a core orchestration class with many methods). Our changes add ~20 lines to existing methods, not a new responsibility |
|
||||
@@ -0,0 +1 @@
|
||||
{"total_tokens": null, "duration_ms": 292000, "total_duration_seconds": 292}
|
||||
Reference in New Issue
Block a user