Merge branch 'steveyegge:main' into main
This commit is contained in:
22
.claude-plugin/commands/bd-version.md
Normal file
22
.claude-plugin/commands/bd-version.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
description: Check beads and plugin versions
|
||||||
|
---
|
||||||
|
|
||||||
|
Check the installed versions of beads components and verify compatibility.
|
||||||
|
|
||||||
|
Use the beads MCP tools to:
|
||||||
|
1. Run `bd --version` via bash to get the CLI version
|
||||||
|
2. Check the plugin version from the environment
|
||||||
|
3. Compare versions and report any mismatches
|
||||||
|
|
||||||
|
Display:
|
||||||
|
- bd CLI version (from `bd --version`)
|
||||||
|
- Plugin version (0.9.0)
|
||||||
|
- MCP server status (from `stats` tool or connection test)
|
||||||
|
- Compatibility status (✓ compatible or ⚠️ update needed)
|
||||||
|
|
||||||
|
If versions are mismatched, provide instructions:
|
||||||
|
- Update bd CLI: `curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash`
|
||||||
|
- Update plugin: `/plugin update beads`
|
||||||
|
|
||||||
|
Suggest checking for updates if the user is on an older version.
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
"agent-memory",
|
"agent-memory",
|
||||||
"mcp-server"
|
"mcp-server"
|
||||||
],
|
],
|
||||||
|
"engines": {
|
||||||
|
"beads": ">=0.9.0"
|
||||||
|
},
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"beads": {
|
"beads": {
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
|
|||||||
44
LINTING.md
44
LINTING.md
@@ -4,11 +4,13 @@ This document explains our approach to `golangci-lint` warnings in this codebase
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
Running `golangci-lint run ./...` currently reports ~100 "issues". However, these are not actual code quality problems - they are false positives or intentional patterns that reflect idiomatic Go practice.
|
Running `golangci-lint run ./...` currently reports ~200 "issues". However, these are not actual code quality problems - they are false positives or intentional patterns that reflect idiomatic Go practice.
|
||||||
|
|
||||||
|
**Note**: The count increased from ~100 to ~200 between Oct 12-14, 2025, due to significant test coverage additions for collision resolution (1100+ lines) and auto-flush features (300+ lines). All new warnings follow the same idiomatic patterns documented below.
|
||||||
|
|
||||||
## Issue Breakdown
|
## Issue Breakdown
|
||||||
|
|
||||||
### errcheck (73 issues)
|
### errcheck (159 issues)
|
||||||
|
|
||||||
**Pattern**: Unchecked errors from `defer` cleanup operations
|
**Pattern**: Unchecked errors from `defer` cleanup operations
|
||||||
**Status**: Intentional and idiomatic
|
**Status**: Intentional and idiomatic
|
||||||
@@ -27,9 +29,9 @@ defer os.RemoveAll(tmpDir) // in tests
|
|||||||
|
|
||||||
Fixing these would add noise without improving code quality. The critical cleanup operations (where errors matter) are already checked explicitly.
|
Fixing these would add noise without improving code quality. The critical cleanup operations (where errors matter) are already checked explicitly.
|
||||||
|
|
||||||
### revive (17 issues)
|
### revive (21 issues)
|
||||||
|
|
||||||
**Pattern 1**: Unused parameters in Cobra command handlers (15 issues)
|
**Pattern 1**: Unused parameters in Cobra command handlers (18 issues)
|
||||||
**Status**: Required by interface
|
**Status**: Required by interface
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -41,13 +43,14 @@ Run: func(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
**Rationale**: Cobra requires this exact function signature. Renaming to `_` would make the code less clear when parameters *are* used.
|
**Rationale**: Cobra requires this exact function signature. Renaming to `_` would make the code less clear when parameters *are* used.
|
||||||
|
|
||||||
**Pattern 2**: Package naming (2 issues)
|
**Pattern 2**: Package naming (3 issues)
|
||||||
- `package types` - Clear and appropriate for a types package
|
- `package types` - Clear and appropriate for a types package
|
||||||
- `SQLiteStorage` - Intentional; `sqlite.Storage` would be confusing with the interface
|
- `SQLiteStorage` - Intentional; `sqlite.Storage` would be confusing with the interface
|
||||||
|
- Blank import comment - Required for database driver registration
|
||||||
|
|
||||||
### gosec (7 issues)
|
### gosec (19 issues)
|
||||||
|
|
||||||
**Pattern 1**: G201 - SQL string formatting (4 issues)
|
**Pattern 1**: G201 - SQL string formatting (6 issues)
|
||||||
**Status**: False positive - all SQL is validated
|
**Status**: False positive - all SQL is validated
|
||||||
|
|
||||||
All dynamic SQL construction uses:
|
All dynamic SQL construction uses:
|
||||||
@@ -55,25 +58,30 @@ All dynamic SQL construction uses:
|
|||||||
- Parameterized queries for all values
|
- Parameterized queries for all values
|
||||||
- Safe string building for clauses like ORDER BY and LIMIT
|
- Safe string building for clauses like ORDER BY and LIMIT
|
||||||
|
|
||||||
**Pattern 2**: G304 - File inclusion via variable (2 issues)
|
**Pattern 2**: G304 - File inclusion via variable (11 issues)
|
||||||
**Status**: Intended feature - user-specified file paths for import/export
|
**Status**: Intended feature - user-specified file paths for import/export/test fixtures
|
||||||
|
|
||||||
**Pattern 3**: G301 - Directory permissions (1 issue)
|
All file paths are either:
|
||||||
**Status**: Acceptable - 0755 is reasonable for a database directory
|
- User-provided CLI arguments (expected for import/export commands)
|
||||||
|
- Test fixtures in controlled test environments
|
||||||
|
- Validated paths with security checks (e.g., markdown.go uses validateMarkdownPath)
|
||||||
|
|
||||||
### dupl (2 issues)
|
**Pattern 3**: G301 - Directory permissions (2 issues)
|
||||||
|
**Status**: Acceptable - 0755 is reasonable for database directories
|
||||||
|
|
||||||
**Pattern**: Test code duplication
|
### gocyclo (1 issue)
|
||||||
|
|
||||||
|
**Pattern**: High cyclomatic complexity in `TestExportImport` (31)
|
||||||
**Status**: Acceptable
|
**Status**: Acceptable
|
||||||
|
|
||||||
Test code duplication is often preferable to premature test abstraction. These tests are clear and maintainable as-is.
|
This comprehensive integration test covers multiple scenarios (export, import, filters, updates). The complexity comes from thorough test coverage, not production code. Splitting would reduce readability.
|
||||||
|
|
||||||
### goconst (1 issue)
|
### goconst (2 issues)
|
||||||
|
|
||||||
**Pattern**: Repeated string constant in tests
|
**Pattern**: Repeated string constants in tests
|
||||||
**Status**: Acceptable
|
**Status**: Acceptable
|
||||||
|
|
||||||
The string `"test-user"` appears multiple times in test code. Extracting this to a constant would not improve test readability.
|
Repeated test strings like `"test-user"` and file paths appear multiple times. Extracting these to constants would not improve test readability or maintainability.
|
||||||
|
|
||||||
## golangci-lint Configuration Challenges
|
## golangci-lint Configuration Challenges
|
||||||
|
|
||||||
@@ -89,7 +97,7 @@ This appears to be a known limitation of golangci-lint's configuration system.
|
|||||||
**For contributors**: Don't be alarmed by the lint warnings. The code quality is high.
|
**For contributors**: Don't be alarmed by the lint warnings. The code quality is high.
|
||||||
|
|
||||||
**For code review**: Focus on:
|
**For code review**: Focus on:
|
||||||
- New issues introduced by changes (not the baseline 100)
|
- New issues introduced by changes (not the baseline ~200)
|
||||||
- Actual logic errors
|
- Actual logic errors
|
||||||
- Missing error checks on critical operations (file writes, database commits)
|
- Missing error checks on critical operations (file writes, database commits)
|
||||||
- Security concerns beyond gosec's false positives
|
- Security concerns beyond gosec's false positives
|
||||||
|
|||||||
56
PLUGIN.md
56
PLUGIN.md
@@ -73,6 +73,10 @@ After installation, restart Claude Code to activate the MCP server.
|
|||||||
|
|
||||||
## Available Commands
|
## Available Commands
|
||||||
|
|
||||||
|
### Version Management
|
||||||
|
|
||||||
|
- **`/bd-version`** - Check bd CLI, plugin, and MCP server versions
|
||||||
|
|
||||||
### Core Workflow Commands
|
### Core Workflow Commands
|
||||||
|
|
||||||
- **`/bd-ready`** - Find tasks with no blockers, ready to work on
|
- **`/bd-ready`** - Find tasks with no blockers, ready to work on
|
||||||
@@ -225,6 +229,58 @@ git pull
|
|||||||
bd ready # Fresh data from git!
|
bd ready # Fresh data from git!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
The beads plugin has three components that may need updating:
|
||||||
|
|
||||||
|
### 1. Plugin Updates
|
||||||
|
|
||||||
|
Check for plugin updates:
|
||||||
|
```bash
|
||||||
|
/plugin update beads
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude Code will pull the latest version from GitHub. After updating, **restart Claude Code** to apply MCP server changes.
|
||||||
|
|
||||||
|
### 2. bd CLI Updates
|
||||||
|
|
||||||
|
The plugin requires the `bd` CLI to be installed. Update it separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick update
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash
|
||||||
|
|
||||||
|
# Or with go
|
||||||
|
go install github.com/steveyegge/beads/cmd/bd@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Version Compatibility
|
||||||
|
|
||||||
|
Check version compatibility:
|
||||||
|
```bash
|
||||||
|
/bd-version
|
||||||
|
```
|
||||||
|
|
||||||
|
This will show:
|
||||||
|
- bd CLI version
|
||||||
|
- Plugin version
|
||||||
|
- MCP server status
|
||||||
|
- Compatibility warnings if versions mismatch
|
||||||
|
|
||||||
|
**Recommended update workflow:**
|
||||||
|
1. Check versions: `/bd-version`
|
||||||
|
2. Update bd CLI if needed (see above)
|
||||||
|
3. Update plugin: `/plugin update beads`
|
||||||
|
4. Restart Claude Code
|
||||||
|
5. Verify: `/bd-version`
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
Beads follows semantic versioning. The plugin version tracks the bd CLI version:
|
||||||
|
- Plugin 0.9.x requires bd CLI 0.9.0+
|
||||||
|
- Major version bumps may introduce breaking changes
|
||||||
|
- Check CHANGELOG.md for release notes
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Plugin not appearing
|
### Plugin not appearing
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ This directory contains examples of how to integrate bd with AI agents and workf
|
|||||||
|
|
||||||
- **[python-agent/](python-agent/)** - Simple Python agent that discovers ready work and completes tasks
|
- **[python-agent/](python-agent/)** - Simple Python agent that discovers ready work and completes tasks
|
||||||
- **[bash-agent/](bash-agent/)** - Bash script showing the full agent workflow
|
- **[bash-agent/](bash-agent/)** - Bash script showing the full agent workflow
|
||||||
|
- **[markdown-to-jsonl/](markdown-to-jsonl/)** - Convert markdown planning docs to bd issues
|
||||||
- **[git-hooks/](git-hooks/)** - Pre-configured git hooks for automatic export/import
|
- **[git-hooks/](git-hooks/)** - Pre-configured git hooks for automatic export/import
|
||||||
- **[branch-merge/](branch-merge/)** - Branch merge workflow with collision resolution
|
- **[branch-merge/](branch-merge/)** - Branch merge workflow with collision resolution
|
||||||
- **[claude-desktop-mcp/](claude-desktop-mcp/)** - MCP server for Claude Desktop integration
|
- **[claude-desktop-mcp/](claude-desktop-mcp/)** - MCP server for Claude Desktop integration
|
||||||
|
|||||||
165
examples/markdown-to-jsonl/README.md
Normal file
165
examples/markdown-to-jsonl/README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Markdown to JSONL Converter
|
||||||
|
|
||||||
|
Convert markdown planning documents into `bd` issues.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This example shows how to bridge the gap between markdown planning docs and tracked issues, without adding complexity to the `bd` core tool.
|
||||||
|
|
||||||
|
The converter script (`md2jsonl.py`) parses markdown files and outputs JSONL that can be imported into `bd`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ **YAML Frontmatter** - Extract metadata (priority, type, assignee)
|
||||||
|
- ✅ **Headings as Issues** - Each H1/H2 becomes an issue
|
||||||
|
- ✅ **Task Lists** - Markdown checklists become sub-issues
|
||||||
|
- ✅ **Dependency Parsing** - Extract "blocks: bd-10" references
|
||||||
|
- ✅ **Customizable** - Modify the script for your conventions
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic conversion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python md2jsonl.py feature.md | bd import
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save to file first
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python md2jsonl.py feature.md > issues.jsonl
|
||||||
|
bd import -i issues.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preview before importing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python md2jsonl.py feature.md | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Markdown Format
|
||||||
|
|
||||||
|
### Frontmatter (Optional)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
priority: 1
|
||||||
|
type: feature
|
||||||
|
assignee: alice
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headings
|
||||||
|
|
||||||
|
Each heading becomes an issue:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Main Feature
|
||||||
|
|
||||||
|
Description of the feature...
|
||||||
|
|
||||||
|
## Sub-task 1
|
||||||
|
|
||||||
|
Details about sub-task...
|
||||||
|
|
||||||
|
## Sub-task 2
|
||||||
|
|
||||||
|
More details...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task Lists
|
||||||
|
|
||||||
|
Task lists are converted to separate issues:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Setup Tasks
|
||||||
|
|
||||||
|
- [ ] Install dependencies
|
||||||
|
- [x] Configure database
|
||||||
|
- [ ] Set up CI/CD
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates 3 issues (second one marked as closed).
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Reference other issues in the description:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Implement API
|
||||||
|
|
||||||
|
This task requires the database schema to be ready first.
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
- blocks: bd-5
|
||||||
|
- related: bd-10, bd-15
|
||||||
|
```
|
||||||
|
|
||||||
|
The script extracts these and creates dependency records.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
See `example-feature.md` for a complete example.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert the example
|
||||||
|
python md2jsonl.py example-feature.md > example-issues.jsonl
|
||||||
|
|
||||||
|
# View the output
|
||||||
|
cat example-issues.jsonl | jq .
|
||||||
|
|
||||||
|
# Import into bd
|
||||||
|
bd import -i example-issues.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
The script is intentionally simple so you can customize it for your needs:
|
||||||
|
|
||||||
|
1. **Different heading levels** - Modify which headings become issues (H1 only? H1-H3?)
|
||||||
|
2. **Custom metadata** - Parse additional frontmatter fields
|
||||||
|
3. **Labels** - Extract hashtags or keywords as labels
|
||||||
|
4. **Epic detection** - Top-level headings become epics
|
||||||
|
5. **Issue templates** - Map different markdown structures to issue types
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
This is a simple example, not a production tool:
|
||||||
|
|
||||||
|
- Basic YAML parsing (no nested structures)
|
||||||
|
- Simple dependency extraction (regex-based)
|
||||||
|
- No validation of referenced issue IDs
|
||||||
|
- Doesn't handle all markdown edge cases
|
||||||
|
|
||||||
|
For production use, you might want to:
|
||||||
|
- Use a proper YAML parser (`pip install pyyaml`)
|
||||||
|
- Use a markdown parser (`pip install markdown` or `python-markdown2`)
|
||||||
|
- Add validation and error handling
|
||||||
|
- Support more dependency formats
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
This example demonstrates the **lightweight extension pattern**:
|
||||||
|
|
||||||
|
- ✅ Keep `bd` core focused and minimal
|
||||||
|
- ✅ Let users customize for their workflows
|
||||||
|
- ✅ Use existing import infrastructure
|
||||||
|
- ✅ Easy to understand and modify
|
||||||
|
|
||||||
|
Rather than adding markdown support to `bd` core (800+ LOC + dependencies + maintenance), we provide a simple converter that users can adapt.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Have improvements? Found a bug? This is just an example, but contributions are welcome!
|
||||||
|
|
||||||
|
Consider:
|
||||||
|
- Better error messages
|
||||||
|
- More markdown patterns
|
||||||
|
- Integration with popular markdown formats
|
||||||
|
- Support for GFM (GitHub Flavored Markdown) extensions
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [bd README](../../README.md) - Main documentation
|
||||||
|
- [Python Agent Example](../python-agent/) - Full agent workflow
|
||||||
|
- [JSONL Format](../../TEXT_FORMATS.md) - Understanding bd's JSONL structure
|
||||||
49
examples/markdown-to-jsonl/example-feature.md
Normal file
49
examples/markdown-to-jsonl/example-feature.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
priority: 1
|
||||||
|
type: feature
|
||||||
|
assignee: alice
|
||||||
|
---
|
||||||
|
|
||||||
|
# User Authentication System
|
||||||
|
|
||||||
|
Implement a complete user authentication system with login, signup, and password recovery.
|
||||||
|
|
||||||
|
This is a critical feature for the application. The authentication should be secure and follow best practices.
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
- blocks: bd-5 (database schema must be ready first)
|
||||||
|
|
||||||
|
## Login Flow
|
||||||
|
|
||||||
|
Implement the login page with email/password authentication. Should support:
|
||||||
|
- Email validation
|
||||||
|
- Password hashing (bcrypt)
|
||||||
|
- Session management
|
||||||
|
- Remember me functionality
|
||||||
|
|
||||||
|
## Signup Flow
|
||||||
|
|
||||||
|
Create new user registration with validation:
|
||||||
|
- Email uniqueness check
|
||||||
|
- Password strength requirements
|
||||||
|
- Email verification
|
||||||
|
- Terms of service acceptance
|
||||||
|
|
||||||
|
## Password Recovery
|
||||||
|
|
||||||
|
Allow users to reset forgotten passwords:
|
||||||
|
|
||||||
|
- [ ] Send recovery email
|
||||||
|
- [ ] Generate secure reset tokens
|
||||||
|
- [x] Create reset password form
|
||||||
|
- [ ] Expire tokens after 24 hours
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
Handle user sessions securely:
|
||||||
|
- JWT tokens
|
||||||
|
- Refresh token rotation
|
||||||
|
- Session timeout after 30 days
|
||||||
|
- Logout functionality
|
||||||
|
|
||||||
|
Related to bd-10 (API endpoints) and discovered-from: bd-2 (security audit).
|
||||||
253
examples/markdown-to-jsonl/md2jsonl.py
Executable file
253
examples/markdown-to-jsonl/md2jsonl.py
Executable file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Convert markdown files to bd JSONL format.
|
||||||
|
|
||||||
|
This is a simple example converter that demonstrates the pattern.
|
||||||
|
Users can customize this for their specific markdown conventions.
|
||||||
|
|
||||||
|
Supported markdown patterns:
|
||||||
|
1. YAML frontmatter for metadata
|
||||||
|
2. H1/H2 headings as issue titles
|
||||||
|
3. Task lists as sub-issues
|
||||||
|
4. Inline issue references (e.g., "blocks: bd-10")
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python md2jsonl.py feature.md | bd import
|
||||||
|
python md2jsonl.py feature.md > issues.jsonl
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownToIssues:
|
||||||
|
"""Convert markdown to bd JSONL format."""
|
||||||
|
|
||||||
|
def __init__(self, prefix: str = "bd"):
|
||||||
|
self.prefix = prefix
|
||||||
|
self.issue_counter = 1
|
||||||
|
self.issues: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def parse_frontmatter(self, content: str) -> tuple[Optional[Dict], str]:
|
||||||
|
"""Extract YAML frontmatter if present."""
|
||||||
|
# Simple frontmatter detection (--- ... ---)
|
||||||
|
if not content.startswith('---\n'):
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
end = content.find('\n---\n', 4)
|
||||||
|
if end == -1:
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
frontmatter_text = content[4:end]
|
||||||
|
body = content[end + 5:]
|
||||||
|
|
||||||
|
# Parse simple YAML (key: value)
|
||||||
|
metadata = {}
|
||||||
|
for line in frontmatter_text.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if ':' in line:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
metadata[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return metadata, body
|
||||||
|
|
||||||
|
def extract_issue_from_heading(
|
||||||
|
self,
|
||||||
|
heading: str,
|
||||||
|
level: int,
|
||||||
|
content: str,
|
||||||
|
metadata: Optional[Dict] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create an issue from a markdown heading and its content."""
|
||||||
|
# Generate ID
|
||||||
|
issue_id = f"{self.prefix}-{self.issue_counter}"
|
||||||
|
self.issue_counter += 1
|
||||||
|
|
||||||
|
# Extract title (remove markdown formatting)
|
||||||
|
title = heading.strip('#').strip()
|
||||||
|
|
||||||
|
# Parse metadata from frontmatter or defaults
|
||||||
|
if metadata is None:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
# Build issue
|
||||||
|
issue = {
|
||||||
|
"id": issue_id,
|
||||||
|
"title": title,
|
||||||
|
"description": content.strip(),
|
||||||
|
"status": metadata.get("status", "open"),
|
||||||
|
"priority": int(metadata.get("priority", 2)),
|
||||||
|
"issue_type": metadata.get("type", "task"),
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
||||||
|
"updated_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional fields
|
||||||
|
if "assignee" in metadata:
|
||||||
|
issue["assignee"] = metadata["assignee"]
|
||||||
|
|
||||||
|
if "design" in metadata:
|
||||||
|
issue["design"] = metadata["design"]
|
||||||
|
|
||||||
|
# Extract dependencies from description
|
||||||
|
dependencies = self.extract_dependencies(content)
|
||||||
|
if dependencies:
|
||||||
|
issue["dependencies"] = dependencies
|
||||||
|
|
||||||
|
return issue
|
||||||
|
|
||||||
|
def extract_dependencies(self, text: str) -> List[Dict[str, str]]:
|
||||||
|
"""Extract dependency references from text."""
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
# Pattern: "blocks: bd-10" or "depends-on: bd-5, bd-6"
|
||||||
|
# Pattern: "discovered-from: bd-20"
|
||||||
|
dep_pattern = r'(blocks|related|parent-child|discovered-from):\s*((?:bd-\d+(?:\s*,\s*)?)+)'
|
||||||
|
|
||||||
|
for match in re.finditer(dep_pattern, text, re.IGNORECASE):
|
||||||
|
dep_type = match.group(1).lower()
|
||||||
|
dep_ids = [id.strip() for id in match.group(2).split(',')]
|
||||||
|
|
||||||
|
for dep_id in dep_ids:
|
||||||
|
dependencies.append({
|
||||||
|
"issue_id": "", # Will be filled by import
|
||||||
|
"depends_on_id": dep_id.strip(),
|
||||||
|
"type": dep_type
|
||||||
|
})
|
||||||
|
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
def parse_task_list(self, content: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Extract task list items as separate issues."""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Pattern: - [ ] Task or - [x] Task
|
||||||
|
task_pattern = r'^-\s+\[([ x])\]\s+(.+)$'
|
||||||
|
|
||||||
|
for line in content.split('\n'):
|
||||||
|
match = re.match(task_pattern, line.strip())
|
||||||
|
if match:
|
||||||
|
is_done = match.group(1) == 'x'
|
||||||
|
task_text = match.group(2)
|
||||||
|
|
||||||
|
issue_id = f"{self.prefix}-{self.issue_counter}"
|
||||||
|
self.issue_counter += 1
|
||||||
|
|
||||||
|
issue = {
|
||||||
|
"id": issue_id,
|
||||||
|
"title": task_text,
|
||||||
|
"description": "",
|
||||||
|
"status": "closed" if is_done else "open",
|
||||||
|
"priority": 2,
|
||||||
|
"issue_type": "task",
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
||||||
|
"updated_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
||||||
|
}
|
||||||
|
|
||||||
|
issues.append(issue)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def parse_markdown(self, content: str, global_metadata: Optional[Dict] = None):
|
||||||
|
"""Parse markdown content into issues."""
|
||||||
|
# Extract frontmatter
|
||||||
|
frontmatter, body = self.parse_frontmatter(content)
|
||||||
|
|
||||||
|
# Merge metadata
|
||||||
|
metadata = global_metadata or {}
|
||||||
|
if frontmatter:
|
||||||
|
metadata.update(frontmatter)
|
||||||
|
|
||||||
|
# Split by headings
|
||||||
|
heading_pattern = r'^(#{1,6})\s+(.+)$'
|
||||||
|
lines = body.split('\n')
|
||||||
|
|
||||||
|
current_heading = None
|
||||||
|
current_level = 0
|
||||||
|
current_content = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
match = re.match(heading_pattern, line)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
# Save previous section
|
||||||
|
if current_heading:
|
||||||
|
content_text = '\n'.join(current_content)
|
||||||
|
|
||||||
|
# Check for task lists
|
||||||
|
task_issues = self.parse_task_list(content_text)
|
||||||
|
if task_issues:
|
||||||
|
self.issues.extend(task_issues)
|
||||||
|
else:
|
||||||
|
# Create issue from heading
|
||||||
|
issue = self.extract_issue_from_heading(
|
||||||
|
current_heading,
|
||||||
|
current_level,
|
||||||
|
content_text,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
self.issues.append(issue)
|
||||||
|
|
||||||
|
# Start new section
|
||||||
|
current_level = len(match.group(1))
|
||||||
|
current_heading = match.group(2)
|
||||||
|
current_content = []
|
||||||
|
else:
|
||||||
|
current_content.append(line)
|
||||||
|
|
||||||
|
# Save final section
|
||||||
|
if current_heading:
|
||||||
|
content_text = '\n'.join(current_content)
|
||||||
|
task_issues = self.parse_task_list(content_text)
|
||||||
|
if task_issues:
|
||||||
|
self.issues.extend(task_issues)
|
||||||
|
else:
|
||||||
|
issue = self.extract_issue_from_heading(
|
||||||
|
current_heading,
|
||||||
|
current_level,
|
||||||
|
content_text,
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
self.issues.append(issue)
|
||||||
|
|
||||||
|
def to_jsonl(self) -> str:
|
||||||
|
"""Convert issues to JSONL format."""
|
||||||
|
lines = []
|
||||||
|
for issue in self.issues:
|
||||||
|
lines.append(json.dumps(issue, ensure_ascii=False))
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python md2jsonl.py <markdown-file>", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("Examples:", file=sys.stderr)
|
||||||
|
print(" python md2jsonl.py feature.md | bd import", file=sys.stderr)
|
||||||
|
print(" python md2jsonl.py feature.md > issues.jsonl", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
markdown_file = Path(sys.argv[1])
|
||||||
|
|
||||||
|
if not markdown_file.exists():
|
||||||
|
print(f"Error: File not found: {markdown_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Read markdown
|
||||||
|
content = markdown_file.read_text()
|
||||||
|
|
||||||
|
# Convert to issues
|
||||||
|
converter = MarkdownToIssues(prefix="bd")
|
||||||
|
converter.parse_markdown(content)
|
||||||
|
|
||||||
|
# Output JSONL
|
||||||
|
print(converter.to_jsonl())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user