Skip to content

Commit 7f69e39

Browse files
authored
feat: sync beads tasks back into plan file (#246)
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). - Watcher starts in BeadsPlugin constructor - 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
1 parent 84c1d35 commit 7f69e39

5 files changed

Lines changed: 813 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist/
22
node_modules/
3+
skill/
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* Beads Instruction Generator
3+
*
4+
* Beads-specific implementation of IInstructionGenerator.
5+
* Generates instructions optimized for beads task management workflow.
6+
*/
7+
8+
import {
9+
type IInstructionGenerator,
10+
type InstructionContext,
11+
type GeneratedInstructions,
12+
type YamlStateMachine,
13+
ProjectDocsManager,
14+
} from '@codemcp/workflows-core';
15+
16+
/**
17+
* Beads-specific instruction generator
18+
*/
19+
export class BeadsInstructionGenerator implements IInstructionGenerator {
20+
private projectDocsManager: ProjectDocsManager;
21+
22+
constructor() {
23+
this.projectDocsManager = new ProjectDocsManager();
24+
}
25+
26+
/**
27+
* Set the state machine definition (interface requirement)
28+
*/
29+
setStateMachine(_stateMachine: YamlStateMachine): void {
30+
// No-op: beads uses CLI for state management
31+
}
32+
33+
/**
34+
* Generate comprehensive instructions optimized for beads workflow
35+
*/
36+
async generateInstructions(
37+
baseInstructions: string,
38+
context: InstructionContext
39+
): Promise<GeneratedInstructions> {
40+
// Apply variable substitution to base instructions
41+
const substitutedInstructions = this.applyVariableSubstitution(
42+
baseInstructions,
43+
context.conversationContext.projectPath,
44+
context.conversationContext.gitBranch
45+
);
46+
47+
// Enhance base instructions with beads-specific guidance
48+
const enhancedInstructions = await this.enhanceBeadsInstructions(
49+
substitutedInstructions,
50+
context
51+
);
52+
53+
return {
54+
instructions: enhancedInstructions,
55+
planFileGuidance:
56+
'Using beads CLI for task management - plan file serves as context only',
57+
metadata: {
58+
phase: context.phase,
59+
planFilePath: context.conversationContext.planFilePath,
60+
transitionReason: context.transitionReason,
61+
isModeled: context.isModeled,
62+
},
63+
};
64+
}
65+
66+
/**
67+
* Apply variable substitution to instructions
68+
*/
69+
private applyVariableSubstitution(
70+
instructions: string,
71+
projectPath: string,
72+
gitBranch?: string
73+
): string {
74+
const substitutions = this.projectDocsManager.getVariableSubstitutions(
75+
projectPath,
76+
gitBranch
77+
);
78+
79+
let result = instructions;
80+
for (const [variable, value] of Object.entries(substitutions)) {
81+
result = result.replace(
82+
new RegExp(this.escapeRegExp(variable), 'g'),
83+
value
84+
);
85+
}
86+
87+
return result;
88+
}
89+
90+
/**
91+
* Escape special regex characters in variable names
92+
*/
93+
private escapeRegExp(string: string): string {
94+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95+
}
96+
97+
/**
98+
* Enhance instructions with beads-specific guidance
99+
*/
100+
private async enhanceBeadsInstructions(
101+
baseInstructions: string,
102+
context: InstructionContext
103+
): Promise<string> {
104+
const { planFileExists } = context;
105+
106+
// Generate beads-specific task management guidance
107+
const beadsTaskGuidance = await this.generateBeadsCLIGuidance(context);
108+
109+
// Beads-optimized instruction structure
110+
let enhanced = `${baseInstructions}
111+
112+
**Plan File Guidance:**
113+
Use the plan file as memory for the current objective
114+
- Update the "Key Decisions" section with important choices made
115+
- Add relevant notes to help maintain context
116+
- Do NOT enter tasks in the plan file, use beads CLI exclusively for task management
117+
118+
${beadsTaskGuidance}`;
119+
120+
// Add plan file creation note if needed
121+
if (!planFileExists) {
122+
enhanced +=
123+
'\n\n**Note**: Plan file will be created when you first update it.';
124+
}
125+
126+
// Add beads-specific reminders
127+
enhanced += `\n\n**Important Reminders:**
128+
- Use ONLY bd CLI tool for task management - do not use your own task management tools
129+
- Call whats_next() after the next user message to maintain the development workflow`;
130+
131+
return enhanced;
132+
}
133+
134+
/**
135+
* Generate beads-specific task management guidance
136+
*/
137+
private async generateBeadsCLIGuidance(
138+
context: InstructionContext
139+
): Promise<string> {
140+
const { instructionSource } = context;
141+
142+
// For whats_next, provide detailed guidance
143+
if (instructionSource === 'whats_next') {
144+
let additionalInstructions = `**bd Task Management:**
145+
`;
146+
147+
const phaseTaskId = await this.extractPhaseTaskId(context);
148+
149+
if (!phaseTaskId) {
150+
return (
151+
additionalInstructions +
152+
`- Use bd CLI tool exclusively
153+
- **Start by listing ready tasks**: \`bd list --parent <phase-task-id> --status open\`
154+
- **Create new tasks**: \`bd create 'Task title' --parent <phase-task-id> -p <priority>\`
155+
- **Update status when working**: \`bd update <task-id> --status in_progress\`
156+
- **Complete tasks**: \`bd close <task-id>\`
157+
- **Focus on ready tasks first** - let beads handle dependencies
158+
- Add new tasks as they are identified during your work with the user`
159+
);
160+
}
161+
162+
return (
163+
additionalInstructions +
164+
`
165+
**Focus on subtasks of \`${phaseTaskId}\`**:
166+
• \`bd list --parent ${phaseTaskId} --status open\` - List ready work items
167+
• \`bd update <task-id> --status in_progress\` - Start working on a specific task
168+
• \`bd close <task-id>\` - Mark task complete when finished
169+
170+
**New Tasks for Current Phase**:
171+
• \`bd create 'Task description' --parent ${phaseTaskId} -p <priority>\` - Create work item under current phase
172+
• \`bd dep add <task-id> <depends-on-id>\` - Define dependencies for a task:`
173+
);
174+
}
175+
176+
return '';
177+
}
178+
179+
private async extractPhaseTaskId(
180+
context: InstructionContext
181+
): Promise<string | null> {
182+
try {
183+
const { readFile } = await import('node:fs/promises');
184+
const content = await readFile(
185+
context.conversationContext.planFilePath,
186+
'utf-8'
187+
);
188+
189+
const phaseName = this.capitalizePhase(context.phase);
190+
const phaseHeader = `## ${phaseName}`;
191+
192+
// Look for the phase header followed by beads-phase-id comment
193+
const phaseSection = content.split('\n');
194+
let foundPhaseHeader = false;
195+
196+
for (const line of phaseSection) {
197+
if (line.trim() === phaseHeader) {
198+
foundPhaseHeader = true;
199+
continue;
200+
}
201+
202+
if (foundPhaseHeader && line.includes('beads-phase-id:')) {
203+
const match = line.match(/beads-phase-id:\s*([\w\d.-]+)/);
204+
if (match) {
205+
return match[1] || null;
206+
}
207+
}
208+
209+
// Stop looking if we hit the next phase header
210+
if (foundPhaseHeader && line.startsWith('##') && line !== phaseHeader) {
211+
break;
212+
}
213+
}
214+
215+
return null;
216+
} catch (_error) {
217+
return null;
218+
}
219+
}
220+
221+
/**
222+
* Capitalize phase name for display
223+
*/
224+
private capitalizePhase(phase: string): string {
225+
return phase
226+
.split('_')
227+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
228+
.join(' ');
229+
}
230+
}

0 commit comments

Comments
 (0)