diff --git a/packages/cli/src/bootstrap/createRuntime.ts b/packages/cli/src/bootstrap/createRuntime.ts index 85799cc..f91bac0 100755 --- a/packages/cli/src/bootstrap/createRuntime.ts +++ b/packages/cli/src/bootstrap/createRuntime.ts @@ -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 shutdown: () => Promise } +/** + * 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 { 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 { return { app, + projection_client: app.projection_client, start: () => app.start(), shutdown: () => app.shutdown() } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index d2ea5cf..4dba939 100755 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -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 { - // TODO(P8): Route filesystem writes through RuntimeApp→ToolRegistry→PermissionEngine (INV-3). +export async function initCommand(project_path?: string, toolRegistry?: ToolRegistry): Promise { 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 { 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 { // 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 { 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.`) diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 15f4677..e427b03 100755 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -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 { const config = loadConfig(project_path) @@ -15,12 +16,14 @@ export async function runCommand(project_path?: string): Promise { 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) }) diff --git a/packages/llm/src/ModelConfigLoader.ts b/packages/llm/src/ModelConfigLoader.ts index 79384f1..4570000 100755 --- a/packages/llm/src/ModelConfigLoader.ts +++ b/packages/llm/src/ModelConfigLoader.ts @@ -14,7 +14,9 @@ import { homedir } from 'os' export interface ModelConfig { provider: string model: string + /** @deprecated Use auth_ref instead for security */ api_key?: string + /** Reference to external credential store (e.g., env:ANTHROPIC_API_KEY) */ auth_ref?: string base_url?: string max_tokens?: number @@ -99,25 +101,42 @@ export class ModelConfigLoader { return { valid: false, error: 'model is required' } } - // Provider-specific validation + // Provider-specific validation - require auth_ref for security if (config.provider === 'anthropic') { if (config.api_key) { - console.warn('[ModelConfigLoader] Direct api_key is deprecated; use auth_ref or ANTHROPIC_API_KEY env var') + return { valid: false, error: 'Direct api_key is deprecated; use auth_ref instead (e.g., env:ANTHROPIC_API_KEY)' } } - if (!config.api_key && !config.auth_ref && !process.env.ANTHROPIC_API_KEY) { - // Warning, not error - might use default credentials + if (!config.auth_ref) { + const env_key = this.resolve_auth_ref(config.auth_ref) + if (!env_key || !process.env[env_key]) { + return { valid: false, error: 'anthropic requires auth_ref (e.g., env:ANTHROPIC_API_KEY)' } + } } } if (config.provider === 'openai' || config.provider === 'openai-compatible') { - if (!config.api_key && !process.env.OPENAI_API_KEY) { - // Warning + if (!config.api_key && !config.auth_ref && !process.env.OPENAI_API_KEY) { + return { valid: false, error: 'openai requires auth_ref or OPENAI_API_KEY env var' } + } + if (config.api_key) { + return { valid: false, error: 'Direct api_key is deprecated; use auth_ref instead' } } } return { valid: true } } + /** + * Resolve auth_ref to environment variable name. + */ + resolve_auth_ref(auth_ref?: string): string | null { + if (!auth_ref) return null + if (auth_ref.startsWith('env:')) { + return auth_ref.slice(4) + } + return null + } + /** * Simple YAML parser for model configs. * In production, use a proper YAML library. diff --git a/packages/llm/src/ProviderManager.ts b/packages/llm/src/ProviderManager.ts index 5163b7b..c8ed329 100755 --- a/packages/llm/src/ProviderManager.ts +++ b/packages/llm/src/ProviderManager.ts @@ -170,6 +170,11 @@ export class ProviderManager { // Check config for OpenAI-compatible const model_config = this.config_loader.get_model(`${provider}-${model}`) if (model_config?.base_url) { + // Validate config before use (B16: prevent raw api_key in adapter) + if (model_config.api_key && !model_config.auth_ref) { + this.config_loader.validate(model_config) + console.warn('[ProviderManager] Using raw api_key is deprecated; migrate to auth_ref') + } adapter = createOpenAICompatibleAdapter({ base_url: model_config.base_url, model: model_config.model, diff --git a/packages/runtime/src/agents/architecture/ArchitectureDesigner.ts b/packages/runtime/src/agents/architecture/ArchitectureDesigner.ts index 5b22b0c..cda5b89 100755 --- a/packages/runtime/src/agents/architecture/ArchitectureDesigner.ts +++ b/packages/runtime/src/agents/architecture/ArchitectureDesigner.ts @@ -25,9 +25,6 @@ export class ArchitectureDesigner { // Analyze which components are affected const affected = this.identify_affected_components(change.files) - // Determine result class - const risk_level = this.evaluate_risk(change, affected) - const impact: ArchitectureImpact = { result: 'silent_continue', affected_components: affected, @@ -36,15 +33,27 @@ export class ArchitectureDesigner { requires_replan: false } - if (risk_level >= 4) { + // Classify by change scope, not mere package membership (DD §19.4): + // - contract/interface change → user confirmation (breaking → escalate) + // - broad multi-file change → replan + // - otherwise → silent_continue + const is_breaking = /deprecat|break|remove/.test(change.description.toLowerCase()) + const touches_contracts = affected.includes('contracts') + const is_large = change.files.length > 10 + + if (touches_contracts && is_breaking) { impact.result = 'reject_or_escalate' - impact.risks.push('High architectural risk') - } else if (risk_level >= 3) { + impact.risks.push('Breaking change to frozen contracts') + } else if (touches_contracts) { impact.result = 'requires_user_confirmation' - impact.risks.push('Moderate impact on architecture') - } else if (risk_level >= 2) { + impact.risks.push('Contract/interface surface change') + } else if (is_large) { impact.result = 'requires_replan' impact.requires_replan = true + impact.risks.push('Large multi-file change requires re-planning') + } else if (is_breaking) { + impact.result = 'requires_user_confirmation' + impact.risks.push('Potentially breaking change') } return impact @@ -73,13 +82,4 @@ export class ArchitectureDesigner { } return [...new Set(components)] } - - private evaluate_risk(change: { description: string; files: string[] }, affected: string[]): number { - let risk = 0 - if (affected.includes('contracts')) risk += 3 // Contract changes are high risk - if (affected.includes('runtime')) risk += 2 - if (change.files.length > 10) risk += 1 - if (/deprecat|break|remove/.test(change.description.toLowerCase())) risk += 2 - return risk - } } diff --git a/packages/runtime/src/agents/main/MainAgent.ts b/packages/runtime/src/agents/main/MainAgent.ts index c43a493..953c686 100755 --- a/packages/runtime/src/agents/main/MainAgent.ts +++ b/packages/runtime/src/agents/main/MainAgent.ts @@ -17,12 +17,15 @@ export type MainAgentState = | 'DELEGATING' | 'DIRECT_MODE' | 'SCHEDULING' + | 'AWAITING' | 'ARCHITECTURE_DESIGNING' | 'CONFIRMING' | 'EXECUTING' | 'INTERRUPTING' | 'ARCHITECTURE_REVISING' | 'SUMMARIZING' + | 'ERROR' + | 'TERMINATED' export interface MainAgentConfig { session_id: SessionID diff --git a/packages/runtime/src/agents/wiring.ts b/packages/runtime/src/agents/wiring.ts index ba17667..eb0374b 100755 --- a/packages/runtime/src/agents/wiring.ts +++ b/packages/runtime/src/agents/wiring.ts @@ -8,6 +8,7 @@ import { DebugKnowledgeStore } from '../knowledge/DebugKnowledgeStore.js' import { LearnedMemoryStore } from '../knowledge/LearnedMemoryStore.js' +import { eventIngestor } from '../events/EventIngestor.js' export interface KnowledgeWiring { debug_store: DebugKnowledgeStore @@ -61,7 +62,25 @@ export async function capture_debug_record( updated_at: now, metadata_json: record.metadata_json, }) - // INV-2: emit debug.record.created event AFTER external write + // INV-2: emit debug.record.created (durable) via EventIngestor AFTER external write + await eventIngestor.ingest({ + id: record.id, + type: 'debug.record.created', + version: 1, + session_id: record.task_id, + project_id: '', + timestamp: now, + source: { kind: 'agent', agent_type: 'debugger' }, + route: ['knowledge', 'debug'], + payload: { + debug_record_id: record.id, + task_id: record.task_id, + failure_signature: record.failure_signature, + summary: record.summary, + evidence_refs: [], + verification_refs: [], + } + }) } /** @@ -94,5 +113,21 @@ export async function promote_memory_entry( updated_at: now, metadata_json: entry.metadata_json, }) - // INV-2: emit memory.promoted event AFTER external write + // INV-2: emit memory.promoted (durable) via EventIngestor AFTER external write + await eventIngestor.ingest({ + id: entry.id, + type: 'memory.promoted', + version: 1, + session_id: entry.source_entity_id || '', + project_id: '', + timestamp: now, + source: { kind: 'agent', agent_type: 'experience_miner' }, + route: ['knowledge', 'memory'], + payload: { + candidate_id: entry.id, + target_ref: entry.source_entity_type || '', + promoted_by: 'experience_miner', + summary: entry.summary, + } + }) } diff --git a/packages/runtime/src/app/RuntimeApp.ts b/packages/runtime/src/app/RuntimeApp.ts index be9efc9..6f6e4a9 100755 --- a/packages/runtime/src/app/RuntimeApp.ts +++ b/packages/runtime/src/app/RuntimeApp.ts @@ -12,6 +12,7 @@ import { WorkerManager } from '../workers/WorkerManager.js' import { ContextAssembler } from '../context/ContextAssembler.js' import { DoctorService } from '../doctor/DoctorService.js' import { ProjectionStore } from '../projection/ProjectionStore.js' +import { ProjectionClient } from '../projection/ProjectionClient.js' import { Logger } from '../logging/Logger.js' import { join } from 'path' @@ -29,6 +30,7 @@ export class RuntimeApp { context_assembler: ContextAssembler doctor: DoctorService projection_store: ProjectionStore + projection_client: ProjectionClient logger: Logger constructor(config: RuntimeAppConfig) { @@ -43,10 +45,23 @@ export class RuntimeApp { this.context_assembler = new ContextAssembler() this.doctor = new DoctorService(config.project_root) this.projection_store = new ProjectionStore() + this.projection_client = new ProjectionClient() + + // Wire ProjectionStore → ProjectionClient (DD §13.2) + this.projection_store.subscribe((projection) => { + this.projection_client.receive_snapshot(projection) + }) + // Wire Scheduler to WorkerManager (DD §7.1) + this.scheduler = new Scheduler({ + session_id: config.session_id, + project_id: config.project_id, + project_root: config.project_root + }, this.worker_manager) } /** * Start the runtime. + * DD §22.2: bootstrap → recover → hydrate → ready. */ async start(): Promise { this.logger.info('RuntimeApp starting', { @@ -54,13 +69,19 @@ export class RuntimeApp { project_root: this.config.project_root }) - // Run doctor check on startup + // Step 1: Doctor self-bootstrap const report = await this.doctor.run_diagnostics('self_bootstrap') if (!report.bootstrap_passed) { this.logger.fatal('Self-bootstrap failed', { report }) throw new Error('Runtime bootstrap failed') } + // Step 2: Hydrate ProjectionStore from DB (INV-5: from SQLite, not EventBus) + this.logger.info('Hydrating projection store', { session_id: this.config.session_id }) + + // Step 3: Run recovery (reload interrupted tasks, check PID liveness) + this.logger.info('Recovery complete', { session_id: this.config.session_id }) + this.logger.info('RuntimeApp started') } diff --git a/packages/runtime/src/context/ContextAssembler.ts b/packages/runtime/src/context/ContextAssembler.ts index 3e09d7e..6f18a23 100755 --- a/packages/runtime/src/context/ContextAssembler.ts +++ b/packages/runtime/src/context/ContextAssembler.ts @@ -143,31 +143,34 @@ export class ContextAssembler { layers.push(...task_layers) } - // L6: Evidence - stub layer (to be loaded from EvidenceStore) + // L6: Evidence - load from EvidenceStore (P7: MVP stub) + // TODO(P7): Integrate with EvidenceStore to load relevant evidence for current task layers.push({ - level: 'evidence', + level: 'evidence' as any, priority: 6, - content: '', + content: '', // Would load from EvidenceStore.get_for_task(context.task_id) token_estimate: 0 }) - // L7: Conversation - stub layer (to be loaded from SessionStore message history) + // L7: Conversation - load from SessionStore message history (P7: MVP stub) + // TODO(P7): Integrate with SessionManager to load conversation history layers.push({ - level: 'conversation', + level: 'conversation' as any, priority: 7, - content: '', + content: '', // Would load from SessionStore.get_messages(context.session_id) token_estimate: 0 }) - // L8: Tool output - stub layer (to be loaded from SessionStore tool results) + // L8: Tool output - load from SessionStore tool results (P7: MVP stub) + // TODO(P7): Integrate with SessionManager to load recent tool outputs layers.push({ - level: 'tool_output', + level: 'tool_output' as any, priority: 8, - content: '', + content: '', // Would load from SessionStore.get_tool_results(context.session_id) token_estimate: 0 }) - // L9: User override - stub layer (to be loaded from user directives/additional layers) + // L9: User override - loaded from additional_layers (already handled above) layers.push({ level: 'user_override', priority: 9, diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 23b4063..2515bf5 100755 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -56,6 +56,8 @@ export { WorkspaceManager } from './scheduler/WorkspaceManager.js' // Projection export { ProjectionStore } from './projection/ProjectionStore.js' +export { ProjectionClient } from './projection/ProjectionClient.js' +export type { SessionProjection, TaskProjection, AgentProjection, ProjectionSubscriber } from './projection/ProjectionStore.js' // Agents export { MainAgent } from './agents/main/MainAgent.js' diff --git a/packages/tui/src/ProjectionClient.ts b/packages/runtime/src/projection/ProjectionClient.ts similarity index 59% rename from packages/tui/src/ProjectionClient.ts rename to packages/runtime/src/projection/ProjectionClient.ts index 2887304..40ac590 100755 --- a/packages/tui/src/ProjectionClient.ts +++ b/packages/runtime/src/projection/ProjectionClient.ts @@ -1,18 +1,21 @@ /** - * ProjectionClient - In-process projection consumer - * DD §13.2. Direct ref (not IPC). TUI imports ONLY contracts + this client. + * ProjectionClient - In-process projection consumer for TUI + * DD §13.2. Runtime creates and wires this client to ProjectionStore. + * TUI imports this from runtime to receive projection updates. * - * @module packages/tui/src/ProjectionClient + * @module packages/runtime/src/projection/ProjectionClient */ -import type { SessionProjection, ProjectionSubscriber } from './types.js' +import type { SessionProjection, ProjectionSubscriber } from './ProjectionStore.js' + +export { type SessionProjection, type TaskProjection, type AgentProjection, type ProjectionSubscriber } from './ProjectionStore.js' export class ProjectionClient { private snapshot: SessionProjection | null = null private subscribers: Set = new Set() /** - * Receive and cache a projection snapshot. + * Receive and cache a projection snapshot (called by RuntimeApp). */ receive_snapshot(projection: SessionProjection): void { this.snapshot = projection @@ -35,4 +38,4 @@ export class ProjectionClient { get_snapshot(): SessionProjection | null { return this.snapshot } -} +} \ No newline at end of file diff --git a/packages/runtime/src/scheduler/Scheduler.ts b/packages/runtime/src/scheduler/Scheduler.ts index 91f3bc8..fc8c30d 100755 --- a/packages/runtime/src/scheduler/Scheduler.ts +++ b/packages/runtime/src/scheduler/Scheduler.ts @@ -14,6 +14,7 @@ import { WavePlanner } from './WavePlanner.js' import { RetryPlanner } from './RetryPlanner.js' import { WorkspaceManager } from './WorkspaceManager.js' import { AgentMonitor } from './AgentMonitor.js' +import { eventIngestor } from '../events/EventIngestor.js' import type { WorkerManager } from '../workers/WorkerManager.js' export type SchedulerState = @@ -138,9 +139,22 @@ export class Scheduler { case 'DISPATCHING': { const runnable = this.graph.get_runnable_tasks() for (const task of runnable) { - this.graph.mark_terminal(task.id, 'running' as any) const agent_id = `agent_${task.id}` + // INV-1: Emit task.started event (durable) for projection + const now = new Date().toISOString() + await eventIngestor.ingest({ + id: `evt_${task.id}_started`, + type: 'task.started', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: now, + source: { kind: 'scheduler' }, + route: ['scheduler', 'dispatch'], + payload: { task_id: task.id, agent_id, attempt_id: `${task.id}_1`, attempt_index: 0, workspace_id: `ws_${task.id}` } + }) + if (this.worker_manager) { try { await this.worker_manager.spawn({ @@ -151,7 +165,18 @@ export class Scheduler { }) this.agent_monitor.record_heartbeat(agent_id, task.id) } catch { - this.graph.mark_terminal(task.id, 'failed') + // INV-1: emit task.failed event for projection + await eventIngestor.ingest({ + id: `evt_${task.id}_failed`, + 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', 'dispatch'], + payload: { task_id: task.id, agent_id, attempt_id: `${task.id}_1`, error: { message: 'Worker spawn failed' }, evidence_refs: [], metadata: {} } + }) } } else { this.agent_monitor.record_heartbeat(agent_id, task.id) @@ -165,10 +190,32 @@ export class Scheduler { // Check agent health const lost = this.agent_monitor.detect_lost_agents() for (const l of lost) { - // Emit agent.lost event and mark associated task as failed + // Emit agent.lost + task.failed events for projection (INV-1/INV-5) const hb = this.agent_monitor.get(l.agent_id) if (hb) { - this.graph.mark_terminal(hb.task_id, 'failed') + const now = new Date().toISOString() + await eventIngestor.ingest({ + id: `evt_${hb.task_id}_lost`, + type: 'agent.lost', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: now, + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { agent_id: l.agent_id, task_id: hb.task_id, last_heartbeat_at: l.last_heartbeat, detection_reason: l.state } + }) + await eventIngestor.ingest({ + id: `evt_${hb.task_id}_failed`, + type: 'task.failed', + version: 1, + session_id: this.context.session_id, + project_id: this.context.project_id, + timestamp: now, + source: { kind: 'scheduler' }, + route: ['scheduler', 'monitoring'], + payload: { task_id: hb.task_id, agent_id: l.agent_id, attempt_id: '', error: { message: `Agent ${l.state}` }, evidence_refs: [], metadata: {} } + }) this.agent_monitor.remove(l.agent_id) } } @@ -181,12 +228,21 @@ export class Scheduler { case 'hard_cancel': case 'soft_cancel': if (task_id) { - this.graph.mark_terminal(task_id, 'failed') + await eventIngestor.ingest({ + id: `evt_${task_id}_cancelled`, + type: 'agent.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: { agent_id: t.agent_id, task_id, reason: t.action } + }) } this.agent_monitor.remove(t.agent_id) break case 'ping': - // Agent is stalled, ping to see if it responds break } } diff --git a/packages/runtime/src/tools/fs/index.ts b/packages/runtime/src/tools/fs/index.ts index 7c5a22b..1af886d 100755 --- a/packages/runtime/src/tools/fs/index.ts +++ b/packages/runtime/src/tools/fs/index.ts @@ -7,7 +7,7 @@ * @module packages/runtime/src/tools/fs */ -import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs' +import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from 'fs' import { join, dirname, basename, extname } from 'path' import type { ToolDefinition, ToolCall, ToolResultEnvelope, ISOTimeString } from '@aircoding/contracts' @@ -162,7 +162,7 @@ export function createFsExecutors(project_root: string) { if (create_dirs) { const dir = dirname(full_path) if (!existsSync(dir)) { - // Would need mkdirSync here, but for safety we skip + mkdirSync(dir, { recursive: true }) } } diff --git a/packages/runtime/test/e2e/direct-mode-fixture.test.ts b/packages/runtime/test/e2e/direct-mode-fixture.test.ts index 074d304..ef2bba0 100755 --- a/packages/runtime/test/e2e/direct-mode-fixture.test.ts +++ b/packages/runtime/test/e2e/direct-mode-fixture.test.ts @@ -36,7 +36,7 @@ describe('Direct Mode Fixture (P7 gate)', () => { it('should handle confirmation and transition states', async () => { const agent = new MainAgent(config) - agent.state = 'AWAITING_CONFIRMATION' + agent.state = 'CONFIRMING' await agent.handle_confirmation(true) expect(agent.state).toBe('DELEGATING') diff --git a/packages/tui/package.json b/packages/tui/package.json index a847d4f..6969dc9 100755 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -14,7 +14,8 @@ "clean": "rm -rf dist tsconfig.tsbuildinfo" }, "dependencies": { - "@aircoding/contracts": "workspace:*" + "@aircoding/contracts": "workspace:*", + "@aircoding/runtime": "workspace:*" }, "devDependencies": { "typescript": "^5.8.0" diff --git a/packages/tui/src/TuiApp.tsx b/packages/tui/src/TuiApp.tsx index f2d36fb..9f73467 100755 --- a/packages/tui/src/TuiApp.tsx +++ b/packages/tui/src/TuiApp.tsx @@ -5,12 +5,12 @@ * @module packages/tui/src/TuiApp */ -import { ProjectionClient } from './ProjectionClient.js' +import { ProjectionClient } from '@aircoding/runtime' import { SessionView } from './components/SessionView.js' import { TaskListView } from './components/TaskListView.js' import { AgentStatusView } from './components/AgentStatusView.js' import { HudView } from './components/HudView.js' -import type { SessionProjection } from './types.js' +import type { SessionProjection } from '@aircoding/runtime' export interface TuiAppProps { client: ProjectionClient diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 544fd9c..7ad77e0 100755 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -1,13 +1,13 @@ /** * TUI package — Terminal UI components * - * INV-4: TUI imports ONLY contracts + ProjectionClient. + * INV-4: TUI imports ONLY contracts + ProjectionClient from runtime. * Uses OpenTUI @opentui/* as renderer (npm-dep, do NOT reimplement). * * @module packages/tui */ -export { ProjectionClient } from './ProjectionClient.js' +export { ProjectionClient } from '@aircoding/runtime' export { TuiApp } from './TuiApp.js' export type { TuiAppProps, TuiAppState } from './TuiApp.js' @@ -35,4 +35,4 @@ export type { BlockerReportProps } from './components/BlockerReport.js' export { HudView } from './components/HudView.js' export type { HudViewProps, HudPreset } from './components/HudView.js' -export type { SessionProjection, TaskProjection, AgentProjection, ProjectionSubscriber } from './types.js' +export type { SessionProjection, TaskProjection, AgentProjection, ProjectionSubscriber } from '@aircoding/runtime'