Skip to content

fix(story-automator): reconcile story File List against git before review#41

Open
Pawel-N-pl wants to merge 3 commits into
mainfrom
fix/dev-step-filelist-reconcile
Open

fix(story-automator): reconcile story File List against git before review#41
Pawel-N-pl wants to merge 3 commits into
mainfrom
fix/dev-step-filelist-reconcile

Conversation

@Pawel-N-pl

@Pawel-N-pl Pawel-N-pl commented Jun 7, 2026

Copy link
Copy Markdown

What

Add a reconcile-story subcommand, run at the end of the dev step (--write)
before status flips to review, so the story File List is computed from git
instead of transcribed by the LLM.

Why

Doc-drift is the #1 recurring MEDIUM review finding across three epics: the dev
step writes the File List from memory, while the ground truth (git status) is
only checked at review — one step too late. Three "be more careful" guidelines
(AI-1 → AI-2.1 → AI-3.1) failed because none changed who computes the value.

Fix

  • reconcile-story --repo --story [--write]: diffs git status --porcelain --untracked-files=all -z (NUL-separated, verbatim paths) against the story
    ### File List; --write rewrites that section deterministically.
  • Reuses the porcelain idiom (cmd_commit_story) and the resolved story key
    (normalize_story_key + implementation_artifacts_dir).
  • Excludes tracked-but-not-source paths — the dynamically-resolved artifacts dir
    (_bmad-output/ or a configured docs/bmad/...) plus agent/IDE config;
    .gitignore already filters deps/build junk.
  • Wired into step-03-execute.md (checks reconcile succeeded before logging); the
    review git cross-check becomes defence-in-depth.

Tests

tests/test_reconcile_story.py — 10 cases (drift detection, idempotent write,
tooling/artifact exclusion incl. configured docs/bmad/..., missing-section
append, rename→destination, non-ASCII paths, parenthesised filenames, key
disambiguation, last-section EOF idempotency). npm run verify green (421 tests).

Scope

File List only. Test-count drift is the other half, tracked in #40.

Summary by CodeRabbit

  • New Features

    • Added reconcile-story command to detect and resolve differences between story file lists and actual git changes, with optional --write flag to update files.
  • Tests

    • Comprehensive test suite covering drift detection, automatic file list updates, path filtering, and handling of edge cases.
  • Chores

    • Updated workflow to automatically reconcile file lists before committing stories.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a reconcile-story command that synchronizes story markdown file lists against git changed files. It includes git-based file scanning with tooling-path exclusions, markdown section parsing and rewriting, CLI registration, workflow integration into the dev-story finalization step, and comprehensive test coverage for drift detection, writing, and edge cases.

Changes

Reconcile-Story Feature

Layer / File(s) Summary
File list exclusion config & git file discovery
skills/bmad-story-automator/src/story_automator/commands/basic.py
FILE_LIST_EXCLUDE_PREFIXES constant filters _bmad-output and IDE directories. _git_changed_files() runs git status --porcelain with NUL-delimited output and handles renames/copies by selecting destination paths.
Markdown file list section parsing & rewriting
skills/bmad-story-automator/src/story_automator/commands/basic.py
_file_list_bounds() locates the ### File List heading and boundary. _paths_from_block() parses bullet entries and normalizes status annotations. _reconcile_section() deterministically regenerates the section's bullet list (with "(no files changed)" placeholder) while preserving all other markdown sections.
Reconcile-story command entry point
skills/bmad-story-automator/src/story_automator/commands/basic.py
cmd_reconcile_story() validates --repo and --story arguments, resolves the target story markdown file, excludes the artifacts directory from git-changed inputs, reconciles the file list, optionally persists changes via --write, computes missing/stale entry counts, and emits JSON describing the sync state.
CLI command registration & help
skills/bmad-story-automator/src/story_automator/cli.py
cmd_reconcile_story is imported, registered in the commands dispatch table, and added to the help usage output.
Dev story workflow reconciliation step
skills/bmad-story-automator/steps-c/step-03b-execute-finish.md
Pre-commit reconciliation gate that runs reconcile-story --write and appends success (with missing/stale counts) or failure (with error details) logs to the step output before the commit snapshot.
Comprehensive test suite
tests/test_reconcile_story.py
10 test cases covering drift detection without --write, file list rewriting with idempotence, tooling path exclusion, missing section appending, configured artifacts directory exclusion, non-ASCII path handling, filename-with-parentheses preservation, story file disambiguation, rename handling, and idempotent formatting when file list is the last section.

Sequence Diagram

