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:
AirCoding
2026-06-09 16:13:16 +08:00
parent e383d5f6a7
commit 5e282a39b4
44 changed files with 2905 additions and 1343 deletions

View File

@@ -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.

View File

@@ -19,6 +19,7 @@ export interface TaskNode {
title?: string
description?: string
acceptance_criteria?: string[]
task_spec?: Record<string, unknown>
}
export interface GraphValidation {