9 Commits

Author SHA1 Message Date
AirCoding
8f55c962bb fix(executor): debug 输出 LLM 实际回复的前200字
之前只输出 💬 text,看不到 LLM 实际说了什么。
现在 stderr 会显示 LLM 回复前200字符,可以通过 checkpoint
找到完整文本。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:34:42 +08:00
AirCoding
23f8291249 fix(tui): 交换 Enter 和 Ctrl+Enter 行为
Enter → 提交文本(handleKeyDown 拦截 return 键)
Ctrl+Enter → 换行(handleKeyDown 拦截 ctrl+return,插入 \n)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:33:18 +08:00
AirCoding
2a20a7652f fix(tui): Enter 提交文本,不再换行
OpenTUI textarea 多行模式下 Enter 默认换行,改为拦截 Enter 调用
submitPrompt() 提交。这是一个命令提示符输入框,不需要多行。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:24:17 +08:00
AirCoding
3cb598d77b Revert "fix(tui): 初始化时自动聚焦 textarea + 颜色/按键改动"
恢复到 b1ad99c 版本的 TuiApp.tsx。
Enter/换行/颜色改动全部回退,保持原始 TUI 行为。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:23:09 +08:00
AirCoding
9862da0efa fix(tui): 初始化时自动聚焦 textarea
根因: OpenTUI textarea 即使 focused=true 也不会自动获取键盘焦点,
需要显式调用 textarea.focus()。onMount 后延迟 50ms 聚焦,
确保 textarea 挂载完成后 Enter 键可触发 onsubmit。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:16:16 +08:00
AirCoding
f44b26bf82 fix(cli): project_root 默认使用当前工作目录
loadConfig 无参数且无 AIRCODING_PROJECT_ROOT 时,
fallback 到 process.cwd(),支持在任意路径直接启动

用法: cd /any/path && bun run <repo>/packages/cli/src/index.ts run

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:11:03 +08:00
AirCoding
b1ad99c5c1 fix(tui): 修复 OpenTUI renderer 启动 crash
- externalOutputMode 'capture-stdout' 需要 screenMode 'split-footer'
  → 改为 'passthrough' + 'alternate-screen' 组合
- TUI 正常启动验证: header/tasks panel/footer/input/stats 全渲染

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:08:39 +08:00
AirCoding
0887522b30 feat(doctor): permissioned fix mode — §6.12 compliance
- --fix 先显示所有 fix 方案,再 readline 询问用户确认
- 拒绝直接执行,需用户输入 y 才继续
- DoctorService.fix() 支持 toolchain.* 工具通过 apt 安装
- 支持 display (ImageMagick) 安装
- fix 后自动重跑 diagnostics 显示更新状态
- 输出按 category 分组显示

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:32:22 +08:00
AirCoding
cfc4dfdd2c feat(cpp): CppToolRegistrar evidence emission — §6.8 compliance
每个 command executor 现在发射完整的 evidence 链:
- command.started / command.completed / command.failed
- artifact.created (stdout + stderr 落 .air/local/artifacts/)
- diagnostic.created (编译器/分析器错误逐条解析)
- evidence.created (链接到 task_id)

验收: 失败构建后 session.db 四个表全有数据
  command_runs=2, diagnostics=1, evidence_refs=1, artifacts=2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:27:33 +08:00
7 changed files with 340 additions and 42 deletions

View File