sequenceDiagram
  participant User
  participant ReconcileCmd
  participant GitScanner
  participant MarkdownParser
  participant FileWriter
  User->>ReconcileCmd: invoke reconcile-story --repo --story [--write]
  ReconcileCmd->>GitScanner: get changed files from git status
  GitScanner-->>ReconcileCmd: sorted paths (filtered)
  ReconcileCmd->>FileWriter: read story markdown file
  FileWriter-->>ReconcileCmd: file content
  ReconcileCmd->>MarkdownParser: locate ### File List bounds
  MarkdownParser-->>ReconcileCmd: section start/end
  ReconcileCmd->>MarkdownParser: reconcile bullet list to git files
  MarkdownParser-->>ReconcileCmd: updated markdown content
  alt --write flag
    ReconcileCmd->>FileWriter: write updated file
    FileWriter-->>ReconcileCmd: wrote=true
  else no --write
    ReconcileCmd-->>User: wrote=false, in_sync=false
  end
  ReconcileCmd-->>User: JSON response with git_files, missing_from_story, stale_in_story
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 A fluffy fix for list drift, no more!
Git and markdown now dance in sync once more,
Files tracked, sections rewritten clean,
The tidiest story file you've ever seen!
Tests protect each edge case with care,
Reconciliation magic floating through the air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a new reconcile-story command that syncs the story's File List against git truth, and integrating it into the workflow before review.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dev-step-filelist-reconcile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@augmentcode

augmentcode Bot commented Jun 7, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Adds deterministic reconciliation of a story’s ### File List against git so doc-drift is eliminated before the review step.

Changes:

  • Introduces a reconcile-story CLI subcommand that derives changed files from git status --porcelain --untracked-files=all and optionally rewrites the File List section.
  • Implements helpers to locate ### File List, parse existing entries, and rewrite that section deterministically while leaving other story sections untouched.
  • Adds a bounded exclusion list for BMAD artifacts and common agent/IDE config directories so non-source files don’t enter the File List.
  • Registers the new command in story_automator/cli.py and updates the review skill text to treat drift checks as defence-in-depth.
  • Wires reconcile-story --write into step-03-execute so reconciliation runs automatically at the end of the dev step.
  • Adds unit tests covering drift detection, idempotent write behavior, exclusion handling, missing-section append, and rename destination selection.

Technical Notes: The command outputs a compact JSON payload (e.g., missing_from_story, stale_in_story, wrote) suitable for logging into the state file.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 7 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread skills/bmad-story-automator/src/story_automator/commands/basic.py
Comment thread skills/bmad-story-automator/src/story_automator/commands/basic.py Outdated
Comment thread skills/bmad-story-automator/src/story_automator/commands/basic.py Outdated
Comment thread skills/bmad-story-automator/src/story_automator/commands/basic.py Outdated
Comment thread skills/bmad-story-automator/src/story_automator/commands/basic.py
Comment thread skills/bmad-story-automator/steps-c/step-03-execute.md Outdated
Comment thread tests/test_reconcile_story.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
skills/bmad-story-automator/src/story_automator/commands/basic.py (2)

223-242: 💤 Low value

Consider adding defensive validation for git output format.

While git porcelain format is well-defined (2 status chars + space + path), adding a length check before line[3:] would make the code more robust against unexpected git output or future format changes.

🛡️ Optional defensive check
     for line in status.output.splitlines():
         if not line.strip():
             continue
+        if len(line) < 3:
+            continue
         path = line[3:]  # strip the 2-char XY status code + separator space
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/bmad-story-automator/src/story_automator/commands/basic.py` around
lines 223 - 242, In _git_changed_files, add defensive validation of each
status.output line before slicing with line[3:]: check the line length (e.g., at
least 3 characters) and that the third character is a space (or otherwise
validate the two-char XY status + separator) and skip/log any malformed lines
instead of slicing; ensure the rest of the logic (handling " -> ", stripping
quotes, and filtering with FILE_LIST_EXCLUDE_PREFIXES) only runs on validated
lines and preserve returning sorted(files) or None on run_cmd failure.

245-258: 💤 Low value

Potential edge case: indented headings in code blocks.

The .lstrip() on line 255 could match indented content in code blocks that starts with #. While markdown headings typically start at column 0, a code block with an indented line like # Comment would match after lstrip.

This is unlikely to cause issues in practice since the story markdown format is controlled, but a more strict check (lines[idx].startswith("#") without lstrip) would be more robust.

♻️ More strict heading detection
     end = len(lines)
     for idx in range(start + 1, len(lines)):
-        if lines[idx].lstrip().startswith("#"):
+        if lines[idx].startswith("#"):
             end = idx
             break
     return start, end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/bmad-story-automator/src/story_automator/commands/basic.py` around
