Files
Claude-Code-Workflow/.codex/skills/ccw-loop/phases/orchestrator.md
catlog22 2819f3597f feat: Add validation action and orchestrator for CCW Loop
- Implemented the VALIDATE action to run tests, check coverage, and generate reports.
- Created orchestrator for managing CCW Loop execution using Codex subagent pattern.
- Defined state schema for unified loop state management.
- Updated action catalog with new actions and their specifications.
- Enhanced CLI and issue routes to support new features and data structures.
- Improved documentation for Codex subagent design principles and action flow.
2026-01-22 22:32:37 +08:00

12 KiB

Orchestrator (Codex Pattern)

Orchestrate CCW Loop using Codex subagent pattern: spawn_agent -> wait -> send_input -> close_agent.

Role

Check control signals -> Read file state -> Select action -> Execute via agent -> Update files -> Loop until complete or paused/stopped.

Codex Pattern Overview

+-- spawn_agent (ccw-loop-executor role) --+
|                                          |
|  Phase 1: INIT or first action           |
|          |                               |
|          v                               |
|  wait() -> get result                    |
|          |                               |
|          v                               |
|  [If needs input] Collect user input     |
|          |                               |
|          v                               |
|  send_input(user choice + next action)   |
|          |                               |
|          v                               |
|  wait() -> get result                    |
|          |                               |
|          v                               |
|  [Loop until COMPLETED/PAUSED/STOPPED]   |
|          |                               |
+----------v-------------------------------+
           |
     close_agent()

State Management (Unified Location)

Read State

const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()

/**
 * Read loop state (unified location)
 * @param loopId - Loop ID (e.g., "loop-v2-20260122-abc123")
 */
function readLoopState(loopId) {
  const stateFile = `.loop/${loopId}.json`

  if (!fs.existsSync(stateFile)) {
    return null
  }

  const state = JSON.parse(Read(stateFile))
  return state
}

Create New Loop State (Direct Call)

/**
 * Create new loop state (only for direct calls, API triggers have existing state)
 */
function createLoopState(loopId, taskDescription) {
  const stateFile = `.loop/${loopId}.json`
  const now = getUtc8ISOString()

  const state = {
    // API compatible fields
    loop_id: loopId,
    title: taskDescription.substring(0, 100),
    description: taskDescription,
    max_iterations: 10,
    status: 'running',  // Direct call sets to running
    current_iteration: 0,
    created_at: now,
    updated_at: now,

    // Skill extension fields
    skill_state: null  // Initialized by INIT action
  }

  // Ensure directories exist
  mkdir -p ".loop"
  mkdir -p ".loop/${loopId}.progress"

  Write(stateFile, JSON.stringify(state, null, 2))
  return state
}

Main Execution Flow (Codex Subagent)

/**
 * Run CCW Loop orchestrator using Codex subagent pattern
 * @param options.loopId - Existing Loop ID (API trigger)
 * @param options.task - Task description (direct call)
 * @param options.mode - 'interactive' | 'auto'
 */