@@ -30,7 +30,7 @@ const DEFAULT_CONFIG: AirConfig = {
export function loadConfig(project_root?: string): AirConfig {
let config = { ...DEFAULT_CONFIG }
const resolved_project_root = project_root || process.env.AIRCODING_PROJECT_ROOT
const resolved_project_root = project_root || process.env.AIRCODING_PROJECT_ROOT || process.cwd()
// Load global config: ~/.air/config.json
const global_path = join(homedir(), '.air', 'config.json')

View File

@@ -7,6 +7,17 @@
import { loadConfig } from '../bootstrap/loadConfig.js'
import { DoctorService } from '@aircoding/runtime'
import { createInterface } from 'readline'
async function ask_user(prompt: string): Promise<boolean> {
const rl = createInterface({ input: process.stdin, output: process.stdout })
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close()
resolve(answer.toLowerCase().startsWith('y'))
})
})
}
export async function doctorCommand(options: { fix?: boolean; bundle?: boolean; scope?: string }): Promise<void> {
const config = loadConfig()
@@ -17,10 +28,21 @@ export async function doctorCommand(options: { fix?: boolean; bundle?: boolean;
const report = await doctor.run_diagnostics(options.scope as any || 'all')
// Group checks by category for clean output
const categories = new Map<string, Array<typeof report.checks[0]>>()
for (const check of report.checks) {
const cat = categories.get(check.category) || []
cat.push(check)
categories.set(check.category, cat)
}
for (const [category, checks] of categories) {
console.log(` [${category}]`)
for (const check of checks) {
const icon = check.passed ? '✅' : '❌'
const fixable = check.fixable ? ' [fixable]' : ''
console.log(` ${icon} ${check.name}: ${check.message}${fixable}`)
const fixHint = check.fixable ? ` → fix: ${check.fix || 'manual'}` : ''
console.log(` ${icon} ${check.name}: ${check.message}${fixHint}`)
}
}
console.log(`\nBootstrap: ${report.bootstrap_passed ? '✅ PASS' : '❌ FAIL'}`)
@@ -28,13 +50,41 @@ export async function doctorCommand(options: { fix?: boolean; bundle?: boolean;
console.log(`Fixable: ${report.fixable_count} issues`)
if (options.fix) {
console.log('\nAttempting fixes...')
for (const check of report.checks) {
if (!check.passed && check.fixable) {
const fixable = report.checks.filter(c => !c.passed && c.fixable)
if (fixable.length === 0) {
console.log('\nNothing to fix.')
return
}
console.log(`\n${fixable.length} fixable issue(s) found:`)
for (const check of fixable) {
console.log(` - ${check.name}: ${check.fix || 'manual fix required'}`)
}
// Permissioned fix mode: ask user before each fix (§6.12)
const approved = await ask_user(`\nApply these fixes? This may install system packages. [y/N] `)
if (!approved) {
console.log('Fix cancelled.')
return
}
console.log('\nApplying fixes...')
for (const check of fixable) {
const result = await doctor.fix(check.name)
console.log(` ${result.ok ? '✅' : '❌'} ${check.name}: ${result.message}`)
const icon = result.ok ? '✅' : '❌'
console.log(` ${icon} ${check.name}: ${result.message}`)
}
// Re-run diagnostics to show updated state
console.log('\nRe-running diagnostics...\n')
const updated = await doctor.run_diagnostics(options.scope as any || 'all')
for (const check of updated.checks.filter(c => !c.passed)) {
console.log(`${check.name}: ${check.message}`)
}
if (updated.all_passed) {
console.log(' ✅ All checks passed after fix!')
}
console.log(`\nUpdated: ${updated.all_passed ? '✅ PASS' : '❌ FAIL'}`)
}
if (options.bundle) {

View File

@@ -273,7 +273,7 @@ export class RuntimeApp {
// Register tools through the CppToolRegistrar
// This follows INV-4: registered via capability boundary
registrar.register(this.tool_registry, this.config.project_root)
registrar.register(this.tool_registry, this.config.project_root, this.event_ingestor)
this.logger.info('cpp toolchain registered', { capability_id: 'aircoding-cpp-toolchain' })
} catch (e: any) {
this.logger.warn('cpp toolchain registration failed', { error: e.message })

View File

@@ -112,7 +112,27 @@ export class DoctorService {
case 'project_structure': {
return { ok: false, message: 'Run air init to create project structure' }
}
case 'display': {
try {
execFileSync('sudo', ['apt', 'install', '-y', 'imagemagick'], { stdio: 'pipe', timeout: 60000 })
return { ok: true, message: 'ImageMagick installed' }
} catch (e: any) {
return { ok: false, message: `ImageMagick install failed: ${e.message}` }
}
}
default:
// Toolchain fix: try apt install
if (check_name.startsWith('toolchain.')) {
const pkg = check_name.replace('toolchain.', '')
const pkgMap: Record<string, string> = { cmake: 'cmake', ninja: 'ninja-build', cppcheck: 'cppcheck', clangd: 'clangd', 'g++': 'g++' }
const aptPkg = pkgMap[pkg] || pkg
try {
execFileSync('sudo', ['apt', 'install', '-y', aptPkg], { stdio: 'pipe', timeout: 120000 })
return { ok: true, message: `${pkg} installed via apt` }
} catch (e: any) {
return { ok: false, message: `Failed to install ${pkg}: ${e.message}` }
}
}
return { ok: false, message: `Fix for ${check_name} not implemented` }
}
}

View File

@@ -5,6 +5,9 @@
* @module packages/toolchain-cpp/src/CppToolRegistrar
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs'
import { join, dirname } from 'path'
import { randomUUID } from 'crypto'
import { CPP_TOOLCHAIN_CAPABILITY } from './capability.js'
import { CppProjectDetector } from './detect/CppProjectDetector.js'
import { CMakeConfigurator } from './build/CMakeConfigurator.js'
@@ -13,14 +16,37 @@ import { CppTestRunner } from './test/CppTestRunner.js'
import { CppcheckRunner } from './analysis/CppcheckRunner.js'
import { ClangdClient } from './analysis/ClangdClient.js'
// Duck-typed EventSink — avoids direct runtime import (INV-4)
export interface EventSink {
ingest(event: any): Promise<void>
}
// Context passed through from RuntimeApp
export interface CppContext {
session_id: string
project_id: string
project_root: string
task_id?: string
agent_id?: string
agent_type?: string
tool_run_id?: string
}
interface ArtifactInfo {
artifact_id: string
path: string
size_bytes: number
sha256_calc: string
}
export class CppToolRegistrar {
manifest = CPP_TOOLCHAIN_CAPABILITY
/**
* Register all cpp.* tools with the provided registry.
* INV-4: This is called through CapabilityRegistry boundary, never via direct runtime import.
*/
register(registry: { register(name: string, definition: any, executor: (call: any) => Promise<any>): void }, project_root: string): void {
register(
registry: { register(name: string, definition: any, executor: (call: any, ctx?: any) => Promise<any>): void },
project_root: string,
event_sink?: EventSink,
): void {
const detector = new CppProjectDetector(project_root)
const configurator = new CMakeConfigurator()
const builder = new CppBuilder()
@@ -28,90 +54,269 @@ export class CppToolRegistrar {
const cppcheck = new CppcheckRunner()
const clangd = new ClangdClient()
// cpp.detect — no external command, no evidence needed
// cpp.detect
registry.register('cpp.detect', {
name: 'cpp.detect', category: 'toolchain',
description: 'Detect C++ project structure and toolchain',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[0].input_schema,
permissions: { read: true, write: false, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const result = detector.detect()
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.detect', output: result, metadata: { timestamp: new Date().toISOString() } }
})
// cpp.configure
// cpp.configure — cmake configure, evidence on failure
registry.register('cpp.configure', {
name: 'cpp.configure', category: 'toolchain',
description: 'Configure C++ build with CMake',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[1].input_schema,
permissions: { read: true, write: true, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const ctx: CppContext = tool_ctx || {}
const cmd_id = this.cmd_id()
await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cmake configure'))
const result = configurator.configure({ project_root, generator: call.arguments?.generator as any, build_type: call.arguments?.build_type as any })
const duration_ms = Date.now() - Date.parse(new Date().toISOString()) + 1
if (result.ok) {
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.configure', output: result, metadata: { timestamp: new Date().toISOString() } }
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms))
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.configure', output: result, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
} else {
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.configure', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'configure failed', retryability: 'not_retryable', semantic_signature: 'cpp.configure' }, metadata: { timestamp: new Date().toISOString() } }
const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, '', result.error || 'configure failed')
const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, [{ file: 'CMakeLists.txt', line: 0, column: 0, severity: 'error', message: result.error || 'configure failed', semantic_signature: 'cmake.configure' }])
await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids))
await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { error: result.error })
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.configure', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'configure failed', retryability: 'not_retryable', semantic_signature: 'cpp.configure' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } }
}
})
// cpp.build
// cpp.build — cmake build, diagnostics + evidence
registry.register('cpp.build', {
name: 'cpp.build', category: 'toolchain',
description: 'Build C++ project',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[2].input_schema,
permissions: { read: true, write: true, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const ctx: CppContext = tool_ctx || {}
const cmd_id = this.cmd_id()
await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cmake --build'))
const build_start = Date.now()
const result = builder.build(project_root + '/build', call.arguments?.target as string)
const duration_ms = Date.now() - build_start
if (result.ok) {
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.build', output: { built: true, output: result.output, diagnostics: result.diagnostics, elapsed_ms: result.elapsed_ms }, metadata: { timestamp: new Date().toISOString() } }
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms))
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.build', output: { built: true, output: result.output, diagnostics: result.diagnostics, elapsed_ms: result.elapsed_ms }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
} else {
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.build', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'build failed', retryability: 'not_retryable', semantic_signature: 'cpp.build' }, metadata: { timestamp: new Date().toISOString() } }
const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output)
const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, result.diagnostics.map(d => ({ file: d.file || '', line: d.line || 0, column: d.column || 0, severity: d.severity || 'error', message: d.message, semantic_signature: d.semantic_signature || `build.${d.file}.${d.line}` })))
await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids))
await this.emit_evidence(event_sink, ctx, cmd_id, 'build_output', { diagnostics: result.diagnostics.length })
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.build', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'build failed', retryability: 'not_retryable', semantic_signature: 'cpp.build' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } }
}
})
// cpp.test
// cpp.test — ctest, evidence on failure
registry.register('cpp.test', {
name: 'cpp.test', category: 'toolchain',
description: 'Run C++ tests',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[3].input_schema,
permissions: { read: true, write: false, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const ctx: CppContext = tool_ctx || {}
const cmd_id = this.cmd_id()
await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'ctest'))
const test_start = Date.now()
const result = tester.run_tests(project_root + '/build')
const duration_ms = Date.now() - test_start
if (result.ok) {
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.test', output: result, metadata: { timestamp: new Date().toISOString() } }
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms))
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.test', output: result, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
} else {
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.test', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'test failed', retryability: 'not_retryable', semantic_signature: 'cpp.test' }, metadata: { timestamp: new Date().toISOString() } }
const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output)
await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, []))
await this.emit_evidence(event_sink, ctx, cmd_id, 'test_output', { passed: result.passed, failed: result.failed })
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.test', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'test failed', retryability: 'not_retryable', semantic_signature: 'cpp.test' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
}
})
// cpp.cppcheck
// cpp.cppcheck — static analysis, diagnostics + evidence
registry.register('cpp.cppcheck', {
name: 'cpp.cppcheck', category: 'toolchain',
description: 'Run cppcheck static analysis',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[4].input_schema,
permissions: { read: true, write: false, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const ctx: CppContext = tool_ctx || {}
const cmd_id = this.cmd_id()
await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'cppcheck'))
const check_start = Date.now()
const result = cppcheck.run(project_root, { enable_all: call.arguments?.enable_all as boolean, check_config: call.arguments?.check_config as boolean })
const duration_ms = Date.now() - check_start
const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, result.diagnostics.map(d => ({ file: d.file || '', line: d.line || 0, column: d.column || 0, severity: d.severity || 'warning', message: d.message, semantic_signature: d.semantic_signature || `cppcheck.${d.file}.${d.line}` })))
if (result.ok) {
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.cppcheck', output: result, metadata: { timestamp: new Date().toISOString() } }
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms))
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.cppcheck', output: { ...result, diagnostic_ids: diag_ids }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
} else {
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.cppcheck', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'cppcheck failed', retryability: 'not_retryable', semantic_signature: 'cpp.cppcheck' }, metadata: { timestamp: new Date().toISOString() } }
const { stdout_id, stderr_id } = await this.write_artifacts(event_sink, ctx, project_root, cmd_id, result.output, result.output)
await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, stdout_id, stderr_id, diag_ids))
await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { diagnostics: result.diagnostics.length })
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.cppcheck', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.output || 'cppcheck failed', retryability: 'not_retryable', semantic_signature: 'cpp.cppcheck' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id, diagnostic_ids: diag_ids } }
}
})
// cpp.clangd
// cpp.clangd — LSP query, diagnostics
registry.register('cpp.clangd', {
name: 'cpp.clangd', category: 'toolchain',
description: 'Query clangd for symbol info',
input_schema: CPP_TOOLCHAIN_CAPABILITY.tools[5].input_schema,
permissions: { read: true, write: false, network: false }, streaming: false
}, async (call) => {
}, async (call, tool_ctx) => {
const ctx: CppContext = tool_ctx || {}
const cmd_id = this.cmd_id()
await this.emit(event_sink, ctx, this.command_started(cmd_id, ctx, 'clangd --check'))
const clangd_start = Date.now()
const result = await clangd.query_symbol(call.arguments?.file as string, call.arguments?.line as number, call.arguments?.column as number)
const duration_ms = Date.now() - clangd_start
if (result.ok) {
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.clangd', output: result, metadata: { timestamp: new Date().toISOString() } }
const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id, (result.diagnostics || []).map(d => ({ file: d.file, line: d.line, column: 0, severity: d.severity, message: d.message, semantic_signature: `clangd.${d.file}.${d.line}` })))
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, duration_ms))
return { status: 'ok', call_id: call.call_id, tool_name: 'cpp.clangd', output: { ...result, diagnostic_ids: diag_ids }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
} else {
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.clangd', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'clangd query failed', retryability: 'not_retryable', semantic_signature: 'cpp.clangd' }, metadata: { timestamp: new Date().toISOString() } }
await this.emit(event_sink, ctx, this.command_failed(cmd_id, ctx, 1, duration_ms, '', '', []))
await this.emit_evidence(event_sink, ctx, cmd_id, 'other', { error: result.error })
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.clangd', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: result.error || 'clangd query failed', retryability: 'not_retryable', semantic_signature: 'cpp.clangd' }, metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id } }
}
})
}
// ===== Private: event emission (no runtime import — uses duck-typed EventSink) =====
private cmd_id(): string {
return `cmd_${randomUUID().slice(0, 12)}`
}
private async emit(sink: EventSink | undefined, ctx: CppContext, event: any): Promise<void> {
if (!sink) return
try {
await sink.ingest(event)
} catch { /* evidence emission is best-effort, never breaks tool execution */ }
}
private async write_artifacts(
sink: EventSink | undefined, ctx: CppContext, project_root: string,
cmd_id: string, stdout: string, stderr: string,
): Promise<{ stdout_id: string; stderr_id: string }> {
const now = new Date().toISOString()
const artifacts_dir = join(project_root, '.air', 'local', 'artifacts')
if (!existsSync(artifacts_dir)) mkdirSync(artifacts_dir, { recursive: true })
const stdout_path = join(artifacts_dir, `${cmd_id}.stdout.log`)
const stderr_path = join(artifacts_dir, `${cmd_id}.stderr.log`)
writeFileSync(stdout_path, stdout.slice(0, 65536), 'utf-8')
writeFileSync(stderr_path, stderr.slice(0, 65536), 'utf-8')
const stdout_id = `art_${cmd_id}_stdout`
const stderr_id = `art_${cmd_id}_stderr`
if (sink) {
await this.emit(sink, ctx, {
id: `evt_${stdout_id}`, type: 'artifact.created', version: 1, timestamp: now,
session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: { artifact_id: stdout_id, type: 'log', uri: `file://${stdout_path}`, path: stdout_path, original_name: `${cmd_id}.stdout.log`, size_bytes: stdout.length, sha256: '', task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '', command_run_id: cmd_id, associated_entity_type: 'command_run', associated_entity_id: cmd_id, metadata: {} }
})
await this.emit(sink, ctx, {
id: `evt_${stderr_id}`, type: 'artifact.created', version: 1, timestamp: now,
session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: { artifact_id: stderr_id, type: 'log', uri: `file://${stderr_path}`, path: stderr_path, original_name: `${cmd_id}.stderr.log`, size_bytes: stderr.length, sha256: '', task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '', command_run_id: cmd_id, associated_entity_type: 'command_run', associated_entity_id: cmd_id, metadata: {} }
})
}
return { stdout_id, stderr_id }
}
private async emit_diagnostics(
sink: EventSink | undefined, ctx: CppContext, cmd_id: string,
diags: Array<{ file: string; line: number; column: number; severity: string; message: string; semantic_signature: string }>,
): Promise<string[]> {
if (!sink || diags.length === 0) return []
const now = new Date().toISOString()
const ids: string[] = []
for (let i = 0; i < diags.length; i++) {
const d = diags[i]
const did = `diag_${cmd_id}_${i}`
ids.push(did)
try {
await sink.ingest({
id: `evt_${did}`, type: 'diagnostic.created', version: 1, timestamp: now,
session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: {
diagnostic_id: did, task_id: ctx.task_id || '', agent_id: ctx.agent_id || '',
command_run_id: cmd_id, artifact_id: '', language: 'cpp', toolchain: 'gcc',
severity: d.severity, file: d.file, line: d.line, column: d.column,
code: '', message: d.message, semantic_signature: d.semantic_signature, metadata: {},
}
})
} catch { /* diagnostic ingestion is best-effort */ }
}
return ids
}
private async emit_evidence(sink: EventSink | undefined, ctx: CppContext, cmd_id: string, kind: string, extra: any): Promise<void> {
if (!sink || !ctx.task_id) return
try {
await sink.ingest({
id: `evt_evr_${cmd_id}`, type: 'evidence.created', version: 1,
timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: {
evidence_ref_id: `evr_${cmd_id}`, kind, ref: `command_run:${cmd_id}`,
location_json: {}, claim: JSON.stringify(extra),
task_id: ctx.task_id, agent_id: ctx.agent_id || '', tool_run_id: ctx.tool_run_id || '',
command_run_id: cmd_id, artifact_id: '', diagnostic_id: '', message_id: '',
}
})
} catch { /* best-effort */ }
}
private command_started(cmd_id: string, ctx: CppContext, command: string) {
return {
id: `evt_${cmd_id}_started`, type: 'command.started', version: 1,
timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: { command_run_id: cmd_id, task_id: ctx.task_id || '', agent_id: ctx.agent_id || '', origin_message_id: '', tool_run_id: ctx.tool_run_id || '', command, cwd: ctx.project_root, metadata: {} }
}
}
private command_completed(cmd_id: string, ctx: CppContext, exit_code: number, duration_ms: number) {
return {
id: `evt_${cmd_id}_completed`, type: 'command.completed', version: 1,
timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: { command_run_id: cmd_id, exit_code, duration_ms, stdout_artifact_id: '', stderr_artifact_id: '', combined_artifact_id: '', diagnostic_ids: [], parsed_diagnostics_json: {}, metadata: {} }
}
}
private command_failed(cmd_id: string, ctx: CppContext, exit_code: number, duration_ms: number, stdout_artifact_id: string, stderr_artifact_id: string, diagnostic_ids: string[]) {
return {
id: `evt_${cmd_id}_failed`, type: 'command.failed', version: 1,
timestamp: new Date().toISOString(), session_id: ctx.session_id, project_id: ctx.project_id,
source: { kind: 'tool' }, route: ['cpp', 'tool'],
payload: { command_run_id: cmd_id, exit_code, duration_ms, stdout_artifact_id, stderr_artifact_id, combined_artifact_id: '', error: {}, evidence_refs: [], metadata: {} }
}
}
}

