fix(P0): close 15 blockers + add 26 regression tests; fix wiring schema regression
Phase A (security red lines) — CLOSED: - B8: 3x command injection fixed (execFileSync + args array in CMake/CppBuilder/Cppcheck) - B6: ToolRegistry permission bypass fixed (real task_scope/profile passed) - B7: ACTION_BRANCHES this-binding crash fixed (instance method) - B17: DeveloperLogEncryptor hardcoded 'dev-key' removed (throws if no key) - B22: CommandRiskAnalyzer 'in' operator bug fixed (includes) - B1: EventStore.project() transaction handle now passed to all repos - B2: workspace projection illegal enum fixed (active/merged) - B4: route_prefix separator unified to '/' - B5: TaskAttempt column mapping fixed Other blockers fixed: - B3: project-level DB schema aligned to db-schema §20 (.air/local, learned_memories) - B9: cpp.* tools registered through PermissionEngine path - B11: Scheduler BLOCKED/CANCELLED states added - B18: CapabilityTrustLevel 5-level enum aligned - B19: PermissionEngine block/refuse/announce_then_run + grant_scope - B20: Worker exit code 4 = parent_cancelled - B24: project_id now randomUUID Regression fix (introduced by B3 schema refactor): - wiring.ts capture_debug_record/promote_memory_entry realigned to refactored DebugRecord/MemoryEntry interfaces (was compile-level decoupling) Tests: 128 regression/unit tests pass (22 regression + 3 unit + 3 e2e suites) Still open (tracked for next round): B10 (INV-2 outbox emit), B12 (Scheduler event projection), B13 (MainAgent LLM classify), B14 (IPC envelope fields), B15 (TUI OpenTUI), B16 (api_key strict), B21 (CLI init INV-3), B23 (e2e real), B25 (MVP tools), B26 (ContextAssembler L6-L9) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { WavePlanner } from './WavePlanner.js'
|
||||
import { RetryPlanner } from './RetryPlanner.js'
|
||||
import { WorkspaceManager } from './WorkspaceManager.js'
|
||||
import { AgentMonitor } from './AgentMonitor.js'
|
||||
import type { WorkerManager } from '../workers/WorkerManager.js'
|
||||
|
||||
export type SchedulerState =
|
||||
| 'IDLE'
|
||||
@@ -27,6 +28,8 @@ export type SchedulerState =
|
||||
| 'REPAIRING_OR_CONTINUING'
|
||||
| 'COMPLETED'
|
||||
| 'TERMINATED'
|
||||
| 'BLOCKED'
|
||||
| 'CANCELLED'
|
||||
|
||||
export interface SchedulerContext {
|
||||
session_id: SessionID
|
||||
@@ -42,14 +45,16 @@ export class Scheduler {
|
||||
private workspace_manager: WorkspaceManager
|
||||
private agent_monitor: AgentMonitor
|
||||
private context: SchedulerContext
|
||||
private worker_manager?: WorkerManager
|
||||
|
||||
constructor(context: SchedulerContext) {
|
||||
constructor(context: SchedulerContext, worker_manager?: WorkerManager) {
|
||||
this.context = context
|
||||
this.graph = new TaskGraph()
|
||||
this.wave_planner = new WavePlanner()
|
||||
this.retry_planner = new RetryPlanner()
|
||||
this.workspace_manager = new WorkspaceManager(context.project_root)
|
||||
this.agent_monitor = new AgentMonitor()
|
||||
this.worker_manager = worker_manager
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +77,12 @@ export class Scheduler {
|
||||
* Run until idle — drives state machine to terminal state.
|
||||
*/
|
||||
async run_until_idle(): Promise<SchedulerState> {
|
||||
while (this.state !== 'COMPLETED' && this.state !== 'TERMINATED') {
|
||||
while (
|
||||
this.state !== 'COMPLETED' &&
|
||||
this.state !== 'TERMINATED' &&
|
||||
this.state !== 'BLOCKED' &&
|
||||
this.state !== 'CANCELLED'
|
||||
) {
|
||||
await this.step()
|
||||
}
|
||||
return this.state
|
||||
@@ -125,17 +135,31 @@ export class Scheduler {
|
||||
break
|
||||
}
|
||||
|
||||
case 'DISPATCHING':
|
||||
// Transition planned tasks to 'running' and register with agent monitor
|
||||
case 'DISPATCHING': {
|
||||
const runnable = this.graph.get_runnable_tasks()
|
||||
for (const task of runnable) {
|
||||
this.graph.mark_terminal(task.id, 'running' as any)
|
||||
// Register with agent monitor for heartbeat tracking
|
||||
const agent_id = `agent_${task.id}`
|
||||
this.agent_monitor.record_heartbeat(agent_id, task.id)
|
||||
|
||||
if (this.worker_manager) {
|
||||
try {
|
||||
await this.worker_manager.spawn({
|
||||
entrypoint: 'packages/workers/src/main.ts',
|
||||
agent_id,
|
||||
session_id: this.context.session_id,
|
||||
project_root: this.context.project_root,
|
||||
})
|
||||
this.agent_monitor.record_heartbeat(agent_id, task.id)
|
||||
} catch {
|
||||
this.graph.mark_terminal(task.id, 'failed')
|
||||
}
|
||||
} else {
|
||||
this.agent_monitor.record_heartbeat(agent_id, task.id)
|
||||
}
|
||||
}
|
||||
this.state = 'MONITORING'
|
||||
break
|
||||
}
|
||||
|
||||
case 'MONITORING':
|
||||
// Check agent health
|
||||
@@ -179,30 +203,33 @@ export class Scheduler {
|
||||
this.state = 'MERGING'
|
||||
break
|
||||
|
||||
case 'MERGING':
|
||||
// Merge completed workspaces
|
||||
case 'MERGING': {
|
||||
const active_ws = this.workspace_manager.get_active()
|
||||
for (const ws of active_ws) {
|
||||
await this.workspace_manager.merge_workspace(ws.id)
|
||||
}
|
||||
this.state = 'REVIEWING_WAVE'
|
||||
break
|
||||
}
|
||||
|
||||
case 'REVIEWING_WAVE':
|
||||
// After review, either continue or repair
|
||||
this.state = 'REPAIRING_OR_CONTINUING'
|
||||
break
|
||||
|
||||
case 'REPAIRING_OR_CONTINUING': {
|
||||
// Check for failed tasks that need retry
|
||||
const counts = this.graph.count_by_status()
|
||||
const failed = counts.failed || 0
|
||||
|
||||
if (failed > 0) {
|
||||
// Retry logic handled by RetryPlanner
|
||||
// Would spawn debug tasks and/or retry with backoff
|
||||
}
|
||||
|
||||
this.state = 'PLANNING_WAVE'
|
||||
break
|
||||
}
|
||||
|
||||
case 'BLOCKED':
|
||||
case 'CANCELLED':
|
||||
case 'COMPLETED':
|
||||
case 'TERMINATED':
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user