chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,10 @@
|
||||
* @module packages/toolchain-cpp/src/CppToolRegistrar
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs'
|
||||
import { writeFileSync, mkdirSync, existsSync, readFileSync, statSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { execFileSync } from 'child_process'
|
||||
import { CPP_TOOLCHAIN_CAPABILITY } from './capability.js'
|
||||
import { CppProjectDetector } from './detect/CppProjectDetector.js'
|
||||
import { CMakeConfigurator } from './build/CMakeConfigurator.js'
|
||||
@@ -198,6 +199,218 @@ export class CppToolRegistrar {
|
||||
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 } }
|
||||
}
|
||||
})
|
||||
|
||||
// FR-017: cpp.debug — Run debug session or parse crash logs
|
||||
registry.register('cpp.debug', {
|
||||
name: 'cpp.debug',
|
||||
category: 'toolchain',
|
||||
description: 'Debug a C++ program or parse crash logs',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
target: { type: 'string', description: 'Binary or process to debug' },
|
||||
source_file: { type: 'string', description: 'Source file for breakpoint' },
|
||||
log_path: { type: 'string', description: 'Path to crash log to parse' },
|
||||
format: { type: 'string', enum: ['gdb', 'lldb', 'valgrind', 'asan'], default: 'gdb', description: 'Log format' }
|
||||
},
|
||||
required: []
|
||||
},
|
||||
permissions: { read: true, write: false, network: false },
|
||||
streaming: false
|
||||
}, async (call, tool_ctx) => {
|
||||
const ctx: CppContext = tool_ctx || {}
|
||||
const cmd_id = this.cmd_id()
|
||||
const { target, source_file, log_path, format } = call.arguments as any
|
||||
|
||||
try {
|
||||
// If log_path is provided, parse the log
|
||||
if (log_path) {
|
||||
const logContent = readFileSync(join(project_root, log_path), 'utf-8')
|
||||
const errors = logContent.split('\n')
|
||||
.filter((l: string) => /error|fail|segfault|assert|abort|exception/i.test(l))
|
||||
.slice(0, 50)
|
||||
|
||||
// Parse GDB/LLDB style backtrace if present
|
||||
const backtrace: string[] = []
|
||||
let inBacktrace = false
|
||||
for (const line of logContent.split('\n')) {
|
||||
if (line.includes('#0') || line.includes('#1') || line.includes('#2')) {
|
||||
inBacktrace = true
|
||||
backtrace.push(line.trim())
|
||||
} else if (inBacktrace && !line.includes('#')) {
|
||||
inBacktrace = false
|
||||
}
|
||||
}
|
||||
|
||||
await this.emit_evidence(event_sink, ctx, cmd_id, 'crash_log', { error_count: errors.length })
|
||||
return {
|
||||
status: 'ok',
|
||||
call_id: call.call_id,
|
||||
tool_name: 'cpp.debug',
|
||||
output: { parsed: true, errors, backtrace, log_path },
|
||||
metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id }
|
||||
}
|
||||
}
|
||||
|
||||
// If target is provided, attempt a simple debug run with gdb batch mode
|
||||
if (target) {
|
||||
const gdbCmd = `gdb -batch -ex "run" -ex "bt" --args ${target}`
|
||||
try {
|
||||
const out = execFileSync('gdb', ['-batch', '-ex', 'run', '-ex', 'bt', '--', target], {
|
||||
cwd: project_root,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
timeout: 60000
|
||||
})
|
||||
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, 0))
|
||||
return {
|
||||
status: 'ok',
|
||||
call_id: call.call_id,
|
||||
tool_name: 'cpp.debug',
|
||||
output: { target, debug_output: out.slice(0, 5000) },
|
||||
metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id }
|
||||
}
|
||||
} catch (e: any) {
|
||||
// GDB might fail if the program runs successfully or has issues
|
||||
await this.emit(event_sink, ctx, this.command_completed(cmd_id, ctx, 0, 0))
|
||||
return {
|
||||
status: 'ok',
|
||||
call_id: call.call_id,
|
||||
tool_name: 'cpp.debug',
|
||||
output: { target, debug_output: e.message || String(e), exit_code: e.status || 1 },
|
||||
metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.debug', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: 'Either target or log_path required', retryability: 'not_retryable', semantic_signature: 'cpp.debug' }, metadata: { timestamp: new Date().toISOString() } }
|
||||
} catch (e: any) {
|
||||
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.debug', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message || String(e), retryability: 'not_retryable', semantic_signature: 'cpp.debug' }, metadata: { timestamp: new Date().toISOString() } }
|
||||
}
|
||||
})
|
||||
|
||||
// FR-017: cpp.fix — Apply scoped fix to C++ file based on diagnostic
|
||||
registry.register('cpp.fix', {
|
||||
name: 'cpp.fix',
|
||||
category: 'toolchain',
|
||||
description: 'Apply a scoped fix to a C++ file (FR-017: scoped fix execution)',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: { type: 'string', description: 'Source file to fix' },
|
||||
line: { type: 'number', description: 'Line number to fix' },
|
||||
fix_type: { type: 'string', enum: ['include', 'syntax', 'memory', 'nullptr', 'typo'], description: 'Type of fix to apply' },
|
||||
fix_content: { type: 'string', description: 'Content to insert/replace' }
|
||||
},
|
||||
required: ['file', 'line', 'fix_type']
|
||||
},
|
||||
permissions: { read: true, write: true, network: false },
|
||||
streaming: false
|
||||
}, async (call, tool_ctx) => {
|
||||
const ctx: CppContext = tool_ctx || {}
|
||||
const cmd_id = this.cmd_id()
|
||||
const { file, line, fix_type, fix_content } = call.arguments as any
|
||||
|
||||
try {
|
||||
const fullPath = join(project_root, file)
|
||||
if (!existsSync(fullPath)) {
|
||||
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.fix', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: `File not found: ${file}`, retryability: 'not_retryable', semantic_signature: 'cpp.fix' }, metadata: { timestamp: new Date().toISOString() } }
|
||||
}
|
||||
|
||||
const lines = readFileSync(fullPath, 'utf-8').split('\n')
|
||||
const targetLine = line - 1 // Convert to 0-indexed
|
||||
|
||||
if (targetLine < 0 || targetLine >= lines.length) {
|
||||
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.fix', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: `Line ${line} out of range (file has ${lines.length} lines)`, retryability: 'not_retryable', semantic_signature: 'cpp.fix' }, metadata: { timestamp: new Date().toISOString() } }
|
||||
}
|
||||
|
||||
// FR-017: Apply scoped fix based on fix_type
|
||||
let newLines = [...lines]
|
||||
switch (fix_type) {
|
||||
case 'include':
|
||||
// Insert include at top of file (after any existing includes)
|
||||
const insertIdx = newLines.findIndex(l => l.startsWith('#include') || l.startsWith('#define') || l.startsWith('#ifdef'))
|
||||
if (insertIdx >= 0) {
|
||||
newLines.splice(insertIdx, 0, fix_content || '#include <new_header>')
|
||||
} else {
|
||||
newLines.unshift(fix_content || '#include <new_header>')
|
||||
}
|
||||
break
|
||||
case 'syntax':
|
||||
case 'memory':
|
||||
case 'nullptr':
|
||||
// Replace the target line
|
||||
newLines[targetLine] = fix_content || newLines[targetLine]
|
||||
break
|
||||
case 'typo':
|
||||
// Simple line replacement
|
||||
newLines[targetLine] = fix_content || newLines[targetLine]
|
||||
break
|
||||
default:
|
||||
newLines[targetLine] = fix_content || newLines[targetLine]
|
||||
}
|
||||
|
||||
writeFileSync(fullPath, newLines.join('\n'), 'utf-8')
|
||||
await this.emit_evidence(event_sink, ctx, cmd_id, 'code_fix', { file, line, fix_type })
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
call_id: call.call_id,
|
||||
tool_name: 'cpp.fix',
|
||||
output: { file, line, fix_type, applied: true, lines_affected: 1 },
|
||||
metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id }
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { status: 'error', call_id: call.call_id, tool_name: 'cpp.fix', error: { error_id: call.call_id, kind: 'tool_error', severity: 'error', message: e.message || String(e), retryability: 'not_retryable', semantic_signature: 'cpp.fix' }, metadata: { timestamp: new Date().toISOString() } }
|
||||
}
|
||||
})
|
||||
|
||||
// FR-017: cpp.verify — Re-run build/test to verify fix and emit evidence
|
||||
registry.register('cpp.verify', {
|
||||
name: 'cpp.verify',
|
||||
category: 'toolchain',
|
||||
description: 'Verify fix by re-running build and tests (FR-017: evidence-backed verification)',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
verify_target: { type: 'string', enum: ['build', 'test', 'both'], default: 'both', description: 'What to verify' },
|
||||
build_target: { type: 'string', description: 'CMake build target' }
|
||||
},
|
||||
required: []
|
||||
},
|
||||
permissions: { read: true, write: false, network: false },
|
||||
streaming: false
|
||||
}, async (call, tool_ctx) => {
|
||||
const ctx: CppContext = tool_ctx || {}
|
||||
const cmd_id = this.cmd_id()
|
||||
const { verify_target = 'both', build_target } = call.arguments as any
|
||||
|
||||
const results: Record<string, any> = {}
|
||||
|
||||
// Run build verification
|
||||
if (verify_target === 'build' || verify_target === 'both') {
|
||||
const buildResult = builder.build(project_root + '/build', build_target)
|
||||
results.build = { ok: buildResult.ok, output: buildResult.output?.slice(0, 2000), diagnostics_count: buildResult.diagnostics?.length }
|
||||
const diag_ids = await this.emit_diagnostics(event_sink, ctx, cmd_id + '_build', buildResult.diagnostics.map(d => ({ file: d.file || '', line: d.line || 0, column: 0, severity: d.severity || 'error', message: d.message, semantic_signature: `verify.build.${d.file}.${d.line}` })))
|
||||
}
|
||||
|
||||
// Run test verification
|
||||
if (verify_target === 'test' || verify_target === 'both') {
|
||||
const testResult = tester.run_tests(project_root + '/build')
|
||||
results.test = { ok: testResult.ok, passed: testResult.passed, failed: testResult.failed, total: testResult.total }
|
||||
await this.emit_evidence(event_sink, ctx, cmd_id + '_test', 'verification', results.test)
|
||||
}
|
||||
|
||||
const all_ok = (verify_target === 'both' ? (results.build?.ok && results.test?.ok) : (verify_target === 'build' ? results.build?.ok : results.test?.ok))
|
||||
|
||||
return {
|
||||
status: all_ok ? 'ok' : 'error',
|
||||
call_id: call.call_id,
|
||||
tool_name: 'cpp.verify',
|
||||
output: results,
|
||||
metadata: { timestamp: new Date().toISOString(), command_run_id: cmd_id }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== Private: event emission (no runtime import — uses duck-typed EventSink) =====
|
||||
|
||||
@@ -65,6 +65,31 @@ export const CPP_TOOLCHAIN_CAPABILITY: CapabilityManifestV1 = {
|
||||
permissions: { read_paths: { allow: ['*'] }, execute: true, network: false },
|
||||
input_schema: { type: 'object', properties: { file: { type: 'string' }, line: { type: 'number' }, column: { type: 'number' } } },
|
||||
output_schema: { type: 'object', properties: {} }
|
||||
},
|
||||
// FR-017: Added missing C++ tools
|
||||
{
|
||||
name: 'cpp.debug', version: 1,
|
||||
description: 'Debug a C++ program or parse crash logs (gdb/lldb/valgrind)',
|
||||
category: 'debug' as const,
|
||||
permissions: { read_paths: { allow: ['*'] }, execute: true, network: false },
|
||||
input_schema: { type: 'object', properties: { target: { type: 'string' }, source_file: { type: 'string' }, log_path: { type: 'string' }, format: { type: 'string', enum: ['gdb', 'lldb', 'valgrind', 'asan'] } } },
|
||||
output_schema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'cpp.fix', version: 1,
|
||||
description: 'Apply a scoped fix to a C++ file (include/syntax/memory/nullptr/typo)',
|
||||
category: 'build' as const,
|
||||
permissions: { read_paths: { allow: ['*'] }, write_paths: { allow: ['*.cpp', '*.cc', '*.cxx', '*.h', '*.hpp'] }, execute: false, network: false },
|
||||
input_schema: { type: 'object', properties: { file: { type: 'string' }, line: { type: 'number' }, fix_type: { type: 'string', enum: ['include', 'syntax', 'memory', 'nullptr', 'typo'] }, fix_content: { type: 'string' } } },
|
||||
output_schema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'cpp.verify', version: 1,
|
||||
description: 'Verify fix by re-running build and tests with evidence emission',
|
||||
category: 'test' as const,
|
||||
permissions: { read_paths: { allow: ['*'] }, execute: true, network: false },
|
||||
input_schema: { type: 'object', properties: { verify_target: { type: 'string', enum: ['build', 'test', 'both'] }, build_target: { type: 'string' } } },
|
||||
output_schema: { type: 'object', properties: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* A1 regression: command injection fix — execFileSync, not execSync
|
||||
* Verifies CMakeConfigurator, CppBuilder, CppcheckRunner use execFileSync.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'bun:test'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const SRC_ROOT = join(import.meta.dir, '..', 'src')
|
||||
|
||||
function reads_file(relative: string): string {
|
||||
return readFileSync(join(SRC_ROOT, relative), 'utf-8')
|
||||
}
|
||||
|
||||
describe('A1: Command injection fix', () => {
|
||||
it('CMakeConfigurator uses execFileSync, not execSync', () => {
|
||||
const src = reads_file('build/CMakeConfigurator.ts')
|
||||
expect(src).toContain('execFileSync')
|
||||
expect(src).not.toMatch(/\bexecSync\s*\(/)
|
||||
})
|
||||
|
||||
it('CppBuilder uses execFileSync, not execSync', () => {
|
||||
const src = reads_file('build/CppBuilder.ts')
|
||||
expect(src).toContain('execFileSync')
|
||||
expect(src).not.toMatch(/\bexecSync\s*\(/)
|
||||
})
|
||||
|
||||
it('CppcheckRunner uses execFileSync, not execSync', () => {
|
||||
const src = reads_file('analysis/CppcheckRunner.ts')
|
||||
expect(src).toContain('execFileSync')
|
||||
expect(src).not.toMatch(/\bexecSync\s*\(/)
|
||||
})
|
||||
|
||||
it('CMakeConfigurator passes args as array to execFileSync', () => {
|
||||
const src = reads_file('build/CMakeConfigurator.ts')
|
||||
expect(src).toMatch(/execFileSync\s*\(\s*'cmake'/)
|
||||
expect(src).not.toMatch(/execFileSync\s*\(\s*'cmake'\s*,\s*[`'"]/)
|
||||
})
|
||||
|
||||
it('CppBuilder passes args as array to execFileSync', () => {
|
||||
const src = reads_file('build/CppBuilder.ts')
|
||||
expect(src).toMatch(/execFileSync\s*\(\s*'cmake'/)
|
||||
expect(src).not.toMatch(/execFileSync\s*\(\s*'cmake'\s*,\s*[`'"]/)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../contracts" }
|
||||
]
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user