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:
AirCoding
2026-06-05 18:39:10 +08:00
parent a2d7aa0339
commit ddefcbb2b1
24 changed files with 1992 additions and 186 deletions

View File

@@ -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,6 +100,89 @@ 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')
const dispatchTask = async (input: string) => {
const taskId = `task_${randomUUID().slice(0, 8)}`
app.scheduler.create_tasks([{
id: taskId,
type: 'execute',
title: input.slice(0, 80),
description: input
}])
console.log(`Task ${taskId} created. Dispatching worker...`)
const runPromise = app.scheduler.run_until_idle()
let dots = 0
const progressInterval = setInterval(() => {
dots = (dots + 1) % 4
process.stdout.write(`\r Running${'.'.repeat(dots)} `)
}, 500)
let finalState: string
try {
finalState = await runPromise
} finally {
clearInterval(progressInterval)
process.stdout.write('\r \r')
}
console.log(`Task complete. Scheduler: ${finalState}`)
const workerResult = app.worker_manager.get_result_for_task?.(taskId)
const resultFiles = Array.isArray(workerResult?.changed_files) ? workerResult.changed_files : []
const recentFiles: Array<{ path: string; size: number }> = []
if (resultFiles.length > 0) {
const { statSync: st, existsSync: ex } = await import('fs')
for (const file of resultFiles) {
if (!file || file.startsWith('.air/') || file.includes('/.air/')) continue
const full = join(project_root, file)
if (!ex(full)) continue
const s = st(full)
if (s.isFile()) recentFiles.push({ path: file, size: s.size })
}
} else {
const { readdirSync: rd, statSync: st, existsSync: ex } = await import('fs')
const scanDir = (d: string, depth: number) => {
if (depth > 3 || !ex(d)) return
try {
for (const e of rd(d)) {
if (e.startsWith('.')) continue
const p = join(d, e)
try {
const s = st(p)
if (s.isDirectory()) scanDir(p, depth + 1)
else if (s.mtimeMs > Date.now() - 60000) recentFiles.push({ path: p.replace(project_root + '/', ''), size: s.size })
} catch {}
}
} catch {}
}
scanDir(project_root, 0)
}
if (recentFiles.length > 0) {
console.log(' Produced files:')
for (const f of recentFiles.slice(0, 10)) {
console.log(` 📄 ${f.path} (${f.size}B)`)
}
}
taskResults.set(taskId, { title: input.slice(0, 80), files: recentFiles, state: finalState })
runtime.projection_client.receive_snapshot({
session_id,
project_id: app.project_id,
status: finalState === 'COMPLETED' ? 'completed' : 'running',
title: project_root.split('/').pop() || 'AirCoding',
tasks: [{ id: taskId as any, type: 'execute', status: finalState === 'COMPLETED' ? 'completed' : 'running', title: input.slice(0, 80), retry_count: 0, attempts: 1, created_at: new Date().toISOString() }],
agents: [],
tool_runs: [],
command_runs: [],
artifacts: [],
permission_prompts: [],
blockers: [],
updated_at: new Date().toISOString()
})
}
// Interactive input loop
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
rl.prompt()
@@ -101,9 +191,26 @@ export async function runCommand(project_path?: string): Promise<void> {
const input = line.trim()
if (!input) { rl.prompt(); return }
if (pendingConfirmation) {
if (/^(y|yes|是|确认|确定)$/i.test(input)) {
const confirmedInput = pendingConfirmation
pendingConfirmation = undefined
await agent.handle_confirmation(true)
await dispatchTask(confirmedInput)
} else if (/^(n|no|否|取消)$/i.test(input)) {
pendingConfirmation = undefined
await agent.handle_confirmation(false)
console.log('Cancelled. No task was created.\n')
} else {
console.log('Please answer y/n to confirm or cancel the pending destructive request.\n')
}
rl.prompt()
return
}
// Handle slash commands
if (input.startsWith('/')) {
await handleSlashCommand(input, app, runtime, tui, rl)
await handleSlashCommand(input, app, runtime, tui, rl, taskResults)
rl.prompt()
return
}
@@ -115,47 +222,12 @@ export async function runCommand(project_path?: string): Promise<void> {
if (classification.action === 'answer') {
console.log('\n' + (classification.response || 'No response') + '\n')
} else if (classification.action === 'delegate') {
// Create task and dispatch through Scheduler
const taskId = `task_${randomUUID().slice(0, 8)}`
app.scheduler.create_tasks([{
id: taskId,
type: 'execute',
title: input.slice(0, 80),
description: input
}])
console.log(`Task ${taskId} created. Dispatching worker...`)
const runPromise = app.scheduler.run_until_idle()
// Show progress while scheduler runs
let dots = 0
const progressInterval = setInterval(() => {
dots = (dots + 1) % 4
process.stdout.write(`\r Running${'.'.repeat(dots)} `)
}, 500)
const finalState = await runPromise
clearInterval(progressInterval)
process.stdout.write('\r \r')
console.log(`Task complete. Scheduler: ${finalState}`)
// Refresh projection with task status
const tasks = app.scheduler.get_graph().count_by_status()
runtime.projection_client.receive_snapshot({
session_id,
project_id: app.project_id,
status: finalState === 'COMPLETED' ? 'completed' : 'running',
title: project_root.split('/').pop() || 'AirCoding',
tasks: [{ id: taskId as any, type: 'execute', status: finalState === 'COMPLETED' ? 'completed' : 'running', title: input.slice(0, 80), retry_count: 0, attempts: 1, created_at: new Date().toISOString() }],
agents: [],
tool_runs: [],
command_runs: [],
artifacts: [],
permission_prompts: [],
blockers: [],
updated_at: new Date().toISOString()
})
if (agent.state === 'CONFIRMING' && classification.response) {
pendingConfirmation = input
console.log('\n' + classification.response + '\n')
} else {
await dispatchTask(input)
}
} else {
console.log(`Result: ${classification.response || 'Done'}`)
}
@@ -177,17 +249,37 @@ export async function runCommand(project_path?: string): Promise<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) {
case 'help':
console.log('\nCommands:')
console.log(' /help — Show this help')
console.log(' /status — Show scheduler and worker status')
console.log(' /tools — List registered tools')
console.log(' /tasks — Show task graph')
console.log(' /quit — Exit AirCoding\n')
console.log(' /help — Show this help')
console.log(' /status — Show scheduler and worker status')
console.log(' /tools — List registered tools')
console.log(' /tasks — Show task graph')
console.log(' /results — Show produced files from completed tasks')
console.log(' /quit — Exit AirCoding\n')
break
case 'results':
if (!taskResults || taskResults.size === 0) {
console.log(' No task results yet. Submit a task first.\n')
} else {
console.log('')
for (const [taskId, result] of taskResults) {
console.log(` Task: ${result.title} [${result.state}]`)
if (result.files.length > 0) {
for (const f of result.files) {
console.log(` 📄 ${f.path} (${f.size}B)`)
}
} else {
console.log(' (no files produced)')
}
}
console.log('')
}
break
case 'status':