@leeovery/claude-technical-workflows 2.0.43 → 2.0.45

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.
@@ -53,9 +53,9 @@ Compare the `format:` field across all discovered plans.
53
53
  ```
54
54
  Mixed output formats detected:
55
55
 
56
- - authentication: beads
57
- - billing-system: linear
58
- - notifications: beads
56
+ - authentication: {format-a}
57
+ - billing-system: {format-b}
58
+ - notifications: {format-a}
59
59
 
60
60
  Cross-topic dependencies can only be wired within the same output format.
61
61
  Please consolidate your plans to use a single output format before linking dependencies.
@@ -78,15 +78,15 @@ For each plan, find the External Dependencies section:
78
78
  ```
79
79
  Dependency Summary
80
80
 
81
- Plan: authentication (format: beads)
81
+ Plan: authentication (format: {format})
82
82
  - billing-system: Invoice generation (unresolved)
83
- - user-management: User profiles → beads-7x2k (resolved)
83
+ - user-management: User profiles → {task-id} (resolved)
84
84
 
85
- Plan: billing-system (format: beads)
85
+ Plan: billing-system (format: {format})
86
86
  - authentication: User context (unresolved)
87
87
  - ~~payment-gateway: Payment processing~~ → satisfied externally
88
88
 
89
- Plan: notifications (format: beads)
89
+ Plan: notifications (format: {format})
90
90
  - authentication: User lookup (unresolved)
91
91
  - billing-system: Invoice events (unresolved)
92
92
  ```
@@ -133,11 +133,11 @@ Present a summary:
133
133
  Dependency Linking Complete
134
134
 
135
135
  RESOLVED (newly linked):
136
- - authentication → billing-system: beads-b7c2.1.1 (Invoice generation)
137
- - notifications → authentication: beads-a3f8.1.2 (Session management)
136
+ - authentication → billing-system: {task-id} (Invoice generation)
137
+ - notifications → authentication: {task-id} (Session management)
138
138
 
139
139
  ALREADY RESOLVED (no action needed):
140
- - authentication → user-management: beads-9m3p
140
+ - authentication → user-management: {task-id}
141
141
 
142
142
  SATISFIED EXTERNALLY (no action needed):
143
143
  - billing-system → payment-gateway
@@ -56,7 +56,7 @@ This outputs structured YAML. Parse it to understand:
56
56
 
57
57
  **From `plans` section:**
58
58
  - `exists` - whether any plans exist
59
- - `files` - list of plans with: name, topic, status, date, format, specification, specification_exists
59
+ - `files` - list of plans with: name, topic, status, date, format, specification, specification_exists, plan_id (if present)
60
60
  - `count` - total number of plans
61
61
 
62
62
  **From `environment` section:**
@@ -105,9 +105,9 @@ Present all discovered plans to help the user make an informed choice.
105
105
  ```
106
106
  Available Plans:
107
107
 
108
- 1. {topic-1} (in-progress) - format: local-markdown
109
- 2. {topic-2} (concluded) - format: local-markdown
110
- 3. {topic-3} (in-progress) - format: beads
108
+ 1. {topic-1} (in-progress) - format: {format}
109
+ 2. {topic-2} (concluded) - format: {format}
110
+ 3. {topic-3} (in-progress) - format: {format}
111
111
 
112
112
  Which plan would you like to implement? (Enter a number or name)
113
113
  ```
@@ -259,6 +259,7 @@ Invoke the [technical-implementation](../../skills/technical-implementation/SKIL
259
259
  Implementation session for: {topic}
260
260
  Plan: docs/workflow/planning/{topic}.md
261
261
  Format: {format}
262
+ Plan ID: {plan_id} (if applicable)
262
263
  Specification: {specification} (exists: {true|false})
263
264
  Scope: {all phases | Phase N | Task N.M | next-available}
264
265
 
@@ -64,7 +64,7 @@ This outputs structured YAML. Parse it to understand:
64
64
 
65
65
  **From `plans` section:**
66
66
  - `exists` - whether any plans exist
67
- - `files` - each plan's name, format, and status
67
+ - `files` - each plan's name, format, status, and plan_id (if present)
68
68
 
69
69
  **From `state` section:**
70
70
  - `scenario` - one of: `"no_specs"`, `"no_ready_specs"`, `"single_ready_spec"`, `"multiple_ready_specs"`
@@ -122,7 +122,7 @@ Cross-cutting specifications (reference context):
122
122
  - {rate-limiting} (concluded)
123
123
 
124
124
  Existing plans:
125
- - {topic-3}.md (in-progress, local-markdown)
125
+ - {topic-3}.md (in-progress, {format})
126
126
  ```
