Skip to content

feat: sync beads tasks back into plan file#246

Merged
mrsimpson merged 8 commits into
mainfrom
claude/sync-beads-to-plan-Bwjas
Apr 2, 2026
Merged

feat: sync beads tasks back into plan file#246
mrsimpson merged 8 commits into
mainfrom
claude/sync-beads-to-plan-Bwjas

Conversation

@mrsimpson
Copy link
Copy Markdown
Collaborator

@mrsimpson mrsimpson commented Mar 29, 2026

Intent

When the beads plugin is active, the plan file becomes unreadable as documentation: every phase's Tasks section is just *Tasks managed via \bd` CLI*` and the actual tasks live exclusively in beads. This change syncs beads tasks back into the plan file so it remains a useful, up-to-date record of what was done and what is left.

Major changes

BeadsPlanSyncer (new)

Reads .beads/issues.jsonl directly (no bd CLI spawn, no-db mode) and rewrites each phase's ### Tasks section with checkbox-formatted lines:

<!-- beads-synced: 2026-03-29 -->
*Auto-synced — do not edit here, use `bd` CLI instead.*

- [x] `proj-1.1.1` Collect user stories
- [ ] `proj-1.1.2` Write acceptance criteria

Only phases with a resolved (non-TBD) <!-- beads-phase-id: ... --> marker are touched. Each phase only receives its own direct children (parent-child dependency).

BeadsPlugin — file watcher

A fs.watch on the .beads/ directory (not the file) is started on plugin construction with a 300 ms debounce. Watching the directory means the watcher works even on fresh projects where issues.jsonl doesn't exist yet. On any change to issues.jsonl, the current plan file is re-synced.

The plan file path is captured from PluginHookContext.planFilePath in afterStartDevelopment and beforePhaseTransition — no GitManager involved. The watcher skips silently if no conversation has started yet.

Integration tests

Real-filesystem tests (temp dirs, no mocks) covering: checkbox state mapping, per-phase child isolation, idempotency, status update on re-sync, and graceful no-ops for missing JSONL, missing plan file, TBD phase IDs, no markers, and malformed JSONL lines.

Key decisions

Decision Rationale
Read JSONL directly, not via bd CLI Avoids spawning a subprocess on every file-change event; JSONL is the beads source of truth in no-db mode
Watch .beads/ directory, not issues.jsonl fs.watch on a non-existent file throws; directory watch works from project init and filters by filename in the callback
Plan file path from PluginHookContext, not GitManager GitManager couples to git state that may not be available; the context already carries the correct resolved path
process.once('exit') in constructor, not in watcher startup Ensures exactly one exit handler regardless of whether the watcher started successfully
ENOENT-specific silent handling Only truly-missing files are silently ignored; EPERM/EISDIR and other unexpected errors propagate to the outer catch for logging

@mrsimpson mrsimpson changed the title feat: sync beads tasks back to plan file feat: sync beads tasks back into plan file Mar 29, 2026
claude added 7 commits April 2, 2026 17:33
Adds a BeadsPlanSyncer that reads .beads/issues.jsonl directly and
rewrites each phase's Tasks section with live checkbox-formatted task
lines (task ID as backtick link, checked if closed).

Two trigger mechanisms:
- File watcher in BeadsPlugin: syncs immediately on any bd command
  (~300ms debounce on issues.jsonl changes)
- Safety-net sync in BeadsInstructionGenerator: ensures fresh state
  on every whats_next call even if the watcher missed an event

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
- Watcher now starts in BeadsPlugin constructor (not afterStartDevelopment)
- sync() derives the plan file path from the current git branch, same
  logic as ConversationManager — no plan file passed around
- Removed activePlanFilePath field
- Removed safety-net sync from BeadsInstructionGenerator
- syncAll() replaced by sync(projectPath); no directory globbing

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
BeadsPlugin captures planFilePath from PluginHookContext in every hook
that fires (afterStartDevelopment, beforePhaseTransition) and stores it
as activePlanFilePath. The watcher uses that value — skipping silently
if no conversation has started yet.

BeadsPlanSyncer.sync() takes planFilePath explicitly again, removing the
GitManager / branch-name derivation entirely.

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
- Watch .beads/ directory instead of issues.jsonl file so the watcher
  works on fresh projects where the file doesn't exist yet
- Move process.once('exit') handler to constructor so it's registered
  exactly once regardless of watcher start outcome
- Replace TOCTOU access()+readFile() pattern with direct readFile()
  that rethrows non-ENOENT errors to the outer catch for logging
- Apply same ENOENT-specific handling to plan file read
- Inline isCompleted() one-liner into its single call site

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
…ries

Covers: task checkboxes (open/closed/in_progress), per-phase child
isolation, placeholder when no tasks, idempotency, re-sync after status
change, and graceful no-ops for missing JSONL, missing plan file, TBD
phase IDs, no phase markers, and malformed JSONL lines.

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
…ections

Both the with-tasks and no-tasks cases now include a consistent note
making it clear the section is auto-synced and must be managed via the
bd CLI, not edited directly in the plan file.

https://claude.ai/code/session_01WXzZCnA8xDv6ghtSZ9iPrp
@mrsimpson mrsimpson force-pushed the claude/sync-beads-to-plan-Bwjas branch from fe56083 to 88aa6b6 Compare April 2, 2026 15:40
@mrsimpson mrsimpson merged commit 7f69e39 into main Apr 2, 2026
3 checks passed
@mrsimpson mrsimpson deleted the claude/sync-beads-to-plan-Bwjas branch April 2, 2026 16:19
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.

2 participants