async function runOrchestrator(options = {}) {
  const { loopId: existingLoopId, task, mode = 'interactive' } = options

  console.log('=== CCW Loop Orchestrator (Codex) Started ===')

  // 1. Determine loopId and initial state
  let loopId
  let state

  if (existingLoopId) {
    // API trigger: use existing loopId
    loopId = existingLoopId
    state = readLoopState(loopId)

    if (!state) {
      console.error(`Loop not found: ${loopId}`)
      return { status: 'error', message: 'Loop not found' }
    }

    console.log(`Resuming loop: ${loopId}`)
    console.log(`Status: ${state.status}`)

  } else if (task) {
    // Direct call: create new loopId
    const timestamp = getUtc8ISOString().replace(/[-:]/g, '').split('.')[0]
    const random = Math.random().toString(36).substring(2, 10)
    loopId = `loop-v2-${timestamp}-${random}`

    console.log(`Creating new loop: ${loopId}`)
    console.log(`Task: ${task}`)

    state = createLoopState(loopId, task)

  } else {
    console.error('Either --loop-id or task description is required')
    return { status: 'error', message: 'Missing loopId or task' }
  }

  const progressDir = `.loop/${loopId}.progress`

  // 2. Create executor agent (single agent for entire loop)
  const agent = spawn_agent({
    message: `
## TASK ASSIGNMENT

### MANDATORY FIRST STEPS (Agent Execute)
1. **Read role definition**: ~/.codex/agents/ccw-loop-executor.md (MUST read first)
2. Read: .workflow/project-tech.json (if exists)
3. Read: .workflow/project-guidelines.json (if exists)

---

## LOOP CONTEXT

- **Loop ID**: ${loopId}
- **State File**: .loop/${loopId}.json
- **Progress Dir**: ${progressDir}
- **Mode**: ${mode}

## CURRENT STATE

${JSON.stringify(state, null, 2)}

## TASK DESCRIPTION

${state.description || task}

## FIRST ACTION

${!state.skill_state ? 'Execute: INIT' : mode === 'auto' ? 'Auto-select next action' : 'Show MENU'}

Read the role definition first, then execute the appropriate action.
`
  })

  // 3. Main orchestration loop
  let iteration = state.current_iteration || 0
  const maxIterations = state.max_iterations || 10
  let continueLoop = true

  while (continueLoop && iteration < maxIterations) {
    iteration++

    // Wait for agent output
    const result = wait({ ids: [agent], timeout_ms: 600000 })

    // Check for timeout
    if (result.timed_out) {
      console.log('Agent timeout, requesting convergence...')
      send_input({
        id: agent,
        message: `
## TIMEOUT NOTIFICATION

Execution timeout reached. Please:
1. Output current progress
2. Save any pending state updates
3. Return ACTION_RESULT with current status
`
      })
      continue
    }

    const output = result.status[agent].completed

    // Parse action result
    const actionResult = parseActionResult(output)

    console.log(`\n[Iteration ${iteration}] Action: ${actionResult.action}, Status: ${actionResult.status}`)

    // Update iteration in state
    state = readLoopState(loopId)
    state.current_iteration = iteration
    state.updated_at = getUtc8ISOString()
    Write(`.loop/${loopId}.json`, JSON.stringify(state, null, 2))

    // Handle different outcomes
    switch (actionResult.next_action) {
      case 'COMPLETED':
        console.log('Loop completed successfully')
        continueLoop = false
        break

      case 'PAUSED':
        console.log('Loop paused by API, exiting gracefully')
        continueLoop = false
        break

      case 'STOPPED':
        console.log('Loop stopped by API')
        continueLoop = false
        break

      case 'WAITING_INPUT':
        // Interactive mode: display menu, get user choice
        if (mode === 'interactive') {
          const userChoice = await displayMenuAndGetChoice(actionResult)

          // Send user choice back to agent
          send_input({
            id: agent,
            message: `
## USER INPUT RECEIVED

Action selected: ${userChoice.action}
${userChoice.data ? `Additional data: ${JSON.stringify(userChoice.data)}` : ''}

## EXECUTE SELECTED ACTION

Read action instructions and execute: ${userChoice.action}
Update state and progress files accordingly.
Output ACTION_RESULT when complete.
`
          })
        }
        break

      default:
        // Continue with next action
        if (actionResult.next_action && actionResult.next_action !== 'NONE') {
          send_input({
            id: agent,
            message: `
## CONTINUE EXECUTION

Previous action completed: ${actionResult.action}
Result: ${actionResult.status}
${actionResult.message ? `Message: ${actionResult.message}` : ''}

## EXECUTE NEXT ACTION

Continue with: ${actionResult.next_action}
Read action instructions and execute.
Output ACTION_RESULT when complete.
`
          })
        } else {
          // No next action specified, check if should continue
          if (actionResult.status === 'failed') {
            console.log(`Action failed: ${actionResult.message}`)
          }
          continueLoop = false
        }
    }
  }

  // 4. Check iteration limit
  if (iteration >= maxIterations) {
    console.log(`\nReached maximum iterations (${maxIterations})`)
    console.log('Consider breaking down the task or taking a break.')
  }

  // 5. Cleanup
  close_agent({ id: agent })

  console.log('\n=== CCW Loop Orchestrator (Codex) Finished ===')

  // Return final state
  const finalState = readLoopState(loopId)
  return {
    status: finalState.status,
    loop_id: loopId,
    iterations: iteration,
    final_state: finalState
  }
}