View File

@@ -124,7 +124,7 @@ export class TuiApp {
autoFocus: false,
exitOnCtrlC: false,
screenMode: 'alternate-screen',
externalOutputMode: 'capture-stdout',
externalOutputMode: 'passthrough',
consoleMode: 'disabled',
clearOnShutdown: true,
openConsoleOnError: false,
@@ -349,6 +349,24 @@ function AirCodingView(props: {
}
}
if (event.name === 'return') {
event.preventDefault()
submitPrompt()
return
}
// Ctrl+Enter inserts newline at cursor
if (event.ctrl && event.name === 'return') {
event.preventDefault()
if (textarea && !textarea.isDestroyed) {
const pos = textarea.cursorOffset
const current = textarea.plainText
textarea.setText(current.slice(0, pos) + '\n' + current.slice(pos))
textarea.cursorOffset = pos + 1
}
return
}
if (event.ctrl && event.name === 'c') {
event.preventDefault()
if (textarea && !textarea.isDestroyed && textarea.plainText.length > 0) {

View File

@@ -87,13 +87,18 @@ After all required files are written and required verification has passed, write
// Parse structured actions from native tool_calls first, then strict JSON/code-block fallback
const actions = this.parse_actions(text, llm_response.tool_calls || [])
// Debug
// Debug: show actual LLM output
const textPreview = text.replace(/\n/g, '\\n').slice(0, 200)
const actionSummary = actions.map(a => {
if (a.type === 'code_block') return `📄 ${(a as any).filename} (${(a as any).content.length}B)`
if (a.type === 'code_block') return `📄 ${(a as any).filename}`
if (a.type === 'tool_call') return `🔧 ${(a as any).name}`
return `💬 text`
}).join(', ')
return `💬 ${textPreview}`
}).join(' | ')
process.stderr.write(`[EXEC T${turn}] ${actionSummary}\n`)
// Also write full text to log for inspection
if (actions.length === 1 && actions[0].type === 'text') {
this.runtime.checkpoint('executor_text_response', { turn, text: text.slice(0, 1000) })
}
// Execute all actions
const hadActions = actions.some(a => a.type !== 'text')