specpulse 1.0.4__py3-none-any.whl → 1.0.5__py3-none-any.whl

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.
Files changed (32) hide show
  1. specpulse/__init__.py +1 -1
  2. specpulse/resources/commands/claude/plan.md +156 -45
  3. specpulse/resources/commands/claude/pulse.md +63 -24
  4. specpulse/resources/commands/claude/spec.md +110 -31
  5. specpulse/resources/commands/claude/task.md +202 -43
  6. specpulse/resources/commands/gemini/plan.toml +13 -5
  7. specpulse/resources/commands/gemini/pulse.toml +4 -1
  8. specpulse/resources/commands/gemini/spec.toml +12 -4
  9. specpulse/resources/commands/gemini/task.toml +17 -5
  10. specpulse/resources/memory/constitution.md +2 -2
  11. specpulse/resources/scripts/pulse-init.ps1 +186 -0
  12. specpulse/resources/scripts/pulse-init.py +171 -0
  13. specpulse/resources/scripts/pulse-init.sh +80 -21
  14. specpulse/resources/scripts/pulse-plan.ps1 +251 -0
  15. specpulse/resources/scripts/pulse-plan.py +191 -0
  16. specpulse/resources/scripts/pulse-plan.sh +113 -12
  17. specpulse/resources/scripts/pulse-spec.ps1 +185 -0
  18. specpulse/resources/scripts/pulse-spec.py +167 -0
  19. specpulse/resources/scripts/pulse-spec.sh +86 -11
  20. specpulse/resources/scripts/pulse-task.ps1 +263 -0
  21. specpulse/resources/scripts/pulse-task.py +237 -0
  22. specpulse/resources/scripts/pulse-task.sh +123 -9
  23. specpulse/resources/templates/plan.md +142 -287
  24. specpulse/resources/templates/spec.md +80 -246
  25. specpulse/resources/templates/task.md +114 -93
  26. {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/METADATA +29 -10
  27. specpulse-1.0.5.dist-info/RECORD +41 -0
  28. specpulse-1.0.4.dist-info/RECORD +0 -33
  29. {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/WHEEL +0 -0
  30. {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/entry_points.txt +0 -0
  31. {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/licenses/LICENSE +0 -0
  32. {specpulse-1.0.4.dist-info → specpulse-1.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SpecPulse Plan Generation Script
4
+ Cross-platform Python equivalent of pulse-plan.sh
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import re
10
+ from pathlib import Path
11
+ import datetime
12
+
13
+ class SpecPulsePlan:
14
+ def __init__(self):
15
+ self.script_name = Path(__file__).name
16
+ self.project_root = Path(__file__).parent.parent.parent
17
+ self.memory_dir = self.project_root / "memory"
18
+ self.context_file = self.memory_dir / "context.md"
19
+ self.templates_dir = self.project_root / "resources" / "templates"
20
+
21
+ def log(self, message):
22
+ """Log messages with timestamp"""
23
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
24
+ try:
25
+ print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
26
+ except UnicodeEncodeError:
27
+ # Fallback for Windows console encoding issues
28
+ message = message.encode('cp1252', errors='replace').decode('cp1252')
29
+ print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
30
+
31
+ def error_exit(self, message):
32
+ """Exit with error message"""
33
+ self.log(f"ERROR: {message}")
34
+ sys.exit(1)
35
+
36
+ def sanitize_feature_dir(self, feature_dir):
37
+ """Sanitize feature directory name"""
38
+ if not feature_dir:
39
+ self.error_exit("Feature directory cannot be empty")
40
+
41
+ sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', feature_dir)
42
+
43
+ if not sanitized:
44
+ self.error_exit(f"Invalid feature directory: '{feature_dir}'")
45
+
46
+ return sanitized
47
+
48
+ def find_feature_directory(self):
49
+ """Find feature directory from context file"""
50
+ if not self.context_file.exists():
51
+ self.error_exit("No context file found and no feature directory provided")
52
+
53
+ try:
54
+ content = self.context_file.read_text(encoding='utf-8')
55
+ match = re.search(r'Active Feature:\s*(.+)', content)
56
+ if match:
57
+ feature_dir = match.group(1).strip()
58
+ self.log(f"Using active feature from context: {feature_dir}")
59
+ return feature_dir
60
+ else:
61
+ self.error_exit("No active feature found in context file")
62
+ except Exception as e:
63
+ self.error_exit(f"Failed to read context file: {e}")
64
+
65
+ def get_current_feature_dir(self, provided_dir):
66
+ """Get current feature directory"""
67
+ if provided_dir:
68
+ return self.sanitize_feature_dir(provided_dir)
69
+ else:
70
+ return self.find_feature_directory()
71
+
72
+ def validate_plan_structure(self, plan_file):
73
+ """Validate plan structure"""
74
+ required_sections = [
75
+ "## Implementation Plan:",
76
+ "## Specification Reference",
77
+ "## Phase -1: Pre-Implementation Gates",
78
+ "## Implementation Phases"
79
+ ]
80
+
81
+ missing_sections = []
82
+ content = plan_file.read_text(encoding='utf-8')
83
+
84
+ for section in required_sections:
85
+ if section not in content:
86
+ missing_sections.append(section)
87
+
88
+ if missing_sections:
89
+ self.log(f"WARNING: Missing required sections: {', '.join(missing_sections)}")
90
+
91
+ return len(missing_sections)
92
+
93
+ def check_constitutional_gates(self, plan_file):
94
+ """Check constitutional gates"""
95
+ constitutional_gates = [
96
+ "Simplicity Gate",
97
+ "Anti-Abstraction Gate",
98
+ "Test-First Gate",
99
+ "Integration-First Gate",
100
+ "Research Gate"
101
+ ]
102
+
103
+ missing_gates = []
104
+ content = plan_file.read_text(encoding='utf-8')
105
+
106
+ for gate in constitutional_gates:
107
+ if gate not in content:
108
+ missing_gates.append(gate)
109
+
110
+ if missing_gates:
111
+ self.log(f"WARNING: Missing constitutional gates: {', '.join(missing_gates)}")
112
+
113
+ # Check gate status
114
+ gate_status_match = re.search(r'Gate Status:\s*\[([^\]]+)\]', content)
115
+ if gate_status_match:
116
+ gate_status = gate_status_match.group(1)
117
+ if gate_status.upper() != "COMPLETED":
118
+ self.log(f"WARNING: Constitutional gates not completed. Status: {gate_status}")
119
+ return gate_status
120
+ else:
121
+ self.log("WARNING: Constitutional gates status not found")
122
+ return "UNKNOWN"
123
+
124
+ return "COMPLETED"
125
+
126
+ def main(self, args):
127
+ """Main execution function"""
128
+ if len(args) < 1:
129
+ self.error_exit("Usage: python pulse-plan.py <feature-dir>")
130
+
131
+ feature_dir = args[0]
132
+
133
+ self.log("Processing implementation plan...")
134
+
135
+ # Get and sanitize feature directory
136
+ sanitized_dir = self.get_current_feature_dir(feature_dir)
137
+
138
+ # Set file paths
139
+ plan_file = self.project_root / "plans" / sanitized_dir / "plan.md"
140
+ template_file = self.templates_dir / "plan.md"
141
+ spec_file = self.project_root / "specs" / sanitized_dir / "spec.md"
142
+
143
+ # Ensure plans directory exists
144
+ plan_file.parent.mkdir(parents=True, exist_ok=True)
145
+
146
+ # Check if specification exists first
147
+ if not spec_file.exists():
148
+ self.error_exit(f"Specification file not found: {spec_file}. Please create specification first.")
149
+
150
+ # Ensure plan template exists
151
+ if not template_file.exists():
152
+ self.error_exit(f"Template not found: {template_file}")
153
+
154
+ # Create plan if it doesn't exist
155
+ if not plan_file.exists():
156
+ self.log(f"Creating implementation plan from template: {plan_file}")
157
+ try:
158
+ plan_file.write_text(template_file.read_text(encoding='utf-8'))
159
+ except Exception as e:
160
+ self.error_exit(f"Failed to copy plan template: {e}")
161
+ else:
162
+ self.log(f"Implementation plan already exists: {plan_file}")
163
+
164
+ # Validate plan structure
165
+ self.log("Validating implementation plan...")
166
+ missing_sections = self.validate_plan_structure(plan_file)
167
+
168
+ # Check Constitutional Gates
169
+ self.log("Checking Constitutional Gates...")
170
+ gate_status = self.check_constitutional_gates(plan_file)
171
+
172
+ # Check if specification has clarifications needed
173
+ if spec_file.exists():
174
+ spec_content = spec_file.read_text(encoding='utf-8')
175
+ clarification_matches = re.findall(r'NEEDS CLARIFICATION', spec_content)
176
+ if clarification_matches:
177
+ clarification_count = len(clarification_matches)
178
+ self.log(f"WARNING: Specification has {clarification_count} clarifications needed - resolve before proceeding")
179
+
180
+ self.log("Implementation plan processing completed successfully")
181
+
182
+ # Output results
183
+ print(f"PLAN_FILE={plan_file}")
184
+ print(f"SPEC_FILE={spec_file}")
185
+ print(f"MISSING_SECTIONS={missing_sections}")
186
+ print(f"CONSTITUTIONAL_GATES_STATUS={gate_status}")
187
+ print("STATUS=ready")
188
+
189
+ if __name__ == "__main__":
190
+ plan = SpecPulsePlan()
191
+ plan.main(sys.argv[1:])
@@ -1,26 +1,127 @@
1
1
  #!/bin/bash
2
2
  # Generate implementation plan
3
3
 
4
- FEATURE_DIR="${1}"
4
+ set -euo pipefail # Exit on error, unset vars, pipe failures
5
5
 
6
+ # Configuration
7
+ SCRIPT_NAME="$(basename "$0")"
8
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
9
+
10
+ # Function to log messages
11
+ log() {
12
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $SCRIPT_NAME: $1" >&2
13
+ }
14
+
15
+ # Function to handle errors
16
+ error_exit() {
17
+ log "ERROR: $1"
18
+ exit 1
19
+ }
20
+
21
+ # Validate arguments
22
+ if [ $# -eq 0 ]; then
23
+ error_exit "Usage: $SCRIPT_NAME <feature-dir>"
24
+ fi
25
+
26
+ FEATURE_DIR="$1"
27
+
28
+ # Sanitize feature directory
29
+ SANITIZED_DIR=$(echo "$FEATURE_DIR" | sed 's/[^a-zA-Z0-9_-]//g')
30
+
31
+ if [ -z "$SANITIZED_DIR" ]; then
32
+ error_exit "Invalid feature directory: '$FEATURE_DIR'"
33
+ fi
34
+
35
+ # Find feature directory if not provided
6
36
  if [ -z "$FEATURE_DIR" ]; then
7
- # Find current feature from context
8
- FEATURE_DIR=$(grep -A1 "Active Feature" memory/context.md | tail -1 | cut -d: -f2 | xargs)
37
+ CONTEXT_FILE="$PROJECT_ROOT/memory/context.md"
38
+ if [ -f "$CONTEXT_FILE" ]; then
39
+ FEATURE_DIR=$(grep -A1 "Active Feature" "$CONTEXT_FILE" | tail -1 | cut -d: -f2 | xargs)
40
+ if [ -z "$FEATURE_DIR" ]; then
41
+ error_exit "No active feature found in context file"
42
+ fi
43
+ log "Using active feature from context: $FEATURE_DIR"
44
+ else
45
+ error_exit "No feature directory provided and no context file found"
46
+ fi
9
47
  fi
10
48
 
11
- PLAN_FILE="plans/${FEATURE_DIR}/plan.md"
49
+ PLAN_FILE="$PROJECT_ROOT/plans/${FEATURE_DIR}/plan.md"
50
+ TEMPLATE_FILE="$PROJECT_ROOT/templates/plan.md"
51
+ SPEC_FILE="$PROJECT_ROOT/specs/${FEATURE_DIR}/spec.md"
52
+
53
+ # Ensure plans directory exists
54
+ mkdir -p "$(dirname "$PLAN_FILE")"
55
+
56
+ # Check if specification exists first
57
+ if [ ! -f "$SPEC_FILE" ]; then
58
+ error_exit "Specification file not found: $SPEC_FILE. Please create specification first."
59
+ fi
60
+
61
+ # Ensure plan template exists
62
+ if [ ! -f "$TEMPLATE_FILE" ]; then
63
+ error_exit "Template not found: $TEMPLATE_FILE"
64
+ fi
12
65
 
13
- # Ensure template exists
66
+ # Create plan if it doesn't exist
14
67
  if [ ! -f "$PLAN_FILE" ]; then
15
- cp templates/plan.md "$PLAN_FILE"
68
+ log "Creating implementation plan from template: $PLAN_FILE"
69
+ cp "$TEMPLATE_FILE" "$PLAN_FILE" || error_exit "Failed to copy plan template"
70
+ else
71
+ log "Implementation plan already exists: $PLAN_FILE"
72
+ fi
73
+
74
+ # Validate plan structure
75
+ log "Validating implementation plan..."
76
+
77
+ # Check for required sections
78
+ REQUIRED_SECTIONS=("## Implementation Plan:" "## Specification Reference" "## Phase -1: Pre-Implementation Gates" "## Implementation Phases")
79
+ MISSING_SECTIONS=()
80
+
81
+ for section in "${REQUIRED_SECTIONS[@]}"; do
82
+ if ! grep -q "$section" "$PLAN_FILE"; then
83
+ MISSING_SECTIONS+=("$section")
84
+ fi
85
+ done
86
+
87
+ if [ ${#MISSING_SECTIONS[@]} -gt 0 ]; then
88
+ log "WARNING: Missing required sections: ${MISSING_SECTIONS[*]}"
89
+ fi
90
+
91
+ # Check Constitutional Gates
92
+ log "Checking Constitutional Gates..."
93
+
94
+ CONSTITUTIONAL_GATES=(
95
+ "Simplicity Gate"
96
+ "Anti-Abstraction Gate"
97
+ "Test-First Gate"
98
+ "Integration-First Gate"
99
+ "Research Gate"
100
+ )
101
+
102
+ for gate in "${CONSTITUTIONAL_GATES[@]}"; do
103
+ if ! grep -q "$gate" "$PLAN_FILE"; then
104
+ log "WARNING: Missing constitutional gate: $gate"
105
+ fi
106
+ done
107
+
108
+ # Check if specification has clarifications needed
109
+ if grep -q "NEEDS CLARIFICATION" "$SPEC_FILE"; then
110
+ CLARIFICATION_COUNT=$(grep -c "NEEDS CLARIFICATION" "$SPEC_FILE")
111
+ log "WARNING: Specification has $CLARIFICATION_COUNT clarifications needed - resolve before proceeding"
112
+ fi
113
+
114
+ # Validate gate compliance
115
+ GATE_STATUS=$(grep -A5 "Gate Status:" "$PLAN_FILE" | tail -1 | sed 's/.*\[\(.*\)\].*/\1/' || echo "PENDING")
116
+
117
+ if [ "$GATE_STATUS" != "COMPLETED" ]; then
118
+ log "WARNING: Constitutional gates not completed. Status: $GATE_STATUS"
16
119
  fi
17
120
 
18
- # Check Phase Gates
19
- echo "Checking Phase Gates..."
20
- echo "- Constitutional compliance"
21
- echo "- Simplicity check (≤3 modules)"
22
- echo "- Test-first strategy"
23
- echo "- Framework selection"
121
+ log "Implementation plan processing completed successfully"
24
122
 
25
123
  echo "PLAN_FILE=$PLAN_FILE"
124
+ echo "SPEC_FILE=$SPEC_FILE"
125
+ echo "MISSING_SECTIONS=${#MISSING_SECTIONS[@]}"
126
+ echo "CONSTITUTIONAL_GATES_STATUS=$GATE_STATUS"
26
127
  echo "STATUS=ready"
@@ -0,0 +1,185 @@
1
+ # SpecPulse Specification Generation Script
2
+ # Cross-platform PowerShell equivalent of pulse-spec.sh
3
+
4
+ param(
5
+ [Parameter(Mandatory=$true)]
6
+ [string]$FeatureDir,
7
+
8
+ [string]$SpecContent = ""
9
+ )
10
+
11
+ $ErrorActionPreference = "Stop"
12
+ $ProgressPreference = "SilentlyContinue"
13
+
14
+ # Configuration
15
+ $ScriptName = $MyInvocation.MyCommand.Name
16
+ $ProjectRoot = $PSScriptRoot | Split-Path -Parent | Split-Path -Parent
17
+ $MemoryDir = Join-Path $ProjectRoot "memory"
18
+ $ContextFile = Join-Path $MemoryDir "context.md"
19
+ $TemplatesDir = Join-Path $ProjectRoot "resources" "templates"
20
+
21
+ function Write-Log {
22
+ param([string]$Message)
23
+ $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
24
+ Write-Host "[$Timestamp] $ScriptName : $Message" -ForegroundColor Yellow
25
+ }
26
+
27
+ function Exit-WithError {
28
+ param([string]$Message)
29
+ Write-Log "ERROR: $Message"
30
+ exit 1
31
+ }
32
+
33
+ function Sanitize-FeatureDir {
34
+ param([string]$Dir)
35
+
36
+ if ([string]::IsNullOrWhiteSpace($Dir)) {
37
+ Exit-WithError "Feature directory cannot be empty"
38
+ }
39
+
40
+ # Remove non-alphanumeric characters except hyphens and underscores
41
+ $Sanitized = $Dir -replace '[^a-zA-Z0-9_-]', ''
42
+
43
+ if ([string]::IsNullOrWhiteSpace($Sanitized)) {
44
+ Exit-WithError "Invalid feature directory: '$Dir'"
45
+ }
46
+
47
+ return $Sanitized
48
+ }
49
+
50
+ function Find-FeatureDirectory {
51
+ if (-not (Test-Path $ContextFile)) {
52
+ Exit-WithError "No context file found and no feature directory provided"
53
+ }
54
+
55
+ try {
56
+ $Content = Get-Content $ContextFile -Raw -Encoding UTF8
57
+ # Look for "Active Feature" section
58
+ if ($Content -match 'Active Feature:\s*(.+)') {
59
+ $FeatureDir = $matches[1].Trim()
60
+ Write-Log "Using active feature from context: $FeatureDir"
61
+ return $FeatureDir
62
+ } else {
63
+ Exit-WithError "No active feature found in context file"
64
+ }
65
+ } catch {
66
+ Exit-WithError "Failed to read context file: $_"
67
+ }
68
+ }
69
+
70
+ function Get-CurrentFeatureDir {
71
+ param([string]$ProvidedDir)
72
+
73
+ if ($ProvidedDir) {
74
+ return Sanitize-FeatureDir -Dir $ProvidedDir
75
+ } else {
76
+ return Find-FeatureDirectory
77
+ }
78
+ }
79
+
80
+ function Validate-Specification {
81
+ param([string]$SpecFile)
82
+
83
+ if (-not (Test-Path $SpecFile)) {
84
+ Exit-WithError "Specification file does not exist: $SpecFile"
85
+ }
86
+
87
+ # Required sections
88
+ $RequiredSections = @(
89
+ "## Specification:",
90
+ "## Metadata",
91
+ "## Functional Requirements",
92
+ "## Acceptance Scenarios"
93
+ )
94
+
95
+ $Content = Get-Content $SpecFile -Raw -Encoding UTF8
96
+ $MissingSections = @()
97
+
98
+ foreach ($Section in $RequiredSections) {
99
+ if ($Content -notmatch [regex]::Escape($Section)) {
100
+ $MissingSections += $Section
101
+ }
102
+ }
103
+
104
+ if ($MissingSections.Count -gt 0) {
105
+ Write-Log "WARNING: Missing required sections: $($MissingSections -join ', ')"
106
+ }
107
+
108
+ # Check for clarifications needed
109
+ $ClarificationMatches = [regex]::Matches($Content, 'NEEDS CLARIFICATION')
110
+ if ($ClarificationMatches.Count -gt 0) {
111
+ $ClarificationCount = $ClarificationMatches.Count
112
+ Write-Log "WARNING: Specification has $ClarificationCount clarifications needed"
113
+ return $ClarificationCount
114
+ }
115
+
116
+ return 0
117
+ }
118
+
119
+ # Main execution
120
+ Write-Log "Processing specification..."
121
+
122
+ # Get and sanitize feature directory
123
+ $SanitizedDir = Get-CurrentFeatureDir -ProvidedDir $FeatureDir
124
+
125
+ # Set file paths
126
+ $SpecFile = Join-Path $ProjectRoot "specs" $SanitizedDir "spec.md"
127
+ $TemplateFile = Join-Path $TemplatesDir "spec.md"
128
+
129
+ # Ensure specs directory exists
130
+ $SpecDir = Split-Path $SpecFile -Parent
131
+ New-Item -ItemType Directory -Path $SpecDir -Force | Out-Null
132
+
133
+ # Handle specification content
134
+ if ($SpecContent) {
135
+ Write-Log "Updating specification: $SpecFile"
136
+ try {
137
+ Set-Content -Path $SpecFile -Value $SpecContent -Encoding UTF8
138
+ } catch {
139
+ Exit-WithError "Failed to write specification content: $_"
140
+ }
141
+ } else {
142
+ # Ensure specification exists from template
143
+ if (-not (Test-Path $SpecFile)) {
144
+ if (-not (Test-Path $TemplateFile)) {
145
+ Exit-WithError "Template not found: $TemplateFile"
146
+ }
147
+
148
+ Write-Log "Creating specification from template: $SpecFile"
149
+ try {
150
+ Copy-Item $TemplateFile $SpecFile -Force
151
+ } catch {
152
+ Exit-WithError "Failed to copy specification template: $_"
153
+ }
154
+ } else {
155
+ Write-Log "Specification already exists: $SpecFile"
156
+ }
157
+ }
158
+
159
+ # Validate specification
160
+ Write-Log "Validating specification..."
161
+ $ClarificationCount = Validate-Specification -SpecFile $SpecFile
162
+
163
+ # Check for missing sections
164
+ $Content = Get-Content $SpecFile -Raw -Encoding UTF8
165
+ $RequiredSections = @(
166
+ "## Specification:",
167
+ "## Metadata",
168
+ "## Functional Requirements",
169
+ "## Acceptance Scenarios"
170
+ )
171
+
172
+ $MissingSections = @()
173
+ foreach ($Section in $RequiredSections) {
174
+ if ($Content -notmatch [regex]::Escape($Section)) {
175
+ $MissingSections += $Section
176
+ }
177
+ }
178
+
179
+ Write-Log "Specification processing completed successfully"
180
+
181
+ # Output results
182
+ Write-Host "SPEC_FILE=$SpecFile"
183
+ Write-Host "CLARIFICATIONS_NEEDED=$ClarificationCount"
184
+ Write-Host "MISSING_SECTIONS=$($MissingSections.Count)"
185
+ Write-Host "STATUS=updated"
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SpecPulse Specification Generation Script
4
+ Cross-platform Python equivalent of pulse-spec.sh
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import re
10
+ from pathlib import Path
11
+ import datetime
12
+
13
+ class SpecPulseSpec:
14
+ def __init__(self):
15
+ self.script_name = Path(__file__).name
16
+ self.project_root = Path(__file__).parent.parent.parent
17
+ self.memory_dir = self.project_root / "memory"
18
+ self.context_file = self.memory_dir / "context.md"
19
+ self.templates_dir = self.project_root / "resources" / "templates"
20
+
21
+ def log(self, message):
22
+ """Log messages with timestamp"""
23
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
24
+ print(f"[{timestamp}] {self.script_name}: {message}", file=sys.stderr)
25
+
26
+ def error_exit(self, message):
27
+ """Exit with error message"""
28
+ self.log(f"ERROR: {message}")
29
+ sys.exit(1)
30
+
31
+ def sanitize_feature_dir(self, feature_dir):
32
+ """Sanitize feature directory name"""
33
+ if not feature_dir:
34
+ self.error_exit("Feature directory cannot be empty")
35
+
36
+ # Remove non-alphanumeric characters except hyphens and underscores
37
+ sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', feature_dir)
38
+
39
+ if not sanitized:
40
+ self.error_exit(f"Invalid feature directory: '{feature_dir}'")
41
+
42
+ return sanitized
43
+
44
+ def find_feature_directory(self):
45
+ """Find feature directory from context file"""
46
+ if not self.context_file.exists():
47
+ self.error_exit("No context file found and no feature directory provided")
48
+
49
+ try:
50
+ content = self.context_file.read_text(encoding='utf-8')
51
+ # Look for "Active Feature" section
52
+ match = re.search(r'Active Feature:\s*(.+)', content)
53
+ if match:
54
+ feature_dir = match.group(1).strip()
55
+ self.log(f"Using active feature from context: {feature_dir}")
56
+ return feature_dir
57
+ else:
58
+ self.error_exit("No active feature found in context file")
59
+ except Exception as e:
60
+ self.error_exit(f"Failed to read context file: {e}")
61
+
62
+ def get_current_feature_dir(self, provided_dir):
63
+ """Get current feature directory"""
64
+ if provided_dir:
65
+ return self.sanitize_feature_dir(provided_dir)
66
+ else:
67
+ return self.find_feature_directory()
68
+
69
+ def validate_specification(self, spec_file):
70
+ """Validate specification structure"""
71
+ if not spec_file.exists():
72
+ self.error_exit(f"Specification file does not exist: {spec_file}")
73
+
74
+ # Required sections
75
+ required_sections = [
76
+ "## Specification:",
77
+ "## Metadata",
78
+ "## Functional Requirements",
79
+ "## Acceptance Scenarios"
80
+ ]
81
+
82
+ missing_sections = []
83
+ content = spec_file.read_text(encoding='utf-8')
84
+
85
+ for section in required_sections:
86
+ if section not in content:
87
+ missing_sections.append(section)
88
+
89
+ if missing_sections:
90
+ self.log(f"WARNING: Missing required sections: {', '.join(missing_sections)}")
91
+
92
+ # Check for clarifications needed
93
+ clarification_matches = re.findall(r'NEEDS CLARIFICATION', content)
94
+ if clarification_matches:
95
+ clarification_count = len(clarification_matches)
96
+ self.log(f"WARNING: Specification has {clarification_count} clarifications needed")
97
+ return clarification_count
98
+
99
+ return 0
100
+
101
+ def main(self, args):
102
+ """Main execution function"""
103
+ if len(args) < 1:
104
+ self.error_exit("Usage: python pulse-spec.py <feature-dir> [spec-content]")
105
+
106
+ feature_dir = args[0]
107
+ spec_content = args[1] if len(args) > 1 else None
108
+
109
+ self.log("Processing specification...")
110
+
111
+ # Get and sanitize feature directory
112
+ sanitized_dir = self.get_current_feature_dir(feature_dir)
113
+
114
+ # Set file paths
115
+ spec_file = self.project_root / "specs" / sanitized_dir / "spec.md"
116
+ template_file = self.templates_dir / "spec.md"
117
+
118
+ # Ensure specs directory exists
119
+ spec_file.parent.mkdir(parents=True, exist_ok=True)
120
+
121
+ # Handle specification content
122
+ if spec_content:
123
+ self.log(f"Updating specification: {spec_file}")
124
+ try:
125
+ spec_file.write_text(spec_content, encoding='utf-8')
126
+ except Exception as e:
127
+ self.error_exit(f"Failed to write specification content: {e}")
128
+ else:
129
+ # Ensure specification exists from template
130
+ if not spec_file.exists():
131
+ if not template_file.exists():
132
+ self.error_exit(f"Template not found: {template_file}")
133
+
134
+ self.log(f"Creating specification from template: {spec_file}")
135
+ try:
136
+ spec_file.write_text(template_file.read_text(encoding='utf-8'))
137
+ except Exception as e:
138
+ self.error_exit(f"Failed to copy specification template: {e}")
139
+ else:
140
+ self.log(f"Specification already exists: {spec_file}")
141
+
142
+ # Validate specification
143
+ self.log("Validating specification...")
144
+ clarification_count = self.validate_specification(spec_file)
145
+
146
+ # Check for missing sections
147
+ required_sections = [
148
+ "## Specification:",
149
+ "## Metadata",
150
+ "## Functional Requirements",
151
+ "## Acceptance Scenarios"
152
+ ]
153
+
154
+ content = spec_file.read_text(encoding='utf-8')
155
+ missing_sections = [s for s in required_sections if s not in content]
156
+
157
+ self.log("Specification processing completed successfully")
158
+
159
+ # Output results
160
+ print(f"SPEC_FILE={spec_file}")
161
+ print(f"CLARIFICATIONS_NEEDED={clarification_count}")
162
+ print(f"MISSING_SECTIONS={len(missing_sections)}")
163
+ print("STATUS=updated")
164
+
165
+ if __name__ == "__main__":
166
+ spec = SpecPulseSpec()
167
+ spec.main(sys.argv[1:])