Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .claude/skills/tech-lead/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,27 +155,39 @@ Provide a structured complexity assessment in this format:

**Complexity Criteria:**

> **IMPORTANT — Evaluation logic:**
> Rate as **Complex** if ANY single Complex criterion below applies.
> Rate as **Medium** only when NO Complex criteria apply AND multiple Medium criteria apply.
> Rate as **Simple** only when ALL Simple criteria apply.
> File count is a supporting signal, NOT the primary driver — qualitative criteria take precedence.

**Hard triggers → always Complex (any one is sufficient):**
- New integration with an external tool, CLI, or service (new agent plugin, new provider, new API client)
- Novel parsing or calculation logic that must be reverse-engineered (undocumented formats, no existing adapter)
- Significant architectural decisions needed (new interface contracts, new layer interactions)
- Cross-cutting concerns that affect security, reliability, or metrics pipeline integrity

**Simple:**
- Single component affected
- Well-defined requirements
- Existing patterns to follow
- Existing patterns to follow exactly (copy-paste with trivial changes)
- No architectural decisions needed
- Estimated 1-3 files to change

**Medium:**
- 2-3 components affected
- 2-3 components affected (no hard triggers above)
- Clear requirements with minor gaps
- May require some architectural decisions
- Adapts an existing pattern with moderate changes
- Estimated 4-8 files to change
- Requires coordination between layers
- Requires coordination between layers but no new contracts

**Complex:**
- 4+ components affected
- Ambiguous or incomplete requirements
- Significant architectural decisions needed
- Any hard trigger above applies, OR
- 4+ distinct components affected
- Ambiguous or incomplete requirements requiring significant inference
- Estimated 9+ files to change
- Cross-cutting concerns (security, performance)
- New integrations or external dependencies
- Novel computation / parsing logic with no existing reference in codebase

### Phase 4: Recommendation

Expand Down
18 changes: 18 additions & 0 deletions bin/codemie-codex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

/**
* Codex Agent Entry Point
* Direct entry point for codemie-codex command
*/

import { AgentCLI } from '../dist/agents/core/AgentCLI.js';
import { AgentRegistry } from '../dist/agents/registry.js';

const agent = AgentRegistry.getAgent('codex');
if (!agent) {
console.error('✗ Codex agent not found in registry');
process.exit(1);
}

