fix(regression): repair 5 regressions from second round, close B10/B12/B15/B16

Round 2 regression fixes:
- B10 (INV-2 outbox, CRITICAL): wiring.ts — switch durable events
  from eventBus.publish (live-only) to eventIngestor.ingest (persistent)
  for debug.record.created and memory.promoted. Add required RuntimeEvent
  fields (id, source, route).
- B12 (Scheduler events, CRITICAL): Scheduler.ts — replace all 4
  eventBus.publish calls with eventIngestor.ingest + registered event
  types (task.started/task.failed/agent.lost/agent.cancelled).
  Remove unregistered task.status.changed references.
- B15 (duplicate ProjectionClient): remove orphan tui/src/ProjectionClient.ts
  (zero references, superseded by runtime/src/projection/ProjectionClient.ts
  re-exported via @aircoding/runtime barrel).
- RuntimeApp: wire Scheduler→WorkerManager in constructor; document
  start() bootstrap→recover→hydrate→ready sequence (DD §22.2).
- createRuntime: read project_id from .air/shared/project.json
  (DD §6.1 stable UUID), fallback to Date.now() only if not initialized.
- B16 (api_key strict): ProviderManager.get_or_create_adapter now calls
  ModelConfigLoader.validate() before passing raw api_key to adapter.

Also fix from R1 regression:
- ArchitectureDesigner: replace broken additive-heuristic risk scoring
  (single runtime file→replan, large refactor→confirmation only) with
  change-scope classification (contracts→confirmation, breaking→escalate,
  large→replan, safe→silent_continue). Remove dead evaluate_risk().
- MainAgent test: update confirmation test from old state name
  AWAITING_CONFIRMATION to canonical CONFIRMING (B13 state machine fix).

