From ddefcbb2b1f9b1fa5f3473c5787d5823db45485e Mon Sep 17 00:00:00 2001 From: AirCoding Date: Fri, 5 Jun 2026 18:39:10 +0800 Subject: [PATCH] fix: integrate audit findings round 1 - tools, worker, scheduler, main agent - Unify ToolResultEnvelope (output vs content) for built-in tools - Fix shell.run AsyncGenerator consumption in ToolRegistry.call/streaming - Scheduler: consume WorkerResult.status instead of marking all running tasks completed - WorkerProcess/WorkerManager: surface exit events and generate failed/cancelled result - MainAgent: integrate ContextAssembler, Chinese destructive regex, ArchitectureDesigner impact gate - run.ts: pendingConfirmation flow, dispatch extracted, .air files filtered from /results - CapabilityRegistry wired into RuntimeApp and ServiceRegistry; DoctorService uses it - release.ts: findRepoRoot/findBun, run air e2e + depcruise + runtime regression - New gates: release-critical-gates, CLI run command regression - 14/14 e2e gates pass; 3/3 release dry-run pass Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/src/commands/ask.ts | 7 +- packages/cli/src/commands/e2e.ts | 1 + packages/cli/src/commands/release.ts | 51 ++- packages/cli/src/commands/run.ts | 188 +++++++--- .../cli/test/run-command-regression.test.ts | 24 ++ packages/runtime/src/agents/main/MainAgent.ts | 67 +++- packages/runtime/src/app/RuntimeApp.ts | 11 +- packages/runtime/src/app/ServiceRegistry.ts | 13 +- .../runtime/src/context/ContextAssembler.ts | 49 ++- packages/runtime/src/scheduler/Scheduler.ts | 80 ++++- .../runtime/src/tools/BuiltInToolRegistrar.ts | 38 +- packages/runtime/src/tools/ToolRegistry.ts | 72 ++-- packages/runtime/src/tools/shell/index.ts | 78 +++-- packages/runtime/src/workers/WorkerManager.ts | 82 ++++- packages/runtime/src/workers/WorkerProcess.ts | 17 +- .../regression/release-critical-gates.test.ts | 103 ++++++ .../tool-registry-permission.test.ts | 7 + .../regression/worker-result-envelope.test.ts | 16 +- packages/workers/src/roles/ExecutorRole.ts | 21 +- 状态交接.md | 230 ++++++++++++ 集成测试阶段Deepseek审查结果.md | 276 +++++++++++++++ 集成测试阶段GLM5.1审查结果.md | 220 ++++++++++++ 集成测试阶段Gpt5.5审查结果.md | 331 ++++++++++++++++++ 集成测试阶段MiniMax-M3审查结果.md | 196 +++++++++++ 24 files changed, 1992 insertions(+), 186 deletions(-) create mode 100755 packages/cli/test/run-command-regression.test.ts create mode 100755 packages/runtime/test/regression/release-critical-gates.test.ts create mode 100755 状态交接.md create mode 100755 集成测试阶段Deepseek审查结果.md create mode 100755 集成测试阶段GLM5.1审查结果.md create mode 100755 集成测试阶段Gpt5.5审查结果.md create mode 100755 集成测试阶段MiniMax-M3审查结果.md diff --git a/packages/cli/src/commands/ask.ts b/packages/cli/src/commands/ask.ts index 50f9853..65f973c 100755 --- a/packages/cli/src/commands/ask.ts +++ b/packages/cli/src/commands/ask.ts @@ -54,6 +54,9 @@ export async function askCommand(prompt: string, opts?: { model?: string; maxTur project_id: 'ask-project', classify_mode: 'regex', provider_manager: provider as any, + context_assembler: app.context_assembler, + project_root: projectRoot, + agent_id: 'ask-agent' as any, classify_model: model }) @@ -88,9 +91,9 @@ async function execute_task( const ctx = { session_id: 'ask-session', project_id: 'ask-project', + project_root: projectRoot, agent_id: 'ask-agent', - permission_template: 'main_direct' as const, - cwd: projectRoot + agent_type: 'executor' as const, } const systemPrompt = `You are an AI coding assistant. Help the user by reading files, writing code, and running commands. diff --git a/packages/cli/src/commands/e2e.ts b/packages/cli/src/commands/e2e.ts index 81eb56e..2893b07 100755 --- a/packages/cli/src/commands/e2e.ts +++ b/packages/cli/src/commands/e2e.ts @@ -108,6 +108,7 @@ export function e2eCommand(): void { return { pass: false, detail: `❌ (${errCount} errors)\n ${tail}` } } }}, + { label: 'P0: Release-critical functional gates', fn: () => runTest('P0-REL', './packages/runtime/test/regression/release-critical-gates.test.ts ./packages/cli/test/run-command-regression.test.ts', repoRoot) }, // P1: Storage/Events { label: 'P1: Storage/Events (test)', fn: () => runTest('P1', './packages/runtime/test/storage/ ./packages/runtime/test/regression/transaction-boundary.test.ts ./packages/runtime/test/regression/event-repository-route.test.ts', repoRoot) }, diff --git a/packages/cli/src/commands/release.ts b/packages/cli/src/commands/release.ts index e2cfd94..817d6d4 100755 --- a/packages/cli/src/commands/release.ts +++ b/packages/cli/src/commands/release.ts @@ -3,6 +3,34 @@ * DD §17. Full validation suite. */ import { execFileSync } from 'child_process' +import { existsSync } from 'fs' +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +function findRepoRoot(): string { + if (process.env.AIRCODING_REPO_ROOT && existsSync(join(process.env.AIRCODING_REPO_ROOT, 'package.json'))) { + return process.env.AIRCODING_REPO_ROOT + } + let dir = dirname(fileURLToPath(import.meta.url)) + while (dir !== dirname(dir)) { + if (existsSync(join(dir, 'package.json')) && existsSync(join(dir, 'packages'))) return dir + dir = dirname(dir) + } + return process.cwd() +} + +function findBun(repoRoot: string): string { + const candidates = [ + process.env.BUN_INSTALL ? join(process.env.BUN_INSTALL, 'bin', 'bun') : '', + join(process.env.HOME || '', '.bun', 'bin', 'bun'), + '/usr/local/bin/bun', + '/usr/bin/bun', + ].filter(Boolean) + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate + } + return 'bun' +} export function releaseCommand(): void { console.log('============================================================') @@ -12,17 +40,20 @@ export function releaseCommand(): void { let passed = 0 let failed = 0 - // Gate 1: typecheck + const repoRoot = findRepoRoot() + const bun = findBun(repoRoot) + + // Gate 1: full e2e validation suite console.log('Running release readiness checks...\n') - const g1 = runCheck('typecheck', 'node_modules/.bin/tsc', ['--noEmit', '-p', 'tsconfig.check.json']) + const g1 = runCheck('e2e', bun, ['run', join(repoRoot, 'packages/cli/src/index.ts'), 'e2e'], repoRoot) if (g1) passed++; else failed++; - // Gate 2: test (run regression suite) - const g2 = runCheck('test', '/home/airlongdian/.bun/bin/bun', ['test', 'packages/runtime/test/regression/']) + // Gate 2: release regression suite + const g2 = runCheck('runtime regression', bun, ['test', 'packages/runtime/test/regression/'], repoRoot) if (g2) passed++; else failed++; - // Gate 3: depcruise - const g3 = runCheck('depcruise', 'node_modules/.bin/depcruise', ['--config', '.dependency-cruiser.js', 'packages/*/src']) + // Gate 3: dependency boundary + const g3 = runCheck('depcruise', join(repoRoot, 'node_modules/.bin/depcruise'), ['--config', join(repoRoot, '.dependency-cruiser.js'), 'packages/cli/src/', 'packages/contracts/src/', 'packages/llm/src/', 'packages/runtime/src/', 'packages/toolchain-cpp/src/', 'packages/tui/src/', 'packages/workers/src/'], repoRoot) if (g3) passed++; else failed++; // Summary @@ -38,18 +69,20 @@ export function releaseCommand(): void { } } -function runCheck(name: string, bin: string, args: string[]): boolean { +function runCheck(name: string, bin: string, args: string[], cwd: string): boolean { console.log(` ${name}...`) try { execFileSync(bin, args, { - cwd: process.cwd(), + cwd, stdio: 'pipe', - timeout: 120000 + timeout: 600000 }) console.log(` PASS`) return true } catch (e: any) { + const output = String(e.stdout || e.stderr || e.message || '').split('\n').slice(-20).join('\n') console.log(` FAIL`) + if (output.trim()) console.log(output) return false } } \ No newline at end of file diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 0d018c1..396b81c 100755 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -81,11 +81,18 @@ export async function runCommand(project_path?: string): Promise { project_id: app.project_id, classify_mode: 'regex', provider_manager: provider as any, + context_assembler: app.context_assembler, + project_root, + agent_id: 'main-agent' as any, classify_model: model }) const session_id = app.session_id + // Track task results for /results command + const taskResults = new Map; state: string }>() + let pendingConfirmation: string | undefined + console.log('') console.log('══════════════════════════════════════════════') console.log(' AirCoding v1.0.0-alpha') @@ -93,6 +100,89 @@ export async function runCommand(project_path?: string): Promise { console.log(' Type your task, or /help for commands, Ctrl+C to quit') console.log('══════════════════════════════════════════════\n') + const dispatchTask = async (input: string) => { + const taskId = `task_${randomUUID().slice(0, 8)}` + app.scheduler.create_tasks([{ + id: taskId, + type: 'execute', + title: input.slice(0, 80), + description: input + }]) + + console.log(`Task ${taskId} created. Dispatching worker...`) + const runPromise = app.scheduler.run_until_idle() + + let dots = 0 + const progressInterval = setInterval(() => { + dots = (dots + 1) % 4 + process.stdout.write(`\r Running${'.'.repeat(dots)} `) + }, 500) + + let finalState: string + try { + finalState = await runPromise + } finally { + clearInterval(progressInterval) + process.stdout.write('\r \r') + } + + console.log(`Task complete. Scheduler: ${finalState}`) + + const workerResult = app.worker_manager.get_result_for_task?.(taskId) + const resultFiles = Array.isArray(workerResult?.changed_files) ? workerResult.changed_files : [] + const recentFiles: Array<{ path: string; size: number }> = [] + if (resultFiles.length > 0) { + const { statSync: st, existsSync: ex } = await import('fs') + for (const file of resultFiles) { + if (!file || file.startsWith('.air/') || file.includes('/.air/')) continue + const full = join(project_root, file) + if (!ex(full)) continue + const s = st(full) + if (s.isFile()) recentFiles.push({ path: file, size: s.size }) + } + } else { + const { readdirSync: rd, statSync: st, existsSync: ex } = await import('fs') + const scanDir = (d: string, depth: number) => { + if (depth > 3 || !ex(d)) return + try { + for (const e of rd(d)) { + if (e.startsWith('.')) continue + const p = join(d, e) + try { + const s = st(p) + if (s.isDirectory()) scanDir(p, depth + 1) + else if (s.mtimeMs > Date.now() - 60000) recentFiles.push({ path: p.replace(project_root + '/', ''), size: s.size }) + } catch {} + } + } catch {} + } + scanDir(project_root, 0) + } + if (recentFiles.length > 0) { + console.log(' Produced files:') + for (const f of recentFiles.slice(0, 10)) { + console.log(` 📄 ${f.path} (${f.size}B)`) + } + } + + taskResults.set(taskId, { title: input.slice(0, 80), files: recentFiles, state: finalState }) + + runtime.projection_client.receive_snapshot({ + session_id, + project_id: app.project_id, + status: finalState === 'COMPLETED' ? 'completed' : 'running', + title: project_root.split('/').pop() || 'AirCoding', + tasks: [{ id: taskId as any, type: 'execute', status: finalState === 'COMPLETED' ? 'completed' : 'running', title: input.slice(0, 80), retry_count: 0, attempts: 1, created_at: new Date().toISOString() }], + agents: [], + tool_runs: [], + command_runs: [], + artifacts: [], + permission_prompts: [], + blockers: [], + updated_at: new Date().toISOString() + }) + } + // Interactive input loop const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' }) rl.prompt() @@ -101,9 +191,26 @@ export async function runCommand(project_path?: string): Promise { const input = line.trim() if (!input) { rl.prompt(); return } + if (pendingConfirmation) { + if (/^(y|yes|是|确认|确定)$/i.test(input)) { + const confirmedInput = pendingConfirmation + pendingConfirmation = undefined + await agent.handle_confirmation(true) + await dispatchTask(confirmedInput) + } else if (/^(n|no|否|取消)$/i.test(input)) { + pendingConfirmation = undefined + await agent.handle_confirmation(false) + console.log('Cancelled. No task was created.\n') + } else { + console.log('Please answer y/n to confirm or cancel the pending destructive request.\n') + } + rl.prompt() + return + } + // Handle slash commands if (input.startsWith('/')) { - await handleSlashCommand(input, app, runtime, tui, rl) + await handleSlashCommand(input, app, runtime, tui, rl, taskResults) rl.prompt() return } @@ -115,47 +222,12 @@ export async function runCommand(project_path?: string): Promise { if (classification.action === 'answer') { console.log('\n' + (classification.response || 'No response') + '\n') } else if (classification.action === 'delegate') { - // Create task and dispatch through Scheduler - const taskId = `task_${randomUUID().slice(0, 8)}` - app.scheduler.create_tasks([{ - id: taskId, - type: 'execute', - title: input.slice(0, 80), - description: input - }]) - - console.log(`Task ${taskId} created. Dispatching worker...`) - const runPromise = app.scheduler.run_until_idle() - - // Show progress while scheduler runs - let dots = 0 - const progressInterval = setInterval(() => { - dots = (dots + 1) % 4 - process.stdout.write(`\r Running${'.'.repeat(dots)} `) - }, 500) - - const finalState = await runPromise - clearInterval(progressInterval) - process.stdout.write('\r \r') - - console.log(`Task complete. Scheduler: ${finalState}`) - - // Refresh projection with task status - const tasks = app.scheduler.get_graph().count_by_status() - runtime.projection_client.receive_snapshot({ - session_id, - project_id: app.project_id, - status: finalState === 'COMPLETED' ? 'completed' : 'running', - title: project_root.split('/').pop() || 'AirCoding', - tasks: [{ id: taskId as any, type: 'execute', status: finalState === 'COMPLETED' ? 'completed' : 'running', title: input.slice(0, 80), retry_count: 0, attempts: 1, created_at: new Date().toISOString() }], - agents: [], - tool_runs: [], - command_runs: [], - artifacts: [], - permission_prompts: [], - blockers: [], - updated_at: new Date().toISOString() - }) + if (agent.state === 'CONFIRMING' && classification.response) { + pendingConfirmation = input + console.log('\n' + classification.response + '\n') + } else { + await dispatchTask(input) + } } else { console.log(`Result: ${classification.response || 'Done'}`) } @@ -177,17 +249,37 @@ export async function runCommand(project_path?: string): Promise { await new Promise(() => {}) // Wait forever } -async function handleSlashCommand(input: string, app: any, runtime: any, tui: any, rl: any): Promise { +async function handleSlashCommand(input: string, app: any, runtime: any, tui: any, rl: any, taskResults?: Map): Promise { const cmd = input.slice(1).toLowerCase() switch (cmd) { case 'help': console.log('\nCommands:') - console.log(' /help — Show this help') - console.log(' /status — Show scheduler and worker status') - console.log(' /tools — List registered tools') - console.log(' /tasks — Show task graph') - console.log(' /quit — Exit AirCoding\n') + console.log(' /help — Show this help') + console.log(' /status — Show scheduler and worker status') + console.log(' /tools — List registered tools') + console.log(' /tasks — Show task graph') + console.log(' /results — Show produced files from completed tasks') + console.log(' /quit — Exit AirCoding\n') + break + + case 'results': + if (!taskResults || taskResults.size === 0) { + console.log(' No task results yet. Submit a task first.\n') + } else { + console.log('') + for (const [taskId, result] of taskResults) { + console.log(` Task: ${result.title} [${result.state}]`) + if (result.files.length > 0) { + for (const f of result.files) { + console.log(` 📄 ${f.path} (${f.size}B)`) + } + } else { + console.log(' (no files produced)') + } + } + console.log('') + } break case 'status': diff --git a/packages/cli/test/run-command-regression.test.ts b/packages/cli/test/run-command-regression.test.ts new file mode 100755 index 0000000..9aa4c36 --- /dev/null +++ b/packages/cli/test/run-command-regression.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'bun:test' +import { readFileSync } from 'fs' +import { join } from 'path' + +describe('run command result presentation', () => { + const source = readFileSync(join(import.meta.dir, '../src/commands/run.ts'), 'utf-8') + + it('prefers WorkerResult.changed_files over recent filesystem scan', () => { + expect(source).toContain('get_result_for_task') + expect(source).toContain('workerResult?.changed_files') + }) + + it('filters .air internals from produced files', () => { + expect(source).toContain("file.startsWith('.air/')") + expect(source).toContain("e.startsWith('.')") + }) + + it('routes destructive confirmation before slash/main dispatch', () => { + expect(source).toContain('pendingConfirmation') + expect(source).toContain('handle_confirmation(true)') + expect(source).toContain('handle_confirmation(false)') + expect(source).toContain('No task was created') + }) +}) diff --git a/packages/runtime/src/agents/main/MainAgent.ts b/packages/runtime/src/agents/main/MainAgent.ts index eef40b6..c2215f7 100755 --- a/packages/runtime/src/agents/main/MainAgent.ts +++ b/packages/runtime/src/agents/main/MainAgent.ts @@ -8,7 +8,9 @@ * @module packages/runtime/src/agents/main/MainAgent */ -import type { SessionID, ProjectID } from '@aircoding/contracts' +import type { SessionID, ProjectID, AgentID, TaskID } from '@aircoding/contracts' +import type { ContextAssembler } from '../../context/ContextAssembler.js' +import { ArchitectureDesigner } from '../architecture/ArchitectureDesigner.js' export type MainAgentState = | 'IDLE' @@ -34,13 +36,23 @@ export interface MainAgentConfig { project_id: ProjectID classify_mode?: ClassifyMode // Alpha default: 'regex'; set to 'llm' to use LLM classification provider_manager?: any // ProviderManager for LLM-based classify - classify_model?: string // Model to use for LLM classification (e.g. 'claude-haiku-4-5') + context_assembler?: ContextAssembler + architecture_designer?: ArchitectureDesigner + project_root?: string + agent_id?: AgentID + task_id?: TaskID + classify_model?: string // Model to use for LLM classification and answer mode } export class MainAgent { private config: MainAgentConfig private classify_mode: ClassifyMode private provider_manager?: any + private context_assembler?: ContextAssembler + private architecture_designer: ArchitectureDesigner + private project_root: string + private agent_id: AgentID + private task_id?: TaskID private classify_model: string state: MainAgentState = 'IDLE' @@ -48,6 +60,11 @@ export class MainAgent { this.config = config this.classify_mode = config.classify_mode || 'regex' this.provider_manager = config.provider_manager + this.context_assembler = config.context_assembler + this.architecture_designer = config.architecture_designer || new ArchitectureDesigner() + this.project_root = config.project_root || process.cwd() + this.agent_id = config.agent_id || 'main-agent' as AgentID + this.task_id = config.task_id this.classify_model = config.classify_model || 'claude-haiku-4-5' } @@ -75,10 +92,19 @@ export class MainAgent { case 'implementation_request': case 'task_request': // Check if this request needs user confirmation (breaking/delete) - if (/break|delete|remove|drop|destroy|truncate/i.test(message)) { + if (/break|delete|remove|drop|destroy|truncate|rm\s|\bdel\b/i.test(message) || /删除|删掉|清除|移除|销毁/.test(message)) { this.state = 'CONFIRMING' return { action: 'delegate', response: 'This appears to be a breaking or destructive change. Are you sure you want to proceed? (y/n)' } } + const impact = this.architecture_designer.assess_impact({ description: message, files: this.infer_changed_files(message) }) + if (impact.result === 'reject_or_escalate' || impact.result === 'requires_replan') { + this.state = 'ARCHITECTURE_DESIGNING' + return { action: 'answer', response: `Architecture review required: ${impact.risks.join('; ') || impact.change_summary}` } + } + if (impact.result === 'requires_user_confirmation') { + this.state = 'CONFIRMING' + return { action: 'delegate', response: `Architecture impact requires confirmation: ${impact.risks.join('; ') || impact.change_summary}. Proceed? (y/n)` } + } this.state = 'DELEGATING' return { action: 'delegate', tasks: ['task-1'] } @@ -105,10 +131,25 @@ export class MainAgent { } try { - const messages = [ - { role: 'system', content: 'You are AirCoding, an AI coding assistant. Help the user with their coding tasks. Be concise and helpful.' }, - { role: 'user', content: user_message } - ] + const assembled = this.context_assembler?.assemble({ + session_id: this.config.session_id, + project_id: this.config.project_id, + project_root: this.project_root, + agent_id: this.agent_id, + agent_type: 'executor', + task_id: this.task_id, + token_budget: 200000, + }) + + const messages = assembled?.messages?.length + ? [ + ...assembled.messages, + { role: 'user', content: user_message } + ] + : [ + { role: 'system', content: 'You are AirCoding, an AI coding assistant. Help the user with their coding tasks. Be concise and helpful.' }, + { role: 'user', content: user_message } + ] const result = await this.provider_manager.complete_text(messages, { model: this.classify_model, @@ -182,9 +223,9 @@ export class MainAgent { ].join('\n') try { - const result = await this.provider_manager.complete( + const result = await this.provider_manager.complete_text( [{ role: 'user', content: classification_prompt }], - { model: this.classify_model } + { model: this.classify_model, max_tokens: 32 } ) const parsed = String(result.content || '').trim().toLowerCase() if (parsed === 'simple_question' || parsed === 'implementation_request' || parsed === 'direct_command') { @@ -198,6 +239,14 @@ export class MainAgent { } } + private infer_changed_files(message: string): string[] { + const files = Array.from(message.matchAll(/[\w./-]+\.(?:ts|tsx|js|jsx|json|md|cpp|c|h|hpp|cmake|txt|yaml|yml)/g)).map(m => m[0]) + if (/contract|协议|契约/i.test(message)) files.push('packages/contracts/src/index.ts') + if (/runtime|调度|scheduler|worker/i.test(message)) files.push('packages/runtime/src/index.ts') + if (/tui|hud|界面/i.test(message)) files.push('packages/tui/src/index.ts') + return [...new Set(files)] + } + /** * Handle confirmation from user. */ diff --git a/packages/runtime/src/app/RuntimeApp.ts b/packages/runtime/src/app/RuntimeApp.ts index 141eff5..16d1a98 100755 --- a/packages/runtime/src/app/RuntimeApp.ts +++ b/packages/runtime/src/app/RuntimeApp.ts @@ -24,6 +24,9 @@ import { EventBus } from '../events/EventBus.js' import { EventStore, eventStore } from '../events/EventStore.js' import { EventIngestorImpl } from '../events/EventIngestor.js' import { TaskRepository } from '../storage/repositories/TaskRepository.js' +import { MessageRepository } from '../storage/repositories/MessageRepository.js' +import { EvidenceRepository } from '../storage/repositories/EvidenceRepository.js' +import { createCapabilityRegistry, type CapabilityRegistry } from '../capabilities/CapabilityRegistry.js' export interface RuntimeAppConfig { project_root: string @@ -43,6 +46,7 @@ export class RuntimeApp { logger: Logger db: DatabaseManager tool_registry: ToolRegistry + capability_registry: CapabilityRegistry get session_id(): SessionID { return this.config.session_id } get project_id(): ProjectID { return this.config.project_id } @@ -64,9 +68,11 @@ export class RuntimeApp { // Core services this.tool_registry = createToolRegistry(config.project_root) + this.capability_registry = createCapabilityRegistry() + this.capability_registry.set_tool_registry(this.tool_registry) this.worker_manager = new WorkerManager(this.tool_registry) this.context_assembler = new ContextAssembler() - this.doctor = new DoctorService(config.project_root) + this.doctor = new DoctorService(config.project_root, this.capability_registry) this.projection_store = new ProjectionStore() this.projection_client = new ProjectionClient() this.event_bus = new EventBus() @@ -145,6 +151,9 @@ export class RuntimeApp { const raw_db = this.db.getRawDatabase() if (raw_db) { const task_repo = new TaskRepository(raw_db as any) + const message_repo = new MessageRepository(raw_db as any) + const evidence_repo = new EvidenceRepository(raw_db as any) + this.context_assembler.set_data_sources({ message_repo, evidence_store: evidence_repo }) this.scheduler.set_task_repo(task_repo) const rehydrated = await this.scheduler.rebuild_from_db() this.logger.info('Scheduler recovery complete', { rehydrated }) diff --git a/packages/runtime/src/app/ServiceRegistry.ts b/packages/runtime/src/app/ServiceRegistry.ts index c0f4577..018dc59 100755 --- a/packages/runtime/src/app/ServiceRegistry.ts +++ b/packages/runtime/src/app/ServiceRegistry.ts @@ -14,6 +14,7 @@ import { ProjectionStore } from '../projection/ProjectionStore.js' import { Logger } from '../logging/Logger.js' import { Scheduler } from '../scheduler/Scheduler.js' import { WorkerManager } from '../workers/WorkerManager.js' +import { createCapabilityRegistry, type CapabilityRegistry } from '../capabilities/CapabilityRegistry.js' export interface ServiceGraph { database: DatabaseManager @@ -21,6 +22,7 @@ export interface ServiceGraph { tool_registry: ToolRegistry context_assembler: ContextAssembler doctor: DoctorService + capability_registry: CapabilityRegistry projection_store: ProjectionStore logger: Logger scheduler: Scheduler | null @@ -38,11 +40,13 @@ export class ServiceRegistry { const database = new DatabaseManager(`${project_root}/.air/sessions/${session_id}.db`) const permission_engine = new PermissionEngine(project_root) const tool_registry = new ToolRegistry(project_root) + const capability_registry = createCapabilityRegistry() + capability_registry.set_tool_registry(tool_registry) const context_assembler = new ContextAssembler() - const doctor = new DoctorService(project_root) + const doctor = new DoctorService(project_root, capability_registry) const projection_store = new ProjectionStore() - const worker_manager = new WorkerManager() - const scheduler = new Scheduler({ session_id, project_id, project_root }) + const worker_manager = new WorkerManager(tool_registry) + const scheduler = new Scheduler({ session_id, project_id, project_root }, worker_manager) // Register all services this.services.set('database', database) @@ -50,6 +54,7 @@ export class ServiceRegistry { this.services.set('tool_registry', tool_registry) this.services.set('context_assembler', context_assembler) this.services.set('doctor', doctor) + this.services.set('capability_registry', capability_registry) this.services.set('projection_store', projection_store) this.services.set('logger', logger) this.services.set('scheduler', scheduler) @@ -57,7 +62,7 @@ export class ServiceRegistry { return { database, permission_engine, tool_registry, - context_assembler, doctor, projection_store, + context_assembler, doctor, capability_registry, projection_store, logger, scheduler, worker_manager } } diff --git a/packages/runtime/src/context/ContextAssembler.ts b/packages/runtime/src/context/ContextAssembler.ts index 261850e..5fac7e8 100755 --- a/packages/runtime/src/context/ContextAssembler.ts +++ b/packages/runtime/src/context/ContextAssembler.ts @@ -11,6 +11,8 @@ import type { SessionID, ProjectID, AgentID, TaskID, ArtifactID, ISOTimeString } from '@aircoding/contracts' +import { readdirSync, statSync } from 'fs' +import { join, relative } from 'path' import { PromptLayerLoader, createPromptLayerLoader } from './PromptLayerLoader.js' import { CompactionPolicy, createCompactionPolicy } from './CompactionPolicy.js' @@ -104,6 +106,32 @@ export class ContextAssembler { } } + private build_project_files_snapshot(project_root: string): string { + const files: string[] = [] + const ignored = new Set(['.air', '.git', 'node_modules', 'build', 'dist', '.claude']) + const walk = (dir: string, depth: number) => { + if (depth > 3 || files.length >= 200) return + let entries: string[] = [] + try { + entries = readdirSync(dir) + } catch { + return + } + for (const entry of entries) { + if (ignored.has(entry)) continue + const full = join(dir, entry) + try { + const stat = statSync(full) + if (stat.isDirectory()) walk(full, depth + 1) + else files.push(relative(project_root, full)) + } catch {} + if (files.length >= 200) return + } + } + walk(project_root, 0) + return ['# Project Files Snapshot (L3)', ...files.map(f => `- ${f}`)].join('\n') + } + /** * Collect all layers in order L0-L9. */ @@ -132,6 +160,13 @@ export class ContextAssembler { project_root: context.project_root }) layers.push(...project_rules) + layers.push({ + level: 'project_files' as any, + priority: 3, + content: this.build_project_files_snapshot(context.project_root), + token_estimate: 300, + source_ref: `project:${context.project_id}:files` + }) // L4: Architecture (if available) if (context.additional_layers) { @@ -162,7 +197,7 @@ export class ContextAssembler { let evidence_content = '' if (this.evidence_store && context.task_id) { try { - const records = this.evidence_store.list_for_entity?.(context.task_id) || [] + const records = this.evidence_store.list_for_entity?.('task_id', context.task_id) || [] if (records.length > 0) { evidence_content = records.map((r: any) => `- [${r.type || 'evidence'}] ${r.summary || r.id}`).join('\n') @@ -218,9 +253,9 @@ export class ContextAssembler { if (this.message_repo) { try { const msgs = this.message_repo.list_by_session?.(context.session_id) || [] - const tool_msgs = msgs.filter((m: any) => m.role === 'tool_result' || m.role === 'tool_use').slice(-10) + const tool_msgs = msgs.filter((m: any) => m.role === 'tool' || m.role === 'tool_result' || m.role === 'tool_use').slice(-10) tool_content = tool_msgs.map((m: any) => - `[${m.role}]: ${String(m.content_json || '').slice(0, 300)}`).join('\n') + `[${m.role}]: ${String(m.content_json || m.content || '').slice(0, 300)}`).join('\n') } catch { /* fall through */ } } layers.push({ @@ -248,12 +283,6 @@ export class ContextAssembler { }) } - // Add any additional layers - if (context.additional_layers) { - const others = context.additional_layers.filter(l => l.level !== 'architecture') - layers.push(...others) - } - return layers } @@ -265,7 +294,7 @@ export class ContextAssembler { // System message: L0 + L1 + L2 + L3 const system_content = layers - .filter(l => ['runtime_invariant', 'role', 'safety', 'project_rules'].includes(l.level)) + .filter(l => ['runtime_invariant', 'role', 'safety', 'project_rules', 'project_files'].includes(l.level)) .map(l => l.content) .join('\n\n---\n\n') diff --git a/packages/runtime/src/scheduler/Scheduler.ts b/packages/runtime/src/scheduler/Scheduler.ts index 144b82c..5e4582e 100755 --- a/packages/runtime/src/scheduler/Scheduler.ts +++ b/packages/runtime/src/scheduler/Scheduler.ts @@ -248,13 +248,79 @@ export class Scheduler { } } - // Workers complete → mark running tasks as completed - if (this.worker_manager && !this.worker_manager.has_running()) { - // Mark ALL running tasks as completed (not just runnable) - const all_tasks = Array.from(this.graph['tasks']?.values() || []) - for (const t of all_tasks) { - if ((t as any).status === 'running') { - this.graph.update_status((t as any).id, 'completed') + // Workers complete → consume explicit WorkerResult status + if (this.worker_manager) { + const running_tasks = this.graph.get_tasks_by_status('running') + for (const task of running_tasks) { + const handle = this.worker_manager.get_handle_for_task(task.id) + const result = this.worker_manager.get_result_for_task(task.id) + if (!handle || !result) continue + + const attempt_id = `${task.id}_1` + if (result.status === 'completed') { + await eventIngestor.ingest({ + id: `evt_${task.id}_completed`, + type: 'task.completed', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: new Date().toISOString(), + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { + task_id: task.id, + agent_id: handle.worker_id, + attempt_id, + worker_result_json: result, + summary: result.summary, + changed_files: result.changed_files, + evidence_refs: result.evidence_refs, + } + }) + this.graph.update_status(task.id, 'completed') + this.agent_monitor.remove(handle.worker_id) + } else if (result.status === 'blocked') { + await eventIngestor.ingest({ + id: `evt_${task.id}_blocked`, + type: 'task.blocked', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: new Date().toISOString(), + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { task_id: task.id, agent_id: handle.worker_id, reason: result.summary, blocker_kind: 'worker_blocked', evidence_refs: result.evidence_refs, suggested_next_step: 'Review worker blocker and retry with corrected plan' } + }) + this.graph.update_status(task.id, 'blocked') + this.agent_monitor.remove(handle.worker_id) + } else if (result.status === 'cancelled') { + await eventIngestor.ingest({ + id: `evt_${task.id}_cancelled_result`, + type: 'task.cancelled', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: new Date().toISOString(), + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { task_id: task.id, reason: result.summary, cancelled_by: handle.worker_id } + }) + this.graph.update_status(task.id, 'cancelled') + this.agent_monitor.remove(handle.worker_id) + } else { + await eventIngestor.ingest({ + id: `evt_${task.id}_failed_result`, + type: 'task.failed', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: new Date().toISOString(), + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { task_id: task.id, agent_id: handle.worker_id, attempt_id, error: { message: result.summary }, evidence_refs: result.evidence_refs, metadata: { worker_status: result.status } } + }) + this.graph.update_status(task.id, 'failed') + this.agent_monitor.remove(handle.worker_id) } } } diff --git a/packages/runtime/src/tools/BuiltInToolRegistrar.ts b/packages/runtime/src/tools/BuiltInToolRegistrar.ts index 08c367d..7b91bba 100755 --- a/packages/runtime/src/tools/BuiltInToolRegistrar.ts +++ b/packages/runtime/src/tools/BuiltInToolRegistrar.ts @@ -178,7 +178,7 @@ export class BuiltInToolRegistrar { const { path } = call.arguments as { path: string } const s = statSync(resolve(project_root, path)) return { status: "ok", call_id: call.call_id, tool_name: 'fs.stat', type: 'text', - content: { path, exists: true, size: s.size, is_dir: s.isDirectory(), is_file: s.isFile(), + output: { path, exists: true, size: s.size, is_dir: s.isDirectory(), is_file: s.isFile(), mode: s.mode, mtime: s.mtime.toISOString(), ctime: s.ctime.toISOString() }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'fs.stat', type: 'ok' } } } catch (e: any) { @@ -192,7 +192,7 @@ export class BuiltInToolRegistrar { const { pid, signal = 'SIGTERM' } = call.arguments as { pid: number; signal?: string } process.kill(pid, signal as NodeJS.Signals) return { status: "ok", call_id: call.call_id, tool_name: 'process.kill', type: 'text', - content: { pid, signal, killed: true }, + output: { pid, signal, killed: true }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name: 'process.kill', type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: 'process.kill' }, @@ -204,7 +204,7 @@ export class BuiltInToolRegistrar { try { const { path, base_ref = 'HEAD' } = call.arguments as { path: string; base_ref?: string } execFileSync('git', ['worktree', 'add', path, base_ref], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' }) - return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { path, base_ref, created: true }, + return { status: "ok", call_id: call.call_id, tool_name, type: 'text', output: { path, base_ref, created: true }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -216,7 +216,7 @@ export class BuiltInToolRegistrar { try { const { workspace_id } = call.arguments as { workspace_id: string; strategy?: string } execFileSync('git', ['merge', '--no-ff', workspace_id], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8' }) - return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { workspace_id, merged: true }, + return { status: "ok", call_id: call.call_id, tool_name, type: 'text', output: { workspace_id, merged: true }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -235,7 +235,7 @@ export class BuiltInToolRegistrar { by_ext[ext] = (by_ext[ext] || 0) + 1 } return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { root: dir, total_files: entries.length, extensions: by_ext }, + output: { root: dir, total_files: entries.length, extensions: by_ext }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -249,7 +249,7 @@ export class BuiltInToolRegistrar { const dir = join(project_root, '.air', 'shared', 'profiles') if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) writeFileSync(join(dir, `${language}.json`), JSON.stringify(profile_json, null, 2)) - return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { language, written: true }, + return { status: "ok", call_id: call.call_id, tool_name, type: 'text', output: { language, written: true }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -263,7 +263,7 @@ export class BuiltInToolRegistrar { const cmake = existsSync(join(root, 'CMakeLists.txt')) const makefile = existsSync(join(root, 'Makefile')) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { has_cmake: cmake, has_makefile: makefile, build_system: cmake ? 'cmake' : makefile ? 'make' : 'none' }, + output: { has_cmake: cmake, has_makefile: makefile, build_system: cmake ? 'cmake' : makefile ? 'make' : 'none' }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -277,7 +277,7 @@ export class BuiltInToolRegistrar { const buildDir = join(project_root, 'build') if (!existsSync(buildDir)) mkdirSync(buildDir, { recursive: true }) execFileSync('cmake', ['-G', generator, '-DCMAKE_BUILD_TYPE=' + build_type, '..'], { cwd: buildDir, stdio: 'pipe' }) - return { status: "ok", call_id: call.call_id, tool_name, type: 'text', content: { generator, build_type, configured: true }, + return { status: "ok", call_id: call.call_id, tool_name, type: 'text', output: { generator, build_type, configured: true }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -291,7 +291,7 @@ export class BuiltInToolRegistrar { const args = target ? ['--build', '.', '--config', config, '--target', target] : ['--build', '.', '--config', config] const out = execFileSync('cmake', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { built: true, output: out.toString().slice(-500) }, + output: { built: true, output: out.toString().slice(-500) }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -305,7 +305,7 @@ export class BuiltInToolRegistrar { const args = filter ? ['--output-on-failure', '-R', filter] : ['--output-on-failure'] const out = execFileSync('ctest', args, { cwd: join(project_root, 'build'), stdio: 'pipe', timeout: 300000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { passed: true, output: out.toString().slice(-1000) }, + output: { passed: true, output: out.toString().slice(-1000) }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -318,7 +318,7 @@ export class BuiltInToolRegistrar { const { path = 'src' } = (call.arguments || {}) as any const out = execFileSync('cppcheck', ['--enable=all', '--quiet', path], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 120000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { output: out.toString().slice(-500), issues_found: 0 }, + output: { output: out.toString().slice(-500), issues_found: 0 }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -331,7 +331,7 @@ export class BuiltInToolRegistrar { const { file, line = 0, column = 0 } = (call.arguments || {}) as any const out = execFileSync('clangd', ['--check=' + file], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 30000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { file, line, column, diagnostics: out.toString().slice(-1000) }, + output: { file, line, column, diagnostics: out.toString().slice(-1000) }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -344,7 +344,7 @@ export class BuiltInToolRegistrar { const { target } = (call.arguments || {}) as any const out = execFileSync('gdb', ['-batch', '-ex', 'run', '-ex', 'bt', '--', target], { cwd: project_root, stdio: 'pipe', encoding: 'utf-8', timeout: 60000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { target, backtrace: out.toString().slice(-2000) }, + output: { target, backtrace: out.toString().slice(-2000) }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -358,7 +358,7 @@ export class BuiltInToolRegistrar { const content = readFileSync(resolve(project_root, log_path), 'utf-8') const errors = content.split('\n').filter(l => /error|fail|segfault|assert|abort|exception/i.test(l)).slice(0, 50) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { log_path, error_count: errors.length, errors }, + output: { log_path, error_count: errors.length, errors }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -373,7 +373,7 @@ export class BuiltInToolRegistrar { const tmpFile = join(tmpDir, `screenshot-${Date.now()}.png`) execFileSync('import', ['-window', 'root', tmpFile], { stdio: 'pipe', timeout: 10000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { captured: true, path: tmpFile }, + output: { captured: true, path: tmpFile }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: `Screenshot not available: ${e.message}`, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -388,7 +388,7 @@ export class BuiltInToolRegistrar { if (filter) args.push(filter) const out = execFileSync('tcpdump', args, { stdio: 'pipe', timeout: (duration_sec + 5) * 1000 }) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { interface: iface, duration_sec, packets: (String(out) || '').split('\n').length }, + output: { interface: iface, duration_sec, packets: (String(out) || '').split('\n').length }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: `Capture not available: ${e.message}`, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -399,7 +399,7 @@ export class BuiltInToolRegistrar { 'permission.request': async (call: any) => { const { tool_name: tn, reason } = call.arguments as { tool_name: string; reason: string } return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { tool_name: tn, reason, status: 'allowed', message: `Permission granted for ${tn}: ${reason}` }, + output: { tool_name: tn, reason, status: 'allowed', message: `Permission granted for ${tn}: ${reason}` }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } }, @@ -416,7 +416,7 @@ export class BuiltInToolRegistrar { checks.push({ name: 'project_structure', passed: hasPkg, message: hasPkg ? 'Valid' : 'No package.json' }) const allPassed = checks.every(c => c.passed) return { status: "ok", call_id: call.call_id, tool_name, type: 'text', - content: { checks, all_passed: allPassed, fixable_count: checks.filter(c => !c.passed).length }, + output: { checks, all_passed: allPassed, fixable_count: checks.filter(c => !c.passed).length }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } } } catch (e: any) { return { status: 'error', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message, retryability: 'not_retryable', semantic_signature: tool_name }, @@ -432,7 +432,7 @@ export class BuiltInToolRegistrar { call_id: call.call_id, tool_name, type: 'text', - content: { message: `Tool ${tool_name} not yet implemented` }, + output: { message: `Tool ${tool_name} not yet implemented` }, metadata: { timestamp: new Date().toISOString(), call_id: call.call_id, tool_name, type: 'ok' } }) } diff --git a/packages/runtime/src/tools/ToolRegistry.ts b/packages/runtime/src/tools/ToolRegistry.ts index 99fdf65..5c7905b 100755 --- a/packages/runtime/src/tools/ToolRegistry.ts +++ b/packages/runtime/src/tools/ToolRegistry.ts @@ -12,8 +12,13 @@ import type { ToolDefinition, ToolCall, ToolResultEnvelope, ISOTimeString } from import { PermissionEngine, createPermissionEngine, type PermissionContext, type PermissionDecision, type PermissionAction } from '../security/PermissionEngine.js' import type { AgentType } from '@aircoding/contracts' +export type ToolExecutionReturn = + | ToolResultEnvelope + | Promise + | AsyncIterable + export interface ToolExecutor { - (call: ToolCall, context: ToolExecutionContext): Promise + (call: ToolCall, context: ToolExecutionContext): ToolExecutionReturn } export interface ToolExecutionContext { @@ -140,28 +145,19 @@ export class ToolRegistry { const permission_context = this.build_permission_context(call, context) const decision = await this.permission_engine.evaluate(call, permission_context, definition) - if (decision.action !== 'allow') { + if (decision.action !== 'allow' && decision.action !== 'announce_then_run') { yield create_error_result(call.call_id, 'permission_denied', decision.reason) return } - // Execute with streaming support - // The executor yields intermediate results, final result comes at end - let final_result: ToolResultEnvelope | undefined + let saw_final = false for await (const chunk of this.execute_streaming(call, context, executor)) { - // Streaming signal: final result is the one with status='ok' whose metadata marks final - if (chunk.metadata && (chunk.metadata as any).is_final === true) { - final_result = chunk - } else { - yield chunk - } + if (chunk.metadata && (chunk.metadata as any).is_final === true) saw_final = true + yield chunk } - // Yield final result exactly once - if (final_result) { - yield final_result - } else { + if (!saw_final) { yield create_error_result(call.call_id, 'no_final_result', 'Streaming tool did not produce final result') } } @@ -251,7 +247,7 @@ export class ToolRegistry { if (!executor) { return create_error_result(call.call_id, 'executor_not_found', 'Executor not registered') } - return executor(call, ctx) + return this.execute_executor_final(executor, call, ctx) } case 'announce_then_run': { @@ -260,7 +256,7 @@ export class ToolRegistry { if (!executor) { return create_error_result(call.call_id, 'executor_not_found', 'Executor not registered') } - const result = await executor(call, ctx) + const result = await this.execute_executor_final(executor, call, ctx) return { ...result, metadata: { ...result.metadata, announced: true }, @@ -269,10 +265,10 @@ export class ToolRegistry { case 'ask_user': // Suspend; emit permission.prompt.requested - return create_error_result('', 'user_prompt_required', 'User confirmation required') + return create_error_result(call.call_id, 'user_prompt_required', 'User confirmation required') case 'deny': - return create_error_result('', 'permission_denied', decision.reason) + return create_error_result(call.call_id, 'permission_denied', decision.reason) case 'block': { // Return blocked outcome → task.blocked upstream @@ -289,6 +285,34 @@ export class ToolRegistry { } } + /** + * Execute a tool and return the final envelope. + * Streaming executors are consumed until their final result. + */ + private async execute_executor_final( + executor: ToolExecutor, + call: ToolCall, + context: ToolExecutionContext, + ): Promise { + const result = executor(call, context) + if (this.is_async_iterable(result)) { + let final_result: ToolResultEnvelope | undefined + let last_chunk: ToolResultEnvelope | undefined + for await (const chunk of result) { + last_chunk = chunk + if (chunk.metadata && (chunk.metadata as any).is_final === true) { + final_result = chunk + } + } + return final_result || last_chunk || create_error_result(call.call_id, 'no_result', 'Tool produced no result') + } + return await result + } + + private is_async_iterable(value: unknown): value is AsyncIterable { + return Boolean(value && typeof (value as any)[Symbol.asyncIterator] === 'function') + } + /** * Execute streaming tool. */ @@ -297,10 +321,12 @@ export class ToolRegistry { context: ToolExecutionContext, executor: ToolExecutor ): AsyncGenerator { - // Tool-specific execution handler - // For now, just execute normally - const result = await executor(call, context) - yield result + const result = executor(call, context) + if (this.is_async_iterable(result)) { + for await (const chunk of result) yield chunk + return + } + yield await result } } diff --git a/packages/runtime/src/tools/shell/index.ts b/packages/runtime/src/tools/shell/index.ts index c1c3d5b..3d96547 100755 --- a/packages/runtime/src/tools/shell/index.ts +++ b/packages/runtime/src/tools/shell/index.ts @@ -43,14 +43,12 @@ export function createShellExecutor(project_root: string) { const cwd = workdir || project_root const timestamp = new Date().toISOString() as ISOTimeString - // Emit command.started event yield { status: 'ok', output: { event: 'command.started', command, cwd }, metadata: { timestamp, streaming: true, call_id: call.call_id, tool_name: 'shell.run' } } - // Execute command const proc = spawn(command, [], { cwd, shell: true, @@ -59,53 +57,81 @@ export function createShellExecutor(project_root: string) { let stdout = '' let stderr = '' - let final_code = 0 + let timed_out = false + const chunks: ToolResultEnvelope[] = [] - // Stream stdout proc.stdout.on('data', (data) => { const text = data.toString() stdout += text - // Emit streaming stdout - // Note: In actual implementation, this would go through EventBus + chunks.push({ + status: 'ok', + output: { event: 'command.stdout', text }, + metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: true, call_id: call.call_id, tool_name: 'shell.run' } + }) }) - // Stream stderr proc.stderr.on('data', (data) => { const text = data.toString() stderr += text + chunks.push({ + status: 'ok', + output: { event: 'command.stderr', text }, + metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: true, call_id: call.call_id, tool_name: 'shell.run' } + }) }) - // Wait for completion or timeout - let timed_out = false - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => { - timed_out = true - proc.kill('SIGKILL') - resolve(124) // standard timeout exit code - }, timeout) + const timeout_id = setTimeout(() => { + timed_out = true + proc.kill('SIGKILL') + }, timeout) + + const exit_code = await new Promise((resolve) => { + proc.on('exit', (code) => resolve(code ?? 0)) + proc.on('error', () => resolve(1)) }) + clearTimeout(timeout_id) - const exitCode = await Promise.race([ - new Promise((resolve) => proc.on('exit', (code) => resolve(code || 0))), - timeoutPromise - ]) + if (stdout) { + yield { + status: 'ok', + output: { event: 'command.stdout', text: stdout.slice(-50000) }, + metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: true, call_id: call.call_id, tool_name: 'shell.run' } + } + } + if (stderr) { + yield { + status: 'ok', + output: { event: 'command.stderr', text: stderr.slice(-10000) }, + metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: true, call_id: call.call_id, tool_name: 'shell.run' } + } + } + + while (chunks.length > 0) { + yield chunks.shift()! + } - final_code = exitCode if (timed_out) { stderr += `\n[Command timed out after ${timeout}ms]` } - // Emit command.completed event yield { - status: final_code === 0 ? 'ok' : 'error', + status: exit_code === 0 ? 'ok' : 'error', output: { event: 'command.completed', - exit_code: final_code, - stdout: stdout.slice(-50000), // Last 50KB - stderr: stderr.slice(-10000), // Last 10KB + exit_code, + stdout: stdout.slice(-50000), + stderr: stderr.slice(-10000), timed_out }, - metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: false } + error: exit_code === 0 ? undefined : { + error_id: call.call_id, + kind: 'tool_error', + severity: 'error', + message: timed_out ? `Command timed out after ${timeout}ms` : `Command exited with code ${exit_code}`, + retryability: timed_out ? 'retryable' : 'not_retryable', + semantic_signature: 'shell.run' + }, + metadata: { timestamp: new Date().toISOString() as ISOTimeString, streaming: false, is_final: true, call_id: call.call_id, tool_name: 'shell.run' } } } } diff --git a/packages/runtime/src/workers/WorkerManager.ts b/packages/runtime/src/workers/WorkerManager.ts index d1a8993..0102e3b 100755 --- a/packages/runtime/src/workers/WorkerManager.ts +++ b/packages/runtime/src/workers/WorkerManager.ts @@ -31,7 +31,7 @@ export interface WorkerHandle { worker_id: string process: WorkerProcess config: WorkerConfig - state: 'starting' | 'ready' | 'running' | 'completed' | 'error' | 'cancelled' + state: 'starting' | 'ready' | 'running' | 'completed' | 'failed' | 'error' | 'cancelled' started_at: string completed_at?: string result?: WorkerResult @@ -107,6 +107,8 @@ export class WorkerManager { proc.set_process(child) + proc.on_exit((exit) => this.handle_worker_exit(config.agent_id, exit)) + // Set up handlers for worker IPC messages this.setup_worker_handlers(proc, config.agent_id) @@ -179,10 +181,10 @@ export class WorkerManager { { session_id: this.execution_context?.session_id || msg.session_id, project_id: this.execution_context?.project_id || '', + project_root: this.execution_context?.project_root || process.cwd(), agent_id, - permission_template: 'executor', - cwd: this.execution_context?.project_root || process.cwd() - } as any + agent_type: 'executor', + } ) this.send_to_worker(agent_id, 'tool.result', { @@ -238,8 +240,11 @@ export class WorkerManager { proc.on_message('worker.result', (msg) => { const handle = this.workers.get(agent_id) if (handle) { - handle.state = 'completed' handle.result = this.wrap_worker_result(msg.payload, handle) + handle.state = handle.result.status === 'completed' ? 'completed' + : handle.result.status === 'cancelled' ? 'cancelled' + : 'failed' + handle.completed_at = new Date().toISOString() } }) @@ -313,29 +318,84 @@ export class WorkerManager { return handle.result } + /** + * Get result for a task. + */ + get_result_for_task(task_id: string): WorkerResult | undefined { + return this.list().find(w => w.config.task_spec?.id === task_id)?.result + } + + /** + * Get handle for a task. + */ + get_handle_for_task(task_id: string): WorkerHandle | undefined { + return this.list().find(w => w.config.task_spec?.id === task_id) + } + // ============================================================================ // Private // ============================================================================ + private handle_worker_exit(agent_id: string, exit: { code: number | null; signal: NodeJS.Signals | null; semantic: string; description: string }): void { + const handle = this.workers.get(agent_id) + if (!handle) return + if (handle.result) return + + const task_id = (handle.config.task_spec?.id as string) || `${agent_id}_task` + const cancelled = exit.semantic === 'parent_cancelled' + handle.state = cancelled ? 'cancelled' : 'failed' + handle.completed_at = new Date().toISOString() + handle.result = { + task_id: task_id as any, + agent_id: handle.config.agent_id as any, + agent_type: 'executor', + status: cancelled ? 'cancelled' : 'failed', + summary: `Worker exited without result: ${exit.semantic}`, + changed_files: [], + artifacts: [], + verification: [], + risks: [], + follow_up_tasks: [], + evidence_refs: [], + result: { exit }, + } + } + /** * Wrap a raw worker payload into a properly typed WorkerResult envelope. * Provides safe defaults for any missing fields. */ private wrap_worker_result(payload: Record, handle: WorkerHandle): WorkerResult { + const raw_status = (payload.status as string) || 'completed' + const status = raw_status === 'completed' || raw_status === 'cancelled' || raw_status === 'blocked' || raw_status === 'failed' + ? raw_status + : raw_status === 'fixed' || raw_status === 'pass' + ? 'completed' + : 'failed' + const changes = Array.isArray((payload as any).changes) ? (payload as any).changes : [] + const changed_files = (payload.changed_files as string[] | undefined) || changes.map((c: any) => String(c.file)).filter(Boolean) + const verification_payload = payload.verification as any + const verification = Array.isArray(verification_payload) ? verification_payload + : verification_payload ? [{ command: 'worker verification', passed: Boolean(verification_payload.passed), output: String(verification_payload.output || '') }] as any[] + : [] + const summary = (payload.summary as string) + || (payload.error ? String(payload.error) : '') + || (changed_files.length > 0 ? `Changed files: ${changed_files.join(', ')}` : `Worker ${status}`) + return { - task_id: (payload.task_id as string) || '' as any, + task_id: (payload.task_id as string) || (handle.config.task_spec?.id as string) || '' as any, agent_id: (payload.agent_id as string) || handle.config.agent_id as any, agent_type: (payload.agent_type as AgentType) || 'executor', - status: (payload.status as WorkerStatus) || 'completed', - summary: (payload.summary as string) || '', - changed_files: (payload.changed_files as string[]) || [], + status: status as WorkerStatus, + summary, + changed_files, diff_ref: (payload.diff_ref as string | undefined) || undefined, artifacts: (payload.artifacts as any[]) || [], - verification: (payload.verification as any[]) || [], + verification, risks: (payload.risks as any[]) || [], follow_up_tasks: (payload.follow_up_tasks as any[]) || [], evidence_refs: (payload.evidence_refs as any[]) || [], - result: (payload.result as unknown) || null, + result: (payload.result as unknown) || payload, } } diff --git a/packages/runtime/src/workers/WorkerProcess.ts b/packages/runtime/src/workers/WorkerProcess.ts index 00dd6ff..fd07ef1 100755 --- a/packages/runtime/src/workers/WorkerProcess.ts +++ b/packages/runtime/src/workers/WorkerProcess.ts @@ -35,6 +35,7 @@ export class WorkerProcess { private proc: ChildProcess | null = null private protocol: WorkerProtocol private message_handlers: Map void> = new Map() + private exit_handlers: Array<(info: { code: number | null; signal: NodeJS.Signals | null; semantic: string; description: string }) => void> = [] private buffer: string = '' constructor() { @@ -68,6 +69,13 @@ export class WorkerProcess { this.message_handlers.set(type, handler) } + /** + * Register an exit handler. + */ + on_exit(handler: (info: { code: number | null; signal: NodeJS.Signals | null; semantic: string; description: string }) => void): void { + this.exit_handlers.push(handler) + } + /** * Get exit code info. */ @@ -124,8 +132,13 @@ export class WorkerProcess { // Exit handler this.proc.on('exit', (code, signal) => { - const info = this.get_exit_code_info(code || 1) - console.log(`[Worker] exited with code ${code} (${info?.semantic || 'unknown'}): ${info?.description || ''}`) + const info = this.get_exit_code_info(code ?? 1) + const semantic = info?.semantic || 'unknown' + const description = info?.description || '' + console.log(`[Worker] exited with code ${code} (${semantic}): ${description}`) + for (const handler of this.exit_handlers) { + handler({ code, signal: signal as NodeJS.Signals | null, semantic, description }) + } }) } diff --git a/packages/runtime/test/regression/release-critical-gates.test.ts b/packages/runtime/test/regression/release-critical-gates.test.ts new file mode 100755 index 0000000..8e15199 --- /dev/null +++ b/packages/runtime/test/regression/release-critical-gates.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'bun:test' +import { mkdtempSync, writeFileSync, existsSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { ToolRegistry } from '../../src/tools/ToolRegistry.js' +import { BuiltInToolRegistrar } from '../../src/tools/BuiltInToolRegistrar.js' +import { Scheduler } from '../../src/scheduler/Scheduler.js' +import { MainAgent } from '../../src/agents/main/MainAgent.js' +import { ContextAssembler } from '../../src/context/ContextAssembler.js' + +function createRegistry(projectRoot: string): ToolRegistry { + const registry = new ToolRegistry(projectRoot) + new BuiltInToolRegistrar(registry).register_all(projectRoot) + return registry +} + +describe('Release critical gates', () => { + it('built-in tool success envelopes use output, not content', async () => { + const projectRoot = mkdtempSync(join(tmpdir(), 'air-tool-envelope-')) + writeFileSync(join(projectRoot, 'sample.txt'), 'hello') + const registry = createRegistry(projectRoot) + const ctx = { session_id: 's', project_id: 'p', project_root: projectRoot, agent_id: 'a', agent_type: 'executor' as const } + + for (const [name, args] of [ + ['fs.stat', { path: 'sample.txt' }], + ['project.scan', { root: '.' }], + ['cpp.detect', { project_root: projectRoot }], + ['doctor.run', { scope: 'all' }], + ] as Array<[string, Record]>) { + const result = await registry.call({ call_id: `call-${name}`, name, arguments: args }, ctx) + expect(result.status).toBe('ok') + expect(result.output).toBeDefined() + expect((result as any).content).toBeUndefined() + } + }) + + it('shell.run returns a final envelope through call and streaming APIs', async () => { + const projectRoot = mkdtempSync(join(tmpdir(), 'air-shell-')) + const registry = createRegistry(projectRoot) + const ctx = { session_id: 's', project_id: 'p', project_root: projectRoot, agent_id: 'a', agent_type: 'executor' as const } + + const final = await registry.call({ call_id: 'shell-call', name: 'shell.run', arguments: { command: 'printf ok' } }, ctx) + expect(final.status).toBe('ok') + expect((final.output as any).exit_code).toBe(0) + expect((final.output as any).stdout).toBe('ok') + expect((final.metadata as any).is_final).toBe(true) + + const chunks = [] + for await (const chunk of registry.call_streaming({ call_id: 'shell-stream', name: 'shell.run', arguments: { command: 'printf ok' } }, ctx)) { + chunks.push(chunk) + } + expect(chunks.length).toBeGreaterThanOrEqual(2) + expect((chunks.at(-1)!.metadata as any).is_final).toBe(true) + }) + + it('scheduler does not mark running tasks completed without a worker result', async () => { + const workerManager = { + has_running: () => false, + get_handle_for_task: () => undefined, + get_result_for_task: () => undefined, + } + const scheduler = new Scheduler({ session_id: 's' as any, project_id: 'p' as any, project_root: process.cwd() }, workerManager as any) + scheduler.create_tasks([{ id: 'task-1' as any, type: 'execute', title: 'Task' }]) + scheduler.get_graph().update_status('task-1' as any, 'running') + + await scheduler.step() + expect(scheduler.get_graph().get_tasks_by_status('running').length).toBe(1) + expect(scheduler.get_graph().get_tasks_by_status('completed').length).toBe(0) + }) + + it('MainAgent answer mode uses assembled project context', async () => { + const projectRoot = mkdtempSync(join(tmpdir(), 'air-context-')) + writeFileSync(join(projectRoot, 'visible.txt'), 'visible') + const assembler = new ContextAssembler() + const provider = { + async complete_text(messages: Array<{ role: string; content: string }>) { + const joined = messages.map(m => m.content).join('\n') + return { content: joined.includes('visible.txt') || joined.includes('Project') ? 'context seen' : 'missing context' } + } + } + const agent = new MainAgent({ + session_id: 's' as any, + project_id: 'p' as any, + provider_manager: provider, + context_assembler: assembler, + project_root: projectRoot, + agent_id: 'main-agent' as any, + }) + + const result = await agent.handle_user_message('what files are in this project?') + expect(result.action).toBe('answer') + expect(result.response).toBe('context seen') + }) + + it('destructive requests enter confirmation and rejection returns to idle', async () => { + const agent = new MainAgent({ session_id: 's' as any, project_id: 'p' as any }) + const result = await agent.handle_user_message('delete hello.txt') + expect(result.action).toBe('delegate') + expect(agent.state).toBe('CONFIRMING') + await agent.handle_confirmation(false) + expect(agent.state).toBe('IDLE') + }) +}) diff --git a/packages/runtime/test/regression/tool-registry-permission.test.ts b/packages/runtime/test/regression/tool-registry-permission.test.ts index 9cc7b5f..8217eb0 100755 --- a/packages/runtime/test/regression/tool-registry-permission.test.ts +++ b/packages/runtime/test/regression/tool-registry-permission.test.ts @@ -49,4 +49,11 @@ describe('A5+A4: ToolRegistry permission fixes', () => { expect(build_match![0]).toContain('context.permission_profile') expect(build_match![0]).not.toMatch(/permission_profile:\s*undefined/) }) + + it('permission denial branches preserve original call_id', () => { + expect(src).toContain("create_error_result(call.call_id, 'user_prompt_required'") + expect(src).toContain("create_error_result(call.call_id, 'permission_denied'") + expect(src).not.toContain("create_error_result('', 'user_prompt_required'") + expect(src).not.toContain("create_error_result('', 'permission_denied'") + }) }) diff --git a/packages/runtime/test/regression/worker-result-envelope.test.ts b/packages/runtime/test/regression/worker-result-envelope.test.ts index d0b65e7..d3ed2db 100755 --- a/packages/runtime/test/regression/worker-result-envelope.test.ts +++ b/packages/runtime/test/regression/worker-result-envelope.test.ts @@ -44,17 +44,13 @@ describe('D3: Worker result envelope', () => { expect(source).toContain('AgentType') }) - it('wrap_worker_result returns WorkerResult with safe defaults', () => { - // Verify safe defaults for key fields + it('wrap_worker_result maps role results into WorkerResult with safe defaults', () => { expect(source).toContain("agent_type: (payload.agent_type as AgentType) || 'executor'") - expect(source).toContain("status: (payload.status as WorkerStatus) || 'completed'") - expect(source).toContain("summary: (payload.summary as string) || ''") - expect(source).toContain('changed_files: (payload.changed_files as string[]) || []') - expect(source).toContain('artifacts: (payload.artifacts as any[]) || []') - expect(source).toContain('verification: (payload.verification as any[]) || []') - expect(source).toContain('risks: (payload.risks as any[]) || []') - expect(source).toContain('follow_up_tasks: (payload.follow_up_tasks as any[]) || []') - expect(source).toContain('evidence_refs: (payload.evidence_refs as any[]) || []') + expect(source).toContain("const raw_status = (payload.status as string) || 'completed'") + expect(source).toContain("raw_status === 'fixed' || raw_status === 'pass'") + expect(source).toContain('changes.map((c: any) => String(c.file))') + expect(source).toContain("verification_payload ? [{ command: 'worker verification'") + expect(source).toContain('result: (payload.result as unknown) || payload') }) it('get_result returns undefined for unknown agent', () => { diff --git a/packages/workers/src/roles/ExecutorRole.ts b/packages/workers/src/roles/ExecutorRole.ts index cff23e9..2dbf4cb 100755 --- a/packages/workers/src/roles/ExecutorRole.ts +++ b/packages/workers/src/roles/ExecutorRole.ts @@ -133,13 +133,17 @@ After completing ALL required files, write: DONE` } } - // After executing, check if LLM indicated completion - if (text.includes('DONE') || text.includes('TASK_COMPLETE')) { + if (this.is_done_signal(text)) { + if (!allSucceeded) { + messages.push({ role: 'assistant', content: text }) + messages.push({ role: 'user', content: 'You signaled DONE, but one or more tool actions failed. Fix the failed actions before signaling DONE.' }) + continue + } await this.runtime.checkpoint('task_completed', { task_id: task_spec.id }) return { status: 'completed', changes, - verification: { passed: allSucceeded, output: `${changes.length} files: ${changes.map(c => c.file).join(', ')}` }, + verification: { passed: true, output: `${changes.length} files: ${changes.map(c => c.file).join(', ')}` }, evidence_refs: [] } } @@ -153,7 +157,7 @@ After completing ALL required files, write: DONE` }) } else { // No code blocks, no tool calls — LLM is just talking - if (text.includes('DONE') || text.includes('TASK_COMPLETE')) { + if (this.is_done_signal(text)) { if (changes.length === 0) { messages.push({ role: 'assistant', content: text }) messages.push({ role: 'user', content: 'You said DONE but no files were created. Please create the required files first.' }) @@ -189,6 +193,13 @@ After completing ALL required files, write: DONE` } } + private is_done_signal(text: string): boolean { + return text + .split(/\r?\n/) + .map(line => line.trim()) + .some(line => line === 'DONE' || line === 'TASK_COMPLETE') + } + /** * Parse ALL actions from LLM response: code blocks and tool calls. */ @@ -219,7 +230,7 @@ After completing ALL required files, write: DONE` filename = extMap[lang] || `${lang}_output.${lang === 'cmake' ? 'txt' : lang}` } - actions.push({ type: 'code_block', filename, content: match[4].trim() }) + actions.push({ type: 'code_block', filename, content: match[4] }) } // ── Pattern 2: Explicit tool calls ── diff --git a/状态交接.md b/状态交接.md new file mode 100755 index 0000000..5b128f8 --- /dev/null +++ b/状态交接.md @@ -0,0 +1,230 @@ +# AirCoding 集成修复第一轮 - 状态交接 + +> 用于在另一台设备继续。包含上下文、真实问题、修复方案、验证结果、剩余事项和复现命令。 + +## 1. 项目与远端 + +- 仓库根:`/home/airlongdian/DataDevices/AirWorkSpace/AirCoding` +- 仓库地址:`http://admin:L753865h@39.106.174.106/admin/AirCoding.git` +- 当前分支:`GLM5-Achieve` +- 推送目标:`origin/GLM5-Achieve` +- 仓库不在 Git 根级 `package.json` 下,子包为 monorepo;用 Bun 运行命令。 + +## 2. 本轮目标与边界 + +用户要求:架构与设计冻结、需求已明确;不允许回退或降级;汇总多轮四视角审查报告的真实问题并修复;修复后跑真实 UAT 演示。 + +**架构与设计冻结点(不要修改)**: +- 7 个 monorepo 包:`contracts / cli / tui / runtime / llm / toolchain-cpp / workers` +- 5 个 Domain Invariants(INV-1~INV-5) +- 工具契约 `ToolResultEnvelope { status, output, error, artifact_ids, evidence_ref_ids, metadata }`(`packages/contracts/src/tool.ts`) +- Worker/Scheduler IPC 协议 +- 状态机:`MainAgent` 15 态、`Scheduler` 13 态 + +**真实阻断问题(来源:MiniMax-M3 / Deepseek / GLM5.1 / Gpt5.5 四份审查报告)**: +- 工具结果 `content` 与 `output` 契约不一致 +- `shell.run` AsyncGenerator 无法被普通 `call()` 消费 +- Scheduler 在 `!has_running()` 时把所有 running task 标 completed +- Worker 无结果退出后状态丢失 +- MainAgent 回答未接入 ContextAssembler +- `run.ts` 危险操作 confirmation y/n 路由断裂 +- ExecutorRole DONE 判定宽松、code block 全局反转义破坏源码 +- E2E/release gates 不覆盖真实成功场景 +- 报告 `集成测试阶段GLM5.1审查结果.md` / `集成测试阶段Gpt5.5审查结果.md` 在仓库根目录 + +## 3. 已完成的源码修复 + +### 3.1 工具契约与 shell.run +- `packages/runtime/src/tools/BuiltInToolRegistrar.ts`:`create_real_executor()` 中 18 个工具成功返回从 `{ type, content }` 改为 canonical `{ status: 'ok', output, metadata }`。 +- `packages/runtime/src/tools/ToolRegistry.ts`:`ToolExecutor` 支持 `Promise | AsyncIterable` 返回;`execute_branch` 通过 `execute_executor_final` 消费 streaming final envelope;`call_streaming` 透传 chunks 并保证 final 出现。 +- `packages/runtime/src/tools/shell/index.ts`:streaming executor 立即 yield `command.started`,stdout/stderr chunk 收集后 yield,final envelope 包含 `metadata.is_final = true` 与 `output.{exit_code, stdout, stderr, timed_out}`。 + +### 3.2 Worker/Scheduler 结果闭环 +- `packages/runtime/src/workers/WorkerProcess.ts`:新增 `on_exit` 回调,触发退出时把信息分发给 WorkerManager。 +- `packages/runtime/src/workers/WorkerManager.ts`: + - `WorkerHandle.state` 增加 `failed`,`worker.result` 按 payload.status 决定 handle state;保留 wrapped result。 + - `wrap_worker_result` 把 `changes[].file → changed_files`、verification object → `verification[]`、role status `fixed/pass → completed`、others → `failed`。 + - `handle_worker_exit` 在 worker 退出但无 result 时构造 `failed/cancelled` result。 + - 暴露 `get_result_for_task` / `get_handle_for_task`。 + - 给 ToolRegistry 调用上下文传 `project_root` + `agent_type: 'executor'`,不再用 `cwd` 伪字段。 +- `packages/runtime/src/scheduler/Scheduler.ts`:`MONITORING` 改为按 `WorkerResult.status` 消费 `task.completed` / `task.blocked` / `task.cancelled` / `task.failed` durable events;删除"`!has_running()` 直接 completed"逻辑。 + +### 3.3 MainAgent 上下文、确认门、ArchitectureDesigner +- `packages/runtime/src/context/ContextAssembler.ts`: + - 修复 L6 `evidence_store.list_for_entity` 调用签名。 + - 修复 L8 兼容 `role: 'tool' / 'tool_result' / 'tool_use'`。 + - 新增 L3 `project_files` 快照层(实现 `build_project_files_snapshot`)。 + - 移除 additional_layers 重复追加。 +- `packages/runtime/src/app/RuntimeApp.ts`:`start()` 中实例化 `CapabilityRegistry`、绑定 ToolRegistry、注入 DoctorService;创建 `MessageRepository` / `EvidenceRepository` 并 `context_assembler.set_data_sources`。 +- `packages/runtime/src/app/ServiceRegistry.ts`:同步接入 `CapabilityRegistry`、把 WorkerManager/Scheduler 走同一个 tool registry。 +- `packages/runtime/src/agents/main/MainAgent.ts`: + - 新增 `context_assembler / architecture_designer / project_root / agent_id / task_id` 字段。 + - `chat_with_llm()` 使用 `ContextAssembler.assemble` 生成 L0-L9 消息再追加 user message。 + - `classify_via_llm` 改用 `complete_text` 与 `ProviderManager` 既有 API 对齐。 + - 危险操作正则覆盖中文:`删除|删掉|清除|移除|销毁`。 + - 委托前调用 `ArchitectureDesigner.assess_impact`,`reject_or_escalate/requires_replan` 转 `ARCHITECTURE_DESIGNING`,`requires_user_confirmation` 转 `CONFIRMING`。 + - 新增 `infer_changed_files` 推断受影响组件。 +- `packages/cli/src/commands/run.ts`: + - MainAgent 构造时传入 `context_assembler` 与 `project_root`。 + - 把调度逻辑抽取为 `dispatchTask`,避免重复代码。 + - `pendingConfirmation` 状态机:进入 CONFIRMING 时只展示提示并保存原始任务;`y` → `handle_confirmation(true)` 后 `dispatchTask`;`n` → `handle_confirmation(false)` 提示取消,不创建任务。 + - `/results` 优先使用 `WorkerResult.changed_files`,过滤 `.air` 内部文件。 +- `packages/cli/src/commands/ask.ts`:MainAgent 接入 `context_assembler`/`project_root`;`execute_task` 中 `ctx` 改为 canonical ToolExecutionContext。 + +### 3.4 ExecutorRole +- `packages/workers/src/roles/ExecutorRole.ts`: + - 引入 `is_done_signal(text)` 严格判定(仅当整行等于 `DONE` 或 `TASK_COMPLETE`)。 + - 工具失败且 LLM 输出 DONE 时不返回 `completed`,而是让 LLM 修复。 + - 移除 code block 内容的全局 `\\n/\\t/\\\\/\\"` 反转义,保留原文。 + +### 3.5 Permission 与 TUI +- `packages/runtime/src/tools/ToolRegistry.ts`:`ask_user`/`deny` 分支保留 `call.call_id`。 +- `packages/cli/src/commands/release.ts`:增加 `findRepoRoot` / `findBun`,将 release gates 改为运行 `air e2e`、runtime regression、depcruise,timeout 600s,并输出失败尾部。 + +## 4. 新增门禁与测试 + +- `packages/runtime/test/regression/release-critical-gates.test.ts`(已接入 e2e P0-REL): + 1. `built-in tool success envelopes use output, not content` + 2. `shell.run returns a final envelope through call and streaming APIs` + 3. `scheduler does not mark running tasks completed without a worker result` + 4. `MainAgent answer mode uses assembled project context` + 5. `destructive requests enter confirmation and rejection returns to idle` +- `packages/cli/test/run-command-regression.test.ts`(已接入 e2e P0-REL): + - 锁定 `run.ts` 使用 WorkerResult.changed_files、过滤 `.air`、维护 pendingConfirmation。 +- `packages/runtime/test/regression/worker-result-envelope.test.ts` 更新断言为新 `wrap_worker_result` 实现。 +- `packages/runtime/test/regression/tool-registry-permission.test.ts` 新增 ask_user/deny 保留 call_id 断言。 +- `packages/cli/src/commands/e2e.ts` 新增 `P0: Release-critical functional gates`。 + +## 5. 验证结果 + +``` +$ /home/airlongdian/.bun/bin/bun run packages/cli/src/index.ts e2e +14/14 gates passed + - P0: Monorepo structure + - P0: depcruise dependency boundary (INV-4) + - P0: tsc strict typecheck (0 errors) + - P0: Release-critical functional gates + - P1: Storage/Events + - P2: Tools/Permission + - P3: Provider/Context + - P4: Worker IPC + - P5: C++ Toolchain + - P6: Projection/TUI + - P7: Agents + - P8: Full regression suite + - SEC: Command injection regression + - CAP: Capability trust regression + +$ /home/airlongdian/.bun/bin/bun run packages/cli/src/index.ts release --dry-run +3/3 gates passed - Release READY +``` + +## 6. 真实 UAT 演示(临时目录 `/tmp/air-uat.Y2H5dz`) + +- LLM 配置:`OPENAI_API_KEY=sk-...`、`OPENAI_BASE_URL=http://newapi.airlongdian.fun`(注意:env 中 `AIRCODING_*` 会被适配器再次加 `/v1`,应直接用 `OPENAI_*`)、`AIRCODING_MODEL=glm-5.1`。 +- 场景结果: + 1. `air ask "创建一个 hello.txt,内容是 HelloWorld"` → 真实写入 `HelloWorld` 磁盘。 + 2. `air ask "我的项目里有哪些文件?"` → 回答引用项目文件快照(`hello.txt`),无幻觉。 + 3. `air run` 输入 `请删除 hello.txt` 后 `n` → 提示 "Cancelled. No task was created.",文件保留。 + 4. `air run` 输入 `请删除 hello.txt` 后 `y` → 调度 `shell.run rm`,文件被删除。 +- UAT 临时发现并修复:MainAgent 原危险正则不匹配中文,补充 `删除|删掉|清除|移除|销毁`。 + +## 7. 复现命令(在新设备上验证) + +```bash +REPO=/home/airlongdian/DataDevices/AirWorkSpace/AirCoding +cd "$REPO" + +# 1) Type check +/home/airlongdian/.bun/bin/bun run node_modules/.bin/tsc --noEmit -p tsconfig.check.json + +# 2) E2E +/home/airlongdian/.bun/bin/bun run packages/cli/src/index.ts e2e + +# 3) Release dry-run +/home/airlongdian/.bun/bin/bun run packages/cli/src/index.ts release --dry-run + +# 4) 临时目录演示 +TMP=$(mktemp -d /tmp/air-uat.XXXXXX) +cd "$TMP" +/home/airlongdian/.bun/bin/bun run "$REPO/packages/cli/src/index.ts" init . +OPENAI_API_KEY= \ +OPENAI_BASE_URL=http://newapi.airlongdian.fun \ +AIRCODING_MODEL=glm-5.1 \ +AIRCODING_REPO_ROOT="$REPO" \ +/home/airlongdian/.bun/bin/bun run "$REPO/packages/cli/src/index.ts" ask "创建一个 hello.txt,内容是 HelloWorld" +cat hello.txt +``` + +## 8. 关键文件清单 + +修改: +- `packages/cli/src/commands/ask.ts` +- `packages/cli/src/commands/e2e.ts` +- `packages/cli/src/commands/release.ts` +- `packages/cli/src/commands/run.ts` +- `packages/runtime/src/agents/main/MainAgent.ts` +- `packages/runtime/src/app/RuntimeApp.ts` +- `packages/runtime/src/app/ServiceRegistry.ts` +- `packages/runtime/src/context/ContextAssembler.ts` +- `packages/runtime/src/scheduler/Scheduler.ts` +- `packages/runtime/src/tools/BuiltInToolRegistrar.ts` +- `packages/runtime/src/tools/ToolRegistry.ts` +- `packages/runtime/src/tools/shell/index.ts` +- `packages/runtime/src/workers/WorkerManager.ts` +- `packages/runtime/src/workers/WorkerProcess.ts` +- `packages/runtime/test/regression/tool-registry-permission.test.ts` +- `packages/runtime/test/regression/worker-result-envelope.test.ts` +- `packages/workers/src/roles/ExecutorRole.ts` + +新增: +- `packages/runtime/test/regression/release-critical-gates.test.ts` +- `packages/cli/test/run-command-regression.test.ts` + +报告(在仓库根目录): +- `集成测试阶段Deepseek审查结果.md` +- `集成测试阶段GLM5.1审查结果.md` +- `集成测试阶段Gpt5.5审查结果.md` +- `集成测试阶段MiniMax-M3审查结果.md` +- `集成测试阶段GLM5.1审查结果.md` +- `状态交接.md`(本文件) + +## 9. 提交策略 + +本轮一次性提交,commit message: + +``` +fix: integrate audit findings (round 1) - tools, worker, scheduler, main agent + +- Unify ToolResultEnvelope (output vs content) for built-in tools +- Fix shell.run AsyncGenerator consumption in ToolRegistry.call +- Scheduler: consume WorkerResult.status instead of marking all running tasks completed +- WorkerProcess/WorkerManager: surface exit events and generate failed/cancelled result +- MainAgent: integrate ContextAssembler, Chinese destructive regex, ArchitectureDesigner impact gate +- run.ts: pendingConfirmation flow, dispatch extracted, .air files filtered from /results +- CapabilityRegistry wired into RuntimeApp and ServiceRegistry; DoctorService uses it +- release.ts: findRepoRoot/findBun, run air e2e + depcruise + runtime regression +- New gates: release-critical-gates, CLI run command regression +- 14/14 e2e gates pass; 3/3 release dry-run pass +``` + +## 10. 已知与待办 + +- TUI 完整输入/快捷键统一收口仍是可改进点(UAT 已在非 TTY 走通 readline),建议后续在 TUI 内增加任务输入框。 +- Projection 事实源收口已做(`/results` 走 WorkerResult,ProjectionClient 接收 ProjectionStore 推送),但 TUI 渲染仍以手工 snapshot 为初值;下一步可以由 EventStore/ProjectionStore 完全驱动。 +- 新增 `集成测试阶段Gpt5.5审查结果.md` 等报告应作为证据纳入未来 release notes。 + +--- + +**会话记录**(从对话恢复继续的关键节点): +1. 四份审计报告汇总去重 → 真实问题清单。 +2. Phase 1: 工具契约 + shell.run 修复(ToolRegistry 消费 AsyncIterable、shell final envelope)。 +3. Phase 2: WorkerProcess exit handler → WorkerManager → Scheduler MONITORING 改按 WorkerResult 状态入事件。 +4. Phase 3: MainAgent 接入 ContextAssembler,CONFIRMING y/n 路由,ArchitectureDesigner 主路径,CapabilityRegistry 接入,Permission call_id 保留。 +5. Phase 4: ExecutorRole 严格 DONE、保留 code block 原文、失败不 completed。 +6. Phase 5: 新增 release-critical-gates / CLI run regression,air e2e 14/14。 +7. Phase 6: run.ts `/results` 走 WorkerResult,release.ts 找 repo root。 +8. 真实 UAT:air ask 创建/追问;air run 中文删除拒绝/确认;运行中 UAT 触发并修复中文确认门正则。 +9. 准备状态交接与一次性 git 提交推送。 + +会话总耗时较长,过程已通过自动化避免重复。回到本机可直接按 §7 复现并继续。 diff --git a/集成测试阶段Deepseek审查结果.md b/集成测试阶段Deepseek审查结果.md new file mode 100755 index 0000000..df8f772 --- /dev/null +++ b/集成测试阶段Deepseek审查结果.md @@ -0,0 +1,276 @@ +# 集成测试阶段 Deepseek 审查结果 + +**审计日期**: 2026-06-05 +**项目**: AirCoding V1.0.0 Alpha +**审计模式**: 四视角交叉审计(系统架构师 / 开发工程师 / 真实用户 / 测试工程师) + +--- + +## 0. 前置验证 + +**TypeScript 类型检查**: PASS (0 errors) +**E2E Gates**: 13/13 PASS + +--- + +## 1. 系统架构师审查报告 + +### 一、实际运行验证结果 + +三项测试全部通过: +- **Test 1 (tsc)**: 零类型错误,17 个 contracts 文件完整 +- **Test 2 (E2E gates)**: 13/13 通过,覆盖 P0 monorepo/depcruise/tsc + P1-P8 回归测试 + SEC 命令注入 + CAP 能力信任 +- **Test 3 (集成测试)**: RuntimeApp 启动成功,MainAgent regex 正确将 "Create hello.txt" 路由为 delegate,Scheduler 调度完成,文件真实创建(内容 "HELLO from AirCoding"),39 个工具注册完毕,fs.read/fs.list 正确返回 + +### 二、FR 实现情况 + +#### 真正实现的 FR(完整端到端链路可通) + +| FR | 描述 | 实现状态 | +|----|------|----------| +| FR-001 | CLI 启动与项目初始化 | ✅ runCommand 含 auto-init、项目检测 | +| FR-002/003 | 项目本地状态与 Session 持久化 | ✅ SQLite session.db + 16 Repository | +| FR-005 | Main Agent 15 状态机 + 分类 | ✅ regex/llm 双模式 + chat_with_llm | +| FR-007 | Scheduler 13 状态机 + TaskGraph | ✅ 完整状态链 + WavePlanner/RetryPlanner | +| FR-008 | 5 种 Worker Agent + NDJSON IPC | ✅ Executor/Reviewer/Debugger/Compactor/ExperienceMiner | +| FR-009 | 执行原语 (code block + tool_call 解析) | ✅ ExecutorRole.parse_actions 三种解析模式 | +| FR-010 | ToolRegistry 39 工具 | ✅ fs/shell/git/cpp/project/artifact 全覆盖 | +| FR-013 | Provider 层 (Anthropic + OpenAI-compatible) | ✅ 适配器边界清晰 | + +#### 名存实亡的 FR(结构存在但深度不足) + +| FR | 描述 | 问题 | +|----|------|------| +| FR-004 | 事件驱动运行时 | EventBus/Store/Ingestor 类齐全,但 run.ts 同步阻塞 push Scheduler,未真正依赖事件总线驱动状态流转 | +| FR-006 | Architecture Designer | 影响评估类完整,但 MainAgent.classify() 路由到 delegate 时从未调用 ArchitectureDesigner——架构审查是死代码 | +| FR-011 | Permission/Security | PermissionEngine 六层模型存在,但 ToolRegistry.call() 未显示逐调用通过 PermissionEngine 评估的证据 | +| FR-012 | Plugin/Capability Foundation | CapabilityRegistry 存在但运行时未深入集成 | +| FR-016 | TUI/HUD | TuiApp.ts 渲染 ANSI 视图,但 run.ts 手工推送构造数据而非 ProjectionStore 作为单一可信源 | +| FR-017 | C++ 工作流 | cpp.* 工具存在但 debug→fix→review 证据闭环未端到端验证 | + +### 三、架构基线遵守评估 + +**遵守良好**: +- 依赖方向正确:tui 不 import runtime,depcruise 零违规 +- IPC 协议正确:NDJSON over stdio,worker.ready 握手,heartbeat 心跳 +- contracts 包独立,17 个文件覆盖所有核心类型 + +**关键偏离**: +1. **依赖边界**: runtime/cli 直接依赖 @aircoding/llm 创建适配器——架构基线要求 runtime 通过接口间接使用 llm +2. **事件驱动承诺未兑现**: 基线 §6 声明 "AirCoding is event-driven",但主循环是同步 pull 模式 +3. **TUI 消费方式偏离**: 基线 §18 要求 "HUD/TUI consumes ProjectionStore only",但 TuiApp 接收手工 snapshot 而非 ProjectionStore hydrate/apply + +### 四、状态机评估 + +**Scheduler 状态机** (13 状态): 逻辑完整但 `MONITORING` 状态存在竞态——200ms 轮询 vs 事件驱动等待,`has_running()` 与 `graph.update_status` 之间的窗口可能导致假完成。 + +**MainAgent 状态机** (15 状态): `CONFIRMING` 状态仅在 regex 检测 breaking 词时触发,但确认回调在 run.ts 中断裂。`ARCHITECTURE_DESIGNING` 和 `ARCHITECTURE_REVISING` 状态无实际触发路径。 + +### 五、架构师结论 + +V1.0.0 Alpha 已实现核心能力闭环:CLI→MainAgent→Scheduler→Worker→LLM→Tool→File 的完整链路可以跑通并创建真实文件。三个关键缺口:**(a)** Architecture Designer 是死代码,**(b)** PermissionEngine 未在 ToolRegistry 调用路径中逐次生效,**(c)** Scheduler 轮询等待对长任务存在竞态窗口。 + +--- + +## 2. 开发工程师审计报告 + +### P0 问题 + +**1. BuiltInToolRegistrar 中 18 个工具的结果形状不一致** (`create_real_executor.ts:175-437`) +`create_real_executor` 返回 `{ status, call_id, tool_name, type, content, metadata }`,但正确的 ToolResultEnvelope 形状使用 `output` 而非 `content`。影响:fs.stat, process.kill, cpp.detect, cpp.build, cpp.test, doctor.run, project.scan, debug.run, gui.screenshot, network.capture 等。 + +WorkerManager 第 191 行执行 `result.output || result.error || {}`——**每个通过 worker IPC 调用这些工具的任务都会收到空内容 `{}`**。ExecutorRole 的 LLM 循环看不到任何工具输出,导致每项任务崩溃或无限循环。 + +### P1 问题 + +**2. shell.run 生成器未被解包** (`shell/index.ts`) +`createShellExecutor` 返回 `async function*`。ToolRegistry.execute_branch 直接调用执行器并返回结果——对生成器函数来说,返回的是生成器对象而非 ToolResultEnvelope。WorkerManager 会再次回退到 `{}`。**shell 命令无法通过 worker chain 工作。** + +**3. MainAgent.classify_via_llm API 不匹配** (`MainAgent.ts:185`) +`classify_via_llm` 调用 `this.provider_manager.complete(...)`,但 run.ts 创建的提供者只挂接了 `complete_text`。如果 `classify_mode: 'llm'`,`provider_manager.complete is not a function` 会导致崩溃。 + +**4. Scheduler DISPATCHING 缺少 await** (`Scheduler.ts:163`) +`this.worker_manager.spawn(...)` 没有 `await`——方法返回 Promise,但计划程序立即进入 MONITORING。worker 进程在调度程序检查 `has_running()` 时可能尚未准备好,任务被错误标记。 + +### P2 问题 + +**5. 资源泄漏**: run.ts progressInterval 在 run_until_idle() 拒绝时永不清理 +**6. 空 catch 块**: scanDir 中的 `catch {}` 默默丢弃所有文件系统错误 +**7. 延迟浪费**: Scheduler.step() 使用 setTimeout(r, 200) 轮询而非事件驱动 +**8. require() 调用**: DoctorService.check_capability_deps 使用同步 require('child_process') 而非静态导入 + +### 用户可感知的核心影响 + +用户输入 "创建一个 C++ hello world 程序",MainAgent 正确分类委托,Scheduler 调度到 DISPATCHING,Worker 启动并调用 LLM。LLM 响应代码块,ExecutorRole 调用 fs.write(成功),然后 LLM 调用 cpp.detect(通过 IPC)。WorkerManager 收到成功结果但提取 `result.output` 为 undefined,向 worker 发送 `{}`。LLM 看不到工具输出,困惑,重试,15 轮后被 BLOCKED。用户只看到 "Task blocked" 而没有文件。**根本原因是形状不一致——约 50% 的工具注册表通过 worker 路径被静默破坏。** + +--- + +## 3. 真实用户测试报告 + +### 测试结果汇总 + +| 测试 | 命令 | 结果 | 关键观察 | +|------|------|------|----------| +| Test 1 | `air init` | **PASS** | 创建 6 个子目录 + project.json,project_id 自动生成 | +| Test 2 | `air doctor` | **PASS** | 5/6 检查通过;project_structure 报 FAIL(缺少 package.json/tsconfig.json),标记为 fixable | +| Test 3 | `air ask 创建hello.txt` | **PASS** | 委托模式正常工作,1 轮完成,文件内容正确 (HelloWorld, 10 bytes) | +| Test 4 | `air ask C++项目` | **部分通过** | main.cpp 和 CMakeLists.txt 创建成功,但 `cpp.build` 和 `shell.run` 报错,LLM 却声称 "程序已成功编译" | +| Test 5 | `air run TUI` | **PASS** | TUI 渲染正常,面板和快捷键显示正确,输入 q 退出干净 | +| Test 6 | `air e2e` | **PASS** | 13/13 gates 全部通过 | +| Test 7 | `air history / session list` | **PASS** | 三个 session 被正确记录 | + +### 用户最痛 3 个问题 + +1. **doctor 语义误导**: "All checks: FAIL" 用红色大字——但只是缺少 package.json 和 tsconfig.json 模板文件。新用户看到 FAIL 会以为产品坏了,其实项目完全正常工作。 + +2. **结果不可信——工具报错但 LLM 说成功**: Test 4 中 `cpp.build` 返回了 Error,`shell.run` 返回了 `Error: undefined`,但最终输出却写着 "程序已成功编译并输出 Hello World"。用户分不清到底是真成功了还是 LLM 幻觉。**这是信任问题。** + +3. **session/history 毫无辨识度**: `air history` 输出裸 session_id 数字加大小,不知道哪个 session 干了什么。用户做了 3 次 air ask,回头想找之前的任务,面对 3 个无区分的数字完全懵了。 + +### 用户体验评分: **5/10** + +扣分项: +- 工具错误被吞掉(**-2 分**):信任问题,用户无法区分真实成功和幻觉 +- doctor 诊断语义不准确(**-1 分**):把可修复警告当成硬失败 +- session/history 不可辨识(**-1 分**):无法快速定位之前的任务 +- 多轮交互体验存疑(**-1 分**) + +加分项: init 流程干净、TUI 渲染正常、e2e 全绿、响应速度快 + +--- + +## 4. QA 测试报告 + +### 测试矩阵 + +| Test | 描述 | 结果 | 严重级别 | +|------|------|------|----------| +| T1 | TypeScript 类型检查 | **PASS** | - | +| T2 | E2E Gates (13门) | **13/13 PASS** | - | +| T3 | 简单文件创建 | **PASS** (Worker 退出信号异常) | P1 | +| T4 | C++ AI 终端 (FR-017) | **PASS** (Worker 退出信号异常) | P1 | +| T5 | 追问上下文验证 | **FAIL** | **P0** | + +### 详细测试分析 + +#### T3: 简单文件创建 — PASS (P1 警告) +- `hello.txt` 成功创建,内容 `HELLO`(6 字节),分类正确为 `delegate`。 +- **警告**: Worker 输出 `[Worker] exited with code 0 (error): Unrecoverable error occurred`。Worker 以 exit code 0 退出但附加 "(error)" 标记——信号噪音导致诊断困难。 + +#### T4: C++ AI 终端 (FR-017) — PASS (P1 警告) +- 成功生成 `main.cpp`(13824 字节,含 `#include` 和 `main()`)、`CMakeLists.txt` 和 `script.sh`。 +- 功能需求基本满足:代码具备 C++ 程序骨架。 +- **警告**: 同样出现 Worker exit code 噪音。 + +#### T5: 追问上下文验证 — **FAIL** (P0) +- LLM 回答: **"抱歉,我目前无法直接访问您的本地文件系统,所以不知道您的项目里有哪些源代码文件。"** +- **根因**: `MainAgent.chat_with_llm()` 方法(MainAgent.ts:102-122)完全绕过 ContextAssembler。仅发送一条裸 system prompt `'You are AirCoding, an AI coding assistant...'`,没有注入项目根路径、文件列表、对话历史等任何上下文。 +- **影响**: 所有问答类交互(占总交互的很大比例)都没有项目上下文感知能力。 + +### 问题分类 + +#### P0 — 阻断发布 + +1. **MainAgent.chat_with_llm 无 ContextAssembler 集成** (MainAgent.ts:102-122) + 修复方向:chat_with_llm 应接收 AssemblyContext 参数,先通过 ContextAssembler 组装上下文再发给 LLM。 + +#### P1 — 重要缺陷 + +2. **Worker 退出信号不一致** (T3/T4): + Worker 以 exit code 0 退出但附带 "(error)" 字符串——应排查 Worker 退出码逻辑。 + +3. **BuiltInToolRegistrar 结果形状不一致** (工程审计 P0): + 18 个工具的返回形状使用 `content` 而非 `output`,导致 WorkerManager 收到空结果。 + +4. **shell.run 生成器未解包** (工程审计 P1) + +#### P2 — 改进项 + +5. 无 workspace files 快照层:ContextAssembler 缺少 project_files 提示层 +6. Scheduler DISPATCHING 缺少 await (竞态) +7. classify_via_llm API 不匹配 +8. history/session 输出缺乏可辨识性 + +### E2E Gates 补充建议 + +当前 13 个 gate 全部基于单元测试和静态检查,缺少以下端到端 gate: + +| 优先级 | 建议 Gate | 检查内容 | +|--------|-----------|----------| +| **P0** | **Answer-Mode Context Gate** | 验证 chat_with_llm 回复包含项目文件信息 | +| **P0** | **Simple E2E Create Gate** | LLM 驱动的文件创建端到端 | +| **P0** | **Tool Output Shape Gate** | 验证所有 39 个工具的返回形状符合 ToolResultEnvelope | +| P1 | **Complex E2E Generate Gate** | 多文件代码生成端到端 | +| P1 | **Worker Exit Consistency Gate** | 验证 Worker exit code 0 不与 "(error)" 同时 | +| P1 | **Follow-up Context Gate** | 新建文件后追问,验证 Agent 感知已有文件 | +| P2 | **ContextAssembler Integration Gate** | 验证所有 Agent 路由经过 ContextAssembler | + +--- + +## 5. 四视角交叉审计综合结论 + +### P0 问题汇总(3 项,阻塞发布) + +| # | 问题 | 来源视角 | 触发条件 | 影响范围 | +|---|------|----------|----------|----------| +| 1 | **MainAgent.chat_with_llm 无项目上下文** | QA + 架构师 + 用户 | 所有 answer 交互 | 用户追问项目状态时 LLM 100% 幻觉 | +| 2 | **BuiltInToolRegistrar 18 个工具结果形状不一致** | 工程师 + QA | Worker IPC 调用这些工具 | 50% 工具通过 worker 返回空结果 | +| 3 | **shell.run 生成器未解包** | 工程师 | 任何 shell.run 调用 | shell 命令 100% 失败 | + +### P1 问题汇总(5 项) + +| # | 问题 | 来源视角 | +|---|------|----------| +| 4 | Worker 退出信号噪音 | QA + 架构师 | +| 5 | Scheduler MONITORING 竞态窗口 | 架构师 + 工程师 | +| 6 | classify_via_llm API 不匹配 | 工程师 | +| 7 | Scheduler DISPATCHING 缺少 await | 工程师 | +| 8 | 工具错误被吞掉 + false-positive 成功 | 用户 | + +### P2 问题汇总(5 项) + +| # | 问题 | 来源视角 | +|---|------|----------| +| 9 | Architecture Designer 是死代码 | 架构师 | +| 10 | PermissionEngine 未在调用路径生效 | 架构师 | +| 11 | history/session 输出不可辨识 | 用户 | +| 12 | doctor 语义误导 (warn→FAIL) | 用户 | +| 13 | 资源泄漏 + 空 catch | 工程师 | + +### 根因分析 + +本轮与前几轮审计相同的模式再次出现: + +1. **E2E gates 框架盲区** — 13/13 gates pass 但真实场景 5/13 (38%) 核心功能失败。gates 检查代码质量(tsc、depcruise、单元测试),不检查功能可用性(工具结果形状、LLM 上下文注入、Worker IPC 往返完整性)。 + +2. **集成测试仅覆盖 Happy Path** — 测试创建 hello.txt 验证了最简单场景,但未覆盖 C++ 多文件任务、shell 执行、追问上下文等复杂度递增的场景。 + +3. **组件间接口契约缺失** — BuiltInToolRegistrar 返回 `{ content }` vs WorkerManager 期望 `{ output }`,这种接口不一致在组件隔离开发时无法发现,只能在集成时暴露。缺少跨组件的 TypeScript 接口强约束。 + +4. **审计员不跑端到端** — 所有审计员检查了 MainAgent.ts 的方法签名、Chat 函数的类型正确性,但没有人实际问一句 "LLM 回到 '我没有文件系统访问' 合理吗?" + +### 发布建议 + +**不建议发布 V1.0.0 Alpha**。3 个 P0 问题影响了核心体验链路: + +- **P0-1**: 用户追问项目状态 → LLM 答"我无法访问文件系统"(每次触发) +- **P0-2**: Worker 调用 cpp.detect / doctor.run / project.scan 等 → 收到空结果 → LLM 困惑 → 任务假完成或失败 +- **P0-3**: LLM 调用 shell.run → 永远返回 `undefined` → 编译/运行/测试全部失败 + +修复 P0 后,必须: +1. 将真实任务端到端集成测试加入 E2E gate suite +2. 统一工具结果形状为 contracts.ToolResultEnvelope (使用 `output` 字段) +3. MainAgent 注入 ContextAssembler 到所有路由 + +--- + +## 6. 修复优先级矩阵 + +| 优先级 | 数量 | 问题 | +|--------|------|------| +| **P0** | 3 | chat_with_llm 无上下文、工具结果形状不一致、shell.run 坏死 | +| **P1** | 5 | Worker exit 噪音、Scheduler 竞态、classify API、spawn await 缺、false-positive | +| **P2** | 5 | ArchDesigner 死代码、PermissionEngine 未生效、history 不可读、doctor 语义、资源泄漏 | +| **E2E gates 增强** | 6 | 新增 answer-mode context gate, simple E2E create gate, tool output shape gate 等 | + +**总计**: 需修复 13 个代码问题 + 新增 6 个 E2E gate diff --git a/集成测试阶段GLM5.1审查结果.md b/集成测试阶段GLM5.1审查结果.md new file mode 100755 index 0000000..9a958d6 --- /dev/null +++ b/集成测试阶段GLM5.1审查结果.md @@ -0,0 +1,220 @@ +# 集成测试阶段 GLM-5.1 审查结果 + +**审计日期**: 2026-06-05 +**项目**: AirCoding V1.0.0 Alpha +**审计模型**: GLM-5.1 +**审计模式**: 四视角交叉审计(系统架构师 / 开发工程师 / 真实用户 / 测试工程师) + +--- + +## 0. 前置验证 + +**TypeScript 类型检查**: PASS (contracts/runtime/llm/toolchain-cpp/workers 5 包通过) +**E2E Gates**: 13/13 PASS (但与用户场景脱节) + +--- + +## 1. 系统架构师审查报告 + +### 架构核心链路评估 + +**已建立完整闭环**: +- CLI → MainAgent → Scheduler → Worker → LLM → Tools → File +- 7-package TypeScript monorepo 结构完整 +- NDJSON over stdio IPC 协议正确 +- 5 Domain Invariants 基本遵守 + +### 关键架构偏离 + +| 设计 | 实际 | 影响 | +|------|------|------| +| FR-014 上下文分层 | MainAgent.chat_with_llm 无 ContextAssembler 集成 | 用户追问 100% 幻觉 | +| ToolResultEnvelope | BuiltInToolRegistrar 18 工具用 content 而非 output | Worker IPC 返回空 | +| AsyncGenerator 解包 | shell.run 生成器未处理 | shell 命令 100% 失败 | +| 事件驱动 | Scheduler MONITORING 轮询 200ms | 假完成风险 | + +### 架构死代码 + +1. **ArchitectureDesigner** - MainAgent.classify() 从未调用 +2. **PermissionEngine** - ToolRegistry.call() 未逐调用评估 + +### 架构师结论 + +V1.0.0 Alpha 架构核心可通,但存在 4 个 P0 结构性缺陷,**不建议发布**。 + +--- + +## 2. 开发工程师审查报告 + +### P0 问题 (阻断发布) + +#### P0-1: MainAgent.chat_with_llm 无 ContextAssembler 集成 +- **位置**: packages/runtime/src/agents/main/MainAgent.ts:102-122 +- **问题**: chat_with_llm() 仅有硬编码 system prompt,无项目上下文注入 +- **影响**: 用户追问"我的项目有哪些文件" → LLM 答"我无法访问文件系统" + +#### P0-2: BuiltInToolRegistrar 18 工具返回形状不一致 +- **位置**: packages/runtime/src/tools/BuiltInToolRegistrar.ts:180-438 +- **问题**: create_real_executor 返回 `{ content }` 而非 `{ output }` +- **影响**: WorkerManager 第 191 行 `result.output || result.error || {}` 返回空对象 +- **受影响工具**: fs.stat, process.kill, git.worktree.create, git.merge_workspace, project.scan, project.profile.write, cpp.detect, cpp.cmake.configure, cpp.build, cpp.test, cpp.static.cppcheck, cpp.clangd.query, debug.run, debug.parse_logs, gui.screenshot, network.capture, permission.request, doctor.run + +#### P0-3: shell.run 生成器未解包 +- **位置**: packages/runtime/src/tools/shell/index.ts:35 +- **问题**: createShellExecutor 返回 `async function*` (AsyncGenerator),ToolRegistry.call() 直接调用返回生成器对象而非 ToolResultEnvelope +- **影响**: shell 命令 100% 失败 + +#### P0-4: 危险操作确认门路由断裂 (新发现) +- **位置**: packages/cli/src/commands/run.ts:115-120 +- **问题**: MainAgent.classify() 返回 CONFIRMING 状态后,run.ts 直接 console.log 确认消息并 rl.prompt(),用户输入的 y/n 永远到不了 MainAgent.handle_confirmation() +- **影响**: 删除/覆盖文件等破坏性操作无防护 + +### P1 问题 (重要缺陷) + +| ID | 问题 | 位置 | 状态 | +|----|------|------|------| +| P1-1 | Scheduler MONITORING 早判完成 | Scheduler.ts:252-260 | 未修复 | +| P1-2 | classify_via_llm API 不匹配 | MainAgent.ts:185 | **已修复** | +| P1-3 | TUI 无文本输入路径 | tui/index.ts | 部分修复 | +| P1-4 | 多文件任务协调缺失 | ExecutorRole.ts | 部分修复 | +| P1-5 | false-positive 成功 banner | ask.ts:182-186 | **新发现** | + +### P2 问题 (改进项) + +- 资源泄漏: run.ts progressInterval 未清理 +- 空 catch 块: scanDir 丢弃所有错误 +- history/session 输出无辨识度 +- doctor 语义误导 (FAIL vs WARNING) + +--- + +## 3. 真实用户测试报告 + +### 测试结果汇总 + +| 测试 | 命令 | 结果 | 观察 | +|------|------|------|------| +| 初始化 | `air init` | ✅ PASS | 创建 .air/ 目录和 project.json | +| 简单文件创建 | `air ask 创建 hello.txt` | ✅ PASS | 文件创建成功 | +| 上下文追问 | `air run: 我的项目有什么文件?` | ❌ FAIL | LLM 答"无文件系统访问" | +| C++ 多文件 | `air ask 用 C++ 写 hello world` | ⚠️ PARTIAL | main.cpp + CMakeLists.txt 创建,但 cpp.build 报错 | +| Shell 执行 | `air ask 列出当前目录` | ❌ FAIL | shell.run 返回 undefined | +| 诊断 | `air doctor` | ⚠️ PARTIAL | 5/6 通过,但 project_structure 报 FAIL(模板文件缺失) | +| 历史记录 | `air history` | ⚠️ PARTIAL | session_id 无辨识度 | + +### 用户最痛 3 个问题 + +1. **shell.run 工具完全坏死** - 任何编译/运行命令都失败 +2. **工具报错但 LLM 说成功** - 用户无法区分真实成功和幻觉 +3. **追问上下文失效** - LLM 每次都"失忆" + +### 用户体验评分: **4.5/10** + +--- + +## 4. QA 测试矩阵 + +### 测试矩阵 + +| Test | 描述 | 结果 | 严重度 | +|------|------|------|--------| +| T1 | TypeScript 类型检查 | PASS | - | +| T2 | E2E Gates (13门) | 13/13 PASS | - | +| T3 | 简单文件创建 | PASS | P1 | +| T4 | C++ 多文件生成 | PARTIAL | P1 | +| T5 | 上下文追问 | **FAIL** | **P0** | +| T6 | Shell 执行 | **FAIL** | **P0** | +| T7 | 工具结果形状验证 | **FAIL** | **P0** | + +### E2E Gates 盲区分析 + +当前 13 个 gate 全部基于静态检查: +- tsc 类型检查 ✓ +- depcruise 依赖检查 ✓ +- 单元测试 ✓ +- 存根数量检查 ✓ + +**缺失的 gate**: +1. ToolResultEnvelope shape 验证 (output 字段) +2. Answer-mode context 验证 +3. Shell.run 功能验证 +4. Worker IPC 往返完整性验证 +5. 危险操作 confirmation 验证 + +--- + +## 5. 与前两轮审查对比 + +### 三轮审查问题对比 + +| 问题 | MiniMax-M3 | Deepseek | GLM-5.1 (本轮) | +|------|------------|----------|----------------| +| P0-1: chat_with_llm 无上下文 | ❌ 未修复 | ❌ 未修复 | ❌ 未修复 | +| P0-2: 18 工具结果形状不一致 | ❌ 未修复 | ❌ 未修复 | ❌ 未修复 | +| P0-3: shell.run 生成器未解包 | ❌ 未修复 | ❌ 未修复 | ❌ 未修复 | +| P0-4: 确认门路由断裂 | ❌ 未发现 | ❌ 未发现 | ⚠️ 新发现 | +| P1-1: Scheduler MONITORING 竞态 | ❌ 未修复 | ❌ 未修复 | ❌ 未修复 | +| P1-2: classify_via_llm API | ❌ 未修复 | ❌ 未修复 | ✅ 已修复 | +| P1-5: false-positive 成功 | ❌ 未发现 | ❌ 未发现 | ⚠️ 新发现 | + +### 根因分析 + +三轮审计得到**相同的 P0 问题**,说明: +1. 问题已被明确识别,但修复优先级不足 +2. E2E gates 无法捕获这些功能性问题 +3. 缺少端到端集成测试验证 + +--- + +## 6. 修复优先级矩阵 + +### 必须立即修复 (P0, 阻塞发布) + +| # | 问题 | 影响范围 | 修复方向 | +|---|------|----------|----------| +| P0-2 | 18 工具返回 content 而非 output | 50% 工具通过 Worker 返回空 | BuiltInToolRegistrar.ts: 将 `{ content }` 改为 `{ output }` | +| P0-3 | shell.run AsyncGenerator 未解包 | shell 命令 100% 失败 | shell/index.ts: 解包 generator 或改为返回 Promise | +| P0-1 | chat_with_llm 无项目上下文 | 追问 100% 幻觉 | MainAgent.ts: 集成 ContextAssembler | +| P0-4 | 确认门路由断裂 | 破坏性操作无防护 | run.ts: 将 y/n 输入路由到 handle_confirmation() | + +### 需要修复 (P1) + +| # | 问题 | 修复方向 | +|---|------|----------| +| P1-1 | Scheduler MONITORING 早判完成 | 检查任务实际状态而非仅 has_running() | +| P1-5 | false-positive 成功 banner | 工具报��时应显示错误而非成功 | + +### 建议改进 (P2) + +- TUI 文本输入路径统一 +- history/session 输出可辨识 +- doctor 语义准确性 + +--- + +## 7. 综合结论 + +### 发布建议: **不推荐发布** + +4 个 P0 问题阻塞 V1.0.0 Alpha 发布: + +1. **shell.run 完全坏死** - 用户无法执行任何编译/运行命令 +2. **50% 工具返回空结果** - cpp.detect / project.scan / doctor.run 等全部失效 +3. **上下文追问 100% 幻觉** - 用户无法询问项目状态 +4. **破坏性操作无防护** - 删除/覆盖文件无确认 + +### 修复后验证清单 + +- [ ] shell.run 能执行 `ls`, `echo` 等基础命令 +- [ ] cpp.detect / project.scan 返回实际项目信息 +- [ ] 追问"我的项目有哪些文件"返回真实文件列表 +- [ ] 删除文件时弹出确认,用户确认后执行 + +### 核心教训 + +**E2E gates 检查代码质量,用户场景验证功能可用性**。两者正交,不能互相替代。 + +--- + +*审计模型: GLM-5.1* +*审计时间: 2026-06-05* \ No newline at end of file diff --git a/集成测试阶段Gpt5.5审查结果.md b/集成测试阶段Gpt5.5审查结果.md new file mode 100755 index 0000000..11f1c9f --- /dev/null +++ b/集成测试阶段Gpt5.5审查结果.md @@ -0,0 +1,331 @@ +# 集成测试阶段 Gpt5.5 审查结果 + +**审计日期**: 2026-06-05 +**项目**: AirCoding V1.0.0 Alpha +**审计模式**: 四视角交叉审计(系统架构师 / 开发工程师 / 真实用户 / QA 测试工程师) +**审计范围**: 原始需求、baselineV1、核心源码、既有 MiniMax-M3 / Deepseek / GLM5.1 审查报告、真实用户临时目录 UAT + +--- + +## 0. 总体结论 + +**不建议发布 V1.0.0 Alpha,也不建议作为完整产品对外演示。** + +本轮四视角结论高度一致:AirCoding 已具备 monorepo、contracts、RuntimeApp、Worker IPC、ToolRegistry、TUI 渲染等骨架能力,但核心产品闭环仍不可信: + +1. **任务失败可能被标记为完成**:Scheduler 仍可能在 worker 停止后直接把 running task 标为 completed。 +2. **shell.run 不可靠**:shell executor 是 AsyncGenerator,但 ToolRegistry.call 直接 await executor,普通工具调用路径无法得到最终 ToolResultEnvelope。 +3. **工具结果契约不统一**:BuiltInToolRegistrar 多个工具返回 `content`,contracts 要求 `output`。 +4. **MainAgent answer 无项目上下文**:`chat_with_llm()` 未接入 ContextAssembler。 +5. **危险操作确认门失效**:`air run` 直接调度 delegate,用户 y/n 不会进入 `handle_confirmation()`。 +6. **E2E gates 失真**:13/13 PASS 不能证明真实用户任务成功。 + +当前只能称为**架构骨架技术预览**,不能称为满足原始需求和 baseline 的 Alpha 发布版。 + +--- + +## 1. 系统架构师审查 + +### 1.1 架构结论 + +AirCoding 的包结构、基础状态机、工具注册、Worker IPC、EventStore/ProjectionStore 类都存在,但关键 baseline 承诺没有在运行路径闭合: + +- FR-004 事件驱动运行时未闭合:Scheduler 主路径大量修改内存 TaskGraph,而不是以 durable event 作为唯一事实源。 +- FR-005 / FR-014 MainAgent 对话未接 ContextAssembler。 +- FR-006 ArchitectureDesigner 仍未进入 MainAgent/Scheduler 主路径。 +- FR-007 Scheduler 完成判定仍不读取 WorkerResult.status。 +- FR-008 IPC 与 contracts 存在实际协议偏差。 +- FR-010 / FR-017 C++ 工具链存在双实现且未形成 detect→configure→build→test→debug→fix→review 闭环。 +- FR-012 CapabilityRegistry 未被 RuntimeApp 真实接入。 +- FR-016 TUI 渲染存在,但 `air run` 手工推送 snapshot,绕过 ProjectionStore/DB/EventStore 投影事实源。 +- FR-020 release/e2e gates 仍偏静态检查,不能证明产品成功场景。 + +### 1.2 架构 P0/P1/P2 + +| 优先级 | 问题 | 影响 | 证据 | +|---|---|---|---| +| P0 | MainAgent answer 路径未接入 ContextAssembler | 追问项目状态会幻觉,FR-005/FR-014 不成立 | `packages/runtime/src/agents/main/MainAgent.ts:70-73`, `packages/runtime/src/agents/main/MainAgent.ts:102-118`, `packages/runtime/src/context/ContextAssembler.ts:74-105` | +| P0 | Scheduler 直接把 running 标 completed | 失败/blocked/cancelled 可假完成 | `packages/runtime/src/scheduler/Scheduler.ts:251-260`, `packages/runtime/src/workers/WorkerManager.ts:237-243` | +| P0 | durable task events 未形成主路径 | FR-004 / INV-1 被削弱,SQLite 不是真正调度事实源 | `packages/runtime/src/scheduler/Scheduler.ts:65-78`, `packages/runtime/src/scheduler/Scheduler.ts:251-260`, `packages/runtime/src/events/EventStore.ts:622-705` | +| P0 | Worker IPC 与 contracts 不一致,事件/心跳未完整进入父进程事件流 | 恢复、投影、审计不可依赖 | `packages/contracts/src/ipc.ts:37-48`, `packages/workers/src/WorkerRuntime.ts:207-219`, `packages/runtime/src/workers/WorkerManager.ts:160-253` | +| P0 | C++ 完整工作流未进入主链路 | FR-017 未达成 | `packages/runtime/src/tools/BuiltInToolRegistrar.ts:260-314`, `packages/toolchain-cpp/src/CppToolRegistrar.ts:31-95`, `packages/workers/src/roles/ExecutorRole.ts:51-57` | +| P0 | ToolResultEnvelope 不统一 | Worker 侧工具输出丢失 | `packages/contracts/src/tool.ts:76-83`, `packages/runtime/src/tools/BuiltInToolRegistrar.ts:180-183`, `packages/runtime/src/tools/BuiltInToolRegistrar.ts:237-239`, `packages/runtime/src/workers/WorkerManager.ts:188-192` | +| P1 | `shell.run` AsyncGenerator 与 `ToolRegistry.call()` 不兼容 | shell 可用性不可靠 | `packages/runtime/src/tools/shell/index.ts:33-110`, `packages/runtime/src/tools/ToolRegistry.ts:248-255` | +| P1 | ArchitectureDesigner 是运行路径死代码 | FR-006 不成立 | `packages/runtime/src/agents/main/MainAgent.ts:1-265`, `packages/runtime/src/agents/architecture/ArchitectureDesigner.ts:22-76` | +| P1 | CapabilityRegistry 未接入 RuntimeApp | FR-012 运行级不成立 | `packages/runtime/src/app/RuntimeApp.ts:65-92`, `packages/runtime/src/capabilities/CapabilityRegistry.ts:29-154` | +| P1 | TUI/HUD 不符合 ProjectionStore-only 链路 | TUI 状态可与 DB/EventStore 不一致 | `packages/cli/src/commands/run.ts:58-72`, `packages/cli/src/commands/run.ts:174-189`, `packages/runtime/src/app/RuntimeApp.ts:81-84` | +| P1 | TUI 与 readline 抢 stdin | TUI 是 viewer,不是完整 coding session 输入界面 | `packages/tui/src/TuiApp.tsx:110-146`, `packages/cli/src/commands/run.ts:99-115` | +| P2 | release/e2e gates 不证明 Alpha 产品可用 | 13/13 PASS 仍可能真实失败 | `packages/cli/src/commands/e2e.ts:81-141`, `packages/cli/src/commands/release.ts:15-27` | +| P2 | ContextAssembler 输出仍偏 string content,不是完整 canonical content blocks | FR-013/FR-014 一致性不足 | `packages/runtime/src/context/ContextAssembler.ts:22-26`, `packages/runtime/src/context/ContextAssembler.ts:263-307` | + +### 1.3 FR-001~FR-020 覆盖度 + +| FR | 判断 | +|---|---| +| FR-001 CLI Startup/Init | 部分达成。CLI/init/start 有实现,但 doctor/project layout/recovery 仍不完整。 | +| FR-002 Project-Local State | 部分达成。`.air` 能创建,但 layout 与 baseline 有偏差,状态事实源未完全闭合。 | +| FR-003 Session Persistence | 部分达成。DB/schema 能力存在,但主路径不完整写入 messages/tasks/tool_runs/artifacts。 | +| FR-004 Event-Driven Runtime | 未达成。EventStore 能力存在,主链路仍大量内存状态/手工 snapshot。 | +| FR-005 Main Agent Conversation | 部分达成。分类/回答存在,但无上下文、无完整 message persistence。 | +| FR-006 Architecture Designer | 未达成。类存在,未进入主运行路径。 | +| FR-007 Scheduler/TaskGraph | 部分达成。状态机骨架存在,完成判定/WorkerResult/事件持久化不足。 | +| FR-008 Independent Worker Agents | 部分达成。子进程/角色存在,但 IPC contract 与结果事件链不完整。 | +| FR-009 Execution Primitives | 部分达成偏低。ToolRegistry/PermissionEngine 有入口,但 read-before-edit、verification-before-completion 不能保证。 | +| FR-010 Built-in Tools | 部分达成。工具注册数量覆盖,但结果 shape、streaming、C++/debug/gui/network 深度不足。 | +| FR-011 Permission/Security | 部分达成。引擎存在,默认/交互闭环/备份策略不足。 | +| FR-012 Plugin/Capability Foundation | 未达成运行级。CapabilityRegistry 未接 RuntimeApp。 | +| FR-013 Provider Layer | 部分达成。Adapter 路径存在,但 canonical content blocks 未贯穿。 | +| FR-014 Context/Compaction | 部分达成。ContextAssembler/CompactionPolicy 有骨架,但 MainAgent/Worker 主路径未使用。 | +| FR-015 Artifact/Evidence | 部分达成。Store/事件类型存在,但主任务完成未强制 evidence-backed。 | +| FR-016 TUI/HUD | 部分达成。渲染可用,输入和 ProjectionStore-only 真实链路不足。 | +| FR-017 C++ Complete Workflow | 未达成。工具存在但未形成完整 detect→configure→build→test→debug→fix→review→verify。 | +| FR-018 Doctor | 部分达成。诊断存在,fix/capability/display/network/toolchain 集成不足。 | +| FR-019 Logging/Diagnostics | 部分达成。Logger/DeveloperLogEncryptor 存在,但完整日志策略未证实闭合。 | +| FR-020 Release Gate | 未达成。当前 gates 不覆盖真实 Alpha 成功条件。 | + +### 1.4 Domain Invariants 核对 + +| Invariant | 判断 | +|---|---| +| INV-1 Session-DB state columns only by EventStore projection | 运行路径未满足。EventStore.project 有能力,但 Scheduler 主路径不完整使用 durable events。 | +| INV-2 Cross-DB/external writes outbox single writer | 部分满足。KnowledgeStore/ArtifactStore 有意图,但主路径 evidence/artifact 不强制闭合。 | +| INV-3 Side effects only through ToolRegistry + PermissionEngine | 部分满足。Worker 工具走 parent ToolRegistry,但 Permission ask_user 无闭环,verification/read-before-edit 不足。 | +| INV-4 Import/dependency one-way | 静态上大体满足,但 toolchain-cpp 未以 capability boundary 真实接入。 | +| INV-5 EventBus transport only, SQLite source of truth | 原则部分实现,真实运行未满足。run.ts 手工 snapshot,ProjectionStore rebuild/repos 链路不完整。 | + +--- + +## 2. 开发工程师审查 + +### 2.1 工程结论 + +工程视角判定:**核心执行链路存在多个发布阻断缺陷**。尤其是 Worker 结果无法可靠进入 Scheduler 状态机、`shell.run` 流式工具接口与 `ToolRegistry` 不兼容、CLI 确认流失效、MainAgent 未使用真实 ContextAssembler。现有 E2E 门禁包含源码字符串检查和 mock lifecycle,不能证明端到端可运行。 + +### 2.2 工程 P0 + +| ID | 问题 | 证据 | 影响 | 阻断发布 | +|---|---|---|---|---| +| P0-1 | `ToolRegistry.call()` 不能执行 `shell.run`,因为 executor 是 `async function*` | `packages/runtime/src/tools/shell/index.ts:35`, `packages/runtime/src/tools/ToolRegistry.ts:254`, `packages/runtime/src/tools/ToolRegistry.ts:302` | Worker 调 shell.run 时父进程会把 generator 当 result 处理 | 是 | +| P0-2 | `shell.run` 声称 streaming,但没有 yield stdout/stderr chunk,最终 envelope 缺 `metadata.is_final` | `packages/runtime/src/tools/shell/index.ts:64-76`, `packages/runtime/src/tools/shell/index.ts:98-109`, `packages/runtime/src/tools/ToolRegistry.ts:152-166` | `call_streaming()` 可能返回 `no_final_result` | 是 | +| P0-3 | Scheduler 不检查 worker exit/result,直接 completed | `packages/runtime/src/scheduler/Scheduler.ts:251-260`, `packages/runtime/src/workers/WorkerManager.ts:237-244`, `packages/workers/src/main.ts:93-96` | Worker 失败/blocked/未上报都可显示成功 | 是 | +| P0-4 | Worker 完成结果只更新内存 handle,没有 durable task event | `packages/runtime/src/workers/WorkerManager.ts:237-244`, `packages/runtime/src/events/EventSchemaRegistry.ts:59-61`, `packages/runtime/src/scheduler/Scheduler.ts:274-277` | 投影、恢复、任务状态与真实结果脱节 | 是 | +| P0-5 | WorkerResult 字段映射错误 | `packages/workers/src/roles/ExecutorRole.ts:11-17`, `packages/workers/src/roles/ExecutorRole.ts:139-143`, `packages/runtime/src/workers/WorkerManager.ts:324-339`, `packages/contracts/src/worker-result.ts:74-88` | changed_files 为空、verification 类型不符、证据丢失 | 是 | +| P0-6 | CLI destructive confirmation 没有 y/n 流程 | `packages/runtime/src/agents/main/MainAgent.ts:77-80`, `packages/cli/src/commands/run.ts:118-132`, `packages/runtime/src/agents/main/MainAgent.ts:204-211` | delete/remove/drop 提示确认但实际不等确认直接执行 | 是 | +| P0-7 | `classify_via_llm` 调 ProviderManager API 错误 | `packages/runtime/src/agents/main/MainAgent.ts:184-189`, `packages/contracts/src/provider.ts:179-190`, `packages/llm/src/ProviderManager.ts:83-92` | 启用 LLM classify 会异常或 fallback,LLM 分类实际未上线 | 是 | + +### 2.3 工程 P1 + +| ID | 问题 | 证据 | 影响 | +|---|---|---|---| +| P1-1 | MainAgent `chat_with_llm` 未使用 ContextAssembler | `packages/runtime/src/agents/main/MainAgent.ts:102-118`, `packages/runtime/src/context/ContextAssembler.ts:74-105` | 主 Agent 对项目上下文失明 | +| P1-2 | `context.assemble` 工具是 stub | `packages/runtime/src/tools/context/index.ts:46-56`, `packages/runtime/src/tools/BuiltInToolRegistrar.ts:62` | 不能作为真实上下文工具 | +| P1-3 | ExecutorRole DONE false-positive | `packages/workers/src/roles/ExecutorRole.ts:136-144`, `packages/workers/src/roles/ExecutorRole.ts:151-152` | 工具失败也可能 completed | +| P1-4 | ExecutorRole 转义顺序破坏源码 | `packages/workers/src/roles/ExecutorRole.ts:222` | 字符串字面量、JSON、正则、路径可能被篡改 | +| P1-5 | 多文件协调缺少验收闭环 | `packages/workers/src/roles/ExecutorRole.ts:147-153`, `packages/workers/src/roles/ExecutorRole.ts:162-168` | 缺文件/缺 build/缺 test 仍成功 | +| P1-6 | Worker IPC 未处理 heartbeat/event | `packages/workers/src/WorkerRuntime.ts:113-135`, `packages/runtime/src/workers/WorkerManager.ts:160-253`, `packages/runtime/src/scheduler/Scheduler.ts:176-177` | 长任务会被误判 stalled/lost | +| P1-7 | `send_and_wait()` 不等 ACK,只 sleep 100ms | `packages/runtime/src/workers/WorkerManager.ts:367-379` | agent.start race condition | +| P1-8 | BuiltInToolRegistrar additional tools 返回非合同 envelope | `packages/runtime/src/tools/BuiltInToolRegistrar.ts:180-183`, `packages/runtime/src/tools/BuiltInToolRegistrar.ts:237-239`, `packages/contracts/src/tool.ts:76-83` | WorkerManager 读取 output 时丢失 content | +| P1-9 | `ToolRegistry` ask_user/deny 返回空 call_id | `packages/runtime/src/tools/ToolRegistry.ts:270-275` | 权限错误不可关联原始 call | +| P1-10 | E2E gates 中 worker lifecycle 是 mock | `packages/cli/src/commands/e2e.ts:121-123`, `packages/runtime/test/e2e/worker-fixture.test.ts:107-141` | 门禁通过不证明真实链路工作 | + +### 2.4 工程修复优先级 + +1. 统一工具执行合同:普通 executor 与 streaming executor 明确分离,`shell.run` 必须可通过 `call_streaming()` 产出 final envelope。 +2. 重做 WorkerManager/Scheduler 结果闭环:监听 exit、`worker.result`、heartbeat、event,并按 WorkerResult.status 发 durable task events。 +3. 修复 CLI confirmation:确认态只显示提示并等待 y/n,拒绝不创建任务。 +4. 对齐 WorkerResult/ExecutorResult envelope。 +5. MainAgent 和 context.assemble 统一接入 ContextAssembler。 +6. 修复 `classify_via_llm` Provider API。 +7. 修复 ExecutorRole DONE 判定、失败处理、acceptance criteria 验证。 +8. 替换假 E2E,加入真实 worker spawn、tool.call roundtrip、shell.run、worker.result→event→projection 测试。 + +--- + +## 3. 真实用户 / UAT 审查 + +### 3.1 用户体验评分 + +**4/10**。 + +UAT 在临时目录 `/tmp/aircoding-uat-xOnpw0` 完成,未修改仓库源码。运行入口为 `/home/airlongdian/.local/bin/air`,指向项目源码 `packages/cli/src/index.ts`。 + +基础 CLI 能启动、初始化、创建文件、显示 slash 命令和状态;但自然语言追问、C++ 编译运行闭环、危险操作确认门、TUI 输入模型都存在明显失败或误导性成功。 + +### 3.2 UAT 测试矩阵 + +| 场景 | 结果 | 观察 | +|---|---|---| +| `air init` | PASS | 成功创建 `.air/shared`、`.air/local`、`.air/sessions`、`.air/logs`、`.air/workspaces` 与 `project.json`。 | +| `air run` 任意目录启动 | PARTIAL | 未初始化目录会自动 init 并启动,体验上可用。 | +| 创建 `hello.txt` | PASS with noise | 文件成功创建,但 worker 输出 `[Worker] exited with code 0 (error): Unrecoverable error occurred`,随后 CLI 显示 `Task complete. Scheduler: COMPLETED`。 | +| 追问“我的项目有哪些文件?” | FAIL | 被分类为 `[answer]`,没有调用 `fs.list` / `project.scan`,无法真实回答项目文件。 | +| C++ hello world 编译运行修复闭环 | FAIL | 创建 `main.cpp` 和 `CMakeLists.txt`,但没有生成 `build/hello`,CLI 仍报告 `Scheduler: COMPLETED`。 | +| 危险操作确认门 | FAIL | 输入 `请删除 hello.txt` / `delete hello.txt` 直接派发任务,输入 `n` 被当普通问答处理。 | +| `/help` | PASS | 输出清晰。 | +| `/status` | PARTIAL | 显示 Scheduler/workers/DB,但普通用户解释性一般。 | +| `/tools` | PASS | 列出 39 个工具和分类。 | +| `/tasks` | PARTIAL | 空任务图无“暂无任务”说明,历史感弱。 | +| `/results` | PARTIAL | 会把 `.air/logs/air.log`、`.air/shared/project.json`、`rules.md` 等系统文件当任务产物。 | +| TUI 是否可输入任务 | FAIL | TUI 和 readline 共享 stdin,输入 `h`、`1` 被当成自然语言任务。 | +| TUI 状态反馈 | PARTIAL | 能渲染状态,但 worker error 与 task completed 冲突。 | +| `history` | PARTIAL | 显示 session id 和 DB 大小,但没有任务摘要/时间/项目路径。 | +| `session list/inspect` | PARTIAL | 能列 active session 和 DB 路径,但用户不易理解。 | +| `doctor` | PARTIAL | 临时目录缺 `package.json` / `tsconfig.json` 报 FAIL,对“任意目录”用户可能误导。 | + +### 3.3 用户最痛问题 + +1. **危险操作确认门不可用**:用户拒绝 `n` 不会取消,破坏性操作不能发布。 +2. **完成状态不可信**:C++ 没有实际 build/run 产物仍显示 COMPLETED。 +3. **追问项目文件不走工具**:典型项目查询被普通 LLM 问答处理。 +4. **TUI 与命令行输入冲突**:快捷键看似存在,实际被 readline 吃掉。 +5. **结果列表污染**:`.air` 内部日志/配置被当作 Produced files。 + +### 3.4 false-positive 风险 + +- fake LLM 明确返回了 `shell.run("cmake -S . -B build && cmake --build build && ./build/hello")`,但 AirCoding 没有产生 build 产物,说明问题在工具执行/Worker 结果处理,不是模型质量。 +- `air doctor` 在任意目录将缺少 `package.json` / `tsconfig.json` 标为 FAIL,对任意目录启动场景会误导用户。 +- CLI 的 `Scheduler: COMPLETED` 与 worker “error” 同时出现,用户无法判断真实状态。 + +### 3.5 是否可演示/可发布 + +- **可演示**:只适合内部有限演示 `air init`、`air run`、`/help`、`/tools`、简单创建 `hello.txt`。 +- **不可发布**:不能演示危险操作、C++ 编译闭环、TUI 快捷键、复杂追问。 + +--- + +## 4. QA / 发布门禁审查 + +### 4.1 QA 总结 + +- TypeScript typecheck:PASS。 +- `air e2e`:13/13 PASS。 +- 结论:现有 gates 不能作为 V1.0.0 Alpha 发布门禁。它们主要覆盖类型、依赖边界、静态源码断言和小范围单元测试,未覆盖真实用户成功完成任务。 + +### 4.2 QA 测试矩阵 + +| 项 | 现状 | 审计结果 | +|---|---|---| +| TypeScript typecheck | `tsconfig.check.json` 覆盖 7 个 package references | PASS | +| `air e2e` | 13/13 PASS | PASS,但门禁有效性不足 | +| P1 Storage/Events | regression/storage 类测试 | PASS,但偏单元 | +| P2 Tools/Permission | permission / command risk / tool stubs | PASS,但没有真实 output shape gate | +| P3 Provider/Context | llm tests + ContextAssembler regression | PASS,但没有 answer-mode 上下文追问 gate | +| P4 Worker IPC | worker fixture + exit/result envelope regression | PASS,但 worker fixture 是 mock,不是真 spawn round-trip | +| P5 C++ Toolchain | toolchain-cpp tests | PASS,但没有复杂 C++ 用户任务生成→构建→验证闭环 | +| P6 Projection/TUI | e2e.ts 引用 `projection-store-apply.test.ts` | 可疑:该文件不存在,当前 P6 实际覆盖弱化 | +| P7 Agents | direct-mode / architecture-review fixtures | PASS,但未覆盖 run.ts confirmation 交互路径 | +| P8 Full regression | runtime regression directory | PASS,但大量测试是源码字符串断言 | +| ToolResultEnvelope shape gate | 无完整 gate | FAIL/缺失 | +| shell.run functional gate | 无真实 functional gate | FAIL/缺失 | +| answer-mode context gate | 无 | FAIL/缺失 | +| simple file create gate | 不是 e2e gate 的一部分 | 缺失 | +| complex C++ generate gate | 不是 e2e gate 的一部分 | 缺失 | +| Worker IPC llm/tool round-trip | mock fixture,不是真 WorkerManager + WorkerRuntime + ToolRegistry 往返 | 缺失 | +| false-positive 成功测试 | 无 | 缺失 | + +### 4.3 缺失发布 gates + +1. `gate:tool-envelope-shape`:遍历所有注册工具,断言成功结果必须符合 `ToolResultEnvelope{status, output, metadata}`。 +2. `gate:shell-run-functional`:通过 ToolRegistry 调用 `shell.run("printf ok")`,断言 exit_code/stdout。 +3. `gate:answer-context`:追问项目文件/上一步结果必须引用真实上下文。 +4. `gate:simple-file-create`:真实执行创建文件任务,断言磁盘内容和 task result。 +5. `gate:complex-cpp`:生成 C++ + CMake,断言多文件、非 stub、可 build/run 或明确失败。 +6. `gate:worker-ipc-roundtrip`:真实 spawn worker,覆盖 `worker.ready → agent.start → llm.request → tool.call → tool.result → worker.result`。 +7. `gate:confirmation`:破坏性请求必须停在确认态,拒绝后不得执行。 +8. `gate:false-positive-success`:工具失败时 CLI/agent 结果必须 failed/blocked,不得打印成功。 + +### 4.4 QA P0/P1/P2 + +| 优先级 | 问题 | +|---|---| +| P0 | e2e gates 失真,13/13 PASS 不能证明用户成功场景。 | +| P0 | ToolResultEnvelope shape 不一致未被 gate 捕获。 | +| P0 | shell.run functional gate 缺失且实现存在 AsyncGenerator 解包风险。 | +| P0 | Worker IPC tool.call / llm.request 未真实端到端验证。 | +| P0 | confirmation CLI 路由缺失。 | +| P0 | false-positive 成功缺少门禁。 | +| P1 | answer-mode context gate 缺失。 | +| P1 | complex C++ generate gate 缺失。 | +| P1 | Scheduler 完成判定偏乐观。 | +| P1 | P6 gate 引用缺失测试文件,覆盖弱化。 | +| P2 | release command 比 `air e2e` 更弱,只跑少量静态/回归门。 | +| P2 | e2e gate 标签 P1-P8 粒度粗,缺少用户场景诊断输出。 | + +--- + +## 5. 与前几轮审查对比 + +已有报告: + +- `集成测试阶段MiniMax-M3审查结果.md` +- `集成测试阶段Deepseek审查结果.md` +- `集成测试阶段GLM5.1审查结果.md` + +| 问题 | MiniMax-M3 | Deepseek | GLM5.1 | Gpt5.5 本轮 | +|---|---|---|---|---| +| MainAgent 无上下文 | 已指出 | 已指出 | 已指出 | 仍成立 | +| shell.run 不可用 / 生成器未解包 | 已指出 | 已指出 | 已指出 | 仍成立,且 streaming final envelope 也有问题 | +| BuiltInToolRegistrar content vs output | 已指出 | 已指出 | 已指出 | 仍成立 | +| Scheduler MONITORING 假完成 | 已指出 | 已指出 | 已指出 | 仍成立 | +| ArchitectureDesigner 死代码 | 已指出 | 已指出 | 已指出 | 仍成立 | +| PermissionEngine 未完整闭环 | 已指出 | 已指出 | 已指出 | 部分有 evaluate,但 ask_user/交互/事件闭环仍不足 | +| TUI 无输入 / ProjectionStore 偏离 | 已指出 | 已指出 | 已指出 | 仍成立,UAT 确认快捷键与 readline 冲突 | +| FR-017 C++ 主链路不联动 | 已指出 | 已指出 | 已指出 | 仍成立,UAT 确认未 build/run | +| E2E gates 失真 | 已指出 | 已指出 | 已指出 | 仍成立,13/13 PASS 仍不能证明可发布 | +| WorkerResult envelope 映射错误 | 部分涉及 | 部分涉及 | 部分涉及 | 本轮工程视角明确列为 P0 | +| Worker real IPC round-trip 缺失 | 部分涉及 | 部分涉及 | 部分涉及 | 本轮 QA 明确确认 P4 fixture 是 mock | + +本轮新增/强化结论: + +1. `shell.run` 不只是 AsyncGenerator 未解包,streaming 路径也缺 final envelope 约定。 +2. WorkerResult/ExecutorResult 字段映射错误会导致 changed_files、verification、evidence 丢失。 +3. Worker checkpoint/heartbeat/event 未完整处理,会影响长任务状态判断。 +4. UAT 实测确认 TUI 快捷键会被 readline 当成自然语言任务。 +5. QA 确认 P4 worker lifecycle gate 仍是 mock,不是真实发布级 round-trip。 + +--- + +## 6. 发布建议与最小阻断修复清单 + +### 6.1 发布建议 + +**不发布 V1.0.0 Alpha。** + +当前可对外表述只能是: + +> 基础 monorepo、contracts、工具注册、Worker IPC、TUI 渲染骨架已存在;尚未达到 baseline 所要求的完整 Alpha 产品闭环。 + +### 6.2 最小阻断修复清单 + +1. 修复 ToolResultEnvelope 统一性:所有工具必须返回 `status/output/error/metadata`,消除非契约 `content` shape。 +2. 修复 `shell.run`:普通调用和 streaming 调用都必须产出可消费的最终结果,ToolRegistry 不得返回裸 AsyncGenerator。 +3. Scheduler 必须基于 WorkerResult.status 产生 durable `task.completed/task.failed/task.blocked/task.cancelled` events,不允许 `has_running()==false` 直接完成任务。 +4. WorkerResult/ExecutorResult 必须对齐 contracts,保留 changed_files、verification、evidence_refs。 +5. MainAgent/Executor/Reviewer/Debugger 必须使用 ContextAssembler,answer 分支必须携带项目/会话/工具历史上下文。 +6. ArchitectureDesigner 必须进入 MainAgent DELEGATING 前的 architecture impact gate。 +7. RuntimeApp 必须实例化并接入 CapabilityRegistry,统一 toolchain-cpp 与 BuiltInToolRegistrar 的 C++ 工具路径。 +8. TUI 输入与 CLI readline 必须合并为单一交互通道,或明确将 TUI 降级标识为非交互 HUD(但这会违背原始需求,不建议)。 +9. PermissionEngine `ask_user` 必须有 permission.prompt.requested/resolved 事件与 CLI/TUI 交互闭环。 +10. Release gates 必须新增真实用户成功场景: + - `air init` 初始化目录布局校验; + - 简单文件创建并验证内容; + - `shell.run("echo ok")`; + - `project.scan` / `cpp.detect` output shape; + - C++ fixture configure/build/test; + - 失败工具不能显示成功; + - 上下文追问能回答真实文件; + - destructive request confirmation; + - WorkerResult failed/blocked 不得被标 completed; + - ProjectionStore 从 SQLite rebuild 后 TUI snapshot 一致。 + +--- + +## 7. 最终判断 + +本轮 Gpt5.5 审查与 MiniMax-M3、Deepseek、GLM5.1 三轮结论一致: + +- **不是测试覆盖不足的小问题,而是主链路事实源、工具契约、Worker 结果、上下文、确认门、发布门禁共同未闭合。** +- 继续只跑现有 `air e2e` 得到 13/13 PASS 没有发布意义。 +- 修复必须进入源码主路径,不能通过新增旁路命令、文档说明或演示规避。 + +**发布状态:BLOCKED。** diff --git a/集成测试阶段MiniMax-M3审查结果.md b/集成测试阶段MiniMax-M3审查结果.md new file mode 100755 index 0000000..24166f7 --- /dev/null +++ b/集成测试阶段MiniMax-M3审查结果.md @@ -0,0 +1,196 @@ +# 集成测试阶段 MiniMax-M3 审查结果 + +**审计日期**: 2026-06-05 +**项目**: AirCoding V1.0.0 Alpha +**审计员**: MiniMax-M3 (整合阶段) +**审计模式**: 四视角交叉审计(系统架构师 / 开发工程师 / 真实用户 / 测试工程师) + +--- + +## 0. 核心矛盾 (E2E Gates 失真) + +13/13 E2E gates 全部通过,但用户视角测试**真实任务成功率近乎为零**。 + +| 维度 | 表现 | +|---|---| +| tsc typecheck | 0 errors | +| 单元测试 | 169/169 passed | +| E2E gates | 13/13 passed | +| 真实用户场景 | **多数失败** | + +E2E gates 仅验证"测试能跑通"和"组件存在",**从未验证"用户场景成功"**。所有 gate 仅检查语法、类型、stub 数量、单元测试通过率;没有一项 gate 验证"LLM 调通 + 文件真的写出 + 多步骤任务真完成"。 + +--- + +## 1. 系统架构师审查 + +### FR-005 (Main Agent 对话) — 部分实现 + +`MainAgent.chat_with_llm()` 每次调用仅构造 system+user 两条消息,**无 conversation history**,无 context reference,无 Anthropic canonical content blocks——严重违反 FR-014 上下文分层。 + +追问"现在什么情况"时,LLM **没有上下文**,是 stateless 单轮调用,必然幻觉。 + +### FR-016 (TUI) — 不支持交互输入 + +`TuiApp.setup_input` 只处理 `q/1/2/3/4/h` 键盘事件,**无文本输入路径**。`run.ts` 用 `readline` 在 TUI 之外独立处理 stdin,TUI 和 CLI 互相竞争字符。TUI 实际是**只读 viewer,不能输入任务**。 + +### FR-007/008 (Scheduler/Worker) — 链路通但判定错误 + +Worker 真实 spawn,IPC 真实 NDJSON,tool.call / llm.request 通过 IPC 回父进程由 ProviderManager 真实调用 LLM——**链路是通的**。 + +但 `Scheduler.step` 的 `MONITORING` 状态存在严重 bug:worker 还在跑就 mark `completed`,任务可能"假完成"。 + +### FR-009 (执行原语) — 不强制 read-before-edit + +`ExecutorRole` 直接接受代码块写入,**不强制 read-before-edit**——违反 FR-009。run.ts 的"扫描最近 60 秒修改的文件"是 post-hoc 发现,不验证文件来源、不执行 verification-before-completion。 + +### FR-017 (C++ 完整闭环) — 工具链与主链路不联动 + +`toolchain-cpp` 实现了 cpp.detect / cpp.build / cpp.test 工具,但**无 fixture 项目、无自动 configure→build→test→fix→review e2e 集成**。Executor 用 fs.write 写 C++ 源文件,**不调用任何 cpp.* 工具**。C++ 工具链对用户不可见地存活,但与主链路不联动。 + +### 未对齐设计的关键差异 + +| 设计 | 实际 | +|---|---| +| ProjectionStore 消费 DB+EventBus | `run.ts` 直接 `receive_snapshot` 构造假数据绕开 EventStore | +| 严格状态机 12 态 | 真实遍历,但 MONITORING 完成判定错误 | +| 5 角色独立 worker | Debugger/Compactor/ExperienceMiner 未被 run.ts 触发 | +| Anthropic canonical messages | MainAgent 用 string content,非 content block[] | + +--- + +## 2. 开发工程师 Bug 报告 + +### P0 阻塞 (2) + +**Bug #2 — Scheduler 早判完成** (Scheduler.ts:252-260) +当 `worker_manager.has_running() === false` 时,将**所有** `running` 状态任务直接标记为 `completed`,**不管 worker 是否实际成功**。失败任务可能显示为成功。 + +**Bug #6 — MainAgent 确认门形同虚设** (run.ts:108-112) +`MainAgent.classify()` 对"delete/remove"类消息返回 `{action: 'delegate', response: 'Are you sure? (y/n)'}`,但 `run.ts:115` 直接 `console.log` `classification.response` 并 `rl.prompt()`——**用户的 y/n 永远到不了 `MainAgent.handle_confirmation()`**。破坏性操作无防护。 + +### P1 重要 (3) + +**Bug #3 — ExecutorRole 转义顺序错误** (ExecutorRole.ts:222) +代码块内容转义还原顺序:`\\n → \n`, `\\t → \t`, `\\" → "`, `\\\\ → \\`。若 LLM 输出 `\n`(单反斜杠 n),会被错误地换成真换行,破坏 JSON/Python 字面量。 + +**Bug #4 — MainAgent 不带上下文** (MainAgent.ts:73) +`chat_with_llm(message)` 只发送单轮 system + user。追问"现在什么情况"时,**LLM 不知道项目里有啥**。 + +**Bug #5 — DONE 竞态** (ask.ts:182-186) +若 LLM 单次输出既有 `fs.write` 又有 `DONE`,执行完 tool_call 后立即返回 `completed`。**剩余未完成的 acceptance_criteria 被静默忽略**。 + +**Bug #7 — 多文件任务漏写** +第一次写到 `src/main.cpp`(正确),第二次写到根 `main.cpp`(简化版 stub)。两个 task 缺乏协调。 + +### P2 nice-to-have (2) + +- **Bug #1 — 退出码误判** (WorkerProcess.ts:127): `code || 1` 误判为错误 +- **Bug #8 — `has_running` 误判** (WorkerManager.ts:303-305): `ready` 状态也算 running + +--- + +## 3. 真实用户测试报告 (UAT) + +| 用例 | 状态 | 关键现象 | +|---|---|---| +| `air init` | ✅ PASS | 创建 .air/ 目录和 project.json | +| `air ask "..."` 写文件 | ✅ PASS | fs.write 工作 | +| TUI 启动 | ✅ PASS | OpenTUI 渲染正确 | +| `air doctor` | ✅ PASS | 检测 bun/sqlite/git/shell | +| **shell.run 任意命令** | ❌ **FAIL** | 10/10 次返回 `Error: undefined` | +| **追问项目内容** | ❌ **FAIL** | LLM 答"我没有文件系统访问" | +| **危险操作 (删除)** | ❌ **FAIL** | 静默通过,文件仍在 | +| **false-positive 成功** | ❌ **FAIL** | tool 报错但 CLI 仍打印 `✅` | +| **多文件 C++ 任务** | ❌ **FAIL** | main.cpp 内容退化为 stub | +| `air doctor --fix` | ❌ FAIL | 标 [fixable] 不修 | + +### 用户最痛 3 个问题 +1. **shell 工具整体坏死**,让 AI 跑任何命令都失败 10/10 次。 +2. **报错说"成功",实际没干**——用户以为工作流跑通了,回头看磁盘空的。false-positive 比直接报错危险十倍。 +3. **TUI 和 CLI 是两套东西**,`air ask` 能用工具,`air run` TUI 输入路径不可见(实际仅 readline 跑,渲染层 OK 但端到端未跑通)。 + +--- + +## 4. QA 测试矩阵 + +| Test | 状态 | 关键观察 | +|---|---|---| +| 1: 简单文件创建 | PASS | hello.txt 创建成功,内容正确。Worker stderr 异常但不影响产出。 | +| 2: 多文件 (C++ + CMake) | **PARTIAL** | main.cpp 内容退化为 stub,CMakeLists.txt 正确。两文件被写到不同位置 | +| 3: 追问项目内容 | **FAIL** | agent.handle_user_message("我的项目有哪几个文件?") 走 answer 分支,LLM 幻觉 | +| 4: TUI 启动 | PASS | OpenTUI 渲染正确,状态栏正常 | +| 5: E2E gates | PASS | 13/13 (但 gates 与用户场景脱节) | + +--- + +## 5. 用户核心痛点 (按严重度排序) + +| 排名 | 问题 | 严重度 | 触发场景 | +|---|---|---|---| +| 1 | **shell.run 工具完全坏死** | P0 阻塞 | 任何命令执行 (编译、运行、git) | +| 2 | **追问上下文失效** (LLM 答"我没文件系统") | P0 阻塞 | 完成任务后问"现在什么情况" | +| 3 | **危险操作无 confirmation 路由** | P0 安全 | 删除、覆盖文件 | +| 4 | **Scheduler 早判完成** (假完成) | P0 阻塞 | 多步任务 | +| 5 | **TUI 无文本输入路径** | P0 用户体验 | TUI 实际只读 | +| 6 | **多文件任务漏写 / 覆盖** | P1 | 复杂任务 | +| 7 | **false-positive 成功 banner** | P1 | 任何报错场景 | +| 8 | **DONE 竞态导致提前退出** | P1 | LLM 同 turn 输出 tool+DONE | +| 9 | **ExecutorRole 转义顺序错误** | P1 | LLM 输出含 `\n` 字符 | +| 10 | **退出码误判** (stderr 噪音) | P2 | 排查时迷惑 | + +--- + +## 6. 根因分析 + +E2E gates 框架本身存在结构性缺陷: + +- 仅做静态检查:tsc/depcruise/单元测试 +- 不做端到端真实任务验证 +- 缺"用户成功完成一个 C++ 任务"等综合场景 gate + +需要新增的 gates: +- 真实 LLM 端到端任务完成 +- 多文件任务产出验证 +- shell.run 工具真实执行 +- 上下文追问验证 +- 危险操作 confirmation 验证 + +--- + +## 7. 修复建议 (按优先级) + +### 立即修复 (P0, 阻塞发布) +1. 修复 `shell.run` AsyncGenerator 序列化问题 +2. 修复 Scheduler MONITORING 早判完成 bug +3. MainAgent 注入工具/项目上下文到 LLM 调用 +4. 修复 confirmation 路由 (run.ts → MainAgent.handle_confirmation) +5. TUI 增加文本输入路径 (或移除 TUI 包装直接 CLI) + +### 必须修复 (P1, 关键) +6. ExecutorRole 转义顺序 +7. DONE 竞态检测 +8. 多文件任务协调 (检查文件已存在) +9. Tool 失败时正确报失败 (false-positive 修复) +10. 真实集成测试加入 E2E gates + +### 长期改进 (P2) +11. 退出码误判 +12. has_running 误判 +13. Worker stderr 去噪 +14. Anthropic canonical content blocks 完整支持 + +--- + +## 8. 综合判断 + +**V1.0.0 Alpha 不应发布**。 + +- 13/13 E2E gates passed 但**用户场景真实成功率极低**——这是 E2E gate 设计缺陷 +- 5 个 P0 问题中,3 个是用户每次使用都会触发的(shell 坏死、追问失效、确认门失效) +- C++ 工作流工具链与主链路未联动,FR-017 名存实亡 +- TUI 渲染层 OK 但无输入路径,FR-016 名存实亡 + +修复 P0 后必须**用真实任务重新端到端测试**,而不是用 E2E gates 替代。需要在 E2E gates 框架中**加入"真实 LLM 任务闭环"维度**,否则同样的审计盲点会再次出现。 + +**核心教训**: 代码质量指标 (tsc/stub 数/单元测试) 与用户可用性指标是正交的。质量分高 ≠ 产品可用。后续每个审计必须包含**真实任务端到端测试**作为硬性 gate。