const cli = new AgentCLI(agent);
await cli.run(process.argv);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"codemie-claude": "./bin/codemie-claude.js",
"codemie-claude-acp": "./bin/codemie-claude-acp.js",
"codemie-gemini": "./bin/codemie-gemini.js",
"codemie-opencode": "./bin/codemie-opencode.js"
"codemie-opencode": "./bin/codemie-opencode.js",
"codemie-codex": "./bin/codemie-codex.js"
},
"files": [
"dist",
Expand Down
7 changes: 4 additions & 3 deletions src/agents/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ describe('AgentRegistry', () => {
it('should register all default agents', () => {
const agentNames = AgentRegistry.getAgentNames();

// Should have all 5 default agents (codemie-code, claude, claude-acp, gemini, opencode)
expect(agentNames).toHaveLength(5);
// Should have all 6 default agents (codemie-code, claude, claude-acp, gemini, opencode, codex)
expect(agentNames).toHaveLength(6);
});

it('should register built-in agent', () => {
Expand Down Expand Up @@ -62,7 +62,7 @@ describe('AgentRegistry', () => {
it('should return all registered agents', () => {
const agents = AgentRegistry.getAllAgents();

expect(agents).toHaveLength(5);
expect(agents).toHaveLength(6);
expect(agents.every((agent) => agent.name)).toBe(true);
});

Expand All @@ -74,6 +74,7 @@ describe('AgentRegistry', () => {
expect(names).toContain('claude-acp');
expect(names).toContain('gemini');
expect(names).toContain('opencode');
expect(names).toContain('codex');
});
});

Expand Down
6 changes: 4 additions & 2 deletions src/agents/core/AgentCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CodeMieCodePluginMetadata } from '../plugins/codemie-code.plugin.js';
import { GeminiPluginMetadata } from '../plugins/gemini/gemini.plugin.js';
import { OpenCodePluginMetadata } from '../plugins/opencode/opencode.plugin.js';
import {ClaudeAcpPluginMetadata} from "../plugins/claude/claude-acp.plugin.js";
import { CodexPluginMetadata } from '../plugins/codex/codex.plugin.js';

/**
* Universal CLI builder for any agent
Expand Down Expand Up @@ -408,6 +409,7 @@ export class AgentCLI {
'gemini': GeminiPluginMetadata,
'opencode': OpenCodePluginMetadata,
'claude-acp': ClaudeAcpPluginMetadata,
'codex': CodexPluginMetadata,
};
return metadataMap[this.adapter.name];
}
Expand All @@ -425,8 +427,8 @@ export class AgentCLI {
const provider = config.provider || 'unknown';
const model = config.model || 'unknown';

// Check provider compatibility
if (!metadata.supportedProviders.includes(provider)) {
// Check provider compatibility (skip when empty — agent manages its own auth)
if (metadata.supportedProviders.length > 0 && !metadata.supportedProviders.includes(provider)) {
logger.error(`Provider '${provider}' is not supported by ${this.adapter.displayName}`);
console.log(chalk.white(`\nSupported providers: ${metadata.supportedProviders.join(', ')}`));
console.log(chalk.white('\nOptions:'));
Expand Down
9 changes: 9 additions & 0 deletions src/agents/core/session/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
* These types are shared across metrics, conversations, and other processors.
*/

/**
* Base normalized message format shared across all conversation processors.
*/
export interface BaseNormalizedMessage {
role: 'user' | 'assistant';
content: string;
timestamp?: string;
}

/**
* Correlation status
*/
Expand Down
38 changes: 38 additions & 0 deletions src/agents/core/session/utils/jsonl-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
import { logger } from '../../../../utils/logger.js';

/**
* Read all records from JSONL file, skipping corrupted lines.
*
* @param filePath - Absolute path to JSONL file
* @param logPrefix - Log prefix for warnings (e.g. '[codex-storage]')
* @returns Array of parsed records (empty if file doesn't exist; corrupted lines skipped)
*/
export async function readJSONLTolerant<T>(filePath: string, logPrefix = '[jsonl-reader]'): Promise<T[]> {
try {
const content = await readFile(filePath, 'utf-8');
const lines = content.trim().split('\n');
const results: T[] = [];
let corruptedCount = 0;

for (const line of lines) {
if (!line.trim()) continue;
try {
results.push(JSON.parse(line) as T);
} catch {
corruptedCount++;
}
}

if (corruptedCount > 0) {
logger.warn(`${logPrefix} Skipped ${corruptedCount} corrupted JSONL lines in ${filePath}`);
}

return results;
} catch (error: unknown) {
const err = error as NodeJS.ErrnoException;
if (err.code === 'ENOENT') {
return [];
}
logger.debug(`${logPrefix} Failed to read JSONL ${filePath}: ${err.message}`);
return [];
}
}

/**
* Read all records from JSONL file.
*
Expand Down
104 changes: 104 additions & 0 deletions src/agents/plugins/codex/codex-message-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// src/agents/plugins/codex/codex-message-types.ts
/**
* TypeScript types for Codex rollout JSONL records.
*
* Codex writes rollout files at:
* ~/.codex/sessions/YYYY/MM/DD/rollout-{ISO8601}-{uuid}.jsonl
*
* Each line is a JSON object (CodexRolloutRecord) with a `type` discriminator.
*
* Confirmed from codex-cli 0.79.0 rollout file inspection.
*
* References:
* - https://github.com/openai/codex/blob/main/codex-rs/docs/cli-reference.md
* - https://github.com/openai/codex/blob/main/codex-rs/docs/configuration.md
*/

import { ConfigurationError } from '../../../utils/errors.js';

/** Top-level wrapper for every JSONL line in a rollout file */
export interface CodexRolloutRecord {
type: 'session_meta' | 'turn_context' | 'response_item' | 'event_msg';
payload: CodexSessionMeta | CodexTurnContext | CodexResponseItem | CodexEventMsg;
}

/** session_meta record — appears once per rollout file */
export interface CodexSessionMeta {
id: string; // UUID (also embedded in filename)
timestamp: string; // ISO 8601 wall-clock time of session start
cwd: string;
originator?: string;
cli_version?: string;
model_provider?: string; // Fallback model identifier (provider name, not full model ID)
git?: {
commit_hash?: string;
branch?: string;
repository_url?: string;
};
}

/** turn_context record — may appear multiple times; last value wins for model extraction */
export interface CodexTurnContext {
cwd: string;
approval_policy?: string;
sandbox_policy?: string;
model?: string; // Actual model string passed to the API (primary model source)
summary?: string;
}

/**
* response_item record — discriminated union on payload.type.
* Relevant sub-types: function_call, function_call_output, message, reasoning.
*/
export interface CodexResponseItem {
type: string; // 'function_call' | 'function_call_output' | 'message' | 'reasoning' | 'ghost_snapshot'
name?: string; // function_call: tool name
arguments?: string; // function_call: JSON-encoded args string
call_id?: string; // function_call + function_call_output: shared correlation ID
output?: string; // function_call_output: tool output
}

/** event_msg record — user messages and other session events */
export interface CodexEventMsg {
type: 'user_message' | string;
message?: string;
}

/**
* Extended ParsedSession metadata specific to the Codex plugin.
* Consumed only within the codex plugin boundary.
*/
export interface CodexSessionMetadata {
projectPath?: string;
createdAt?: string; // ISO 8601 from session_meta.timestamp
repository?: string;
branch?: string;
codexSessionId: string; // UUID from session_meta.id (unique per rollout file)
model?: string; // Resolved model string (turn_context.model or session_meta.model_provider)
cliVersion?: string;
}

/**
* Type guard: asserts metadata is CodexSessionMetadata.
* Throws if required field codexSessionId is missing.
*/
export function validateCodexMetadata(metadata: unknown): asserts metadata is CodexSessionMetadata {
if (
typeof metadata !== 'object' ||
metadata === null ||
typeof (metadata as CodexSessionMetadata).codexSessionId !== 'string'
) {
throw new ConfigurationError('Invalid Codex session metadata: codexSessionId is required');
}
}

/**
* Type guard: returns true if metadata has a valid codexSessionId field.
*/
export function hasCodexMetadata(metadata: unknown): metadata is CodexSessionMetadata {
return (
typeof metadata === 'object' &&
metadata !== null &&
typeof (metadata as CodexSessionMetadata).codexSessionId === 'string'
);
}
45 changes: 45 additions & 0 deletions src/agents/plugins/codex/codex.paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// src/agents/plugins/codex/codex.paths.ts
/**
* Codex path utilities.
*
* Codex stores rollout files at:
* ~/.codex/sessions/YYYY/MM/DD/rollout-{ISO8601}-{uuid}.jsonl
*
* Unlike OpenCode, Codex does NOT use XDG conventions — ~/.codex is fixed.
*
* References:
* - https://github.com/openai/codex/blob/main/codex-rs/docs/configuration.md
*/

import { homedir } from 'os';
import { join } from 'path';
import { existsSync } from 'fs';

/**
* Returns the Codex home directory: ~/.codex
*/
export function getCodexHomePath(): string {
return join(homedir(), '.codex');
}

/**
* Returns the Codex sessions base directory: ~/.codex/sessions
* Returns null if the directory does not exist (Codex not run yet).
*/
export function getCodexSessionsPath(): string | null {
const sessionsPath = join(homedir(), '.codex', 'sessions');
return existsSync(sessionsPath) ? sessionsPath : null;
}

/**
* Returns the day-specific session directory for a given date:
* ~/.codex/sessions/YYYY/MM/DD
*
* Note: This directory may not exist yet.
*/
export function getCodexSessionDayPath(date: Date): string {
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return join(getCodexHomePath(), 'sessions', year, month, day);
}
Loading
Loading