127
127
 
128
128
  **Legend:**
@@ -56,7 +56,7 @@ This outputs structured YAML. Parse it to understand:
56
56
 
57
57
  **From `plans` section:**
58
58
  - `exists` - whether any plans exist
59
- - `files` - list of plans with: name, topic, status, date, format, specification, specification_exists
59
+ - `files` - list of plans with: name, topic, status, date, format, specification, specification_exists, plan_id (if present)
60
60
  - `count` - total number of plans
61
61
 
62
62
  **From `state` section:**
@@ -154,6 +154,7 @@ Invoke the [technical-review](../../skills/technical-review/SKILL.md) skill for
154
154
  Review session for: {topic}
155
155
  Plan: docs/workflow/planning/{topic}.md
156
156
  Format: {format}
157
+ Plan ID: {plan_id} (if applicable)
157
158
  Specification: {specification} (exists: {true|false})
158
159
  Scope: {all changes | specific paths | from git status}
159
160
 
@@ -388,8 +388,41 @@ Present the groupings with FULL status information.
388
388
 
389
389
  For each grouping, show:
390
390
  - The grouping name
391
- - Whether a specification already exists for this grouping
392
- - Each discussion in the grouping and whether it has an individual spec
391
+ - Whether a specification already exists for this grouping (and its effective status)
392
+ - Each discussion in the grouping and its incorporation status
393
+
394
+ ### Determining Discussion Status Within a Grouping
395
+
396
+ For each grouping, first check if a grouped specification exists:
397
+ 1. Convert the grouping name to kebab-case (lowercase, spaces to hyphens)
398
+ 2. Check if `docs/workflow/specification/{kebab-name}.md` exists
399
+ 3. If it exists, get its `sources` array from the discovery output
400
+
401
+ The sources array uses object format with explicit status tracking:
402
+ ```yaml
403
+ sources:
404
+ - name: topic-a
405
+ status: incorporated
406
+ - name: topic-b
407
+ status: pending
408
+ ```
409
+
410
+ **If a grouped spec exists for the grouping:**
411
+
412
+ For each discussion in the grouping:
413
+ 1. Look up the discussion in the spec's `sources` array (by `name` field)
414
+ 2. If found → use the source's `status` field (`incorporated` or `pending`)
415
+ 3. If NOT found → status is `"pending"` (new source not yet added to spec)
416
+
417
+ Calculate the **effective spec status**:
418
+ - If ALL discussions in the grouping have `status: incorporated` → use the spec's actual status from file
419
+ - If ANY discussion has `status: pending` OR is not in sources → effective status is `"needs update"`
420
+
421
+ **If NO grouped spec exists:**
422
+
423
+ For each discussion in the grouping:
424
+ - If the discussion has an individual spec (`has_individual_spec: true`) → status is `"spec: {spec_status}"`
425
+ - If the discussion has no spec → status is `"ready"`
393
426
 
394
427
  **Format:**
395
428
 
@@ -398,19 +431,20 @@ For each grouping, show:
398
431
 
399
432
  Recommended Groupings:
400
433
 
401
- ### 1. {Grouping Name} {if spec exists: "(spec: {spec_status})"}
434
+ ### 1. {Grouping Name} (spec: {effective_status})
402
435
  | Discussion | Status |
403
436
  |------------|--------|
404
- | {topic-a} | discussion only |
405
- | {topic-b} | spec: {spec_status} |
406
- | {topic-c} | discussion only |
437
+ | {topic-a} | incorporated |
438
+ | {topic-b} | incorporated |
439
+ | {topic-c} | pending |
407
440
 
408
441
  Coupling: {explanation}
409
442
 
410
443
  ### 2. {Another Grouping}
411
444
  | Discussion | Status |
412
445
  |------------|--------|
413
- | {topic-d} | discussion only |
446
+ | {topic-d} | ready |
447
+ | {topic-e} | spec: in-progress |
414
448
 
