fix(state): support both bold and plain field formats in all state.cjs functions
Expand dual-format parsing to cover all 8 field extraction and replacement locations in state.cjs. The previous fix only covered cmdStateSnapshot and buildStateFrontmatter (read path), leaving 6 write-path functions with bold-only regex that silently failed on plain-format STATE.md files. Functions fixed: - stateExtractField: shared read helper (cascades to cmdStateAdvancePlan, cmdStateRecordSession) - stateReplaceField: shared write helper (cascades to all state mutations) - cmdStateGet: individual field lookup - cmdStatePatch: batch field updates - cmdStateUpdate: single field updates - cmdStateUpdateProgress: progress bar writes - cmdStateSnapshot session section: Last Date, Stopped At, Resume File Each function now tries **Field:** bold format first (preserving existing behavior), then falls back to plain Field: format. This eliminates the read/write asymmetry where state-snapshot could read plain-format fields but state-update/state-patch could not modify them. Closes #730 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,19 @@ function cmdStateGet(cwd, section, raw) {
|
||||
// Try to find markdown section or field
|
||||
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Check for **field:** value
|
||||
const fieldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||
const fieldMatch = content.match(fieldPattern);
|
||||
if (fieldMatch) {
|
||||
output({ [section]: fieldMatch[1].trim() }, raw, fieldMatch[1].trim());
|
||||
// Check for **field:** value (bold format)
|
||||
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) {
|
||||
output({ [section]: boldMatch[1].trim() }, raw, boldMatch[1].trim());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for field: value (plain format)
|
||||
const plainPattern = new RegExp(`^${fieldEscaped}:\\s*(.*)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
if (plainMatch) {
|
||||
output({ [section]: plainMatch[1].trim() }, raw, plainMatch[1].trim());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,10 +114,15 @@ function cmdStatePatch(cwd, patches, raw) {
|
||||
|
||||
for (const [field, value] of Object.entries(patches)) {
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
|
||||
if (boldPattern.test(content)) {
|
||||
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
results.updated.push(field);
|
||||
} else if (plainPattern.test(content)) {
|
||||
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
results.updated.push(field);
|
||||
} else {
|
||||
results.failed.push(field);
|
||||
@@ -135,9 +148,15 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
try {
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
if (boldPattern.test(content)) {
|
||||
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true });
|
||||
} else if (plainPattern.test(content)) {
|
||||
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true });
|
||||
} else {
|
||||
@@ -152,16 +171,26 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
|
||||
function stateExtractField(content, fieldName) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
||||
const match = content.match(pattern);
|
||||
return match ? match[1].trim() : null;
|
||||
// Try **Field:** bold format first
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) return boldMatch[1].trim();
|
||||
// Fall back to plain Field: format
|
||||
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
function stateReplaceField(content, fieldName, newValue) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (pattern.test(content)) {
|
||||
return content.replace(pattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
||||
if (plainPattern.test(content)) {
|
||||
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -256,9 +285,15 @@ function cmdStateUpdateProgress(cwd, raw) {
|
||||
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
||||
const progressStr = `[${bar}] ${percent}%`;
|
||||
|
||||
const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
||||
if (progressPattern.test(content)) {
|
||||
content = content.replace(progressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
// Try **Progress:** bold format first, then plain Progress: format
|
||||
const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
||||
const plainProgressPattern = /^(Progress:\s*).*/im;
|
||||
if (boldProgressPattern.test(content)) {
|
||||
content = content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
||||
} else if (plainProgressPattern.test(content)) {
|
||||
content = content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
||||
} else {
|
||||
@@ -483,9 +518,12 @@ function cmdStateSnapshot(cwd, raw) {
|
||||
const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
if (sessionMatch) {
|
||||
const sessionSection = sessionMatch[1];
|
||||
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i);
|
||||
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i);
|
||||
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i);
|
||||
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Last Date:\s*(.+)/im);
|
||||
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Stopped At:\s*(.+)/im);
|
||||
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i)
|
||||
|| sessionSection.match(/^Resume File:\s*(.+)/im);
|
||||
|
||||
if (lastDateMatch) session.last_date = lastDateMatch[1].trim();
|
||||
if (stoppedAtMatch) session.stopped_at = stoppedAtMatch[1].trim();
|
||||
|
||||
Reference in New Issue
Block a user