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

@@ -12,7 +12,7 @@ import { execFileSync } from 'child_process'
export interface DoctorCheck {
name: string
category: 'self_bootstrap' | 'capability' | 'project' | 'runtime'
category: 'self_bootstrap' | 'capability' | 'project' | 'runtime' | 'toolchain' | 'display' | 'network' | 'provider'
passed: boolean
message: string
fixable: boolean
@@ -66,6 +66,14 @@ export class DoctorService {
checks.push(this.check_capability_deps())
}
// FR-018/§6.12: toolchain / display / network / provider checks
if (scope === 'all') {
checks.push(...this.check_cpp_toolchain())
checks.push(this.check_display())
checks.push(await this.check_network())
checks.push(...await this.check_provider())
}
const all_passed = checks.every(c => c.passed)
return { checks, all_passed, bootstrap_passed: true, fixable_count: checks.filter(c => c.fixable).length }
}
@@ -219,4 +227,98 @@ export class DoctorService {
return { name: 'capability_deps', category: 'capability', passed: false, message: `Capability check failed: ${e.message}`, fixable: true }
}
}
// ===== FR-018/§6.12: 5 new categories =====
private check_cpp_toolchain(): DoctorCheck[] {
const tools = ['cmake', 'ninja', 'cppcheck', 'clangd', 'g++']
const reports: DoctorCheck[] = []
for (const t of tools) {
try {
const v = execFileSync('which', [t], { stdio: 'pipe', timeout: 3000 }).toString().trim()
reports.push({ name: `toolchain.${t}`, category: 'toolchain', passed: true, message: `${t} found at ${v}`, fixable: false })
} catch {
reports.push({ name: `toolchain.${t}`, category: 'toolchain', passed: false, message: `${t} not found`, fixable: true, fix: `apt install ${t === 'cmake' ? 'cmake' : t === 'ninja' ? 'ninja-build' : t}` })
}
}
return reports
}
private check_display(): DoctorCheck {
const display = process.env.DISPLAY
const wayland = process.env.WAYLAND_DISPLAY
if (!display && !wayland) {
return { name: 'display', category: 'display', passed: false, message: 'No DISPLAY/WAYLAND_DISPLAY (gui.screenshot will fail)', fixable: false }
}
try {
execFileSync('which', ['import'], { stdio: 'pipe' })
return { name: 'display', category: 'display', passed: true, message: `Display ${display || wayland} + ImageMagick available`, fixable: false }
} catch {
return { name: 'display', category: 'display', passed: false, message: 'ImageMagick not installed', fixable: true, fix: 'apt install imagemagick' }
}
}
private async check_network(): Promise<DoctorCheck> {
try {
const r = await fetch('https://1.1.1.1', { method: 'HEAD', signal: AbortSignal.timeout(3000) })
return { name: 'network.internet', category: 'network', passed: r.ok || r.status > 0, message: `HTTP ${r.status}`, fixable: false }
} catch (e: any) {
return { name: 'network.internet', category: 'network', passed: false, message: e.message, fixable: false }
}
}
private async check_provider(): Promise<DoctorCheck[]> {
const reports: DoctorCheck[] = []
const apiKey = process.env.AIRCODING_API_KEY || process.env.OPENAI_API_KEY
const baseUrl = process.env.OPENAI_BASE_URL || process.env.AIRCODING_API_URL
const model = process.env.AIRCODING_MODEL
reports.push({
name: 'provider.api_key',
category: 'provider',
passed: Boolean(apiKey),
message: apiKey ? `API key set (${apiKey.slice(0, 7)}...)` : 'No API key set',
fixable: false,
})
reports.push({
name: 'provider.base_url',
category: 'provider',
passed: Boolean(baseUrl),
message: baseUrl ? `Base URL: ${baseUrl}` : 'No base URL set',
fixable: false,
})
reports.push({
name: 'provider.model',
category: 'provider',
passed: Boolean(model),
message: model || 'No model set',
fixable: false,
})
if (apiKey && baseUrl) {
try {
const r = await fetch(`${baseUrl.replace(/\/$/, '')}/v1/models`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${apiKey}` },
signal: AbortSignal.timeout(5000),
})
reports.push({
name: 'provider.connectivity',
category: 'provider',
passed: r.ok || r.status > 0,
message: `HTTP ${r.status}`,
fixable: false,
})
} catch (e: any) {
reports.push({
name: 'provider.connectivity',
category: 'provider',
passed: false,
message: e.message,
fixable: false,
})
}
}
return reports
}
}