Merge branch 'next' into feat/1279-fail-first-prover

This commit is contained in:
Rezolv
2026-06-16 17:35:55 -04:00
committed by GitHub
30 changed files with 653 additions and 96 deletions

View File

@@ -1342,6 +1342,21 @@ function writePlanningFileSet(writes: WriteSpec[]): void {
}
}
function phaseDisplayNameFromRoadmap(roadmapContent: string | null, phaseNum: string | null): string | null {
if (!roadmapContent || !phaseNum) return null;
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:\\s*([^\\n]+)`, 'im'));
if (!heading) return null;
const name = heading[1].replace(/\(INSERTED\)/i, '').trim();
return name || null;
}
function phaseDisplayNameFromSlug(slug: string | null): string | null {
if (!slug) return null;
const name = slug.replace(/-/g, ' ').trim();
return name || null;
}
function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
if (!phaseNum) {
error('phase number required for phase complete');
@@ -1642,6 +1657,9 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
let stateContent = originalStateContent;
const phaseValue = nextPhaseNum || phaseNum;
const nextPhaseDisplayName =
phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
phaseDisplayNameFromSlug(nextPhaseName);
const existingPhaseField =
stateExtractField(stateContent, 'Current Phase') ||
stateExtractField(stateContent, 'Phase');
@@ -1651,12 +1669,14 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
if (totalMatch) {
const total = totalMatch[1];
const nameStr = nextPhaseName
? ` (${nextPhaseName.replace(/-/g, ' ')})`
const nameStr = nextPhaseDisplayName
? ` (${nextPhaseDisplayName})`
: nameMatch
? ` (${nameMatch[1]})`
: '';
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
} else if (nextPhaseDisplayName) {
newPhaseValue = `${phaseValue} — ${nextPhaseDisplayName}`;
}
}
stateContent = stateReplaceFieldWithFallback(
@@ -1666,13 +1686,10 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
newPhaseValue,
);
if (nextPhaseName) {
stateContent = stateReplaceFieldWithFallback(
stateContent,
'Current Phase Name',
null,
nextPhaseName.replace(/-/g, ' '),
);
if (nextPhaseDisplayName) {
stateContent =
stateReplaceField(stateContent, 'Current Phase Name', nextPhaseDisplayName) ||
stateContent;
}
stateContent = stateReplaceFieldWithFallback(
@@ -1689,19 +1706,20 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
'Not started',
);
stateContent = stateReplaceFieldWithFallback(
stateContent,
'Last Activity',
'Last activity',
today,
);
const lastActivityDescription = `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`;
if (/^Last activity:/m.test(stateContent)) {
stateContent =
stateReplaceField(stateContent, 'Last activity', `${today} — ${lastActivityDescription}`) ||
stateContent;
} else {
stateContent =
stateReplaceField(stateContent, 'Last Activity', today) ||
stateContent;
}
stateContent = stateReplaceFieldWithFallback(
stateContent,
'Last Activity Description',
null,
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`,
);
stateContent =
stateReplaceField(stateContent, 'Last Activity Description', lastActivityDescription) ||
stateContent;
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
if (completedRaw !== null) {

View File

@@ -309,6 +309,10 @@ function skillFrontmatterName(skillDirName) {
return skillDirName;
}
function normalizeClaudeSkillEffort(effort) {
return effort === 'xhigh' ? 'max' : effort;
}
/**
* Qwen Code skills accept an optional numeric `priority` frontmatter field.
* Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -405,7 +409,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
// token-budget tier). Fields are Claude-specific; unknown frontmatter
// fields are silently ignored by other runtimes (backward-compatible).
if (context) fm += `context: ${context}\n`;
if (effort) fm += `effort: ${effort}\n`;
if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
if (toolsBlock) fm += toolsBlock;
fm += '---';

View File

@@ -1050,6 +1050,31 @@ function matchSessionSection(body: string): RegExpMatchArray | null {
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i);
}
function parseProsePhaseField(value: string | null): { phase: string | null; name: string | null } {
if (!value) return { phase: null, name: null };
const phaseMatch = value.match(/\b(\d+[A-Z]?(?:\.\d+)*)\b/i);
const parenName = value.match(/\(([^)]+)\)/);
const dashName = value.match(/—\s*([^(\n]+?)(?:\s*\(|$)/);
const rawName = parenName?.[1] ?? dashName?.[1] ?? null;
const name = rawName && !/^(?:complete|executing|not started)$/i.test(rawName.trim())
? rawName.trim()
: null;
return {
phase: phaseMatch ? phaseMatch[1] : null,
name,
};
}
function parseProseLastActivityField(value: string | null): { date: string | null; description: string | null } {
if (!value) return { date: null, description: null };
const match = value.match(/^(\d{4}-\d{2}-\d{2})(?:\s+[—-]{1,2}\s+(.+))?$/);
if (!match) return { date: value, description: null };
return {
date: match[1],
description: match[2]?.trim() || null,
};
}
function cmdStateSnapshot(cwd: string, raw: boolean): void {
const statePath = planningPaths(cwd).state;
@@ -1080,15 +1105,18 @@ function cmdStateSnapshot(cwd: string, raw: boolean): void {
};
// Extract basic fields — frontmatter keys take precedence over body
const currentPhase = fmScalar('current_phase') ?? stateExtractField(body, 'Current Phase');
const currentPhaseName = fmScalar('current_phase_name') ?? stateExtractField(body, 'Current Phase Name');
const prosePhase = parseProsePhaseField(stateExtractField(body, 'Phase'));
const currentPhase = fmScalar('current_phase') ?? stateExtractField(body, 'Current Phase') ?? prosePhase.phase;
const currentPhaseName = fmScalar('current_phase_name') ?? stateExtractField(body, 'Current Phase Name') ?? prosePhase.name;
const totalPhasesRaw = fmScalar('total_phases') ?? stateExtractField(body, 'Total Phases');
const currentPlan = fmScalar('current_plan') ?? stateExtractField(body, 'Current Plan');
const totalPlansRaw = fmScalar('total_plans_in_phase') ?? stateExtractField(body, 'Total Plans in Phase');
const status = fmScalar('status') ?? stateExtractField(body, 'Status');
const progressRaw = fmScalar('progress') ?? stateExtractField(body, 'Progress');
const lastActivity = fmScalar('last_activity') ?? stateExtractField(body, 'Last Activity');
const lastActivityDesc = fmScalar('last_activity_desc') ?? stateExtractField(body, 'Last Activity Description');
const rawLastActivity = stateExtractField(body, 'Last Activity') ?? stateExtractField(body, 'Last activity');
const proseLastActivity = parseProseLastActivityField(rawLastActivity);
const lastActivity = fmScalar('last_activity') ?? proseLastActivity.date ?? rawLastActivity;
const lastActivityDesc = fmScalar('last_activity_desc') ?? stateExtractField(body, 'Last Activity Description') ?? proseLastActivity.description;
const pausedAt = fmScalar('paused_at') ?? stateExtractField(body, 'Paused At');
// Parse numeric fields
@@ -1180,14 +1208,18 @@ function cmdStateSnapshot(cwd: string, raw: boolean): void {
* reliably via `state json` instead of fragile regex parsing.
*/
function buildStateFrontmatter(bodyContent: string, cwd: string | undefined): Record<string, unknown> {
const currentPhase = stateExtractField(bodyContent, 'Current Phase');
const currentPhaseName = stateExtractField(bodyContent, 'Current Phase Name');
const prosePhase = parseProsePhaseField(stateExtractField(bodyContent, 'Phase'));
const currentPhase = stateExtractField(bodyContent, 'Current Phase') ?? prosePhase.phase;
const currentPhaseName = stateExtractField(bodyContent, 'Current Phase Name') ?? prosePhase.name;
const currentPlan = stateExtractField(bodyContent, 'Current Plan');
const totalPhasesRaw = stateExtractField(bodyContent, 'Total Phases');
const totalPlansRaw = stateExtractField(bodyContent, 'Total Plans in Phase');
const status = stateExtractField(bodyContent, 'Status');
const progressRaw = stateExtractField(bodyContent, 'Progress');
const lastActivity = stateExtractField(bodyContent, 'Last Activity');
const rawLastActivity = stateExtractField(bodyContent, 'Last Activity') ?? stateExtractField(bodyContent, 'Last activity');
const proseLastActivity = parseProseLastActivityField(rawLastActivity);
const lastActivity = proseLastActivity.date ?? rawLastActivity;
const lastActivityDesc = stateExtractField(bodyContent, 'Last Activity Description') ?? proseLastActivity.description;
// Bug #2444: scope Stopped At extraction to the ## Session section so that
// historical "Stopped at:" prose elsewhere in the body (e.g. in a
// Session Continuity Archive section) never overwrites the current value.
@@ -1326,6 +1358,7 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined): Re
if (pausedAt) fm['paused_at'] = pausedAt;
fm['last_updated'] = realClock.nowIso();
if (lastActivity) fm['last_activity'] = lastActivity;
if (lastActivityDesc) fm['last_activity_desc'] = lastActivityDesc;
const progress: Record<string, unknown> = {};
if (totalPhases !== null) progress['total_phases'] = totalPhases;
@@ -1889,13 +1922,13 @@ function cmdStateBeginPhase(cwd: string, phaseNumber: string | number, phaseName
}
// Update Last activity line if present
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
const newActivity = `Last activity: ${today} — Phase ${phaseNumber} execution started`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
} else {
// Pipe-table format in Current Position (#1255)
// Value must match the inline branch (date + narrative), not bare date.
const activityValue = `${today} -- Phase ${phaseNumber} execution started`;
const activityValue = `${today} — Phase ${phaseNumber} execution started`;
const replaced = stateReplaceField(posBody, 'Last Activity', activityValue)
?? stateReplaceField(posBody, 'Last activity', activityValue);
if (replaced !== null) posBody = replaced;
@@ -1912,7 +1945,7 @@ function cmdStateBeginPhase(cwd: string, phaseNumber: string | number, phaseName
if (positionMatch) {
const header = positionMatch[1];
let posBody = positionMatch[2];
const resumeActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution resumed (wave continue)`;
const resumeActivity = `Last activity: ${today} — Phase ${phaseNumber} execution resumed (wave continue)`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, resumeActivity);
body = body.replace(positionPattern, () => `${header}${posBody}`);
@@ -2083,7 +2116,7 @@ function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, planCou
// Update Current Position section
body = updateCurrentPositionFields(body, {
status: 'Ready to execute',
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
lastActivity: `${today} — Phase ${phaseNumber} planning complete`,
});
return reassemble(body);
@@ -2659,13 +2692,13 @@ function cmdStateCompletePhase(cwd: string, raw: boolean, overridePhase?: string
}
// Update Last activity line if present
const newActivity = `Last activity: ${today} -- Phase ${currentPhase} marked complete`;
const newActivity = `Last activity: ${today} — Phase ${currentPhase} marked complete`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
} else {
// Pipe-table format in Current Position (#1255)
// Value must match the inline branch (date + narrative), not bare date.
const activityValue = `${today} -- Phase ${currentPhase} marked complete`;
const activityValue = `${today} — Phase ${currentPhase} marked complete`;
const replaced = stateReplaceField(posBody, 'Last Activity', activityValue)
?? stateReplaceField(posBody, 'Last activity', activityValue);
if (replaced !== null) posBody = replaced;

View File

@@ -200,6 +200,13 @@ function parseCurrentTest(content: string): CurrentTest {
const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
if (!numberMatch && !nameMatch && !expectedBlockMatch && !expectedInlineMatch) {
const pendingTest = parseFirstPendingTest(content);
if (pendingTest) {
return pendingTest;
}
error('Current Test section is non-structured and no pending UAT test remains to resume');
}
error('Current Test section is malformed');
}
@@ -222,6 +229,63 @@ function parseCurrentTest(content: string): CurrentTest {
};
}
function parseFirstPendingTest(content: string): CurrentTest | null {
const testsMatch = content.match(/##\s*Tests\s*\n([\s\S]*?)(?=\n##\s|$)/i);
if (!testsMatch) {
return null;
}
const testsSection = testsMatch[1];
const headingPattern = /^###\s*(\d+)\.\s*([^\n]+)\s*$/gm;
const headings: Array<{ index: number; number: number; name: string }> = [];
let headingMatch: RegExpExecArray | null;
while ((headingMatch = headingPattern.exec(testsSection)) !== null) {
headings.push({
index: headingMatch.index,
number: parseInt(headingMatch[1], 10),
name: headingMatch[2].trim(),
});
}
for (let i = 0; i < headings.length; i += 1) {
const current = headings[i];
const next = headings[i + 1];
const block = testsSection.slice(current.index, next ? next.index : undefined);
if (!/^result:\s*\[?pending\]?\s*$/im.test(block)) {
continue;
}
const expected = parseExpectedFromTestBlock(block);
if (!expected) {
error(`Pending UAT test ${current.number} is missing an expected field`);
}
return {
complete: false,
number: current.number,
name: sanitizeForDisplay(current.name),
expected: sanitizeForDisplay(expected),
};
}
return null;
}
function parseExpectedFromTestBlock(block: string): string | null {
const expectedBlockMatch = block.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
|| block.match(/^expected:\s*\|\n([\s\S]+)/m);
if (expectedBlockMatch) {
return expectedBlockMatch[1]
.split('\n')
.map((line: string) => line.replace(/^ {2}/, ''))
.join('\n')
.trim();
}
const expectedInlineMatch = block.match(/^expected:\s*(.+)\s*$/m);
return expectedInlineMatch ? expectedInlineMatch[1].trim() : null;
}
// ─── buildCheckpoint ──────────────────────────────────────────────────────────
function buildCheckpoint(currentTest: { number: number; name: string; expected: string }): string {

View File

@@ -416,6 +416,7 @@ interface CleanupManifestEntry {
worktree_path: string;
branch: string;
expected_base: string;
allowed_bases?: string[];
}
function normalizeCleanupManifestEntry(entry: unknown): CleanupManifestEntry | null {
@@ -428,11 +429,16 @@ function normalizeCleanupManifestEntry(entry: unknown): CleanupManifestEntry | n
const expectedBase = typeof e.expected_base === 'string' ? e.expected_base : '';
if (!worktreePath || !branch || !expectedBase) return null;
if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch)) return null;
const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
const allowedBases = Array.from(new Set(
[expectedBase, ...rawAllowedBases.filter((base): base is string => typeof base === 'string' && base.length > 0)]
));
return {
agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
worktree_path: worktreePath,
branch,
expected_base: expectedBase,
allowed_bases: allowedBases,
};
}
@@ -693,7 +699,10 @@ function executeWorktreeWaveCleanupPlan(plan: WaveCleanupPlan | null, deps: Work
}
const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
if (!gitResultOk(mergeBase) || mergeBase.stdout.trim() !== entry.expected_base) {
const allowedBases = Array.isArray(entry.allowed_bases) && entry.allowed_bases.length > 0
? entry.allowed_bases
: [entry.expected_base];
if (!gitResultOk(mergeBase) || !allowedBases.includes(mergeBase.stdout.trim())) {
result.status = 'blocked';
result.reason = 'base_mismatch';
result.stderr = mergeBase?.stderr || '';