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

@@ -8,6 +8,8 @@
import { WorkerRuntime } from '../WorkerRuntime.js'
const MEMORY_TYPES = new Set(['project_rule', 'toolchain_rule', 'skill_update', 'debug_experience'])
export interface ExperienceMinerResult {
status: 'completed' | 'no_patterns' | 'blocked'
entries: Array<{
@@ -26,7 +28,7 @@ export class ExperienceMinerRole {
this.runtime = runtime
}
async run(mine_spec: { task_ids: string[]; focus_categories?: string[] }): Promise<ExperienceMinerResult> {
async run(mine_spec: { task_ids?: string[]; focus_categories?: string[]; source_refs?: Array<Record<string, unknown>>; evidence_refs?: string[] }): Promise<ExperienceMinerResult> {
const result: ExperienceMinerResult = {
status: 'no_patterns',
entries: [],
@@ -34,68 +36,73 @@ export class ExperienceMinerRole {
}
try {
this.runtime.emit('mining.started', { task_ids: mine_spec.task_ids })
const task_ids = mine_spec.task_ids ?? []
const evidence_refs = mine_spec.evidence_refs ?? task_ids.map((task_id) => `task:${task_id}`)
const focus = mine_spec.focus_categories?.length ? mine_spec.focus_categories : ['project_rule', 'toolchain_rule', 'debug_experience']
// Read completed task results to extract patterns
const task_summaries: string[] = []
for (const task_id of mine_spec.task_ids) {
try {
// Emit that we're reading a task
this.runtime.emit('mining.task', { task_id })
task_summaries.push(`Task ${task_id}: completed`)
} catch { /* skip failed task reads */ }
}
if (task_summaries.length === 0) {
result.status = 'no_patterns'
result.summary = 'No completed tasks available for mining'
if (task_ids.length === 0 && evidence_refs.length === 0) {
result.summary = 'No task or evidence refs available for memory mining'
return result
}
// Use LLM to extract patterns
const messages = [
{ role: 'system', content: 'You are an experience mining expert. Extract reusable patterns, best practices, and lessons learned from completed tasks. Output one pattern per line in format: CATEGORY: pattern description' },
{ role: 'user', content: `Analyze these completed tasks and extract reusable patterns:\n${task_summaries.join('\n')}\n\nFocus categories: ${(mine_spec.focus_categories || ['implementation', 'debugging', 'testing']).join(', ')}` }
{ role: 'system', content: 'Extract durable learning candidates only when supported by evidence. Output one candidate per line as memory_type: concise summary. Valid memory_type values: project_rule, toolchain_rule, skill_update, debug_experience. Do not promote or archive memories.' },
{ role: 'user', content: `Evidence refs:\n${evidence_refs.join('\n')}\n\nTask ids: ${task_ids.join(', ') || '(none)'}\nFocus categories: ${focus.join(', ')}` }
]
try {
const analysis = await this.runtime.call_llm({ messages, max_tokens: 2048, temperature: 0.3 })
const lines = (analysis.content || '').split('\n').filter(l => l.includes(':'))
for (const line of lines) {
const analysis = await this.runtime.call_llm({ messages, max_tokens: 2048, temperature: 0.2 })
for (const line of (analysis.content || '').split('\n')) {
const colon_idx = line.indexOf(':')
if (colon_idx > 0) {
const category = line.slice(0, colon_idx).trim().toLowerCase()
const pattern = line.slice(colon_idx + 1).trim()
if (pattern.length > 5) {
result.entries.push({
category,
pattern,
source_task_id: mine_spec.task_ids[0] || '',
description: pattern
})
}
}
if (colon_idx <= 0) continue
const category = line.slice(0, colon_idx).trim().toLowerCase()
const memory_type = MEMORY_TYPES.has(category) ? category : 'project_rule'
const pattern = line.slice(colon_idx + 1).trim()
if (pattern.length < 8) continue
result.entries.push({ category: memory_type, pattern, source_task_id: task_ids[0] || '', description: pattern })
}
} catch {
// LLM unavailable — extract basic patterns from task metadata
result.entries.push({
category: 'execution',
pattern: 'Tasks completed via Executor→LLM→Tool loop',
source_task_id: mine_spec.task_ids[0] || '',
description: 'Standard execution pattern for code changes'
category: 'project_rule',
pattern: `Review evidence before promoting memory from ${evidence_refs[0] || task_ids[0]}`,
source_task_id: task_ids[0] || '',
description: 'LLM unavailable; created a conservative candidate that requires human/runtime review before promotion.',
})
}
if (result.entries.length === 0 && evidence_refs.length > 0) {
const category = focus.find((item) => MEMORY_TYPES.has(item)) || 'project_rule'
result.entries.push({
category,
pattern: `Review evidence before promoting memory from ${evidence_refs[0]}`,
source_task_id: task_ids[0] || '',
description: 'Created a conservative candidate because no structured LLM-supported pattern was returned.',
})
}
for (const entry of result.entries) {
const candidate_id = `mem_${crypto.randomUUID()}`
this.runtime.emit('memory.candidate.created', {
event_id: `evt_${candidate_id}`,
candidate_id,
source_ref: {
entity_type: entry.source_task_id ? 'task' : 'evidence',
entity_id: entry.source_task_id || evidence_refs[0] || '',
},
memory_type: entry.category,
summary: entry.pattern,
evidence_refs,
})
}
if (result.entries.length === 0) {
result.status = 'no_patterns'
result.summary = `No patterns extracted from ${mine_spec.task_ids.length} tasks`
result.summary = `No supported memory candidates extracted from ${task_ids.length} tasks`
} else {
result.status = 'completed'
result.summary = `Mined ${result.entries.length} patterns from ${mine_spec.task_ids.length} tasks`
result.summary = `Created ${result.entries.length} memory candidates from ${task_ids.length} tasks`
}
this.runtime.checkpoint('mining_completed', { patterns_found: result.entries.length })
this.runtime.checkpoint('experience_mining_completed', { candidates: result.entries.length })
return result
} catch (error) {