@leeovery/claude-technical-workflows 2.0.42 → 2.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/link-dependencies.md +10 -10
- package/commands/workflow/start-discussion.md +162 -120
- package/commands/workflow/start-implementation.md +147 -66
- package/commands/workflow/start-planning.md +122 -43
- package/commands/workflow/start-research.md +90 -13
- package/commands/workflow/start-review.md +86 -41
- package/commands/workflow/start-specification.md +18 -11
- package/package.json +4 -2
- package/scripts/discovery-for-discussion.sh +198 -0
- package/scripts/discovery-for-implementation-and-review.sh +131 -0
- package/scripts/discovery-for-planning.sh +186 -0
- package/scripts/{specification-discovery.sh → discovery-for-specification.sh} +7 -2
- package/scripts/migrations/002-specification-frontmatter.sh +207 -0
- package/scripts/migrations/003-planning-frontmatter.sh +191 -0
- package/skills/technical-planning/references/output-backlog-md.md +1 -1
- package/skills/technical-planning/references/output-beads.md +3 -3
- package/skills/technical-planning/references/output-linear.md +3 -3
- package/skills/technical-planning/references/output-local-markdown.md +17 -5
- package/skills/technical-research/SKILL.md +10 -1
- package/skills/technical-research/references/template.md +39 -0
- package/skills/technical-specification/references/specification-guide.md +20 -11
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Discovers the current state of specifications and plans
|
|
4
|
+
# for the /start-planning command.
|
|
5
|
+
#
|
|
6
|
+
# Outputs structured YAML that the command can consume directly.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
set -eo pipefail
|
|
10
|
+
|
|
11
|
+
SPEC_DIR="docs/workflow/specification"
|
|
12
|
+
PLAN_DIR="docs/workflow/planning"
|
|
13
|
+
|
|
14
|
+
# Helper: Extract a frontmatter field value from a file
|
|
15
|
+
# Usage: extract_field <file> <field_name>
|
|
16
|
+
extract_field() {
|
|
17
|
+
local file="$1"
|
|
18
|
+
local field="$2"
|
|
19
|
+
local value=""
|
|
20
|
+
|
|
21
|
+
# Extract from YAML frontmatter (file must start with ---)
|
|
22
|
+
if head -1 "$file" 2>/dev/null | grep -q "^---$"; then
|
|
23
|
+
value=$(sed -n '2,/^---$/p' "$file" 2>/dev/null | \
|
|
24
|
+
grep -i -m1 "^${field}:" | \
|
|
25
|
+
sed -E "s/^${field}:[[:space:]]*//i" || true)
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "$value"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Start YAML output
|
|
32
|
+
echo "# Planning Command State Discovery"
|
|
33
|
+
echo "# Generated: $(date -Iseconds)"
|
|
34
|
+
echo ""
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# SPECIFICATIONS
|
|
38
|
+
#
|
|
39
|
+
echo "specifications:"
|
|
40
|
+
|
|
41
|
+
feature_count=0
|
|
42
|
+
feature_ready_count=0
|
|
43
|
+
crosscutting_count=0
|
|
44
|
+
|
|
45
|
+
if [ -d "$SPEC_DIR" ] && [ -n "$(ls -A "$SPEC_DIR" 2>/dev/null)" ]; then
|
|
46
|
+
echo " exists: true"
|
|
47
|
+
echo " feature:"
|
|
48
|
+
|
|
49
|
+
# First pass: feature specifications
|
|
50
|
+
for file in "$SPEC_DIR"/*.md; do
|
|
51
|
+
[ -f "$file" ] || continue
|
|
52
|
+
|
|
53
|
+
name=$(basename "$file" .md)
|
|
54
|
+
status=$(extract_field "$file" "status")
|
|
55
|
+
status=${status:-"active"}
|
|
56
|
+
spec_type=$(extract_field "$file" "type")
|
|
57
|
+
spec_type=${spec_type:-"feature"}
|
|
58
|
+
|
|
59
|
+
# Skip cross-cutting specs in this pass
|
|
60
|
+
[ "$spec_type" = "cross-cutting" ] && continue
|
|
61
|
+
|
|
62
|
+
# Check if plan exists
|
|
63
|
+
has_plan="false"
|
|
64
|
+
if [ -f "$PLAN_DIR/${name}.md" ]; then
|
|
65
|
+
has_plan="true"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
echo " - name: \"$name\""
|
|
69
|
+
echo " status: \"$status\""
|
|
70
|
+
echo " has_plan: $has_plan"
|
|
71
|
+
|
|
72
|
+
feature_count=$((feature_count + 1))
|
|
73
|
+
# "concluded" specs without plans are ready for planning
|
|
74
|
+
if [ "$status" = "concluded" ] && [ "$has_plan" = "false" ]; then
|
|
75
|
+
feature_ready_count=$((feature_ready_count + 1))
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ "$feature_count" -eq 0 ]; then
|
|
80
|
+
echo " [] # No feature specifications"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo " crosscutting:"
|
|
84
|
+
|
|
85
|
+
# Second pass: cross-cutting specifications
|
|
86
|
+
for file in "$SPEC_DIR"/*.md; do
|
|
87
|
+
[ -f "$file" ] || continue
|
|
88
|
+
|
|
89
|
+
name=$(basename "$file" .md)
|
|
90
|
+
spec_type=$(extract_field "$file" "type")
|
|
91
|
+
spec_type=${spec_type:-"feature"}
|
|
92
|
+
|
|
93
|
+
# Only cross-cutting specs in this pass
|
|
94
|
+
[ "$spec_type" != "cross-cutting" ] && continue
|
|
95
|
+
|
|
96
|
+
status=$(extract_field "$file" "status")
|
|
97
|
+
status=${status:-"active"}
|
|
98
|
+
|
|
99
|
+
echo " - name: \"$name\""
|
|
100
|
+
echo " status: \"$status\""
|
|
101
|
+
|
|
102
|
+
crosscutting_count=$((crosscutting_count + 1))
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
if [ "$crosscutting_count" -eq 0 ]; then
|
|
106
|
+
echo " [] # No cross-cutting specifications"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
echo " counts:"
|
|
110
|
+
echo " feature: $feature_count"
|
|
111
|
+
echo " feature_ready: $feature_ready_count"
|
|
112
|
+
echo " crosscutting: $crosscutting_count"
|
|
113
|
+
else
|
|
114
|
+
echo " exists: false"
|
|
115
|
+
echo " feature: []"
|
|
116
|
+
echo " crosscutting: []"
|
|
117
|
+
echo " counts:"
|
|
118
|
+
echo " feature: 0"
|
|
119
|
+
echo " feature_ready: 0"
|
|
120
|
+
echo " crosscutting: 0"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
echo ""
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# PLANS
|
|
127
|
+
#
|
|
128
|
+
echo "plans:"
|
|
129
|
+
|
|
130
|
+
if [ -d "$PLAN_DIR" ] && [ -n "$(ls -A "$PLAN_DIR" 2>/dev/null)" ]; then
|
|
131
|
+
echo " exists: true"
|
|
132
|
+
echo " files:"
|
|
133
|
+
|
|
134
|
+
for file in "$PLAN_DIR"/*.md; do
|
|
135
|
+
[ -f "$file" ] || continue
|
|
136
|
+
|
|
137
|
+
name=$(basename "$file" .md)
|
|
138
|
+
format=$(extract_field "$file" "format")
|
|
139
|
+
format=${format:-"MISSING"}
|
|
140
|
+
status=$(extract_field "$file" "status")
|
|
141
|
+
status=${status:-"unknown"}
|
|
142
|
+
plan_id=$(extract_field "$file" "plan_id")
|
|
143
|
+
|
|
144
|
+
echo " - name: \"$name\""
|
|
145
|
+
echo " format: \"$format\""
|
|
146
|
+
echo " status: \"$status\""
|
|
147
|
+
if [ -n "$plan_id" ]; then
|
|
148
|
+
echo " plan_id: \"$plan_id\""
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
else
|
|
152
|
+
echo " exists: false"
|
|
153
|
+
echo " files: []"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
echo ""
|
|
157
|
+
|
|
158
|
+
#
|
|
159
|
+
# WORKFLOW STATE SUMMARY
|
|
160
|
+
#
|
|
161
|
+
echo "state:"
|
|
162
|
+
|
|
163
|
+
specs_exist="false"
|
|
164
|
+
plans_exist="false"
|
|
165
|
+
|
|
166
|
+
if [ -d "$SPEC_DIR" ] && [ -n "$(ls -A "$SPEC_DIR" 2>/dev/null)" ]; then
|
|
167
|
+
specs_exist="true"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
if [ -d "$PLAN_DIR" ] && [ -n "$(ls -A "$PLAN_DIR" 2>/dev/null)" ]; then
|
|
171
|
+
plans_exist="true"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
echo " has_specifications: $specs_exist"
|
|
175
|
+
echo " has_plans: $plans_exist"
|
|
176
|
+
|
|
177
|
+
# Determine workflow state for routing
|
|
178
|
+
if [ "$specs_exist" = "false" ]; then
|
|
179
|
+
echo " scenario: \"no_specs\""
|
|
180
|
+
elif [ "$feature_ready_count" -eq 0 ]; then
|
|
181
|
+
echo " scenario: \"no_ready_specs\""
|
|
182
|
+
elif [ "$feature_ready_count" -eq 1 ]; then
|
|
183
|
+
echo " scenario: \"single_ready_spec\""
|
|
184
|
+
else
|
|
185
|
+
echo " scenario: \"multiple_ready_specs\""
|
|
186
|
+
fi
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
# discover-spec-state.sh
|
|
4
|
-
#
|
|
5
3
|
# Discovers the current state of discussions, specifications, and cache
|
|
6
4
|
# for the /start-specification command.
|
|
7
5
|
#
|
|
@@ -68,13 +66,20 @@ if [ -d "$DISCUSSION_DIR" ] && [ -n "$(ls -A "$DISCUSSION_DIR" 2>/dev/null)" ];
|
|
|
68
66
|
|
|
69
67
|
# Check if this discussion has a corresponding individual spec
|
|
70
68
|
has_individual_spec="false"
|
|
69
|
+
spec_status=""
|
|
71
70
|
if [ -f "$SPEC_DIR/${name}.md" ]; then
|
|
72
71
|
has_individual_spec="true"
|
|
72
|
+
# Extract spec status in real-time (not from cache)
|
|
73
|
+
spec_status=$(extract_field "$SPEC_DIR/${name}.md" "status")
|
|
74
|
+
spec_status=${spec_status:-"in-progress"}
|
|
73
75
|
fi
|
|
74
76
|
|
|
75
77
|
echo " - name: \"$name\""
|
|
76
78
|
echo " status: \"$status\""
|
|
77
79
|
echo " has_individual_spec: $has_individual_spec"
|
|
80
|
+
if [ "$has_individual_spec" = "true" ]; then
|
|
81
|
+
echo " spec_status: \"$spec_status\""
|
|
82
|
+
fi
|
|
78
83
|
done
|
|
79
84
|
else
|
|
80
85
|
echo " [] # No discussions found"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# 002-specification-frontmatter.sh
|
|
4
|
+
#
|
|
5
|
+
# Migrates specification documents from legacy markdown header format to YAML frontmatter.
|
|
6
|
+
#
|
|
7
|
+
# Legacy format:
|
|
8
|
+
# # Specification: {Topic}
|
|
9
|
+
#
|
|
10
|
+
# **Status**: Building specification | Complete
|
|
11
|
+
# **Type**: feature | cross-cutting
|
|
12
|
+
# **Last Updated**: YYYY-MM-DD
|
|
13
|
+
#
|
|
14
|
+
# New format:
|
|
15
|
+
# ---
|
|
16
|
+
# topic: {topic-name}
|
|
17
|
+
# status: in-progress | concluded
|
|
18
|
+
# type: feature | cross-cutting | (empty if unknown)
|
|
19
|
+
# date: YYYY-MM-DD
|
|
20
|
+
# sources: # Optional - only if Sources field exists
|
|
21
|
+
# - discussion-one
|
|
22
|
+
# - discussion-two
|
|
23
|
+
# ---
|
|
24
|
+
#
|
|
25
|
+
# # Specification: {Topic}
|
|
26
|
+
#
|
|
27
|
+
# Status mapping (normalized across all document types):
|
|
28
|
+
# Building specification, Building, Draft → in-progress
|
|
29
|
+
# Complete, Completed, Done → concluded
|
|
30
|
+
#
|
|
31
|
+
# Type handling:
|
|
32
|
+
# feature → feature
|
|
33
|
+
# cross-cutting → cross-cutting
|
|
34
|
+
# (not found or unrecognized) → empty (requires manual review)
|
|
35
|
+
#
|
|
36
|
+
# This script is sourced by migrate.sh and has access to:
|
|
37
|
+
# - is_migrated "filepath" "migration_id"
|
|
38
|
+
# - record_migration "filepath" "migration_id"
|
|
39
|
+
# - report_update "filepath" "description"
|
|
40
|
+
# - report_skip "filepath"
|
|
41
|
+
#
|
|
42
|
+
|
|
43
|
+
MIGRATION_ID="002"
|
|
44
|
+
SPEC_DIR="docs/workflow/specification"
|
|
45
|
+
|
|
46
|
+
# Skip if no specification directory
|
|
47
|
+
if [ ! -d "$SPEC_DIR" ]; then
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Process each specification file
|
|
52
|
+
for file in "$SPEC_DIR"/*.md; do
|
|
53
|
+
[ -f "$file" ] || continue
|
|
54
|
+
|
|
55
|
+
# Check if already migrated via tracking
|
|
56
|
+
if is_migrated "$file" "$MIGRATION_ID"; then
|
|
57
|
+
report_skip "$file"
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Check if file already has YAML frontmatter
|
|
62
|
+
if head -1 "$file" 2>/dev/null | grep -q "^---$"; then
|
|
63
|
+
# Already has frontmatter - just record and skip
|
|
64
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
65
|
+
report_skip "$file"
|
|
66
|
+
continue
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Check if file has legacy format (look for **Status**: or **Status:** or **Type**: or **Last Updated**:)
|
|
70
|
+
if ! grep -q '^\*\*Status\*\*:\|^\*\*Status:\*\*\|^\*\*Type\*\*:\|^\*\*Last Updated\*\*:' "$file" 2>/dev/null; then
|
|
71
|
+
# No legacy format found - might be malformed, skip
|
|
72
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
73
|
+
report_skip "$file"
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# Extract values from legacy format
|
|
79
|
+
#
|
|
80
|
+
|
|
81
|
+
# Use filename as topic (canonical identifier throughout the workflow)
|
|
82
|
+
topic_kebab=$(basename "$file" .md)
|
|
83
|
+
|
|
84
|
+
# Extract status from **Status**: Value or **Status:** Value
|
|
85
|
+
# Handle variations: "Building specification", "Building", "Complete", "Completed", etc.
|
|
86
|
+
status_raw=$(grep -m1 '^\*\*Status\*\*:\|^\*\*Status:\*\*' "$file" | \
|
|
87
|
+
sed 's/^\*\*Status\*\*:[[:space:]]*//' | \
|
|
88
|
+
sed 's/^\*\*Status:\*\*[[:space:]]*//' | \
|
|
89
|
+
tr '[:upper:]' '[:lower:]' | \
|
|
90
|
+
xargs)
|
|
91
|
+
|
|
92
|
+
# Map legacy status to normalized values (consistent across all document types)
|
|
93
|
+
case "$status_raw" in
|
|
94
|
+
"building specification"|"building"|"draft"|"in progress"|"in-progress"|"exploring"|"deciding")
|
|
95
|
+
status_new="in-progress"
|
|
96
|
+
;;
|
|
97
|
+
"complete"|"completed"|"done"|"finished"|"concluded")
|
|
98
|
+
status_new="concluded"
|
|
99
|
+
;;
|
|
100
|
+
*)
|
|
101
|
+
status_new="in-progress" # Default for unknown
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
104
|
+
|
|
105
|
+
# Extract type from **Type**: Value (may not exist in legacy files)
|
|
106
|
+
type_raw=$(grep -m1 '^\*\*Type\*\*:\|^\*\*Type:\*\*' "$file" 2>/dev/null | \
|
|
107
|
+
sed 's/^\*\*Type\*\*:[[:space:]]*//' | \
|
|
108
|
+
sed 's/^\*\*Type:\*\*[[:space:]]*//' | \
|
|
109
|
+
tr '[:upper:]' '[:lower:]' | \
|
|
110
|
+
xargs || echo "")
|
|
111
|
+
|
|
112
|
+
# Normalize type (leave empty if not found or unrecognized - requires manual review)
|
|
113
|
+
case "$type_raw" in
|
|
114
|
+
"feature")
|
|
115
|
+
type_new="feature"
|
|
116
|
+
;;
|
|
117
|
+
"cross-cutting"|"crosscutting"|"cross cutting")
|
|
118
|
+
type_new="cross-cutting"
|
|
119
|
+
;;
|
|
120
|
+
*)
|
|
121
|
+
type_new="" # Empty - requires manual classification
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
# Extract date from **Last Updated**: YYYY-MM-DD or **Date**: YYYY-MM-DD
|
|
126
|
+
date_value=$(grep -m1 '^\*\*Last Updated\*\*:\|^\*\*Date\*\*:' "$file" | \
|
|
127
|
+
grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo "")
|
|
128
|
+
|
|
129
|
+
# Use today's date if none found
|
|
130
|
+
if [ -z "$date_value" ]; then
|
|
131
|
+
date_value=$(date +%Y-%m-%d)
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Extract sources from **Sources**: source1, source2, ... (comma-separated list of discussion names)
|
|
135
|
+
# These link the specification back to the discussions that informed it
|
|
136
|
+
sources_raw=$(grep -m1 '^\*\*Sources\*\*:' "$file" 2>/dev/null | \
|
|
137
|
+
sed 's/^\*\*Sources\*\*:[[:space:]]*//' | \
|
|
138
|
+
xargs || echo "")
|
|
139
|
+
|
|
140
|
+
# Convert comma-separated list to array
|
|
141
|
+
sources_array=()
|
|
142
|
+
if [ -n "$sources_raw" ]; then
|
|
143
|
+
IFS=',' read -ra sources_parts <<< "$sources_raw"
|
|
144
|
+
for src in "${sources_parts[@]}"; do
|
|
145
|
+
# Trim whitespace
|
|
146
|
+
src=$(echo "$src" | xargs)
|
|
147
|
+
if [ -n "$src" ]; then
|
|
148
|
+
sources_array+=("$src")
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
#
|
|
154
|
+
# Build new file content
|
|
155
|
+
#
|
|
156
|
+
|
|
157
|
+
# Create frontmatter (conditionally include sources if present)
|
|
158
|
+
if [ ${#sources_array[@]} -gt 0 ]; then
|
|
159
|
+
sources_yaml=""
|
|
160
|
+
for src in "${sources_array[@]}"; do
|
|
161
|
+
sources_yaml="${sources_yaml}
|
|
162
|
+
- $src"
|
|
163
|
+
done
|
|
164
|
+
frontmatter="---
|
|
165
|
+
topic: $topic_kebab
|
|
166
|
+
status: $status_new
|
|
167
|
+
type: $type_new
|
|
168
|
+
date: $date_value
|
|
169
|
+
sources:$sources_yaml
|
|
170
|
+
---"
|
|
171
|
+
else
|
|
172
|
+
frontmatter="---
|
|
173
|
+
topic: $topic_kebab
|
|
174
|
+
status: $status_new
|
|
175
|
+
type: $type_new
|
|
176
|
+
date: $date_value
|
|
177
|
+
---"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Extract H1 heading (preserve original)
|
|
181
|
+
h1_heading=$(grep -m1 "^# " "$file")
|
|
182
|
+
|
|
183
|
+
# Find line number of first ## heading (start of real content after metadata)
|
|
184
|
+
first_section_line=$(grep -n "^## " "$file" | head -1 | cut -d: -f1)
|
|
185
|
+
|
|
186
|
+
# Get content from first ## onwards (preserves all content)
|
|
187
|
+
if [ -n "$first_section_line" ]; then
|
|
188
|
+
content=$(tail -n +$first_section_line "$file")
|
|
189
|
+
else
|
|
190
|
+
# No ## found - take everything after the metadata block
|
|
191
|
+
# Find first blank line after the metadata, then take from there
|
|
192
|
+
content=""
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Write new content: frontmatter + H1 + blank line + content
|
|
196
|
+
{
|
|
197
|
+
echo "$frontmatter"
|
|
198
|
+
echo ""
|
|
199
|
+
echo "$h1_heading"
|
|
200
|
+
echo ""
|
|
201
|
+
echo "$content"
|
|
202
|
+
} > "$file"
|
|
203
|
+
|
|
204
|
+
# Record and report
|
|
205
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
206
|
+
report_update "$file" "added frontmatter (status: $status_new, type: $type_new)"
|
|
207
|
+
done
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# 003-planning-frontmatter.sh
|
|
4
|
+
#
|
|
5
|
+
# Migrates plan documents from legacy format to full YAML frontmatter.
|
|
6
|
+
#
|
|
7
|
+
# Legacy format (partial frontmatter + inline metadata):
|
|
8
|
+
# ---
|
|
9
|
+
# format: {format}
|
|
10
|
+
# ---
|
|
11
|
+
#
|
|
12
|
+
# # Implementation Plan: {Feature/Project Name}
|
|
13
|
+
#
|
|
14
|
+
# **Date**: YYYY-MM-DD
|
|
15
|
+
# **Status**: Draft | Ready | In Progress | Completed
|
|
16
|
+
# **Specification**: `docs/workflow/specification/{topic}.md`
|
|
17
|
+
#
|
|
18
|
+
# New format (all metadata in frontmatter):
|
|
19
|
+
# ---
|
|
20
|
+
# topic: {topic-name}
|
|
21
|
+
# status: in-progress | concluded
|
|
22
|
+
# date: YYYY-MM-DD
|
|
23
|
+
# format: {format} # Required - no default, MISSING if not present
|
|
24
|
+
# specification: {topic}.md
|
|
25
|
+
# plan_id: {id} # Optional - migrated from 'epic' or 'project' fields
|
|
26
|
+
# ---
|
|
27
|
+
#
|
|
28
|
+
# # Implementation Plan: {Feature/Project Name}
|
|
29
|
+
#
|
|
30
|
+
# Status mapping (normalized across all document types):
|
|
31
|
+
# Draft, Ready, In Progress → in-progress
|
|
32
|
+
# Completed, Done → concluded
|
|
33
|
+
#
|
|
34
|
+
# This script is sourced by migrate.sh and has access to:
|
|
35
|
+
# - is_migrated "filepath" "migration_id"
|
|
36
|
+
# - record_migration "filepath" "migration_id"
|
|
37
|
+
# - report_update "filepath" "description"
|
|
38
|
+
# - report_skip "filepath"
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
MIGRATION_ID="003"
|
|
42
|
+
PLAN_DIR="docs/workflow/planning"
|
|
43
|
+
|
|
44
|
+
# Skip if no planning directory
|
|
45
|
+
if [ ! -d "$PLAN_DIR" ]; then
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Process each plan file
|
|
50
|
+
for file in "$PLAN_DIR"/*.md; do
|
|
51
|
+
[ -f "$file" ] || continue
|
|
52
|
+
|
|
53
|
+
# Check if already migrated via tracking
|
|
54
|
+
if is_migrated "$file" "$MIGRATION_ID"; then
|
|
55
|
+
report_skip "$file"
|
|
56
|
+
continue
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Check if file already has full frontmatter (topic field present)
|
|
60
|
+
if head -10 "$file" 2>/dev/null | grep -q "^topic:"; then
|
|
61
|
+
# Already has full frontmatter - just record and skip
|
|
62
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
63
|
+
report_skip "$file"
|
|
64
|
+
continue
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Check if file has legacy format indicators
|
|
68
|
+
# Legacy format has partial frontmatter (format:) OR inline **Date**/**Status**/**Specification**
|
|
69
|
+
has_partial_frontmatter=$(head -5 "$file" 2>/dev/null | grep -c "^format:" || true)
|
|
70
|
+
has_inline_metadata=$(grep -c '^\*\*Date\*\*:\|^\*\*Status\*\*:\|^\*\*Specification\*\*:' "$file" 2>/dev/null || true)
|
|
71
|
+
|
|
72
|
+
if [ "${has_partial_frontmatter:-0}" = "0" ] && [ "${has_inline_metadata:-0}" = "0" ]; then
|
|
73
|
+
# No legacy format found - might be malformed, skip
|
|
74
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
75
|
+
report_skip "$file"
|
|
76
|
+
continue
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# Extract values from legacy format
|
|
81
|
+
#
|
|
82
|
+
|
|
83
|
+
# Use filename as topic (canonical identifier throughout the workflow)
|
|
84
|
+
topic_kebab=$(basename "$file" .md)
|
|
85
|
+
|
|
86
|
+
# Extract format from existing frontmatter (if present)
|
|
87
|
+
format_value=$(sed -n '/^---$/,/^---$/p' "$file" 2>/dev/null | grep "^format:" | sed 's/^format:[[:space:]]*//' | xargs || echo "")
|
|
88
|
+
if [ -z "$format_value" ]; then
|
|
89
|
+
format_value="MISSING" # No default - missing format is an error
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Extract plan_id from existing frontmatter - could be 'epic' (beads) or 'project' (linear/backlog)
|
|
93
|
+
# These are migrated to a unified 'plan_id' field
|
|
94
|
+
plan_id_value=$(sed -n '/^---$/,/^---$/p' "$file" 2>/dev/null | grep "^epic:" | sed 's/^epic:[[:space:]]*//' | xargs || echo "")
|
|
95
|
+
if [ -z "$plan_id_value" ]; then
|
|
96
|
+
plan_id_value=$(sed -n '/^---$/,/^---$/p' "$file" 2>/dev/null | grep "^project:" | sed 's/^project:[[:space:]]*//' | xargs || echo "")
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Extract status from **Status**: Value
|
|
100
|
+
status_raw=$(grep -m1 '^\*\*Status\*\*:' "$file" 2>/dev/null | \
|
|
101
|
+
sed 's/^\*\*Status\*\*:[[:space:]]*//' | \
|
|
102
|
+
tr '[:upper:]' '[:lower:]' | \
|
|
103
|
+
xargs || echo "")
|
|
104
|
+
|
|
105
|
+
# Map legacy status to normalized values
|
|
106
|
+
case "$status_raw" in
|
|
107
|
+
"draft"|"ready"|"in progress"|"in-progress")
|
|
108
|
+
status_new="in-progress"
|
|
109
|
+
;;
|
|
110
|
+
"completed"|"complete"|"done"|"concluded")
|
|
111
|
+
status_new="concluded"
|
|
112
|
+
;;
|
|
113
|
+
*)
|
|
114
|
+
status_new="in-progress" # Default for unknown
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
118
|
+
# Extract date from **Date**: YYYY-MM-DD or **Created**: YYYY-MM-DD
|
|
119
|
+
date_value=$(grep -m1 '^\*\*Date\*\*:\|^\*\*Created\*\*:' "$file" 2>/dev/null | \
|
|
120
|
+
grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo "")
|
|
121
|
+
|
|
122
|
+
# Use today's date if none found
|
|
123
|
+
if [ -z "$date_value" ]; then
|
|
124
|
+
date_value=$(date +%Y-%m-%d)
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Extract specification from **Specification**: `docs/workflow/specification/{topic}.md`
|
|
128
|
+
# We just want the filename, not the full path
|
|
129
|
+
spec_raw=$(grep -m1 '^\*\*Specification\*\*:' "$file" 2>/dev/null | \
|
|
130
|
+
sed 's/^\*\*Specification\*\*:[[:space:]]*//' | \
|
|
131
|
+
sed 's/`//g' | \
|
|
132
|
+
xargs || echo "")
|
|
133
|
+
|
|
134
|
+
# Extract just the filename from the path
|
|
135
|
+
if [ -n "$spec_raw" ]; then
|
|
136
|
+
spec_value=$(basename "$spec_raw")
|
|
137
|
+
else
|
|
138
|
+
spec_value="${topic_kebab}.md" # Default to topic name
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
#
|
|
142
|
+
# Build new file content
|
|
143
|
+
#
|
|
144
|
+
|
|
145
|
+
# Create frontmatter (conditionally include plan_id if present)
|
|
146
|
+
if [ -n "$plan_id_value" ]; then
|
|
147
|
+
frontmatter="---
|
|
148
|
+
topic: $topic_kebab
|
|
149
|
+
status: $status_new
|
|
150
|
+
date: $date_value
|
|
151
|
+
format: $format_value
|
|
152
|
+
specification: $spec_value
|
|
153
|
+
plan_id: $plan_id_value
|
|
154
|
+
---"
|
|
155
|
+
else
|
|
156
|
+
frontmatter="---
|
|
157
|
+
topic: $topic_kebab
|
|
158
|
+
status: $status_new
|
|
159
|
+
date: $date_value
|
|
160
|
+
format: $format_value
|
|
161
|
+
specification: $spec_value
|
|
162
|
+
---"
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Extract H1 heading (preserve original)
|
|
166
|
+
h1_heading=$(grep -m1 "^# " "$file")
|
|
167
|
+
|
|
168
|
+
# Find line number of first ## heading (start of real content after metadata)
|
|
169
|
+
first_section_line=$(grep -n "^## " "$file" | head -1 | cut -d: -f1)
|
|
170
|
+
|
|
171
|
+
# Get content from first ## onwards (preserves all content)
|
|
172
|
+
if [ -n "$first_section_line" ]; then
|
|
173
|
+
content=$(tail -n +$first_section_line "$file")
|
|
174
|
+
else
|
|
175
|
+
# No ## found - might be empty or malformed
|
|
176
|
+
content=""
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Write new content: frontmatter + H1 + blank line + content
|
|
180
|
+
{
|
|
181
|
+
echo "$frontmatter"
|
|
182
|
+
echo ""
|
|
183
|
+
echo "$h1_heading"
|
|
184
|
+
echo ""
|
|
185
|
+
echo "$content"
|
|
186
|
+
} > "$file"
|
|
187
|
+
|
|
188
|
+
# Record and report
|
|
189
|
+
record_migration "$file" "$MIGRATION_ID"
|
|
190
|
+
report_update "$file" "added full frontmatter (status: $status_new, format: $format_value)"
|
|
191
|
+
done
|
|
@@ -240,7 +240,7 @@ Create `docs/workflow/planning/{topic}.md`:
|
|
|
240
240
|
```markdown
|
|
241
241
|
---
|
|
242
242
|
format: beads
|
|
243
|
-
|
|
243
|
+
plan_id: bd-{EPIC_ID}
|
|
244
244
|
---
|
|
245
245
|
|
|
246
246
|
# Plan Reference: {Topic Name}
|
|
@@ -304,7 +304,7 @@ The `format: beads` frontmatter tells implementation to use beads CLI:
|
|
|
304
304
|
```yaml
|
|
305
305
|
---
|
|
306
306
|
format: beads
|
|
307
|
-
|
|
307
|
+
plan_id: bd-a3f8
|
|
308
308
|
---
|
|
309
309
|
```
|
|
310
310
|
|
|
@@ -327,7 +327,7 @@ In the task body:
|
|
|
327
327
|
|
|
328
328
|
### Reading Plans
|
|
329
329
|
|
|
330
|
-
1. Extract `
|
|
330
|
+
1. Extract `plan_id` (beads epic ID) from frontmatter
|
|
331
331
|
2. Check `.beads/config.yaml` for `no-db` setting
|
|
332
332
|
3. Run `bd ready` to get unblocked tasks
|
|
333
333
|
4. View task details with `bd show bd-{id}`
|
|
@@ -148,7 +148,7 @@ Create `docs/workflow/planning/{topic}.md`:
|
|
|
148
148
|
```markdown
|
|
149
149
|
---
|
|
150
150
|
format: linear
|
|
151
|
-
|
|
151
|
+
plan_id: {PROJECT_NAME}
|
|
152
152
|
project_id: {ID from MCP response}
|
|
153
153
|
team: {TEAM_NAME}
|
|
154
154
|
---
|
|
@@ -207,7 +207,7 @@ The frontmatter contains all information needed to query Linear:
|
|
|
207
207
|
```yaml
|
|
208
208
|
---
|
|
209
209
|
format: linear
|
|
210
|
-
|
|
210
|
+
plan_id: USER-AUTH-FEATURE
|
|
211
211
|
project_id: abc123-def456
|
|
212
212
|
team: Engineering
|
|
213
213
|
---
|
|
@@ -257,7 +257,7 @@ Linear:
|
|
|
257
257
|
|
|
258
258
|
### Reading Plans
|
|
259
259
|
|
|
260
|
-
1. Extract `
|
|
260
|
+
1. Extract `plan_id` (Linear project name) from frontmatter
|
|
261
261
|
2. Query Linear MCP for project issues
|
|
262
262
|
3. Filter issues by phase label (e.g., `phase-1`, `phase-2`)
|
|
263
263
|
4. Process in phase order
|