Files
catlog22 c62d26183b feat: Add interactive pre-flight checklists for ccw-loop and workflow-plan, including validation and task transformation steps
- Implemented `prep-loop.md` for ccw-loop, detailing source discovery, validation, task transformation, and auto-loop configuration.
- Created `prep-plan.md` for workflow planning, covering environment checks, task quality assessment, execution preferences, and final confirmation.
- Defined schemas and integration points for `prep-package.json` in both ccw-loop and workflow-plan skills, ensuring proper validation and task handling.
- Added error handling mechanisms for various scenarios during the preparation phases.
2026-02-09 15:02:38 +08:00

7.9 KiB

Phase 1: Session Initialization

Create or resume a development loop, initialize state file and directory structure.

Objective

  • Parse user arguments (TASK, --loop-id, --auto)
  • Create new loop with unique ID OR resume existing loop
  • Initialize directory structure for progress files
  • Create master state file
  • Output: loopId, state, progressDir

Execution

Step 0: Determine Project Root

// Step 0: Determine Project Root
const projectRoot = Bash('git rev-parse --show-toplevel 2>/dev/null || pwd').trim()

Step 1.1: Parse Arguments & Load Prep Package

const { loopId: existingLoopId, task, mode = 'interactive' } = options

// Validate mutual exclusivity
if (!existingLoopId && !task) {
  console.error('Either --loop-id or task description is required')
  return { status: 'error', message: 'Missing loopId or task' }
}

// Determine mode
const executionMode = options['--auto'] ? 'auto' : 'interactive'

// ── Prep Package: Detect → Validate → Consume ──
let prepPackage = null
let prepTasks = null
const prepPath = `${projectRoot}/.workflow/.loop/prep-package.json`

if (fs.existsSync(prepPath)) {
  const raw = JSON.parse(Read(prepPath))
  const checks = validateLoopPrepPackage(raw, projectRoot)

  if (checks.valid) {
    prepPackage = raw

    // Load pre-built tasks
    const tasksPath = `${projectRoot}/.workflow/.loop/prep-tasks.jsonl`
    prepTasks = loadPrepTasks(tasksPath)

    if (prepTasks && prepTasks.length > 0) {
      console.log(`✓ Prep package loaded: ${prepTasks.length} tasks from ${prepPackage.source.tool}`)
      console.log(`  Checks passed: ${checks.passed.join(', ')}`)
    } else {
      console.warn(`⚠ Prep tasks file empty or invalid, falling back to default INIT`)
      prepPackage = null
      prepTasks = null
    }
  } else {
    console.warn(`⚠ Prep package found but failed validation:`)
    checks.failures.forEach(f => console.warn(`  ✗ ${f}`))
    console.warn(`  → Falling back to default behavior (prep-package ignored)`)
    prepPackage = null
  }
}

/**
 * Validate prep-package.json integrity before consumption.
 * Returns { valid: bool, passed: string[], failures: string[] }
 */
function validateLoopPrepPackage(prep, projectRoot) {
  const passed = []
  const failures = []

  // Check 1: prep_status must be "ready"
  if (prep.prep_status === 'ready') {
    passed.push('status=ready')
  } else {
    failures.push(`prep_status is "${prep.prep_status}", expected "ready"`)
  }

  // Check 2: target_skill must match
  if (prep.target_skill === 'ccw-loop') {
    passed.push('target_skill match')
  } else {
    failures.push(`target_skill is "${prep.target_skill}", expected "ccw-loop"`)
  }

  // Check 3: project_root must match current project
  if (prep.environment?.project_root === projectRoot) {
    passed.push('project_root match')
  } else {
    failures.push(`project_root mismatch: prep="${prep.environment?.project_root}", current="${projectRoot}"`)
  }

  // Check 4: generated_at must be within 24 hours
  const generatedAt = new Date(prep.generated_at)
  const hoursSince = (Date.now() - generatedAt.getTime()) / (1000 * 60 * 60)
  if (hoursSince <= 24) {
    passed.push(`age=${Math.round(hoursSince)}h`)
  } else {
    failures.push(`prep-package is ${Math.round(hoursSince)}h old (max 24h), may be stale`)
  }

  // Check 5: prep-tasks.jsonl must exist
  const tasksPath = `${projectRoot}/.workflow/.loop/prep-tasks.jsonl`
  if (fs.existsSync(tasksPath)) {
    passed.push('prep-tasks.jsonl exists')
  } else {
    failures.push('prep-tasks.jsonl not found')
  }

  // Check 6: task count > 0
  if ((prep.tasks?.total || 0) > 0) {
    passed.push(`tasks=${prep.tasks.total}`)
  } else {
    failures.push('task count is 0')
  }

  return {
    valid: failures.length === 0,
    passed,
    failures
  }
}

