From ba1f99f8589bd593326f2ece8843d97a91f15453 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 23 Feb 2026 23:25:16 +0800 Subject: [PATCH] feat(codex): add execution logging to planex-executor agents Both issue-devpipeline and team-planex executors now maintain execution.md (overview + task table + summary) and execution-events.md (chronological event stream with START/COMPLETE/FAIL per task, test verification, commit records) during execution. --- .../agents/planex-executor.md | 267 +++++++++++++++++- .../team-planex/agents/planex-executor.md | 237 +++++++++++++++- 2 files changed, 478 insertions(+), 26 deletions(-) diff --git a/.codex/skills/issue-devpipeline/agents/planex-executor.md b/.codex/skills/issue-devpipeline/agents/planex-executor.md index 4ad2d9f5..126ef2e0 100644 --- a/.codex/skills/issue-devpipeline/agents/planex-executor.md +++ b/.codex/skills/issue-devpipeline/agents/planex-executor.md @@ -18,6 +18,147 @@ skill: issue-devpipeline 3. **测试验证**: 运行相关测试确保变更正确且不破坏现有功能 4. **变更提交**: 将实现的代码 commit 到 git +## Execution Logging + +执行过程中**必须**实时维护两个日志文件,记录每个任务的执行状态和细节。 + +### Session Folder + +```javascript +// sessionFolder 从 TASK ASSIGNMENT 中的 session_dir 获取,或使用默认路径 +const sessionFolder = taskAssignment.session_dir || `.workflow/.team/PEX-${issueId}` +``` + +### execution.md — 执行概览 + +在开始实现前初始化,任务完成/失败时更新状态。 + +```javascript +function initExecution(issueId, solution) { + const executionMd = `# Execution Overview + +## Session Info +- **Issue**: ${issueId} +- **Solution**: ${solution.bound?.id || 'N/A'} +- **Started**: ${getUtc8ISOString()} +- **Executor**: planex-executor (issue-devpipeline) +- **Execution Mode**: Direct inline + +## Solution Tasks + +| # | Task | Files | Status | +|---|------|-------|--------| +${(solution.bound?.tasks || []).map((t, i) => + `| ${i+1} | ${t.title || t.description || 'Task ' + (i+1)} | ${(t.files || []).join(', ') || '-'} | pending |` +).join('\n')} + +## Execution Timeline +> Updated as tasks complete + +## Execution Summary +> Updated after completion +` + write_file(`${sessionFolder}/execution.md`, executionMd) +} +``` + +### execution-events.md — 事件流 + +每个任务的 START/COMPLETE/FAIL 实时追加记录。 + +```javascript +function initEvents(issueId) { + const eventsHeader = `# Execution Events + +**Issue**: ${issueId} +**Executor**: planex-executor (issue-devpipeline) +**Started**: ${getUtc8ISOString()} + +--- + +` + write_file(`${sessionFolder}/execution-events.md`, eventsHeader) +} + +function appendEvent(content) { + // Append to execution-events.md + const existing = read_file(`${sessionFolder}/execution-events.md`) + write_file(`${sessionFolder}/execution-events.md`, existing + content) +} + +function recordTaskStart(task, index) { + appendEvent(`## ${getUtc8ISOString()} — Task ${index + 1}: ${task.title || task.description || 'Unnamed'} + +**Status**: ⏳ IN PROGRESS +**Files**: ${(task.files || []).join(', ') || 'TBD'} + +### Execution Log +`) +} + +function recordTaskComplete(task, index, filesModified, changeSummary, duration) { + appendEvent(` +**Status**: ✅ COMPLETED +**Duration**: ${duration} +**Files Modified**: ${filesModified.join(', ')} + +#### Changes Summary +${changeSummary} + +--- +`) +} + +function recordTaskFailed(task, index, error, duration) { + appendEvent(` +**Status**: ❌ FAILED +**Duration**: ${duration} +**Error**: ${error} + +--- +`) +} + +function updateTaskStatus(taskIndex, status) { + // Update the task row in execution.md table: replace "pending" → status + const content = read_file(`${sessionFolder}/execution.md`) + // Find and update the Nth task row status + // (Edit the specific table row) +} + +function finalizeExecution(totalTasks, succeeded, failedCount, filesModified) { + const summary = ` +## Execution Summary + +- **Completed**: ${getUtc8ISOString()} +- **Total Tasks**: ${totalTasks} +- **Succeeded**: ${succeeded} +- **Failed**: ${failedCount} +- **Success Rate**: ${Math.round(succeeded / totalTasks * 100)}% +- **Files Modified**: ${filesModified.join(', ')} +` + // Append summary to execution.md + const content = read_file(`${sessionFolder}/execution.md`) + write_file(`${sessionFolder}/execution.md`, + content.replace('> Updated after completion', summary)) + + // Append session footer to execution-events.md + appendEvent(` +--- + +# Session Summary + +- **Issue**: ${issueId} +- **Completed**: ${getUtc8ISOString()} +- **Tasks**: ${succeeded} completed, ${failedCount} failed +`) +} + +function getUtc8ISOString() { + return new Date(Date.now() + 8 * 3600000).toISOString().replace('Z', '+08:00') +} +``` + ## Execution Process ### Step 1: Context Loading @@ -32,6 +173,7 @@ skill: issue-devpipeline - **Issue ID**: 目标 issue 标识 - **Solution ID**: 绑定的 solution 标识 - **Dependencies**: 依赖的其他 issues(应已完成) + - **Session Dir**: 日志文件存放路径 - **Deliverables**: Expected JSON output format ### Step 2: Solution Loading & Implementation @@ -43,23 +185,55 @@ const solJson = shell(`ccw issue solutions ${issueId} --json`) const solution = JSON.parse(solJson) if (!solution.bound) { - // No bound solution — report error outputError(`No bound solution for ${issueId}`) return } +// ── Initialize execution logs ── +shell(`mkdir -p ${sessionFolder}`) +initExecution(issueId, solution) +initEvents(issueId) + // Update issue status shell(`ccw issue update ${issueId} --status in-progress`) // ── Implement according to solution plan ── const plan = solution.bound const tasks = plan.tasks || [] +let succeeded = 0, failedCount = 0 +const allFilesModified = [] -for (const task of tasks) { - // 1. Read target files - // 2. Apply changes following existing patterns - // 3. Write/Edit files - // 4. Verify no syntax errors +for (let i = 0; i < tasks.length; i++) { + const task = tasks[i] + const startTime = Date.now() + + // Record START event + recordTaskStart(task, i) + + try { + // 1. Read target files + // 2. Apply changes following existing patterns + // 3. Write/Edit files + // 4. Verify no syntax errors + + const endTime = Date.now() + const duration = `${Math.round((endTime - startTime) / 1000)}s` + const filesModified = getModifiedFiles() + allFilesModified.push(...filesModified) + + // Record COMPLETE event + recordTaskComplete(task, i, filesModified, changeSummary, duration) + updateTaskStatus(i, 'completed') + succeeded++ + } catch (error) { + const endTime = Date.now() + const duration = `${Math.round((endTime - startTime) / 1000)}s` + + // Record FAIL event + recordTaskFailed(task, i, error.message, duration) + updateTaskStatus(i, 'failed') + failedCount++ + } } ``` @@ -69,7 +243,7 @@ for (const task of tasks) { - 最小化变更,不做超出 solution 范围的修改 - 每个 task 完成后验证无语法错误 -### Step 3: Testing & Commit +### Step 3: Testing, Commit & Finalize Logs ```javascript // ── Detect test command ── @@ -79,40 +253,78 @@ try { if (pkgJson.scripts?.test) testCmd = 'npm test' else if (pkgJson.scripts?.['test:unit']) testCmd = 'npm run test:unit' } catch { - // Try common test runners if (fileExists('pytest.ini') || fileExists('setup.py')) testCmd = 'pytest' else if (fileExists('Cargo.toml')) testCmd = 'cargo test' } // ── Run tests ── +const testStartTime = Date.now() +appendEvent(`## ${getUtc8ISOString()} — Integration Test Verification + +**Status**: ⏳ IN PROGRESS +**Command**: \`${testCmd}\` + +### Test Log +`) + const testResult = shell(`${testCmd} 2>&1`) -const testsPassed = !testResult.includes('FAIL') && testResult.exitCode === 0 +let testsPassed = !testResult.includes('FAIL') && testResult.exitCode === 0 if (!testsPassed) { - // Attempt fix: analyze failures, apply fix, re-test (max 2 retries) let retries = 0 while (retries < 2 && !testsPassed) { - // Analyze test output, identify failure cause, apply fix + appendEvent(`- Retry ${retries + 1}: fixing test failures...\n`) retries++ const retestResult = shell(`${testCmd} 2>&1`) testsPassed = !retestResult.includes('FAIL') && retestResult.exitCode === 0 } } +const testDuration = `${Math.round((Date.now() - testStartTime) / 1000)}s` + +if (testsPassed) { + appendEvent(` +**Status**: ✅ TESTS PASSED +**Duration**: ${testDuration} + +--- +`) +} else { + appendEvent(` +**Status**: ❌ TESTS FAILED +**Duration**: ${testDuration} +**Output** (truncated): +\`\`\` +${testResult.slice(0, 500)} +\`\`\` + +--- +`) +} + // ── Commit if tests pass ── let commitHash = null let committed = false if (testsPassed) { - // Stage changed files shell('git add -A') shell(`git commit -m "feat(${issueId}): implement solution ${solution.bound.id}"`) commitHash = shell('git rev-parse --short HEAD').trim() committed = true - // Update issue status + appendEvent(`## ${getUtc8ISOString()} — Git Commit + +**Commit**: \`${commitHash}\` +**Message**: feat(${issueId}): implement solution ${solution.bound.id} + +--- +`) + shell(`ccw issue update ${issueId} --status resolved`) } + +// ── Finalize execution logs ── +finalizeExecution(tasks.length, succeeded, failedCount, [...new Set(allFilesModified)]) ``` ### Step 4: Output Delivery @@ -131,7 +343,11 @@ if (testsPassed) { "committed": true, "commit_hash": "abc1234", "error": null, - "summary": "实现用户登录功能,添加 2 个文件,通过所有测试" + "summary": "实现用户登录功能,添加 2 个文件,通过所有测试", + "execution_logs": { + "execution_md": "${sessionFolder}/execution.md", + "events_md": "${sessionFolder}/execution-events.md" + } } ``` @@ -146,10 +362,27 @@ if (testsPassed) { "committed": false, "commit_hash": null, "error": "Tests failing: login.test.ts:42 - Expected 200 got 401", - "summary": "代码实现完成但测试未通过,需要 solution 修订" + "summary": "代码实现完成但测试未通过,需要 solution 修订", + "execution_logs": { + "execution_md": "${sessionFolder}/execution.md", + "events_md": "${sessionFolder}/execution-events.md" + } } ``` +## Execution Log Output Structure + +``` +${sessionFolder}/ +├── execution.md # 执行概览:session info, task table, summary +└── execution-events.md # 事件流:每个 task 的 START/COMPLETE/FAIL 详情 +``` + +| File | Purpose | +|------|---------| +| `execution.md` | 概览:solution tasks 表格、执行统计、最终结果 | +| `execution-events.md` | 时间线:每个任务和测试验证的详细事件记录 | + ## Role Boundaries ### MUST @@ -174,6 +407,10 @@ if (testsPassed) { **ALWAYS**: - Read role definition file as FIRST action (Step 1) +- **Initialize execution.md + execution-events.md BEFORE starting implementation** +- **Record START event before each solution task** +- **Record COMPLETE/FAIL event after each task with duration and details** +- **Finalize logs after testing and commit** - Load solution plan before implementing - Follow existing code patterns in the project - Run tests before committing diff --git a/.codex/skills/team-planex/agents/planex-executor.md b/.codex/skills/team-planex/agents/planex-executor.md index 29fd4ab6..52331db4 100644 --- a/.codex/skills/team-planex/agents/planex-executor.md +++ b/.codex/skills/team-planex/agents/planex-executor.md @@ -20,6 +20,156 @@ skill: team-planex 4. **Commit Management**: 每个 solution 完成后 git commit 5. **Result Reporting**: 输出结构化 IMPL_COMPLETE / WAVE_DONE 数据 +## Execution Logging + +执行过程中**必须**实时维护两个日志文件,记录每个任务的执行状态和细节。 + +### Session Folder + +```javascript +// sessionFolder 从 TASK ASSIGNMENT 中的 session_dir 获取,或使用默认路径 +const sessionFolder = taskAssignment.session_dir + || `.workflow/.team/PEX-wave${waveNum}-${new Date().toISOString().slice(0,10)}` +``` + +### execution.md — 执行概览 + +在开始实现前初始化,任务完成/失败时更新状态。 + +```javascript +function initExecution(waveNum, execTasks, executionMethod) { + const executionMd = `# Execution Overview + +## Session Info +- **Wave**: ${waveNum} +- **Started**: ${getUtc8ISOString()} +- **Total Tasks**: ${execTasks.length} +- **Executor**: planex-executor (team-planex) +- **Execution Method**: ${executionMethod} +- **Execution Mode**: Sequential by dependency + +## Task Overview + +| # | Issue ID | Solution | Title | Priority | Dependencies | Status | +|---|----------|----------|-------|----------|--------------|--------| +${execTasks.map((t, i) => + `| ${i+1} | ${t.issue_id} | ${t.solution_id} | ${t.title} | ${t.priority} | ${(t.depends_on || []).join(', ') || '-'} | pending |` +).join('\n')} + +## Execution Timeline +> Updated as tasks complete + +## Execution Summary +> Updated after all tasks complete +` + shell(`mkdir -p ${sessionFolder}`) + write_file(`${sessionFolder}/execution.md`, executionMd) +} +``` + +### execution-events.md — 事件流 + +每个任务的 START/COMPLETE/FAIL 实时追加记录。 + +```javascript +function initEvents(waveNum) { + const eventsHeader = `# Execution Events + +**Wave**: ${waveNum} +**Executor**: planex-executor (team-planex) +**Started**: ${getUtc8ISOString()} + +--- + +` + write_file(`${sessionFolder}/execution-events.md`, eventsHeader) +} + +function appendEvent(content) { + const existing = read_file(`${sessionFolder}/execution-events.md`) + write_file(`${sessionFolder}/execution-events.md`, existing + content) +} + +function recordTaskStart(issueId, title, executor, files) { + appendEvent(`## ${getUtc8ISOString()} — ${issueId}: ${title} + +**Executor Backend**: ${executor} +**Status**: ⏳ IN PROGRESS +**Files**: ${files || 'TBD'} + +### Execution Log +`) +} + +function recordTaskComplete(issueId, executor, commitHash, filesModified, duration) { + appendEvent(` +**Status**: ✅ COMPLETED +**Duration**: ${duration} +**Executor**: ${executor} +**Commit**: \`${commitHash}\` +**Files Modified**: ${filesModified.join(', ')} + +--- +`) +} + +function recordTaskFailed(issueId, executor, error, resumeHint, duration) { + appendEvent(` +**Status**: ❌ FAILED +**Duration**: ${duration} +**Executor**: ${executor} +**Error**: ${error} +${resumeHint ? `**Resume**: \`${resumeHint}\`` : ''} + +--- +`) +} + +function recordTestVerification(issueId, passed, testOutput, duration) { + appendEvent(` +#### Test Verification — ${issueId} +- **Result**: ${passed ? '✅ PASS' : '❌ FAIL'} +- **Duration**: ${duration} +${!passed ? `- **Output** (truncated):\n\`\`\`\n${testOutput.slice(0, 500)}\n\`\`\`\n` : ''} +`) +} + +function updateTaskStatus(issueId, status) { + // Update the task row in execution.md table: replace "pending" with status + const content = read_file(`${sessionFolder}/execution.md`) + // Find row containing issueId, replace "pending" → status +} + +function finalizeExecution(totalTasks, succeeded, failedCount) { + const summary = ` +## Execution Summary + +- **Completed**: ${getUtc8ISOString()} +- **Total Tasks**: ${totalTasks} +- **Succeeded**: ${succeeded} +- **Failed**: ${failedCount} +- **Success Rate**: ${Math.round(succeeded / totalTasks * 100)}% +` + const content = read_file(`${sessionFolder}/execution.md`) + write_file(`${sessionFolder}/execution.md`, + content.replace('> Updated after all tasks complete', summary)) + + appendEvent(` +--- + +# Session Summary + +- **Wave**: ${waveNum} +- **Completed**: ${getUtc8ISOString()} +- **Tasks**: ${succeeded} completed, ${failedCount} failed +`) +} + +function getUtc8ISOString() { + return new Date(Date.now() + 8 * 3600000).toISOString().replace('Z', '+08:00') +} +``` + ## Execution Process ### Step 1: Context Loading @@ -37,7 +187,7 @@ skill: team-planex ### Step 2: Implementation (Sequential by Dependency) -Process each task in the wave, respecting dependency order. +Process each task in the wave, respecting dependency order. **Record every task to execution logs.** ```javascript const tasks = taskAssignment.exec_tasks @@ -45,6 +195,10 @@ const executionMethod = taskAssignment.execution_config.execution_method const codeReview = taskAssignment.execution_config.code_review const waveNum = taskAssignment.wave_number +// ── Initialize execution logs ── +initExecution(waveNum, tasks, executionMethod) +initEvents(waveNum) + let completed = 0 let failed = 0 @@ -53,12 +207,18 @@ const sorted = topologicalSort(tasks) for (const task of sorted) { const issueId = task.issue_id + const taskStartTime = Date.now() // --- Load solution --- const solJson = shell(`ccw issue solution ${issueId} --json`) const solution = JSON.parse(solJson) if (!solution.bound) { + recordTaskStart(issueId, task.title, 'N/A', '') + recordTaskFailed(issueId, 'N/A', 'No bound solution', null, + `${Math.round((Date.now() - taskStartTime) / 1000)}s`) + updateTaskStatus(issueId, 'failed') + console.log(`IMPL_COMPLETE:\n${JSON.stringify({ issue_id: issueId, status: "failed", @@ -77,6 +237,12 @@ for (const task of sorted) { const taskCount = solution.bound.task_count || solution.bound.tasks?.length || 0 const executor = resolveExecutor(executionMethod, taskCount) + // --- Record START event --- + const solutionFiles = (solution.bound.tasks || []) + .flatMap(t => t.files || []).join(', ') + recordTaskStart(issueId, task.title, executor, solutionFiles) + updateTaskStatus(issueId, 'in_progress') + // --- Build execution prompt --- const prompt = buildExecutionPrompt(issueId, solution) @@ -85,6 +251,7 @@ for (const task of sorted) { if (executor === 'agent') { // Spawn code-developer subagent (synchronous) + appendEvent(`- Spawning code-developer agent...\n`) const devAgent = spawn_agent({ message: ` ## TASK ASSIGNMENT @@ -103,24 +270,27 @@ ${prompt} const devResult = wait({ ids: [devAgent], timeout_ms: 900000 }) if (devResult.timed_out) { + appendEvent(`- Agent timed out, urging convergence...\n`) send_input({ id: devAgent, message: "Please finalize implementation and output results." }) wait({ ids: [devAgent], timeout_ms: 120000 }) } close_agent({ id: devAgent }) - implSuccess = true // will verify with tests below + appendEvent(`- code-developer agent completed\n`) + implSuccess = true } else if (executor === 'codex') { - // Codex CLI execution const fixedId = `planex-${issueId}` + appendEvent(`- Executing via Codex CLI (id: ${fixedId})...\n`) shell(`ccw cli -p "${prompt}" --tool codex --mode write --id ${fixedId}`) - // Wait for CLI completion (synchronous in agent context) + appendEvent(`- Codex CLI completed\n`) implSuccess = true } else if (executor === 'gemini') { - // Gemini CLI execution const fixedId = `planex-${issueId}` + appendEvent(`- Executing via Gemini CLI (id: ${fixedId})...\n`) shell(`ccw cli -p "${prompt}" --tool gemini --mode write --id ${fixedId}`) + appendEvent(`- Gemini CLI completed\n`) implSuccess = true } @@ -132,10 +302,23 @@ ${prompt} else if (pkgJson.scripts?.['test:unit']) testCmd = 'npm run test:unit' } catch { /* use default */ } + const testStartTime = Date.now() + appendEvent(`- Running tests: \`${testCmd}\`...\n`) const testResult = shell(`${testCmd} 2>&1 || echo "TEST_FAILED"`) const testPassed = !testResult.includes('TEST_FAILED') && !testResult.includes('FAIL') + const testDuration = `${Math.round((Date.now() - testStartTime) / 1000)}s` + + recordTestVerification(issueId, testPassed, testResult, testDuration) if (!testPassed) { + const duration = `${Math.round((Date.now() - taskStartTime) / 1000)}s` + const resumeHint = executor !== 'agent' + ? `ccw cli -p "Fix failing tests" --resume planex-${issueId} --tool ${executor} --mode write` + : null + + recordTaskFailed(issueId, executor, 'Tests failing after implementation', resumeHint, duration) + updateTaskStatus(issueId, 'failed') + console.log(`IMPL_COMPLETE:\n${JSON.stringify({ issue_id: issueId, status: "failed", @@ -144,9 +327,7 @@ ${prompt} test_result: "fail", test_output: testResult.slice(0, 500), commit: "N/A", - resume_hint: executor !== 'agent' - ? `ccw cli -p "Fix failing tests" --resume planex-${issueId} --tool ${executor} --mode write` - : "Re-spawn code-developer with fix instructions" + resume_hint: resumeHint || "Re-spawn code-developer with fix instructions" }, null, 2)}`) failed++ continue @@ -154,6 +335,7 @@ ${prompt} // --- Optional code review --- if (codeReview && codeReview !== 'Skip') { + appendEvent(`- Running code review (${codeReview})...\n`) executeCodeReview(codeReview, issueId) } @@ -161,10 +343,18 @@ ${prompt} shell(`git add -A && git commit -m "feat(${issueId}): implement solution ${task.solution_id}"`) const commitHash = shell('git rev-parse --short HEAD').trim() + appendEvent(`- Committed: \`${commitHash}\`\n`) + // --- Update issue status --- shell(`ccw issue update ${issueId} --status completed`) - // --- Report completion --- + // --- Record completion --- + const duration = `${Math.round((Date.now() - taskStartTime) / 1000)}s` + const filesModified = shell('git diff --name-only HEAD~1 HEAD').trim().split('\n') + + recordTaskComplete(issueId, executor, commitHash, filesModified, duration) + updateTaskStatus(issueId, 'completed') + console.log(`IMPL_COMPLETE:\n${JSON.stringify({ issue_id: issueId, status: "success", @@ -177,16 +367,37 @@ ${prompt} } ``` -### Step 3: Wave Completion Report +### Step 3: Wave Completion Report & Log Finalization ```javascript +// ── Finalize execution logs ── +finalizeExecution(sorted.length, completed, failed) + +// ── Output structured wave result ── console.log(`WAVE_DONE:\n${JSON.stringify({ wave_number: waveNum, completed: completed, - failed: failed + failed: failed, + execution_logs: { + execution_md: `${sessionFolder}/execution.md`, + events_md: `${sessionFolder}/execution-events.md` + } }, null, 2)}`) ``` +## Execution Log Output Structure + +``` +${sessionFolder}/ +├── execution.md # 执行概览:wave info, task table, summary +└── execution-events.md # 事件流:每个 task 的 START/COMPLETE/FAIL + 测试验证详情 +``` + +| File | Purpose | +|------|---------| +| `execution.md` | 概览:wave task 表格(issue/solution/status)、执行统计、最终结果 | +| `execution-events.md` | 时间线:每个 task 的后端选择、实现日志、测试验证、commit 记录 | + ## Execution Method Resolution ```javascript @@ -291,6 +502,10 @@ function topologicalSort(tasks) { **ALWAYS**: - Read role definition file as FIRST action (Step 1) +- **Initialize execution.md + execution-events.md BEFORE starting any task** +- **Record START event before each task implementation** +- **Record COMPLETE/FAIL event after each task with duration and details** +- **Finalize logs at wave completion** - Follow structured output template (IMPL_COMPLETE / WAVE_DONE) - Verify tests pass before committing - Respect dependency ordering within the wave