@leeovery/claude-technical-workflows 2.0.44 → 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.
@@ -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.44",
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>",
@@ -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
@@ -84,16 +84,18 @@ for file in "$PLAN_DIR"/*.md; do
84
84
  topic_kebab=$(basename "$file" .md)
85
85
 
86
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 "")
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 "")
88
90
  if [ -z "$format_value" ]; then
89
91
  format_value="MISSING" # No default - missing format is an error
90
92
  fi
91
93
 
92
94
  # Extract plan_id from existing frontmatter - could be 'epic' (beads) or 'project' (linear/backlog)
93
95
  # 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 "")
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 "")
95
97
  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 "")
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 "")
97
99
  fi
98
100
 
99
101
  # Extract status from **Status**: Value
@@ -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
@@ -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