/**
 * Load pre-built tasks from prep-tasks.jsonl.
 * Returns array of task objects or null on failure.
 */
function loadPrepTasks(tasksPath) {
  if (!fs.existsSync(tasksPath)) return null

  const content = Read(tasksPath)
  const lines = content.trim().split('\n').filter(l => l.trim())
  const tasks = []

  for (const line of lines) {
    try {
      const task = JSON.parse(line)
      if (task.id && task.description) {
        tasks.push(task)
      }
    } catch (e) {
      console.warn(`⚠ Skipping invalid task line: ${e.message}`)
    }
  }

  return tasks.length > 0 ? tasks : null
}

Step 1.2: Utility Functions

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

function readLoopState(loopId) {
  const stateFile = `${projectRoot}/.workflow/.loop/${loopId}.json`
  if (!fs.existsSync(stateFile)) {
    return null
  }
  return JSON.parse(Read(stateFile))
}

Step 1.3: New Loop Creation

When TASK is provided (no --loop-id):

// Generate unique loop ID
const timestamp = getUtc8ISOString().replace(/[-:]/g, '').split('.')[0]
const random = Math.random().toString(36).substring(2, 10)
const loopId = `loop-v2-${timestamp}-${random}`

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

Create Directory Structure

mkdir -p ${projectRoot}/.workflow/.loop/${loopId}.progress

Initialize State File

function createLoopState(loopId, taskDescription) {
  const stateFile = `${projectRoot}/.workflow/.loop/${loopId}.json`
  const now = getUtc8ISOString()

  const state = {
    // API compatible fields
    loop_id: loopId,
    title: taskDescription.substring(0, 100),
    description: taskDescription,
    max_iterations: prepPackage?.auto_loop?.max_iterations || 10,
    status: 'running',
    current_iteration: 0,
    created_at: now,
    updated_at: now,

    // Skill extension fields
    // When prep tasks available, pre-populate skill_state instead of null
    skill_state: prepTasks ? {
      current_action: 'init',
      last_action: null,
      completed_actions: [],
      mode: executionMode,

      develop: {
        total: prepTasks.length,
        completed: 0,
        current_task: null,
        tasks: prepTasks,
        last_progress_at: null
      },

      debug: {
        active_bug: null,
        hypotheses_count: 0,
        hypotheses: [],
        confirmed_hypothesis: null,
        iteration: 0,
        last_analysis_at: null
      },

      validate: {
        pass_rate: 0,
        coverage: 0,
        test_results: [],
        passed: false,
        failed_tests: [],
        last_run_at: null
      },

      errors: []
    } : null,

    // Prep package metadata (for traceability)
    prep_source: prepPackage?.source || null
  }

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

Step 1.4: Resume Existing Loop

When --loop-id is provided:

const loopId = existingLoopId
const 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}`)

Step 1.5: Control Signal Check

Before proceeding, verify loop status allows continuation:

function checkControlSignals(loopId) {
  const state = readLoopState(loopId)

  switch (state?.status) {
    case 'paused':
      return { continue: false, action: 'pause_exit' }
    case 'failed':
      return { continue: false, action: 'stop_exit' }
    case 'running':
      return { continue: true, action: 'continue' }
    default:
      return { continue: false, action: 'stop_exit' }
  }
}

Output

  • Variable: loopId - Unique loop identifier
  • Variable: state - Initialized or resumed loop state object
  • Variable: progressDir - ${projectRoot}/.workflow/.loop/${loopId}.progress
  • Variable: mode - 'interactive' or 'auto'
  • TodoWrite: Mark Phase 1 completed, Phase 2 in_progress

Next Phase

Return to orchestrator, then auto-continue to Phase 2: Orchestration Loop.