lines 245 - 258, The heading-detection in _file_list_bounds currently uses
lines[idx].lstrip().startswith("#") which will match indented lines (e.g.,
inside code blocks); change this to check the raw line start
(lines[idx].startswith("#")) so only column-0 headings terminate the file-list
block; update the condition in _file_list_bounds accordingly and ensure any unit
tests or callers expecting the previous behavior still pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@skills/bmad-story-automator/src/story_automator/commands/basic.py`:
- Around line 223-242: In _git_changed_files, add defensive validation of each
status.output line before slicing with line[3:]: check the line length (e.g., at
least 3 characters) and that the third character is a space (or otherwise
validate the two-char XY status + separator) and skip/log any malformed lines
instead of slicing; ensure the rest of the logic (handling " -> ", stripping
quotes, and filtering with FILE_LIST_EXCLUDE_PREFIXES) only runs on validated
lines and preserve returning sorted(files) or None on run_cmd failure.
- Around line 245-258: The heading-detection in _file_list_bounds currently uses
lines[idx].lstrip().startswith("#") which will match indented lines (e.g.,
inside code blocks); change this to check the raw line start
(lines[idx].startswith("#")) so only column-0 headings terminate the file-list
block; update the condition in _file_list_bounds accordingly and ensure any unit
tests or callers expecting the previous behavior still pass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b6b41cf-e1e9-4f4d-b4bf-b044f6a63fd5

📥 Commits

Reviewing files that changed from the base of the PR and between f332173 and 6dff6b4.

📒 Files selected for processing (5)
  • skills/bmad-story-automator-review/instructions.xml
  • skills/bmad-story-automator/src/story_automator/cli.py
  • skills/bmad-story-automator/src/story_automator/commands/basic.py
  • skills/bmad-story-automator/steps-c/step-03-execute.md
  • tests/test_reconcile_story.py

@Pawel-N-pl

Copy link
Copy Markdown
Author

Thanks — addressed all review feedback in 38aeb16.

Finding Fix
Augment: artifact dir assumed _bmad-output/ (high) exclude the dynamically-resolved artifacts dir → docs/bmad/... story files excluded too
Augment: quoted/escaped git paths (med) parse git status -z (NUL-separated, verbatim)
Augment: matches[0] may pick wrong story (med) prefer the resolved normalize_story_key key before the prefix glob
Augment: step ignores reconcile exit status (med) step checks .ok, logs WARNING on failure instead of false "reconciled"
Augment: trailing-paren strip mangles foo(bar).py (low) strip only a closed set of known annotations
Augment: extra EOF newline when File List is last (low) blank separator only when a section follows
Augment: tests miss configured artifact dirs (low) added a docs/bmad/... exclusion test
CodeRabbit: defensive length check guard len(entry) < 3 before slicing
CodeRabbit: indented # in code blocks match column-0 headings only

npm run verify green (421 tests).

@Pawel-N-pl

Copy link
Copy Markdown
Author

Thanks @coderabbitai — both nitpicks were already resolved in 38aeb16 (pushed right after this review, which covered the earlier 6dff6b4):

  • _git_changed_files defensive slicing: the helper was rewritten to NUL-separated git status -z parsing with an explicit if len(entry) < 3: continue guard before slicing — covers the malformed-line concern (and quoted/non-ASCII paths).
  • Indented-heading detection: the bounds scan now uses lines[idx].startswith("#") (no lstrip), so indented # inside code blocks no longer terminate the section.

No further changes needed here.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Pawel-N-pl Thanks for the detailed confirmation. Both fixes look correct:

  • Defensive slicing via git status -z: NUL-separated output avoids the quoted/escaped path issue entirely, and the len(entry) < 3 guard cleanly handles any malformed entries before the status-prefix slice.
  • startswith("#") without lstrip: Column-0 anchoring is the right call — it correctly ignores # characters that appear inside indented code blocks or prose.

Appreciate the thorough summary. 🐇

…close

The reconcile gate was wired into step-03-execute (right after dev-story, before
the `auto` step). But the automator runs `auto` (qa-generate-e2e-tests) AFTER dev
and BEFORE review, and that step adds test files — so a File List reconciled at
dev-close goes stale the moment `auto` runs, and the committed story record drifts
again. Observed on a live Epic 4 build-cycle run: the dev-close gate held for the
first story but the record drifted on later ones once `auto` added tests.

Move the reconcile to step-03b-execute-finish, immediately before commit-story —
the last point the File List can change (after `auto` and the review loop). Revert
the review instructions comment: review now runs before the gate, so it is the
drift detector again, not defence-in-depth.

Companion PR (test counts) gets the same done-close treatment.
@Pawel-N-pl

Copy link
Copy Markdown
Author

Repositioned the reconcile gate from step-03-execute (dev-close) to step-03b-execute-finish (done-close), immediately before commit-story. New commit on top — no force-push here.

Why: the automator runs auto (qa-generate-e2e-tests) after dev and before review, and that step adds test files. A File List reconciled at dev-close therefore goes stale the moment auto runs, and the committed story record drifts again — exactly the drift this PR set out to kill. Caught on a live Epic 4 build-cycle run: the dev-close gate held for the first story, but the record drifted on later ones once auto added tests.

Done-close (after auto and the review loop, right before the commit snapshots the record) is the last point the File List can change, so that's where the gate belongs. Also reverted the review-instructions comment: review now runs before the gate, so it's the drift detector again, not defence-in-depth.

Companion PR #42 (test counts) gets the same done-close treatment — rebased onto this and wired next to the File List reconcile. Surfaced as action item AI-4.1 in a downstream retrospective; happy to share the write-up if useful.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_reconcile_story.py`:
- Line 144: The test assertion in tests/test_reconcile_story.py uses a hardcoded
forward-slash string check
(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
which fails on Windows; replace it with a Path-based, platform-independent check
such as using
pathlib.Path(payload["story_file"]).match("**/docs/bmad/implementation-artifacts/1-2-example.md")
or converting to a POSIX path with
pathlib.Path(payload["story_file"]).as_posix().endswith("docs/bmad/implementation-artifacts/1-2-example.md")
so the comparison works across OSes and update imports if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82f4af73-441d-49f3-957f-4c61e04c4d70

📥 Commits

Reviewing files that changed from the base of the PR and between 6dff6b4 and 95e2e62.

📒 Files selected for processing (3)
  • skills/bmad-story-automator/src/story_automator/commands/basic.py
  • skills/bmad-story-automator/steps-c/step-03b-execute-finish.md
  • tests/test_reconcile_story.py


payload = self._run("--write")
self.assertEqual(payload["git_files"], ["src/a.py"])
self.assertTrue(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Platform portability: path separator assumption.

The assertion uses a hardcoded forward-slash path, but str(Path(...)) on Windows produces backslashes. This test would fail on Windows.

🔧 Proposed fix for cross-platform compatibility
-        self.assertTrue(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
+        # Normalize separators for cross-platform comparison
+        story_file_normalized = payload["story_file"].replace("\\", "/")
+        self.assertTrue(story_file_normalized.endswith("docs/bmad/implementation-artifacts/1-2-example.md"))

Alternatively, use Path-based matching:

-        self.assertTrue(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
+        self.assertTrue(Path(payload["story_file"]).match("*/docs/bmad/implementation-artifacts/1-2-example.md"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.assertTrue(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
# Normalize separators for cross-platform comparison
story_file_normalized = payload["story_file"].replace("\\", "/")
self.assertTrue(story_file_normalized.endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_reconcile_story.py` at line 144, The test assertion in
tests/test_reconcile_story.py uses a hardcoded forward-slash string check
(payload["story_file"].endswith("docs/bmad/implementation-artifacts/1-2-example.md"))
which fails on Windows; replace it with a Path-based, platform-independent check
such as using
pathlib.Path(payload["story_file"]).match("**/docs/bmad/implementation-artifacts/1-2-example.md")
or converting to a POSIX path with
pathlib.Path(payload["story_file"]).as_posix().endswith("docs/bmad/implementation-artifacts/1-2-example.md")
so the comparison works across OSes and update imports if needed.

@bma-d bma-d left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the commit path below. There is also an existing open thread on tests/test_reconcile_story.py:144 about path separator portability that should remain unresolved until fixed.

echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** WARNING: File List reconcile failed: $(printf '%s' "$reconcile" | jq -c '.error // .')" >> "{outputFile}"
fi

commit=$("{scriptsDir}" commit-story --repo "{project-root}" --story {story_id} --title "{title}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently treats reconcile failure as a warning and still runs commit-story immediately afterward. That means the story record can be committed even when the final File List sync failed, which undercuts this block's stated purpose of syncing immediately before the commit snapshot. Suggested fix: make reconcile success a prerequisite for commit-story; on failure, log the warning and stop or escalate before this command runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants