diff --git a/.changeset/3805-fast-md-log-to-state-schema-aware.md b/.changeset/3805-fast-md-log-to-state-schema-aware.md new file mode 100644 index 000000000..d8feddfdc --- /dev/null +++ b/.changeset/3805-fast-md-log-to-state-schema-aware.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3805 +--- +**`fast.md` `log_to_state` no longer appends a malformed row when the STATE.md table uses the 5-column quick.md schema (#3805)** — the workflow unconditionally wrote a hardcoded 4-cell row (`| date | fast | task | ✅ |`) into STATE.md; when `quick.md` Step 7 had already created a 5-column "Quick Tasks Completed" table the mismatched row broke the Markdown table. `fast.md` now reads the existing header, counts columns, and verifies the quick.md column names before appending a properly-formed 5-cell row. If the schema is unrecognized the write is skipped with a warning rather than corrupting the table. diff --git a/get-shit-done/workflows/fast.md b/get-shit-done/workflows/fast.md index 729bcc32e..2ebc9d9fa 100644 --- a/get-shit-done/workflows/fast.md +++ b/get-shit-done/workflows/fast.md @@ -63,14 +63,33 @@ Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` -If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table. -If the table doesn't exist, skip this step silently. +If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row +that matches the existing table's schema. If no table exists, skip silently. +If the table's schema is unrecognized, skip with a brief log rather than append a +malformed row. ```bash -# Check if STATE.md has quick tasks table +# Detect whether STATE.md has a Quick Tasks Completed table if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then - # Append entry — workflow handles the format - echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md + # Read the table header line to determine the column schema. + # quick.md Step 7 creates a 5-column table: + # | # | Description | Date | Commit | Directory | + # Count pipe characters in the header to determine column count. + HEADER_LINE=$(grep -A2 "Quick Tasks Completed" .planning/STATE.md 2>/dev/null | grep "^|" | head -1) + # Count columns: number of | separators minus 1 gives column count + COL_COUNT=$(echo "$HEADER_LINE" | awk -F'|' '{print NF-1}') + + if [ "$COL_COUNT" -eq 5 ] && echo "$HEADER_LINE" | grep -qi "Description" && echo "$HEADER_LINE" | grep -qi "Commit" && echo "$HEADER_LINE" | grep -qi "Directory"; then + # 5-column schema from quick.md Step 7: | # | Description | Date | Commit | Directory | + # Determine the next row number by counting existing data rows (non-separator, non-header). + NEXT_NUM=$(awk '/Quick Tasks Completed/{found=1} found && /^\|/ && !/^[|][-: |]*[|]$/ && !/Description/{count++} END{print count+1}' .planning/STATE.md 2>/dev/null || echo "1") + # Get the latest commit hash (short) + COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "—") + echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — |" >> .planning/STATE.md + else + # Unrecognized table schema — skip to avoid appending a malformed row. + echo "⚠ fast.md log_to_state: Quick Tasks Completed table has unrecognized schema (${COL_COUNT} columns); skipping STATE.md update." + fi fi ``` diff --git a/tests/bug-3805-fast-md-log-to-state-schema.test.cjs b/tests/bug-3805-fast-md-log-to-state-schema.test.cjs new file mode 100644 index 000000000..877ec9670 --- /dev/null +++ b/tests/bug-3805-fast-md-log-to-state-schema.test.cjs @@ -0,0 +1,143 @@ +'use strict'; + +// allow-test-rule: source-text-is-the-product +// Reads get-shit-done/workflows/fast.md whose deployed text IS the product — +// the workflow markdown is executed verbatim by LLM runtimes. + +/** + * #3805 — fast.md log_to_state appends a schema-blind 4-column row to the + * 5-column "Quick Tasks Completed" table created by quick.md Step 7. + * + * quick.md Step 7 creates the table with 5 columns: + * | # | Description | Date | Commit | Directory | + * + * Before this fix, fast.md's log_to_state step appended a hardcoded 4-cell + * row unconditionally: + * echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md + * + * This produces malformed Markdown when the existing table has a different + * column count. + * + * Covers: + * - fast.md does NOT contain the hardcoded 4-cell echo template + * - fast.md log_to_state step reads/introspects the existing table header + * before appending (schema-aware insertion) + * - fast.md log_to_state step matches the 5-column schema from quick.md + * Step 7 when that table is present + * - fast.md log_to_state step skips (does not corrupt) the STATE.md write + * when the table schema is unrecognized + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const REPO_ROOT = path.join(__dirname, '..'); +const FAST_MD_PATH = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'fast.md'); + +// The 5-column schema defined in quick.md Step 7 (non-validate mode). +// Column count is 5: # | Description | Date | Commit | Directory +// Named constant for traceability — mirrors quick.md Step 7's table header. +const QUICK_MD_STEP7_COL_COUNT = 5; +const QUICK_MD_STEP7_COLUMNS = ['#', 'Description', 'Date', 'Commit', 'Directory']; + +describe('bug #3805: fast.md log_to_state must be schema-aware', () => { + let fastMdContent; + + test('fast.md workflow file exists and is readable', () => { + assert.ok(fs.existsSync(FAST_MD_PATH), `fast.md not found at ${FAST_MD_PATH}`); + fastMdContent = fs.readFileSync(FAST_MD_PATH, 'utf-8'); + }); + + test('fast.md log_to_state step does NOT hardcode a 4-cell row template', () => { + // The old broken template: | date | fast | task | ✅ | + // This regex matches the exact hardcoded pattern that ignores table schema. + // A 4-cell row has exactly 4 pipe-delimited fields plus the surrounding pipes. + const hardcoded4CellPattern = /echo\s+["'][|][^|]*[|][^|]*[|][^|]*[|][^|]*[|]\s*["']/; + const match = fastMdContent.match(hardcoded4CellPattern); + assert.ok( + !match, + [ + 'fast.md log_to_state still contains the hardcoded 4-cell row template:', + ` "${match?.[0]}"`, + 'This appends a malformed row to the 5-column Quick Tasks Completed table', + 'created by quick.md Step 7.', + `Expected table schema (${QUICK_MD_STEP7_COL_COUNT} cols): | ${QUICK_MD_STEP7_COLUMNS.join(' | ')} |`, + ].join('\n') + ); + }); + + test('fast.md log_to_state step reads the existing table header (schema introspection)', () => { + // The fix must inspect the existing STATE.md table header before appending. + // Acceptable signals: reading STATE.md content, grepping for the header line, + // or parsing columns from the header row. + const hasHeaderRead = + // Reads STATE.md to inspect it (awk/sed/grep on the file for header detection) + /grep.*Quick Tasks Completed.*STATE\.md/.test(fastMdContent) || + /awk.*Quick Tasks Completed/.test(fastMdContent) || + /sed.*Quick Tasks Completed/.test(fastMdContent) || + // Reads the header line explicitly (head -n, sed -n, awk NR==) + /head\s+-n/.test(fastMdContent) && /STATE\.md/.test(fastMdContent) || + // Counts pipe separators / columns from existing header + /col.*count|column.*count|count.*col|NF|awk.*\|/.test(fastMdContent) || + // References schema detection in prose + /schema|header|column\s+count|existing.*table/.test(fastMdContent); + + assert.ok( + hasHeaderRead, + [ + 'fast.md log_to_state step does not appear to introspect the existing table schema.', + 'The step must read the STATE.md table header to detect column count before appending.', + 'quick.md Step 7 uses schema-aware matching — fast.md must follow the same discipline.', + ].join('\n') + ); + }); + + test('fast.md log_to_state step references the 5-column quick.md schema', () => { + // The fix must handle the 5-col schema: | # | Description | Date | Commit | Directory | + // Test that all 5 column names appear in the log_to_state step's context. + // Extract the log_to_state step content to scope the check. + const logToStateMatch = fastMdContent.match(/([\s\S]*?)<\/step>/); + assert.ok(logToStateMatch, 'fast.md must contain a element'); + + const stepContent = logToStateMatch[1]; + + // All 5 column names from quick.md Step 7 must be referenced in the step. + for (const col of QUICK_MD_STEP7_COLUMNS) { + assert.ok( + stepContent.toLowerCase().includes(col.toLowerCase()), + [ + `fast.md log_to_state step does not reference column "${col}"`, + `Expected all 5 columns from quick.md Step 7: ${QUICK_MD_STEP7_COLUMNS.join(', ')}`, + `(${QUICK_MD_STEP7_COL_COUNT}-column schema)`, + ].join('\n') + ); + } + }); + + test('fast.md log_to_state step skips STATE.md write on unrecognized schema', () => { + // The fix must not blindly append when the table schema is unknown. + // Check for a guard that skips or logs rather than corrupting the file. + const logToStateMatch = fastMdContent.match(/([\s\S]*?)<\/step>/); + assert.ok(logToStateMatch, 'fast.md must contain a element'); + + const stepContent = logToStateMatch[1]; + + // Must have a skip/guard path for unrecognized schemas + const hasSkipGuard = + /skip/i.test(stepContent) || + /unrecognized|unknown|mismatch/i.test(stepContent) || + /else\b/.test(stepContent) || + /warn|log/i.test(stepContent); + + assert.ok( + hasSkipGuard, + [ + 'fast.md log_to_state step does not appear to guard against unrecognized table schemas.', + 'When the existing STATE.md table does not match an expected schema,', + 'the step must skip the write (with a brief log) rather than append a malformed row.', + ].join('\n') + ); + }); +});