415
449
  Coupling: {explanation}
416
450
 
@@ -429,6 +463,13 @@ How would you like to proceed?
429
463
  (Enter 'refresh' to re-analyze)
430
464
  ```
431
465
 
466
+ **Status Legend:**
467
+ - `incorporated` - Discussion content has been woven into the grouped specification
468
+ - `pending` - Discussion is part of the group but not yet incorporated into the spec
469
+ - `ready` - Discussion has no spec yet (ready to be specified)
470
+ - `spec: {status}` - Discussion has its own individual specification
471
+ - `needs update` - Grouped spec exists but has pending sources to incorporate
472
+
432
473
  **STOP.** Wait for user to choose.
433
474
 
434
475
  → Based on choice, proceed to **Step 8**.
@@ -636,7 +677,9 @@ Proceed? (y/n)
636
677
  ```
637
678
  {Creating / Continuing} specification: {topic}
638
679
 
639
- Source: docs/workflow/discussion/{topic}.md
680
+ Sources:
681
+ - docs/workflow/discussion/{topic}.md
682
+
640
683
  Output: docs/workflow/specification/{topic}.md
641
684
 
642
685
  Proceed? (y/n)
@@ -680,7 +723,9 @@ Invoke the [technical-specification](../../skills/technical-specification/SKILL.
680
723
  ```
681
724
  Specification session for: {topic}
682
725
 
683
- Source: docs/workflow/discussion/{topic}.md
726
+ Sources:
727
+ - docs/workflow/discussion/{topic}.md
728
+
684
729
  Output: docs/workflow/specification/{topic}.md
685
730
 
686
731
  Additional context: {summary of user's answers from Step 10}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeovery/claude-technical-workflows",
3
- "version": "2.0.43",
3
+ "version": "2.0.45",
4
4
  "description": "Technical workflow skills & commands for Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Lee Overy <me@leeovery.com>",
@@ -55,9 +55,10 @@ if [ -d "$PLAN_DIR" ] && [ -n "$(ls -A "$PLAN_DIR" 2>/dev/null)" ]; then
55
55
  date=$(extract_field "$file" "date")
56
56
  date=${date:-"unknown"}
57
57
  format=$(extract_field "$file" "format")
58
- format=${format:-"local-markdown"}
58
+ format=${format:-"MISSING"}
59
59
  specification=$(extract_field "$file" "specification")
60
60
  specification=${specification:-"${name}.md"}
61
+ plan_id=$(extract_field "$file" "plan_id")
61
62
 
62
63
  # Check if linked specification exists
63
64
  spec_exists="false"
@@ -73,6 +74,9 @@ if [ -d "$PLAN_DIR" ] && [ -n "$(ls -A "$PLAN_DIR" 2>/dev/null)" ]; then
73
74
  echo " format: \"$format\""
74
75
  echo " specification: \"$specification\""
75
76
  echo " specification_exists: $spec_exists"
77
+ if [ -n "$plan_id" ]; then
78
+ echo " plan_id: \"$plan_id\""
79
+ fi
76
80
 
77
81
  plan_count=$((plan_count + 1))
78
82
  done
@@ -136,13 +136,17 @@ if [ -d "$PLAN_DIR" ] && [ -n "$(ls -A "$PLAN_DIR" 2>/dev/null)" ]; then
136
136
 
137
137
  name=$(basename "$file" .md)
138
138
  format=$(extract_field "$file" "format")
139
- format=${format:-"local-markdown"}
139
+ format=${format:-"MISSING"}
140
140
  status=$(extract_field "$file" "status")
141
141
  status=${status:-"unknown"}
142
+ plan_id=$(extract_field "$file" "plan_id")
142
143
 
143
144
  echo " - name: \"$name\""
144
145
  echo " format: \"$format\""
145
146
  echo " status: \"$status\""
147
+ if [ -n "$plan_id" ]; then
148
+ echo " plan_id: \"$plan_id\""
149
+ fi
146
150
  done
147
151
  else
148
152
  echo " exists: false"
@@ -35,9 +35,8 @@ extract_array_field() {
35
35
  local file="$1"
36
36
  local field="$2"
37
37
  local result
38
- # Look for field followed by array items (- item), excluding --- delimiters
39
- result=$(sed -n '/^---$/,/^---$/p' "$file" 2>/dev/null | \
40
- grep -v "^---$" | \
38
+ # Look for field followed by array items (- item), within frontmatter only
39
+ result=$(awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null | \
41
40
  sed -n "/^${field}:/,/^[a-z_]*:/p" | \
42
41
  grep "^[[:space:]]*-" | \
43
42
  sed 's/^[[:space:]]*-[[:space:]]*//' | \
@@ -46,6 +45,62 @@ extract_array_field() {
46
45
  echo "$result"
47
46
  }
48
47
 
48
+ # Helper: Extract sources with status from object format
49
+ # Outputs YAML-formatted source entries with name and status
50
+ # Usage: extract_sources_with_status <file>
51
+ #
52
+ # Note: This only handles the object format. Legacy simple array format
53
+ # is converted by migration 004 before discovery runs.
54
+ extract_sources_with_status() {
55
+ local file="$1"
56
+ local in_sources=false
57
+ local current_name=""
58
+ local current_status=""
59
+
60
+ # Read frontmatter and parse sources block
61
+ while IFS= read -r line; do
62
+ # Detect start of sources block
63
+ if [[ "$line" =~ ^sources: ]]; then
64
+ in_sources=true
65
+ continue
66
+ fi
67
+
68
+ # Detect end of sources block (next top-level field)
69
+ if $in_sources && [[ "$line" =~ ^[a-z_]+: ]] && [[ ! "$line" =~ ^[[:space:]] ]]; then
70
+ # Output last source if pending
71
+ if [ -n "$current_name" ]; then
72
+ echo " - name: \"$current_name\""
73
+ echo " status: \"${current_status:-incorporated}\""
74
+ fi
75
+ break
76
+ fi
77
+
78
+ if $in_sources; then
79
+ # Object format: " - name: value"
80
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+)$ ]]; then
81
+ # Output previous source if exists
82
+ if [ -n "$current_name" ]; then
83
+ echo " - name: \"$current_name\""
84
+ echo " status: \"${current_status:-incorporated}\""
85
+ fi
86
+ current_name="${BASH_REMATCH[1]}"
87
+ current_name=$(echo "$current_name" | sed 's/^"//' | sed 's/"$//' | xargs)
88
+ current_status=""
89
+ # Status line: " status: value"
90
+ elif [[ "$line" =~ ^[[:space:]]*status:[[:space:]]*(.+)$ ]]; then
91
+ current_status="${BASH_REMATCH[1]}"
92
+ current_status=$(echo "$current_status" | sed 's/^"//' | sed 's/"$//' | xargs)
93
+ fi
94
+ fi
95
+ done < <(awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null)
96
+
97
+ # Output last source if pending (end of frontmatter)
98
+ if [ -n "$current_name" ]; then
99
+ echo " - name: \"$current_name\""
100
+ echo " status: \"${current_status:-incorporated}\""
101
+ fi
102
+ }
103
+
49
104
  # Start YAML output
50
105
  echo "# Specification Command State Discovery"
51
106
  echo "# Generated: $(date -Iseconds)"
@@ -101,7 +156,6 @@ if [ -d "$SPEC_DIR" ] && [ -n "$(ls -A "$SPEC_DIR" 2>/dev/null)" ]; then
101
156
  status=${status:-"active"}
102
157
 
103
158
  superseded_by=$(extract_field "$file" "superseded_by")
104
- sources=$(extract_array_field "$file" "sources")
105
159
 
106
160
  echo " - name: \"$name\""
107
161
  echo " status: \"$status\""
@@ -110,11 +164,11 @@ if [ -d "$SPEC_DIR" ] && [ -n "$(ls -A "$SPEC_DIR" 2>/dev/null)" ]; then
110
164
  echo " superseded_by: \"$superseded_by\""
111
165
  fi
112
166
 
113
- if [ -n "$sources" ]; then
167
+ # Extract sources with status (handles both old and new format)
168
+ sources_output=$(extract_sources_with_status "$file")
169
+ if [ -n "$sources_output" ]; then
114
170
  echo " sources:"
115
- for src in $sources; do
116
- echo " - \"$src\""
117
- done
171
+ echo "$sources_output"
118
172
  fi
119
173
  done
120
174
  else
@@ -116,8 +116,10 @@ for script in "${MIGRATION_SCRIPTS[@]}"; do
116
116
  MIGRATIONS_RUN=$((MIGRATIONS_RUN + 1))
117
117
  done
118
118
 
119
- # Only output if files were actually updated
119
+ # Report results
120
120
  if [ "$FILES_UPDATED" -gt 0 ]; then
121
121
  echo ""
122
122
  echo "$FILES_UPDATED file(s) migrated. Review with \`git diff\`, then proceed."
123
+ else
124
+ echo "[SKIP] No changes needed"
123
125
  fi
@@ -17,6 +17,9 @@
17
17
  # status: in-progress | concluded
18
18
  # type: feature | cross-cutting | (empty if unknown)
19
19
  # date: YYYY-MM-DD
20
+ # sources: # Optional - only if Sources field exists
21
+ # - discussion-one
22
+ # - discussion-two
20
23
  # ---
21
24
  #
22
25
  # # Specification: {Topic}
@@ -128,17 +131,51 @@ for file in "$SPEC_DIR"/*.md; do
128
131
  date_value=$(date +%Y-%m-%d)
129
132
  fi
130
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
+
131
153
  #
132
154
  # Build new file content
133
155
  #
134
156
 
135
- # Create frontmatter
136
- frontmatter="---
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="---
137
165
  topic: $topic_kebab
138
166
  status: $status_new
139
167
  type: $type_new
140
168
  date: $date_value
169
+ sources:$sources_yaml
141
170
  ---"
171
+ else
172
+ frontmatter="---
173
+ topic: $topic_kebab
174
+ status: $status_new
175
+ type: $type_new
176
+ date: $date_value
177
+ ---"
178
+ fi
142
179
 
143
180
  # Extract H1 heading (preserve original)
144
181
  h1_heading=$(grep -m1 "^# " "$file")
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # Legacy format (partial frontmatter + inline metadata):
8
8
  # ---
9
- # format: local-markdown
9
+ # format: {format}
10
10
  # ---
11
11
  #
12
12
  # # Implementation Plan: {Feature/Project Name}
@@ -20,8 +20,9 @@
20
20
  # topic: {topic-name}
21
21
  # status: in-progress | concluded
22
22
  # date: YYYY-MM-DD
23
- # format: local-markdown
23
+ # format: {format} # Required - no default, MISSING if not present
24
24
  # specification: {topic}.md
25
+ # plan_id: {id} # Optional - migrated from 'epic' or 'project' fields
25
26
  # ---
26
27
  #
27
28
  # # Implementation Plan: {Feature/Project Name}
@@ -83,9 +84,18 @@ for file in "$PLAN_DIR"/*.md; do
83
84
  topic_kebab=$(basename "$file" .md)
84
85
 
85
86
  # Extract format from existing frontmatter (if present)
86
- format_value=$(sed -n '/^---$/,/^---$/p' "$file" 2>/dev/null | grep "^format:" | sed 's/^format:[[:space:]]*//' | xargs || echo "")
87
+ # Use awk to extract only the first frontmatter block (between first pair of --- delimiters)
88
+ # This avoids matching --- horizontal rules in body content
89
+ format_value=$(awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null | grep "^format:" | sed 's/^format:[[:space:]]*//' | xargs || echo "")
87
90
  if [ -z "$format_value" ]; then
88
- format_value="local-markdown" # Default format
91
+ format_value="MISSING" # No default - missing format is an error
92
+ fi
93
+
94
+ # Extract plan_id from existing frontmatter - could be 'epic' (beads) or 'project' (linear/backlog)
95
+ # These are migrated to a unified 'plan_id' field
96
+ plan_id_value=$(awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null | grep "^epic:" | sed 's/^epic:[[:space:]]*//' | xargs || echo "")
97
+ if [ -z "$plan_id_value" ]; then
98
+ plan_id_value=$(awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null | grep "^project:" | sed 's/^project:[[:space:]]*//' | xargs || echo "")
89
99
  fi
90
100
 
91
101
  # Extract status from **Status**: Value
@@ -107,8 +117,8 @@ for file in "$PLAN_DIR"/*.md; do
107
117
  ;;
108
118
  esac
109
119
 
110
- # Extract date from **Date**: YYYY-MM-DD
111
- date_value=$(grep -m1 '^\*\*Date\*\*:' "$file" 2>/dev/null | \
120
+ # Extract date from **Date**: YYYY-MM-DD or **Created**: YYYY-MM-DD
121
+ date_value=$(grep -m1 '^\*\*Date\*\*:\|^\*\*Created\*\*:' "$file" 2>/dev/null | \
112
122
  grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo "")
113
123
 
114
124
  # Use today's date if none found
@@ -134,14 +144,25 @@ for file in "$PLAN_DIR"/*.md; do
134
144
  # Build new file content
135
145
  #
136
146
 
137
- # Create frontmatter
138
- frontmatter="---
147
+ # Create frontmatter (conditionally include plan_id if present)
148
+ if [ -n "$plan_id_value" ]; then
149
+ frontmatter="---
150
+ topic: $topic_kebab
151
+ status: $status_new
152
+ date: $date_value
153
+ format: $format_value
154
+ specification: $spec_value
155
+ plan_id: $plan_id_value
156
+ ---"
157
+ else
158
+ frontmatter="---
139
159
  topic: $topic_kebab
140
160
  status: $status_new
141
161
  date: $date_value
142
162
  format: $format_value
143
163
  specification: $spec_value
144
164
  ---"
165
+ fi
145
166
 
146
167
  # Extract H1 heading (preserve original)
147
168
  h1_heading=$(grep -m1 "^# " "$file")
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # 004-sources-object-format.sh
4
+ #
5
+ # Migrates specification sources from simple array format to object format
6
+ # with status tracking. Also ensures all specs have a sources field.
7
+ #
8
+ # Previous format (from 002-specification-frontmatter.sh):
9
+ # sources:
10
+ # - topic-a
11
+ # - topic-b
12
+ #
13
+ # New format:
14
+ # sources:
15
+ # - name: topic-a
16
+ # status: incorporated
17
+ # - name: topic-b
18
+ # status: incorporated
19
+ #
20
+ # Status values:
21
+ # - pending: Source selected but content not yet extracted
22
+ # - incorporated: Source content has been fully woven into the specification
23
+ #
24
+ # For existing sources, we assume "incorporated" since they were part of
25
+ # the specification when it was created/worked on.
26
+ #
27
+ # For specs WITHOUT a sources field:
28
+ # - If a matching discussion exists (same filename), add it as incorporated
29
+ # - If no matching discussion, add empty sources: [] and report for user review
30
+ #
31
+ # This script is sourced by migrate.sh and has access to:
32
+ # - is_migrated "filepath" "migration_id"
33
+ # - record_migration "filepath" "migration_id"
34
+ # - report_update "filepath" "description"
35
+ # - report_skip "filepath"
36
+ #
37
+
38
+ MIGRATION_ID="004"
39
+ SPEC_DIR="docs/workflow/specification"
40
+ DISCUSSION_DIR="docs/workflow/discussion"
41
+
42
+ # Skip if no specification directory
43
+ if [ ! -d "$SPEC_DIR" ]; then
44
+ return 0
45
+ fi
46
+
47
+ # Helper: Extract ONLY the frontmatter content (between first pair of --- delimiters)
48
+ # Documents may contain --- elsewhere (horizontal rules), so sed range matching
49
+ # can return content beyond frontmatter. Use awk for precise first-block extraction.
50
+ extract_frontmatter() {
51
+ local file="$1"
52
+ awk 'BEGIN{c=0} /^---$/{c++; if(c==2) exit; next} c==1{print}' "$file" 2>/dev/null
53
+ }
54
+
55
+ # Helper: Check if sources are already in object format
56
+ # Returns 0 if already migrated (has "name:" entries), 1 if not
57
+ sources_already_object_format() {
58
+ local file="$1"
59
+ # Look for "- name:" pattern within the sources block (frontmatter only)
60
+ # This indicates the new object format
61
+ # Using subshell with || false to ensure proper exit code without pipefail issues
62
+ ( extract_frontmatter "$file" | \
63
+ sed -n '/^sources:/,/^[a-z_]*:/p' | \
64
+ grep -q "^[[:space:]]*-[[:space:]]*name:" 2>/dev/null ) || return 1
65
+ return 0
66
+ }
67
+
68
+ # Helper: Extract sources array items (simple string format)
69
+ # Returns space-separated list of source names
70
+ extract_simple_sources() {
71
+ local file="$1"
72
+ # Extract sources from frontmatter only, then find the sources block
73
+ extract_frontmatter "$file" | \
74
+ sed -n '/^sources:/,/^[a-z_]*:/p' | \
75
+ grep -v "^sources:" | \
76
+ grep -v "^[a-z_]*:" | \
77
+ { grep "^[[:space:]]*-[[:space:]]" || true; } | \
78
+ { grep -v "name:" || true; } | \
79
+ sed 's/^[[:space:]]*-[[:space:]]*//' | \
80
+ sed 's/^"//' | \
81
+ sed 's/"$//' | \
82
+ tr '\n' ' ' | \
83
+ sed 's/[[:space:]]*$//' || true
84
+ }
85
+
86
+ # Process each specification file
87
+ for file in "$SPEC_DIR"/*.md; do
88
+ [ -f "$file" ] || continue
89
+
90
+ # Check if already migrated via tracking
91
+ if is_migrated "$file" "$MIGRATION_ID"; then
92
+ report_skip "$file"
93
+ continue
94
+ fi
95
+
96
+ # Check if file has YAML frontmatter
97
+ if ! head -1 "$file" 2>/dev/null | grep -q "^---$"; then
98
+ record_migration "$file" "$MIGRATION_ID"
99
+ report_skip "$file"
100
+ continue
101
+ fi
102
+
103
+ # Check if file has sources field at all
104
+ has_sources_field=false
105
+ if grep -q "^sources:" "$file" 2>/dev/null; then
106
+ has_sources_field=true
107
+ fi
108
+
109
+ # If sources field exists, check if already in object format
110
+ if $has_sources_field && sources_already_object_format "$file"; then
111
+ record_migration "$file" "$MIGRATION_ID"
112
+ report_skip "$file"
113
+ continue
114
+ fi
115
+
116
+ #
117
+ # Build new sources block in object format
118
+ #
119
+ new_sources_block="sources:"
120
+ sources_added=false
121
+
122
+ if $has_sources_field; then
123
+ # Extract existing sources from simple array format
124
+ sources=$(extract_simple_sources "$file")
125
+
126
+ for src in $sources; do
127
+ # Clean the source name (trim whitespace, sed avoids xargs quote issues)
128
+ src=$(echo "$src" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
129
+ if [ -n "$src" ]; then
130
+ new_sources_block="${new_sources_block}
131
+ - name: $src
132
+ status: incorporated"
133
+ sources_added=true
134
+ fi
135
+ done
136
+ else
137
+ # No sources field - check for matching discussion by filename
138
+ spec_name=$(basename "$file" .md)
139
+ discussion_file="$DISCUSSION_DIR/${spec_name}.md"
140
+
141
+ if [ -f "$discussion_file" ]; then
142
+ # Matching discussion found - add it as incorporated
143
+ new_sources_block="${new_sources_block}
144
+ - name: $spec_name
145
+ status: incorporated"
146
+ sources_added=true
147
+ fi
148
+ fi
149
+
150
+ # If no sources were added, use empty array format
151
+ if ! $sources_added; then
152
+ new_sources_block="sources: []"
153
+ # Echo info for Claude to prompt user about unmatched specs
154
+ spec_name=$(basename "$file" .md)
155
+ echo "MIGRATION_INFO: Specification '$spec_name' has no matching discussion. Sources field set to empty - please review and add sources manually."
156
+ fi
157
+
158
+ #
159
+ # Update sources block in file
160
+ #
161
+
162
+ # Extract frontmatter (only the first block between --- delimiters)
163
+ frontmatter=$(extract_frontmatter "$file")
164
+
165
+ if $has_sources_field; then
166
+ # Remove old sources block from frontmatter
167
+ # First, remove lines from "sources:" until the next top-level field or end of frontmatter
168
+ new_frontmatter=$(echo "$frontmatter" | awk '
169
+ /^sources:/ { skip=1; next }
170
+ /^[a-z_]+:/ && skip { skip=0 }
171
+ skip == 0 { print }
172
+ ')
173
+ else
174
+ # No existing sources field - use frontmatter as-is
175
+ new_frontmatter="$frontmatter"
176
+ fi
177
+
178
+ # Add new sources block at the end
179
+ new_frontmatter="${new_frontmatter}
180
+ ${new_sources_block}"
181
+
182
+ # Extract content after frontmatter (everything after the second ---)
183
+ # Uses awk to skip only the first two --- delimiters, preserving any --- in body content
184
+ content=$(awk '/^---$/ && c<2 {c++; next} c>=2 {print}' "$file")
185
+
186
+ # Write new file
187
+ {
188
+ echo "---"
189
+ echo "$new_frontmatter"
190
+ echo "---"
191
+ echo "$content"
192
+ } > "$file"
193
+
194
+ record_migration "$file" "$MIGRATION_ID"
195
+
196
+ # Report appropriate message based on what was done
197
+ if $has_sources_field; then
198
+ report_update "$file" "converted sources to object format"
199
+ elif $sources_added; then
200
+ report_update "$file" "added sources field with matching discussion"
201
+ else
202
+ report_update "$file" "added empty sources field (no matching discussion found)"
203
+ fi
204
+ done
@@ -53,7 +53,7 @@ The plan file in `docs/workflow/planning/{topic}.md` serves as the reference poi
53
53
  ```markdown
54
54
  ---
55
55
  format: backlog-md
56
- project: {TOPIC_NAME}
56
+ plan_id: {TOPIC_NAME}
57
57
  ---
58
58
 
59
59
  # Plan Reference: {Topic Name}
@@ -240,7 +240,7 @@ Create `docs/workflow/planning/{topic}.md`:
240
240
  ```markdown
241
241
  ---
242
242
  format: beads
243
- epic: bd-{EPIC_ID}
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
- epic: bd-a3f8
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 `epic` ID from frontmatter
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
- project: {PROJECT_NAME}
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
- project: USER-AUTH-FEATURE
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 `project_id` from frontmatter
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
@@ -148,6 +148,11 @@ topic: {topic-name}
148
148
  status: in-progress
149
149
  type: feature
150
150
  date: YYYY-MM-DD # Use today's actual date
151
+ sources:
152
+ - name: discussion-one
153
+ status: incorporated
154
+ - name: discussion-two
155
+ status: pending
151
156
  ---
152
157
 
153
158
  # Specification: [Topic Name]
@@ -169,6 +174,45 @@ date: YYYY-MM-DD # Use today's actual date
169
174
  - **status**: `in-progress` (building) or `concluded` (complete)
170
175
  - **type**: `feature` (something to build) or `cross-cutting` (patterns/policies)
171
176
  - **date**: Last updated date
177
+ - **sources**: Array of source discussions with incorporation status (see below)
178
+
179
+ ### Sources and Incorporation Status
180
+
181
+ **All specifications must track their sources**, even when built from a single discussion. This enables proper tracking when additional discussions are later added to the same grouping.
182
+
183
+ When a specification is built from discussion(s), track each source with its incorporation status:
184
+
185
+ ```yaml
186
+ sources:
187
+ - name: auth-flow
188
+ status: incorporated
189
+ - name: api-design
190
+ status: pending
191
+ ```
192
+
193
+ **Status values:**
194
+ - `pending` - Source has been selected for this specification but content extraction is not complete
195
+ - `incorporated` - Source content has been fully extracted and woven into the specification
196
+
197
+ **When to update source status:**
198
+
199
+ 1. **When creating the specification**: All sources start as `pending`
200
+ 2. **After completing exhaustive extraction from a source**: Mark that source as `incorporated`
201
+ 3. **When adding a new source to an existing spec**: Add it with `status: pending`
202
+
203
+ **How to determine if a source is incorporated:**
204
+
205
+ A source is `incorporated` when you have:
206
+ - Performed exhaustive extraction (reviewed ALL content in the source for relevant material)
207
+ - Presented and logged all relevant content from that source
208
+ - No more content from that source needs to be extracted
209
+
210
+ **Important**: The specification's overall `status: concluded` should only be set when:
211
+ - All sources are marked as `incorporated`
212
+ - Both review phases are complete
213
+ - User has signed off
214
+
215
+ If a new source is added to a concluded specification (via grouping analysis), the specification effectively needs updating - even if the file still says `status: concluded`, the presence of `pending` sources indicates work remains.
172
216
 
173
217
  ## Specification Types
174
218