feat(round2+round3): 完整实现 A/B/C/D 主线 + round3-F/H 修复
Round2 主线:
- A: 事件落库地基 (RuntimeApp EventStore 单例 + 14 repo wiring)
- B: 执行体对齐 (read-before-edit, verification-before-completion)
- C: 界面对齐 (@opentui/solid, 删除 runtime 依赖)
- D: 经验闭环 (ExperienceMiner, DebuggerRole, CompactorRole)
Round2 补充修复:
- fail-on-missing 反作弊门禁
- projection-store-apply.test.ts 补写
- 3个空壳测试转行为 (evidence-store, recovery-impl, knowledge-store)
- ask 项目根支持 AIRCODING_PROJECT_ROOT
- Worker 事件契约修复 (task.attempt.started → checkpoint)
Round3-F: cpp 工具切换
- 删除 BuiltInToolRegistrar cpp.* 闭包
- 接入 toolchain-cpp 真实 CppToolRegistrar
- canonical envelope {status/output/metadata}
- ExecutorRole system prompt 对齐新工具名
Round3-H: Doctor 5 类报告
- toolchain (cmake/ninja/cppcheck/clangd/g++)
- display (X11/Wayland + ImageMagick)
- network (internet connectivity)
- provider (api_key/base_url/model/connectivity)
Secret 脱敏:
- 状态交接.md: sk- → \${OPENAI_API_KEY}
- .gitignore: 添加 .air/ .claude/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { WavePlanner } from './WavePlanner.js'
|
||||
import { RetryPlanner } from './RetryPlanner.js'
|
||||
import { WorkspaceManager } from './WorkspaceManager.js'
|
||||
import { AgentMonitor } from './AgentMonitor.js'
|
||||
import { eventIngestor } from '../events/EventIngestor.js'
|
||||
import { eventIngestor, type IEventIngestor } from '../events/EventIngestor.js'
|
||||
import type { WorkerManager } from '../workers/WorkerManager.js'
|
||||
|
||||
export type SchedulerState =
|
||||
@@ -48,9 +48,11 @@ export class Scheduler {
|
||||
private context: SchedulerContext
|
||||
private worker_manager?: WorkerManager
|
||||
private task_repo?: any
|
||||
private event_ingestor: IEventIngestor
|
||||
|
||||
constructor(context: SchedulerContext, worker_manager?: WorkerManager) {
|
||||
constructor(context: SchedulerContext, worker_manager?: WorkerManager, ingestor: IEventIngestor = eventIngestor) {
|
||||
this.context = context
|
||||
this.event_ingestor = ingestor
|
||||
this.graph = new TaskGraph()
|
||||
this.wave_planner = new WavePlanner()
|
||||
this.retry_planner = new RetryPlanner()
|
||||
@@ -62,18 +64,19 @@ export class Scheduler {
|
||||
/**
|
||||
* Create tasks from specifications.
|
||||
*/
|
||||
async create_tasks(tasks: Array<{ id: TaskID; type: string; title: string; description?: string; depends_on?: string[] }>): Promise<void> {
|
||||
async create_tasks(tasks: Array<{ id: TaskID; type: string; title: string; description?: string; depends_on?: string[]; task_spec?: Record<string, unknown> }>): Promise<void> {
|
||||
for (const task of tasks) {
|
||||
this.graph.add_task({
|
||||
id: task.id,
|
||||
status: 'pending',
|
||||
type: task.type, title: task.title,
|
||||
description: task.description,
|
||||
task_spec: task.task_spec,
|
||||
dependencies: task.depends_on?.map(d => ({ task_id: d, type: 'hard' as const })) || []
|
||||
})
|
||||
|
||||
// Emit task.created events (INV-1: via event store for projection)
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_created`,
|
||||
type: 'task.created',
|
||||
version: 1,
|
||||
@@ -86,7 +89,7 @@ export class Scheduler {
|
||||
task_id: task.id,
|
||||
type: task.type,
|
||||
title: task.title,
|
||||
task_spec_json: { description: task.description || '' },
|
||||
task_spec_json: task.task_spec || { description: task.description || '' },
|
||||
dependencies: (task.depends_on || []).map(d => ({ depends_on_task_id: d, dependency_type: 'hard', reason: '' })),
|
||||
metadata: {},
|
||||
}
|
||||
@@ -132,25 +135,23 @@ export class Scheduler {
|
||||
break
|
||||
|
||||
case 'PLANNING_WAVE': {
|
||||
// Check if all tasks done
|
||||
const counts = this.graph.count_by_status()
|
||||
const remaining = (counts.pending || 0) + (counts.running || 0)
|
||||
const pending = counts.pending || 0
|
||||
const running = counts.running || 0
|
||||
|
||||
if (remaining === 0) {
|
||||
this.state = 'COMPLETED'
|
||||
if (pending === 0 && running === 0) {
|
||||
this.state = this.terminal_state_from_counts(counts)
|
||||
return
|
||||
}
|
||||
|
||||
if (running > 0) {
|
||||
this.state = 'MONITORING'
|
||||
return
|
||||
}
|
||||
|
||||
// Plan next wave
|
||||
const plan = this.wave_planner.plan(this.graph)
|
||||
if (plan.length === 0) {
|
||||
// Check for blocked tasks
|
||||
const pending = this.graph.count_by_status().pending || 0
|
||||
if (pending > 0) {
|
||||
this.state = 'REPAIRING_OR_CONTINUING'
|
||||
return
|
||||
}
|
||||
this.state = 'COMPLETED'
|
||||
this.state = pending > 0 ? 'REPAIRING_OR_CONTINUING' : this.terminal_state_from_counts(counts)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ export class Scheduler {
|
||||
|
||||
// INV-1: Emit task.started event (durable) for projection
|
||||
const now = new Date().toISOString()
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_started`,
|
||||
type: 'task.started',
|
||||
version: 1,
|
||||
@@ -185,7 +186,7 @@ export class Scheduler {
|
||||
session_id: this.context.session_id,
|
||||
project_root: this.context.project_root,
|
||||
task_type: task.type || 'execute',
|
||||
task_spec: {
|
||||
task_spec: task.task_spec || {
|
||||
id: task.id,
|
||||
title: task.title || task.id,
|
||||
description: task.description || '',
|
||||
@@ -196,7 +197,7 @@ export class Scheduler {
|
||||
this.agent_monitor.record_heartbeat(agent_id, task.id)
|
||||
|
||||
// INV-1: Emit agent.started event (durable) for projection
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${agent_id}_started`,
|
||||
type: 'agent.started',
|
||||
version: 1,
|
||||
@@ -218,7 +219,7 @@ export class Scheduler {
|
||||
})
|
||||
} catch {
|
||||
// INV-1: emit task.failed event for projection
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_failed`,
|
||||
type: 'task.failed',
|
||||
version: 1,
|
||||
@@ -245,7 +246,7 @@ export class Scheduler {
|
||||
const hb = this.agent_monitor.get(l.agent_id)
|
||||
if (hb) {
|
||||
const now = new Date().toISOString()
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${hb.task_id}_lost`,
|
||||
type: 'agent.lost',
|
||||
version: 1,
|
||||
@@ -269,7 +270,7 @@ export class Scheduler {
|
||||
case 'hard_cancel':
|
||||
case 'soft_cancel':
|
||||
if (task_id) {
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task_id}_cancelled`,
|
||||
type: 'agent.cancelled',
|
||||
version: 1,
|
||||
@@ -299,7 +300,7 @@ export class Scheduler {
|
||||
|
||||
const attempt_id = `${task.id}_1`
|
||||
if (result.status === 'completed') {
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_completed`,
|
||||
type: 'task.completed',
|
||||
version: 1,
|
||||
@@ -320,7 +321,7 @@ export class Scheduler {
|
||||
})
|
||||
this.graph.update_status(task.id, 'completed')
|
||||
// INV-1: Emit agent.completed event (durable) for projection
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${handle.worker_id}_completed`,
|
||||
type: 'agent.completed',
|
||||
version: 1,
|
||||
@@ -333,7 +334,7 @@ export class Scheduler {
|
||||
})
|
||||
this.agent_monitor.remove(handle.worker_id)
|
||||
} else if (result.status === 'blocked') {
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_blocked`,
|
||||
type: 'task.blocked',
|
||||
version: 1,
|
||||
@@ -347,7 +348,7 @@ export class Scheduler {
|
||||
this.graph.update_status(task.id, 'blocked')
|
||||
this.agent_monitor.remove(handle.worker_id)
|
||||
} else if (result.status === 'cancelled') {
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_cancelled_result`,
|
||||
type: 'task.cancelled',
|
||||
version: 1,
|
||||
@@ -361,7 +362,7 @@ export class Scheduler {
|
||||
this.graph.update_status(task.id, 'cancelled')
|
||||
this.agent_monitor.remove(handle.worker_id)
|
||||
} else {
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${task.id}_failed_result`,
|
||||
type: 'task.failed',
|
||||
version: 1,
|
||||
@@ -374,7 +375,7 @@ export class Scheduler {
|
||||
})
|
||||
this.graph.update_status(task.id, 'failed')
|
||||
// INV-1: Emit agent.failed event (durable) for projection
|
||||
await eventIngestor.ingest({
|
||||
await this.event_ingestor.ingest({
|
||||
id: `evt_${handle.worker_id}_failed`,
|
||||
type: 'agent.failed',
|
||||
version: 1,
|
||||
@@ -440,6 +441,13 @@ export class Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
private terminal_state_from_counts(counts: Record<string, number>): SchedulerState {
|
||||
if ((counts.failed || 0) > 0) return 'TERMINATED'
|
||||
if ((counts.blocked || 0) > 0) return 'BLOCKED'
|
||||
if ((counts.cancelled || 0) > 0) return 'CANCELLED'
|
||||
return 'COMPLETED'
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild scheduler state from SQLite (INV-5: from EventStore, not EventBus).
|
||||
* Loads pending/running tasks from the tasks table and reconstructs the in-memory graph.
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface TaskNode {
|
||||
title?: string
|
||||
description?: string
|
||||
acceptance_criteria?: string[]
|
||||
task_spec?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GraphValidation {
|
||||
|
||||
Reference in New Issue
Block a user