Test: 148/148 pass (regression + e2e + llm + toolchain-cpp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-03 17:19:36 +08:00
parent 20bad8ca29
commit 7d3b2b4a4c
18 changed files with 276 additions and 78 deletions

View File

@@ -5,24 +5,43 @@
* @module packages/cli/src/bootstrap/createRuntime
*/
import { RuntimeApp } from '@aircoding/runtime'
import { readFileSync, existsSync } from 'fs'
import { join } from 'path'
import { RuntimeApp, ProjectionClient } from '@aircoding/runtime'
import type { AirConfig } from './loadConfig.js'
export interface BootResult {
app: RuntimeApp
projection_client: ProjectionClient
start: () => Promise<void>
shutdown: () => Promise<void>
}
/**
* Load project_id from .air/shared/project.json (DD §6.1 stable UUID).
* Falls back to generated UUID if project is not initialized.
*/
function load_project_id(project_root: string): string {
const project_json = join(project_root, '.air', 'shared', 'project.json')
if (existsSync(project_json)) {
try {
const parsed = JSON.parse(readFileSync(project_json, 'utf-8'))
if (parsed.project_id) return parsed.project_id
} catch { /* fall through to fallback */ }
}
// Fallback: generate if project not yet initialized
return `proj_${Date.now().toString(36)}`
}
/**
* Create and start the AirCoding runtime.
*/
export async function createRuntime(config: AirConfig): Promise<BootResult> {
const project_root = config.project_root || process.cwd()
// Generate session and project IDs
// Load stable project_id from .air/shared/project.json
const session_id = `session_${Date.now()}`
const project_id = `project_${Date.now()}` // Would be loaded from .air/shared/project.json
const project_id = load_project_id(project_root)
const app = new RuntimeApp({
project_root,
@@ -33,6 +52,7 @@ export async function createRuntime(config: AirConfig): Promise<BootResult> {
return {
app,
projection_client: app.projection_client,
start: () => app.start(),
shutdown: () => app.shutdown()
}

View File

@@ -1,21 +1,39 @@
/**
* InitCommand - First-run project initialization wizard
* DD §17.
* DD §17. Routes filesystem writes through ToolRegistry (INV-3).
*
* @module packages/cli/src/commands/init
*/
import { mkdirSync, writeFileSync, existsSync } from 'fs'
import { existsSync } from 'fs'
import { join } from 'path'
import { randomUUID } from 'crypto'
import { loadConfig } from '../bootstrap/loadConfig.js'
import { ToolRegistry, createToolRegistry, register_builtin_tools } from '@aircoding/runtime'
import type { ToolExecutionContext } from '@aircoding/runtime'
export async function initCommand(project_path?: string): Promise<void> {
// TODO(P8): Route filesystem writes through RuntimeApp→ToolRegistry→PermissionEngine (INV-3).
export async function initCommand(project_path?: string, toolRegistry?: ToolRegistry): Promise<void> {
const project_root = project_path || process.cwd()
console.log(`Initializing AirCoding project at ${project_root}`)
// Create .air directory structure
// Create minimal ToolRegistry if not provided (INV-3 compliance)
let registry = toolRegistry
if (!registry) {
registry = createToolRegistry(project_root)
register_builtin_tools(registry)
}
const context: ToolExecutionContext = {
session_id: 'init',
project_id: `proj_${randomUUID()}`,
project_root,
agent_id: 'cli-init',
agent_type: 'executor',
task_scope: { allowed_paths: [project_root], denied_paths: [] },
permission_profile: 'executor'
}
// Create .air directory structure via fs.write tool (INV-3)
const dirs = [
join(project_root, '.air', 'shared'),
join(project_root, '.air', 'local'),
@@ -26,7 +44,8 @@ export async function initCommand(project_path?: string): Promise<void> {
for (const dir of dirs) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
// Use fs.write with empty content to create directory
await registry.call({ name: 'fs.write', arguments: { path: join(dir, '.gitkeep'), content: '', create_dirs: true } }, context)
console.log(` Created ${dir}`)
}
}
@@ -34,7 +53,7 @@ export async function initCommand(project_path?: string): Promise<void> {
// Generate project_id
const project_id = `proj_${randomUUID()}`
// Write project.json
// Write project.json via fs.write (INV-3)
const project_json = {
project_id,
name: project_root.split('/').pop() || 'aircoding-project',
@@ -42,17 +61,25 @@ export async function initCommand(project_path?: string): Promise<void> {
version: '1.0.0-alpha'
}
writeFileSync(
join(project_root, '.air', 'shared', 'project.json'),
JSON.stringify(project_json, null, 2)
)
await registry.call({
name: 'fs.write',
arguments: {
path: join(project_root, '.air', 'shared', 'project.json'),
content: JSON.stringify(project_json, null, 2),
create_dirs: true
}
}, context)
console.log(` Created .air/shared/project.json (project_id: ${project_id})`)
// Write default rules
writeFileSync(
join(project_root, '.air', 'shared', 'rules.md'),
'# Project Rules\n\nAdd your project-specific rules here.\n'
)
// Write default rules via fs.write (INV-3)
await registry.call({
name: 'fs.write',
arguments: {
path: join(project_root, '.air', 'shared', 'rules.md'),
content: '# Project Rules\n\nAdd your project-specific rules here.\n',
create_dirs: true
}
}, context)
console.log('\nProject initialized successfully!')
console.log(`Run 'air run' to start a session.`)

View File

@@ -7,6 +7,7 @@
import { loadConfig } from '../bootstrap/loadConfig.js'
import { createRuntime } from '../bootstrap/createRuntime.js'
import { TuiApp } from '@aircoding/tui'
export async function runCommand(project_path?: string): Promise<void> {
const config = loadConfig(project_path)
@@ -15,12 +16,14 @@ export async function runCommand(project_path?: string): Promise<void> {
const runtime = await createRuntime(config)
await runtime.start()
// Would spawn TUI here
console.log('TUI would start here (P6 integration pending)')
// Wire TUI to runtime's ProjectionClient (B15 fix)
const tui = new TuiApp({ client: runtime.projection_client })
await tui.start()
// Graceful shutdown handler
process.on('SIGINT', async () => {
console.log('\nShutting down...')
tui.stop()
await runtime.shutdown()
process.exit(0)
})