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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -81,11 +81,18 @@ export async function runCommand(project_path?: string): Promise<void> {
|
||||
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<string, { title: string; files: Array<{ path: string; size: number }>; state: string }>()
|
||||
let pendingConfirmation: string | undefined
|
||||
|
||||
console.log('')
|
||||
console.log('══════════════════════════════════════════════')
|
||||
console.log(' AirCoding v1.0.0-alpha')
|
||||
@@ -93,29 +100,7 @@ export async function runCommand(project_path?: string): Promise<void> {
|
||||
console.log(' Type your task, or /help for commands, Ctrl+C to quit')
|
||||
console.log('══════════════════════════════════════════════\n')
|
||||
|
||||
// Interactive input loop
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
||||
rl.prompt()
|
||||
|
||||
rl.on('line', async (line: string) => {
|
||||
const input = line.trim()
|
||||
if (!input) { rl.prompt(); return }
|
||||
|
||||
// Handle slash commands
|
||||
if (input.startsWith('/')) {
|
||||
await handleSlashCommand(input, app, runtime, tui, rl)
|
||||
rl.prompt()
|
||||
return
|
||||
}
|
||||
|
||||
// Route through MainAgent
|
||||
const classification = await agent.handle_user_message(input)
|
||||
console.log(`[${classification.action}]`)
|
||||
|
||||
if (classification.action === 'answer') {
|
||||
console.log('\n' + (classification.response || 'No response') + '\n')
|
||||
} else if (classification.action === 'delegate') {
|
||||
// Create task and dispatch through Scheduler
|
||||
const dispatchTask = async (input: string) => {
|
||||
const taskId = `task_${randomUUID().slice(0, 8)}`
|
||||
app.scheduler.create_tasks([{
|
||||
id: taskId,
|
||||
@@ -127,21 +112,61 @@ export async function runCommand(project_path?: string): Promise<void> {
|
||||
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
|
||||
let finalState: string
|
||||
try {
|
||||
finalState = await runPromise
|
||||
} finally {
|
||||
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()
|
||||
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,
|
||||
@@ -156,6 +181,53 @@ export async function runCommand(project_path?: string): Promise<void> {
|
||||
blockers: [],
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// Interactive input loop
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
||||
rl.prompt()
|
||||
|
||||
rl.on('line', async (line: string) => {
|
||||
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, taskResults)
|
||||
rl.prompt()
|
||||
return
|
||||
}
|
||||
|
||||
// Route through MainAgent
|
||||
const classification = await agent.handle_user_message(input)
|
||||
console.log(`[${classification.action}]`)
|
||||
|
||||
if (classification.action === 'answer') {
|
||||
console.log('\n' + (classification.response || 'No response') + '\n')
|
||||
} else if (classification.action === 'delegate') {
|
||||
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,7 +249,7 @@ export async function runCommand(project_path?: string): Promise<void> {
|
||||
await new Promise(() => {}) // Wait forever
|
||||
}
|
||||
|
||||
async function handleSlashCommand(input: string, app: any, runtime: any, tui: any, rl: any): Promise<void> {
|
||||
async function handleSlashCommand(input: string, app: any, runtime: any, tui: any, rl: any, taskResults?: Map<string, any>): Promise<void> {
|
||||
const cmd = input.slice(1).toLowerCase()
|
||||
|
||||
switch (cmd) {
|
||||
@@ -187,9 +259,29 @@ async function handleSlashCommand(input: string, app: any, runtime: any, tui: an
|
||||
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':
|
||||
console.log(`\n Scheduler: ${app.scheduler.get_state()}`)
|
||||
console.log(` Workers: ${app.worker_manager.list().length}`)
|
||||
|
||||
24
packages/cli/test/run-command-regression.test.ts
Executable file
24
packages/cli/test/run-command-regression.test.ts
Executable file
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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,7 +131,22 @@ export class MainAgent {
|
||||
}
|
||||
|
||||
try {
|
||||
const messages = [
|
||||
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 }
|
||||
]
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<ToolResultEnvelope>
|
||||
| AsyncIterable<ToolResultEnvelope>
|
||||
|
||||
export interface ToolExecutor {
|
||||
(call: ToolCall, context: ToolExecutionContext): Promise<ToolResultEnvelope>
|
||||
(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 {
|
||||
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<ToolResultEnvelope> {
|
||||
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<ToolResultEnvelope> {
|
||||
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<ToolResultEnvelope> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<number>((resolve) => {
|
||||
setTimeout(() => {
|
||||
const timeout_id = setTimeout(() => {
|
||||
timed_out = true
|
||||
proc.kill('SIGKILL')
|
||||
resolve(124) // standard timeout exit code
|
||||
}, timeout)
|
||||
|
||||
const exit_code = await new Promise<number>((resolve) => {
|
||||
proc.on('exit', (code) => resolve(code ?? 0))
|
||||
proc.on('error', () => resolve(1))
|
||||
})
|
||||
clearTimeout(timeout_id)
|
||||
|
||||
const exitCode = await Promise.race([
|
||||
new Promise<number>((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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<unknown>
|
||||
@@ -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<unknown> | 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<string, unknown>, handle: WorkerHandle): WorkerResult<unknown> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class WorkerProcess {
|
||||
private proc: ChildProcess | null = null
|
||||
private protocol: WorkerProtocol
|
||||
private message_handlers: Map<string, (msg: WorkerMessage) => 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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
103
packages/runtime/test/regression/release-critical-gates.test.ts
Executable file
103
packages/runtime/test/regression/release-critical-gates.test.ts
Executable file
@@ -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<string, unknown>]>) {
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -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'")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
230
状态交接.md
Executable file
230
状态交接.md
Executable file
@@ -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=<your_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 复现并继续。
|
||||
276
集成测试阶段Deepseek审查结果.md
Executable file
276
集成测试阶段Deepseek审查结果.md
Executable file
@@ -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
|
||||
220
集成测试阶段GLM5.1审查结果.md
Executable file
220
集成测试阶段GLM5.1审查结果.md
Executable file
@@ -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 | 工具报<E585B7><E68AA5>时应显示错误而非成功 |
|
||||
|
||||
### 建议改进 (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*
|
||||
331
集成测试阶段Gpt5.5审查结果.md
Executable file
331
集成测试阶段Gpt5.5审查结果.md
Executable file
@@ -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。**
|
||||
196
集成测试阶段MiniMax-M3审查结果.md
Executable file
196
集成测试阶段MiniMax-M3审查结果.md
Executable file
@@ -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。
|
||||
Reference in New Issue
Block a user