/**
 * Parse action result from agent output
 */
function parseActionResult(output) {
  const result = {
    action: 'unknown',
    status: 'unknown',
    message: '',
    state_updates: {},
    next_action: 'NONE'
  }

  // Parse ACTION_RESULT block
  const actionMatch = output.match(/ACTION_RESULT:\s*([\s\S]*?)(?:FILES_UPDATED:|NEXT_ACTION_NEEDED:|$)/)
  if (actionMatch) {
    const lines = actionMatch[1].split('\n')
    for (const line of lines) {
      const match = line.match(/^-\s*(\w+):\s*(.+)$/)
      if (match) {
        const [, key, value] = match
        if (key === 'state_updates') {
          try {
            result.state_updates = JSON.parse(value)
          } catch (e) {
            // Try parsing multi-line JSON
          }
        } else {
          result[key] = value.trim()
        }
      }
    }
  }

  // Parse NEXT_ACTION_NEEDED
  const nextMatch = output.match(/NEXT_ACTION_NEEDED:\s*(\S+)/)
  if (nextMatch) {
    result.next_action = nextMatch[1]
  }

  return result
}

/**
 * Display menu and get user choice (interactive mode)
 */
async function displayMenuAndGetChoice(actionResult) {
  // Parse MENU_OPTIONS from output
  const menuMatch = actionResult.message.match(/MENU_OPTIONS:\s*([\s\S]*?)(?:WAITING_INPUT:|$)/)

  if (menuMatch) {
    console.log('\n' + menuMatch[1])
  }

  // Use AskUserQuestion to get choice
  const response = await AskUserQuestion({
    questions: [{
      question: "Select next action:",
      header: "Action",
      multiSelect: false,
      options: [
        { label: "develop", description: "Continue development" },
        { label: "debug", description: "Start debugging" },
        { label: "validate", description: "Run validation" },
        { label: "complete", description: "Complete loop" },
        { label: "exit", description: "Exit and save" }
      ]
    }]
  })

  return { action: response["Action"] }
}

Action Catalog

Action Purpose Preconditions Effects
INIT Initialize session status=running, skill_state=null skill_state initialized
MENU Display menu skill_state != null, mode=interactive Wait for user input
DEVELOP Execute dev task pending tasks > 0 Update progress.md
DEBUG Hypothesis debug needs debugging Update understanding.md
VALIDATE Run tests needs validation Update validation.md
COMPLETE Finish loop all done status=completed

Termination Conditions

  1. API Paused: state.status === 'paused' (Skill exits, wait for resume)
  2. API Stopped: state.status === 'failed' (Skill terminates)
  3. Task Complete: NEXT_ACTION_NEEDED === 'COMPLETED'
  4. Iteration Limit: current_iteration >= max_iterations
  5. User Exit: User selects 'exit' in interactive mode

Error Recovery

Error Type Recovery Strategy
Agent timeout send_input requesting convergence
Action failed Log error, continue or prompt user
State corrupted Rebuild from progress files
Agent closed unexpectedly Re-spawn with previous output in message

Codex Best Practices Applied

  1. Single Agent Pattern: One agent handles entire loop lifecycle
  2. Deep Interaction via send_input: Multi-phase without context loss
  3. Delayed close_agent: Only after confirming no more interaction
  4. Explicit wait(): Always get results before proceeding
  5. Role Path Passing: Agent reads role file, no content embedding