This commit is contained in:
2026-03-01 12:24:11 -08:00
parent 628e688ab9
commit 63656d339d
33 changed files with 1018 additions and 539 deletions
+3 -11
View File
@@ -99,13 +99,9 @@ function buildCohort(runs: RunMetrics[], cohortKey: string): CohortMetrics {
const avgReqCoverage = avg(runs.map(r => r.step2_document.requirement_coverage_percent));
const avgVerifItems = avg(runs.map(r => r.step2_document.verification_items_added));
// Step 3 (loop only)
const loopRuns = runs.filter(r => r.step3_implement.used_loop_mode);
const avgCompletionRate = avg(loopRuns.map(r => r.step3_implement.task_completion_rate));
const avgBlockerCount = avg(loopRuns.map(r => r.step3_implement.blocker_count));
const avgTotalIter = avg(loopRuns.map(r => r.step3_implement.total_iterations));
const avgIterDuration = avg(loopRuns.map(r => r.step3_implement.avg_iteration_duration_ms));
const avgMarkerSuccess = avg(loopRuns.map(r => r.step3_implement.completion_marker_success_rate));
// Step 3 averages
const avgCompletionRate = avg(runs.map(r => r.step3_implement.task_completion_rate));
const avgBlockerCount = avg(runs.map(r => r.step3_implement.blocker_count));
// Step 4 averages
const avgCompletionAtAudit = avg(runs.map(r => r.step4_finalize.completion_rate_at_audit));
@@ -140,12 +136,8 @@ function buildCohort(runs: RunMetrics[], cohortKey: string): CohortMetrics {
avg_requirement_coverage_percent: avgReqCoverage,
avg_verification_items_added: avgVerifItems,
loop_run_count: loopRuns.length,
avg_task_completion_rate: avgCompletionRate,
avg_blocker_count: avgBlockerCount,
avg_total_iterations: avgTotalIter,
avg_iteration_duration_ms: avgIterDuration,
avg_completion_marker_success_rate: avgMarkerSuccess,
avg_completion_rate_at_audit: avgCompletionAtAudit,
avg_verification_failures_found: avgVerifFailures,
+3 -3
View File
@@ -57,12 +57,12 @@ export interface AnalyzerOptions {
aggregatedPath: string; // Path to aggregated.json
plan2codeRoot: string; // Path to plan2code repo root
proposalsDir: string; // Where to save diagnosis output
model?: string; // AI model to use (default: claude-opus-4-6)
model?: string; // AI model to use (default: agent's default)
agent?: AgentType; // Agent to use (default: claude-code)
}
export async function runAnalysis(opts: AnalyzerOptions): Promise<string> {
const { aggregatedPath, plan2codeRoot, proposalsDir, model = 'claude-opus-4-6', agent = 'claude-code' } = opts;
const { aggregatedPath, plan2codeRoot, proposalsDir, model = 'default', agent = 'claude-code' } = opts;
// Load aggregated metrics
let aggregated: AggregatedMetrics | null = null;
@@ -102,7 +102,7 @@ export async function runAnalysis(opts: AnalyzerOptions): Promise<string> {
});
// Invoke Claude
console.log(`\nInvoking AI analysis (model: ${model})...`);
console.log(`\nInvoking AI analysis (agent: ${agent}, model: ${model === 'default' ? 'user default' : model})...`);
console.log('This may take a minute...\n');
let diagnosisContent: string;
+227 -31
View File
@@ -5,6 +5,7 @@
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import { select, input, confirm } from '@inquirer/prompts';
import chalk from 'chalk';
@@ -18,6 +19,33 @@ import type { AggregatedMetrics, CohortMetrics } from './types.js';
import { METRIC_TARGETS } from './types.js';
import { AGENTS, type AgentType } from './invoke-llm.js';
// ── Session state (set at startup via interactive prompts) ───────────────────
let resolvedPlan2CodeRoot = '';
let resolvedProjectRoot = '';
// ── Persisted user config (~/.plan2code-metrics.json) ──────────────────────
const USER_CONFIG_PATH = path.join(os.homedir(), '.plan2code-metrics.json');
interface UserConfig {
plan2codeRepoPath?: string;
}
function loadUserConfig(): UserConfig {
try {
return JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf8'));
} catch {
return {};
}
}
function saveUserConfig(config: UserConfig): void {
try {
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
} catch { /* best-effort */ }
}
// ── Defaults ──────────────────────────────────────────────────────────────────
const DEFAULT_METRICS_DIR = '.plan2code-metrics';
@@ -25,7 +53,7 @@ const DEFAULT_RUNS_SUBDIR = 'runs';
const DEFAULT_AGGREGATED_FILE = 'aggregated.json';
const DEFAULT_PROPOSALS_SUBDIR = 'proposals';
function getMetricsDirs(baseDir = process.cwd()) {
function getMetricsDirs(baseDir = resolvedProjectRoot) {
const metricsDir = path.join(baseDir, DEFAULT_METRICS_DIR);
return {
metricsDir,
@@ -89,13 +117,7 @@ async function selectAgentAndModel(): Promise<{ agent: AgentType; model: string
choices: Object.values(AGENTS).map(a => ({ name: a.displayName, value: a.name })),
});
const agentDef = AGENTS[agentChoice];
const model = await select({
message: 'AI model:',
choices: agentDef.models.map(m => ({ name: m.label, value: m.value })),
});
return { agent: agentChoice, model };
return { agent: agentChoice, model: AGENTS[agentChoice].defaultModel };
}
// ── Flow: Collect metrics ─────────────────────────────────────────────────────
@@ -107,8 +129,8 @@ async function flowCollect(): Promise<void> {
const sourceChoice = await select({
message: 'Where is the completed project spec?',
choices: [
{ name: 'Active spec directory (specs/<feature-name>/)', value: 'active' },
{ name: 'Archived spec (specs--completed/<feature-name>/)', value: 'archived' },
{ name: 'Active spec directory (specs/<feature-name>/)', value: 'active' },
{ name: 'Custom path', value: 'custom' },
],
});
@@ -118,10 +140,10 @@ async function flowCollect(): Promise<void> {
if (sourceChoice === 'active' || sourceChoice === 'archived') {
const baseSubdir = sourceChoice === 'active' ? 'specs' : 'specs--completed';
const baseDir = path.join(process.cwd(), baseSubdir);
const baseDir = path.join(resolvedProjectRoot, baseSubdir);
if (!fs.existsSync(baseDir)) {
console.log(chalk.red(`No ${baseSubdir}/ directory found in ${process.cwd()}`));
console.log(chalk.red(`No ${baseSubdir}/ directory found in ${resolvedProjectRoot}`));
return;
}
@@ -152,7 +174,7 @@ async function flowCollect(): Promise<void> {
}
// Detect plan2code root
const plan2codeRoot = detectPlan2CodeRoot() ?? process.cwd();
const plan2codeRoot = resolvedPlan2CodeRoot;
const plan2codeVersion = detectPlan2CodeVersion(plan2codeRoot);
const { runsDir, aggregatedPath } = getMetricsDirs();
@@ -179,7 +201,7 @@ async function flowCollect(): Promise<void> {
console.log(chalk.bold('Collected:'));
console.log(` Step 1 (Plan): ${metrics.step1_plan.present ? chalk.green('✓') : chalk.gray('—')}`);
console.log(` Step 2 (Document): ${metrics.step2_document.present ? chalk.green('✓') : chalk.gray('—')}`);
console.log(` Step 3 (Implement): ${metrics.step3_implement.present ? (metrics.step3_implement.used_loop_mode ? chalk.green('✓ (loop)') : chalk.yellow('✓ (manual)')) : chalk.gray('—')}`);
console.log(` Step 3 (Implement): ${metrics.step3_implement.present ? chalk.green('✓') : chalk.gray('—')}`);
console.log(` Step 4 (Finalize): ${metrics.step4_finalize.present ? chalk.green('✓') : chalk.gray('—')}`);
console.log(` User Feedback: ${metrics.user_feedback ? chalk.green(`✓ (${metrics.user_feedback.overall_rating}/10)`) : chalk.gray('—')}`);
@@ -334,15 +356,13 @@ async function flowViewStatus(): Promise<void> {
console.log(` avg_verification_items: ${metricStatus('avg_verification_items_added', cohort.avg_verification_items_added)}`);
}
// Step 3 metrics (loop only)
if (cohort.loop_run_count > 0) {
console.log(chalk.bold(` Step 3 (Implement)${cohort.loop_run_count} loop run(s):`));
// Step 3 metrics
if (cohort.avg_task_completion_rate != null || cohort.avg_blocker_count != null) {
console.log(chalk.bold(' Step 3 (Implement):'));
if (cohort.avg_task_completion_rate != null)
console.log(` avg_task_completion_rate: ${metricStatus('avg_task_completion_rate', cohort.avg_task_completion_rate)}`);
if (cohort.avg_blocker_count != null)
console.log(` avg_blocker_count: ${metricStatus('avg_blocker_count', cohort.avg_blocker_count)}`);
if (cohort.avg_completion_marker_success_rate != null)
console.log(` avg_marker_success_rate: ${metricStatus('avg_completion_marker_success_rate', cohort.avg_completion_marker_success_rate)}`);
}
// Step 4 metrics
@@ -402,7 +422,7 @@ async function flowRunAnalysis(): Promise<string | null> {
console.log(chalk.bold.cyan('── Run Analysis (Diagnose Weak Steps) ──'));
const { aggregatedPath, proposalsDir } = getMetricsDirs();
const plan2codeRoot = detectPlan2CodeRoot() ?? process.cwd();
const plan2codeRoot = resolvedPlan2CodeRoot;
const aggregated = loadAggregated(aggregatedPath);
if (!aggregated || aggregated.total_runs === 0) {
@@ -460,7 +480,7 @@ async function flowGenerateProposal(): Promise<void> {
console.log(chalk.bold.cyan('── Generate Improvement Proposal ──'));
const { aggregatedPath, proposalsDir, runsDir } = getMetricsDirs();
const plan2codeRoot = detectPlan2CodeRoot() ?? process.cwd();
const plan2codeRoot = resolvedPlan2CodeRoot;
// Find diagnosis files
let diagFiles: string[] = [];
@@ -540,7 +560,7 @@ async function flowReviewAndApply(): Promise<void> {
console.log(chalk.bold.cyan('── Review and Apply a Proposal ──'));
const { proposalsDir } = getMetricsDirs();
const plan2codeRoot = detectPlan2CodeRoot() ?? process.cwd();
const plan2codeRoot = resolvedPlan2CodeRoot;
if (!fs.existsSync(proposalsDir)) {
console.log(chalk.yellow('No proposals directory found. Generate a proposal first.'));
@@ -577,6 +597,138 @@ async function flowReviewAndApply(): Promise<void> {
});
}
// ── Flow: Delete metrics data ─────────────────────────────────────────────────
async function flowDelete(): Promise<void> {
console.log();
console.log(chalk.bold.cyan('── Delete Metrics Data ──'));
const { metricsDir, runsDir, aggregatedPath, proposalsDir } = getMetricsDirs();
if (!fs.existsSync(metricsDir)) {
console.log(chalk.yellow('No metrics data found (.plan2code-metrics/ does not exist).'));
return;
}
const scope = await select({
message: 'What do you want to delete?',
choices: [
{ name: 'Delete specific run(s)', value: 'select' },
{ name: 'Delete all runs and aggregated data', value: 'runs' },
{ name: 'Delete everything (runs, aggregated data, proposals)', value: 'all' },
{ name: 'Cancel', value: 'cancel' },
],
});
if (scope === 'cancel') return;
if (scope === 'select') {
const runs = loadRunFiles(runsDir);
if (runs.length === 0) {
console.log(chalk.yellow('No run files found.'));
return;
}
const choices = runs.map(r => ({
name: `${r.run_id} ${r.project.name} v${r.plan2code_version}`,
value: r.run_id,
}));
// Select runs one at a time since @inquirer/prompts select is single-choice
const toDelete: string[] = [];
let selecting = true;
while (selecting) {
const remaining = choices.filter(c => !toDelete.includes(c.value));
if (remaining.length === 0) break;
const chosen = await select({
message: `Select a run to delete (${toDelete.length} selected so far):`,
choices: [
...remaining,
{ name: toDelete.length > 0 ? `Done selecting (delete ${toDelete.length})` : 'Cancel', value: '__done__' },
],
});
if (chosen === '__done__') {
selecting = false;
} else {
toDelete.push(chosen);
console.log(chalk.gray(` + ${chosen}`));
}
}
if (toDelete.length === 0) return;
const confirmed = await confirm({
message: `Delete ${toDelete.length} run(s)? This cannot be undone.`,
default: false,
});
if (!confirmed) return;
let deleted = 0;
for (const runId of toDelete) {
const filePath = path.join(runsDir, `${runId}.json`);
try {
fs.unlinkSync(filePath);
deleted++;
} catch {
console.log(chalk.yellow(` Could not delete ${runId}.json`));
}
}
console.log(chalk.green(`✓ Deleted ${deleted} run(s).`));
// Re-aggregate with remaining runs
const remainingRuns = loadRunFiles(runsDir);
if (remainingRuns.length > 0) {
aggregate(runsDir, aggregatedPath);
console.log(chalk.gray(` Aggregated metrics updated (${remainingRuns.length} runs remaining).`));
} else {
try { fs.unlinkSync(aggregatedPath); } catch { /* ignore */ }
console.log(chalk.gray(' No runs remaining — aggregated data removed.'));
}
return;
}
// scope === 'runs' or 'all'
const label = scope === 'all'
? 'ALL metrics data (runs, aggregated data, and proposals)'
: 'all runs and aggregated data';
const confirmed = await confirm({
message: `Delete ${label}? This cannot be undone.`,
default: false,
});
if (!confirmed) return;
// Delete run files
let runCount = 0;
if (fs.existsSync(runsDir)) {
const files = fs.readdirSync(runsDir).filter(f => f.endsWith('.json'));
for (const f of files) {
try { fs.unlinkSync(path.join(runsDir, f)); runCount++; } catch { /* ignore */ }
}
}
// Delete aggregated file
try { fs.unlinkSync(aggregatedPath); } catch { /* ignore */ }
console.log(chalk.green(`✓ Deleted ${runCount} run(s) and aggregated data.`));
if (scope === 'all') {
// Delete proposals
let proposalCount = 0;
if (fs.existsSync(proposalsDir)) {
const files = fs.readdirSync(proposalsDir);
for (const f of files) {
try { fs.unlinkSync(path.join(proposalsDir, f)); proposalCount++; } catch { /* ignore */ }
}
try { fs.rmdirSync(proposalsDir); } catch { /* ignore */ }
}
console.log(chalk.green(`✓ Deleted ${proposalCount} proposal file(s).`));
}
}
// ── Main menu ─────────────────────────────────────────────────────────────────
export async function runCLI(): Promise<void> {
@@ -585,18 +737,58 @@ export async function runCLI(): Promise<void> {
console.log(chalk.gray('Recursive self-improvement toolchain for plan2code contributors'));
console.log();
// Check if we're in (or near) a plan2code repo
const plan2codeRoot = detectPlan2CodeRoot();
if (!plan2codeRoot) {
console.log(chalk.yellow('⚠ Could not detect plan2code repository (src/plan2code-*.md not found nearby).'));
console.log(chalk.gray(' Prompt version hashing and analysis will be limited.'));
console.log();
} else {
const version = detectPlan2CodeVersion(plan2codeRoot);
console.log(chalk.gray(`plan2code root: ${plan2codeRoot} (v${version})`));
console.log();
// ── Prompt for paths ────────────────────────────────────────────────────────
const userConfig = loadUserConfig();
const savedRoot = userConfig.plan2codeRepoPath && fs.existsSync(userConfig.plan2codeRepoPath)
? userConfig.plan2codeRepoPath
: null;
const detectedRoot = savedRoot ?? detectPlan2CodeRoot();
const s2cPath = await input({
message: 'Path to plan2code repo:',
default: detectedRoot ?? undefined,
validate: (v) => {
if (!v.trim()) return 'Path is required';
const resolved = path.resolve(v.trim());
if (!fs.existsSync(resolved)) return 'Directory not found';
return true;
},
});
resolvedPlan2CodeRoot = path.resolve(s2cPath.trim());
// Persist the path for next run
if (resolvedPlan2CodeRoot !== userConfig.plan2codeRepoPath) {
saveUserConfig({ ...userConfig, plan2codeRepoPath: resolvedPlan2CodeRoot });
}
// Warn if the path doesn't look like a plan2code repo
const hasSrcPrompts =
fs.existsSync(path.join(resolvedPlan2CodeRoot, 'src', 'plan2code-1--plan.md')) &&
fs.existsSync(path.join(resolvedPlan2CodeRoot, 'src', 'plan2code-2--document.md'));
if (!hasSrcPrompts) {
console.log(chalk.yellow('⚠ No src/plan2code-*.md prompts found at that path. Hashing and analysis may be limited.'));
}
const projPath = await input({
message: 'Path to project repo (metrics source):',
default: process.cwd(),
validate: (v) => {
if (!v.trim()) return 'Path is required';
const resolved = path.resolve(v.trim());
if (!fs.existsSync(resolved)) return 'Directory not found';
return true;
},
});
resolvedProjectRoot = path.resolve(projPath.trim());
// Display resolved paths
const version = detectPlan2CodeVersion(resolvedPlan2CodeRoot);
console.log();
console.log(chalk.gray(`plan2code repo: ${resolvedPlan2CodeRoot} (v${version})`));
console.log(chalk.gray(`project repo: ${resolvedProjectRoot}`));
console.log();
let continueLoop = true;
while (continueLoop) {
const action = await select({
@@ -608,6 +800,7 @@ export async function runCLI(): Promise<void> {
{ name: 'Run analysis (diagnose weak steps)', value: 'analyze' },
{ name: 'Generate improvement proposal', value: 'propose' },
{ name: 'Review and apply a proposal', value: 'apply' },
{ name: chalk.red('Delete metrics data'), value: 'delete' },
{ name: 'Exit', value: 'exit' },
],
});
@@ -631,6 +824,9 @@ export async function runCLI(): Promise<void> {
case 'apply':
await flowReviewAndApply();
break;
case 'delete':
await flowDelete();
break;
case 'exit':
continueLoop = false;
break;
+114 -146
View File
@@ -36,6 +36,34 @@ function readFileSafe(filePath: string): string | null {
}
}
/** Strip the "## Success Criteria" section so its checkboxes aren't counted as tasks. */
function stripSuccessCriteriaSection(overview: string): string {
return overview.replace(/^## Success Criteria\s*\n[\s\S]*?(?=\n## |\n*$)/m, '');
}
/**
* Extract canonical task counts from the Completion Summary section.
* Supports two formats found in real specs:
* - Bold text: "**Completion Rate:** 100% (5/5 tasks)"
* - Table row: "| Total Tasks | 28 (28/28 complete — 100%) |"
* Returns null when no parseable counts are found.
*/
function parseCompletionSummaryTaskCount(overview: string): { completed: number; total: number } | null {
const summaryMatch = overview.match(/## Completion Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
if (!summaryMatch) return null;
const summary = summaryMatch[1];
// "Completion Rate: X% (Y/Z tasks)"
const rateMatch = summary.match(/Completion Rate[:\s*]*\d{1,3}%\s*\((\d+)\/(\d+)\s*tasks?\)/i);
if (rateMatch) return { completed: parseInt(rateMatch[1], 10), total: parseInt(rateMatch[2], 10) };
// "Total Tasks | 28 (28/28 complete"
const tableMatch = summary.match(/Total Tasks\s*\|\s*(\d+)\s*\((\d+)\/(\d+)\s*complete/i);
if (tableMatch) return { completed: parseInt(tableMatch[2], 10), total: parseInt(tableMatch[3], 10) };
return null;
}
function generateRunId(): string {
const now = new Date();
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
@@ -173,13 +201,10 @@ export function collectStep2(specDir: string): Step2DocumentMetrics {
requirement_coverage_percent: null, verification_items_added: null };
}
// Count all checkbox tasks: - [ ], - [x], - [!], - [/]
const allTasks = (overview.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
// Detect Parallel Execution Groups table
const parallelGroups = (overview.match(/Parallel\s+Execution\s+Group/gi) ?? []).length;
// Count phase files
// Count phase files (scanned first so we can use sum as a total_tasks fallback)
let phaseCount = 0;
let tasksPerPhase: number[] = [];
try {
@@ -190,13 +215,31 @@ export function collectStep2(specDir: string): Step2DocumentMetrics {
phaseCount = phaseFiles.length;
for (const pf of phaseFiles) {
const content = readFileSafe(path.join(specDir, pf)) ?? '';
const count = (content.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
const count = (content.match(/^\s*-\s+\[[ x!/?]\]\s+\*\*Task\s+\d/gm) ?? []).length;
tasksPerPhase.push(count);
}
} catch {
// leave empty
}
// total_tasks priority:
// 1. Completion Summary canonical count
// 2. Sum of phase file task counts
// 3. Stripped overview checkboxes (excluding Success Criteria)
const summaryCount = parseCompletionSummaryTaskCount(overview);
const phaseSum = tasksPerPhase.length > 0 ? tasksPerPhase.reduce((a, b) => a + b, 0) : 0;
const strippedOverview = stripSuccessCriteriaSection(overview);
const strippedCheckboxCount = (strippedOverview.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
let totalTasks: number;
if (summaryCount) {
totalTasks = summaryCount.total;
} else if (phaseSum > 0) {
totalTasks = phaseSum;
} else {
totalTasks = strippedCheckboxCount;
}
// Requirement coverage: look for coverage percentage in overview
let reqCoverage: number | null = null;
const covMatch = overview.match(/[Cc]overage[:\s]+(\d{1,3})%/);
@@ -207,7 +250,7 @@ export function collectStep2(specDir: string): Step2DocumentMetrics {
return {
present: true,
total_tasks: allTasks || null,
total_tasks: totalTasks || null,
tasks_per_phase: tasksPerPhase.length > 0 ? tasksPerPhase : null,
phase_count: phaseCount || null,
parallel_groups_identified: parallelGroups || 0,
@@ -216,124 +259,67 @@ export function collectStep2(specDir: string): Step2DocumentMetrics {
};
}
// ── Step 3: Implement (loop data) ─────────────────────────────────────────────
// ── Step 3: Implement ──────────────────────────────────────────────────────────
export function collectStep3(specDir: string): Step3ImplementMetrics {
const loopDir = path.join(specDir, '.plan2code-loop');
const iterLogPath = path.join(loopDir, 'iteration.log');
const configPath = path.join(loopDir, 'config.json');
if (!fs.existsSync(loopDir)) {
return { present: true, used_loop_mode: false, loop_mode: null,
task_completion_rate: null, tasks_completed: null, tasks_total: null,
blocker_count: null, blocker_categories: null, total_iterations: null,
avg_iteration_duration_ms: null, exit_code_distribution: null,
completion_marker_success_rate: null };
// Discover phase-*.md files
let phaseFiles: string[] = [];
try {
const files = fs.readdirSync(specDir);
phaseFiles = files
.filter(f => /^phase-\d+\.md$/i.test(f))
.sort();
} catch {
// leave empty
}
// Parse config.json for loop mode
let loopMode: 'task' | 'phase' | null = null;
const configContent = readFileSafe(configPath);
if (configContent) {
try {
const config = JSON.parse(configContent);
loopMode = config.loopMode ?? null;
} catch {
// ignore
}
}
// Parse NDJSON iteration.log
const iterLogContent = readFileSafe(iterLogPath);
if (!iterLogContent) {
return { present: true, used_loop_mode: true, loop_mode: loopMode,
task_completion_rate: null, tasks_completed: null, tasks_total: null,
blocker_count: null, blocker_categories: null, total_iterations: null,
avg_iteration_duration_ms: null, exit_code_distribution: null,
completion_marker_success_rate: null };
}
interface IterEntry {
iteration: number;
timestamp: string;
duration: number;
exitCode: number;
status: string;
completionMarker?: string;
}
const entries: IterEntry[] = [];
for (const line of iterLogContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
entries.push(JSON.parse(trimmed));
} catch {
// skip malformed lines
}
}
const totalIterations = entries.length;
if (totalIterations === 0) {
return { present: true, used_loop_mode: true, loop_mode: loopMode,
task_completion_rate: null, tasks_completed: null, tasks_total: null,
blocker_count: null, blocker_categories: null, total_iterations: 0,
avg_iteration_duration_ms: null, exit_code_distribution: null,
completion_marker_success_rate: null };
}
// Exit code distribution
const exitCodes: Record<string, number> = {};
for (const e of entries) {
const key = String(e.exitCode ?? 'other');
exitCodes[key] = (exitCodes[key] ?? 0) + 1;
}
// Average duration
const durations = entries.map(e => e.duration).filter(d => d != null && d > 0);
const avgDuration = durations.length > 0
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
: null;
// Completion markers
const markerEntries = entries.filter(e => e.completionMarker && e.completionMarker.trim() !== '');
const taskCompleteEntries = markerEntries.filter(e =>
e.completionMarker?.startsWith('TASK_COMPLETE')
);
const blockedEntries = markerEntries.filter(e =>
e.completionMarker?.startsWith('TASK_BLOCKED')
);
// Completion marker success rate: entries where a valid marker was detected vs total
const validMarkerCount = markerEntries.length;
const markerSuccessRate = totalIterations > 0
? validMarkerCount / totalIterations
: null;
// Task counts from markers (used for completion_marker_success_rate)
const blockerCount = blockedEntries.length;
// Blocker categories (parse from "TASK_BLOCKED: X.Y - reason")
const blockerCategories: string[] = [];
for (const e of blockedEntries) {
const match = e.completionMarker?.match(/TASK_BLOCKED:\s*[\d.]+\s*-\s*(.+)/);
if (match) {
// Normalize to snake_case category
const reason = match[1].toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
if (!blockerCategories.includes(reason)) {
blockerCategories.push(reason);
}
}
}
// Count tasks from overview.md checkboxes (consistent numerator and denominator)
// Read overview.md for Completion Summary
const overviewPath = path.join(specDir, 'overview.md');
const overview = readFileSafe(overviewPath);
const summaryCount = overview ? parseCompletionSummaryTaskCount(overview) : null;
// present = true if phase files exist OR overview has completion data
const present = phaseFiles.length > 0 || summaryCount != null;
if (!present) {
return { present: false, task_completion_rate: null, tasks_completed: null,
tasks_total: null, blocker_count: null };
}
// Count checkboxes in phase files
let phaseTotal = 0;
let phaseCompleted = 0;
let blockerCount = 0;
for (const pf of phaseFiles) {
const content = readFileSafe(path.join(specDir, pf)) ?? '';
phaseTotal += (content.match(/^\s*-\s+\[[ x!/?]\]\s+\*\*Task\s+\d/gm) ?? []).length;
phaseCompleted += (content.match(/^\s*-\s+\[x\]\s+\*\*Task\s+\d/gm) ?? []).length;
blockerCount += (content.match(/^\s*-\s+\[!\]\s+\*\*Task\s+\d/gm) ?? []).length;
}
// Count checkboxes in overview (stripped of Success Criteria)
let overviewTotal = 0;
let overviewCompleted = 0;
if (overview) {
const stripped = stripSuccessCriteriaSection(overview);
overviewTotal = (stripped.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
overviewCompleted = (stripped.match(/^\s*-\s+\[x\]/gm) ?? []).length;
}
// Task counts priority:
// 1. Completion Summary in overview.md
// 2. Checkbox counting in phase files
// 3. Checkbox counting in overview.md (stripped of Success Criteria)
let tasksTotal: number | null = null;
let tasksCompleted: number | null = null;
if (overview) {
tasksTotal = (overview.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length || null;
tasksCompleted = (overview.match(/^\s*-\s+\[x\]/gm) ?? []).length || null;
if (summaryCount) {
tasksTotal = summaryCount.total;
tasksCompleted = summaryCount.completed;
} else if (phaseTotal > 0) {
tasksTotal = phaseTotal;
tasksCompleted = phaseCompleted;
} else if (overviewTotal > 0) {
tasksTotal = overviewTotal;
tasksCompleted = overviewCompleted;
}
const taskCompletionRate = tasksTotal && tasksTotal > 0 && tasksCompleted != null
@@ -342,17 +328,10 @@ export function collectStep3(specDir: string): Step3ImplementMetrics {
return {
present: true,
used_loop_mode: true,
loop_mode: loopMode,
task_completion_rate: taskCompletionRate,
tasks_completed: tasksCompleted || null,
tasks_completed: tasksCompleted,
tasks_total: tasksTotal,
blocker_count: blockerCount,
blocker_categories: blockerCategories.length > 0 ? blockerCategories : null,
total_iterations: totalIterations,
avg_iteration_duration_ms: avgDuration,
exit_code_distribution: exitCodes,
completion_marker_success_rate: markerSuccessRate,
};
}
@@ -380,10 +359,17 @@ export function collectStep4(specDir: string): Step4FinalizeMetrics {
archival_succeeded: archivalSucceeded };
}
// Completion rate: completed tasks / total tasks
const totalTasks = (overview.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
const completedTasks = (overview.match(/^\s*-\s+\[x\]/gm) ?? []).length;
const completionRate = totalTasks > 0 ? completedTasks / totalTasks : null;
// Completion rate: prefer Completion Summary, fall back to stripped overview checkboxes
let completionRate: number | null = null;
const summaryCount = parseCompletionSummaryTaskCount(overview);
if (summaryCount) {
completionRate = summaryCount.total > 0 ? summaryCount.completed / summaryCount.total : null;
} else {
const stripped = stripSuccessCriteriaSection(overview);
const totalTasks = (stripped.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
const completedTasks = (stripped.match(/^\s*-\s+\[x\]/gm) ?? []).length;
completionRate = totalTasks > 0 ? completedTasks / totalTasks : null;
}
// Verification failures: look for [!] tasks or "verification failed" text
const blockedTasks = (overview.match(/^\s*-\s+\[!\]/gm) ?? []).length;
@@ -476,24 +462,6 @@ export async function collectRun(opts: CollectorOptions): Promise<RunMetrics> {
// leave null
}
// Try to get started_at from earliest iteration log entry
const loopDir = path.join(specDir, '.plan2code-loop');
const iterLogPath = path.join(loopDir, 'iteration.log');
const iterLogContent = readFileSafe(iterLogPath);
if (iterLogContent) {
const lines = iterLogContent.split('\n').filter(l => l.trim());
if (lines.length > 0) {
try {
const first = JSON.parse(lines[0]);
if (first.timestamp) startedAt = first.timestamp;
} catch { /* ignore */ }
try {
const last = JSON.parse(lines[lines.length - 1]);
if (last.timestamp) completedAt = last.timestamp;
} catch { /* ignore */ }
}
}
const metrics: RunMetrics = {
schema_version: '1.0',
run_id: runId,
+2 -2
View File
@@ -154,7 +154,7 @@ export interface ImproverResult {
}
export async function generateImprovement(opts: ImproverOptions): Promise<ImproverResult> {
const { diagnosisPath, plan2codeRoot, proposalsDir, runsDir, model = 'claude-opus-4-6', agent = 'claude-code' } = opts;
const { diagnosisPath, plan2codeRoot, proposalsDir, runsDir, model = 'default', agent = 'claude-code' } = opts;
// Load diagnosis
let diagnosisContent: string;
@@ -193,7 +193,7 @@ export async function generateImprovement(opts: ImproverOptions): Promise<Improv
});
// Invoke Claude
console.log(`\nInvoking AI improvement proposal (model: ${model})...`);
console.log(`\nInvoking AI improvement proposal (agent: ${agent}, model: ${model === 'default' ? 'user default' : model})...`);
console.log('This may take a minute...\n');
let aiResponse: string;
+7 -15
View File
@@ -18,7 +18,7 @@ export interface AgentDef {
name: AgentType;
displayName: string;
command: string;
models: Array<{ value: string; label: string }>;
defaultModel: string;
}
export const AGENTS: Record<AgentType, AgentDef> = {
@@ -26,23 +26,13 @@ export const AGENTS: Record<AgentType, AgentDef> = {
name: 'claude-code',
displayName: 'Claude Code',
command: 'claude',
models: [
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6 (Recommended)' },
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 (faster)' },
],
defaultModel: 'default',
},
'copilot-cli': {
name: 'copilot-cli',
displayName: 'GitHub Copilot CLI',
command: 'copilot',
models: [
{ value: 'claude-sonnet-4', label: 'Claude Sonnet 4 (Default)' },
{ value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' },
{ value: 'claude-opus-4.5', label: 'Claude Opus 4.5' },
{ value: 'gpt-5', label: 'GPT-5' },
{ value: 'gpt-5-mini', label: 'GPT-5 Mini' },
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' },
],
defaultModel: 'claude-sonnet-4',
},
};
@@ -69,7 +59,8 @@ export async function invokeLLM(opts: InvokeLLMOptions): Promise<string> {
'--print',
'--dangerously-skip-permissions',
];
if (model) {
// Only add --model if not using default
if (model && model !== 'default') {
args.push('--model', model);
}
@@ -84,7 +75,8 @@ export async function invokeLLM(opts: InvokeLLMOptions): Promise<string> {
} else {
// Copilot CLI: pipe prompt via stdin string
const args: string[] = [];
if (model) {
// Only add --model if not using default
if (model && model !== 'default') {
args.push('--model', model);
}
args.push('--allow-all-tools', '-s');
-1
View File
@@ -35,7 +35,6 @@ The following are the current contents of the plan2code workflow prompt files be
| avg_verification_items_added (Step 2) | ≤ 1.5 | lower is better |
| avg_task_completion_rate (Step 3) | ≥ 0.95 | higher is better |
| avg_blocker_count (Step 3) | ≤ 1.5 | lower is better |
| avg_completion_marker_success_rate (Step 3) | ≥ 0.95 | higher is better |
| avg_verification_failures_found (Step 4) | ≤ 1.0 | lower is better |
| archival_success_rate (Step 4) | ≥ 0.99 | higher is better |
| avg_user_rating (Feedback) | ≥ 7.0 | higher is better (1-10 scale, null if no feedback) |
+1 -2
View File
@@ -28,7 +28,7 @@ The following diagnosis was produced by the analysis step:
1. **Char limit:** Each target file MUST stay under 11,000 characters after your edit is applied. This is enforced by code — edits that violate it will be automatically rejected.
2. **Maximum 5 edits per cycle.** Focus on the highest-impact changes only.
3. **Each edit must cite a specific metric** in its `expected_metric_impact` field (e.g., "avg_completion_marker_success_rate", "avg_blocker_count").
3. **Each edit must cite a specific metric** in its `expected_metric_impact` field (e.g., "avg_task_completion_rate", "avg_blocker_count").
4. **`old_text` must be verbatim** from the file. Copy-paste exactly — include surrounding whitespace/newlines as they appear. Edits with mismatched old_text will be automatically rejected.
5. **Do NOT modify Role sections** (lines starting with "You are" at the top of each file) or step headings (lines starting with `#`).
6. **Each edit must be independently applicable** — no edit should depend on another edit being applied first.
@@ -41,7 +41,6 @@ The following diagnosis was produced by the analysis step:
- Target the specific sections identified in "Recommended Improvement Targets" from the diagnosis.
- For high `avg_clarification_rounds`: Add more upfront specification examples or decision criteria to Step 1.
- For low `avg_completion_marker_success_rate`: Clarify or simplify the completion marker format in Step 3.
- For high `avg_blocker_count`: Add blocker-recovery guidance or prerequisite check instructions.
- For low `avg_confidence`: Strengthen the confidence calculation instructions with clearer rubrics.
- For low `avg_parallel_groups`: Add explicit guidance for identifying parallel tasks in Step 2.
+1 -13
View File
@@ -41,17 +41,10 @@ export interface Step2DocumentMetrics {
export interface Step3ImplementMetrics {
present: boolean;
used_loop_mode: boolean;
loop_mode: 'task' | 'phase' | null;
task_completion_rate: number | null;
tasks_completed: number | null;
tasks_total: number | null;
blocker_count: number | null;
blocker_categories: string[] | null;
total_iterations: number | null;
avg_iteration_duration_ms: number | null;
exit_code_distribution: Record<string, number> | null;
completion_marker_success_rate: number | null;
}
export interface Step4FinalizeMetrics {
@@ -132,13 +125,9 @@ export interface CohortMetrics {
avg_requirement_coverage_percent: number | null;
avg_verification_items_added: number | null;
// Step 3 averages (loop only)
loop_run_count: number;
// Step 3 averages
avg_task_completion_rate: number | null;
avg_blocker_count: number | null;
avg_total_iterations: number | null;
avg_iteration_duration_ms: number | null;
avg_completion_marker_success_rate: number | null;
// Step 4 averages
avg_completion_rate_at_audit: number | null;
@@ -168,7 +157,6 @@ export const METRIC_TARGETS = {
avg_verification_items_added: { target: 1.5, direction: 'lte' as const },
avg_task_completion_rate: { target: 0.95, direction: 'gte' as const },
avg_blocker_count: { target: 1.5, direction: 'lte' as const },
avg_completion_marker_success_rate: { target: 0.95, direction: 'gte' as const },
avg_verification_failures_found: { target: 1.0, direction: 'lte' as const },
archival_success_rate: { target: 0.99, direction: 'gte' as const },
avg_user_rating: { target: 7.0, direction: 'gte' as const },