From 8c017034a3d19b2179012d78af75ea9803d6675f Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sat, 28 Feb 2026 05:34:43 -0800 Subject: [PATCH] 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 --- get-shit-done/bin/lib/state.cjs | 84 ++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index ba4ef8697..732e1b652 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -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();