specpulse 1.0.3__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.
- specpulse/__init__.py +1 -1
- specpulse/resources/commands/claude/plan.md +180 -50
- specpulse/resources/commands/claude/pulse.md +79 -24
- specpulse/resources/commands/claude/spec.md +156 -43
- specpulse/resources/commands/claude/task.md +229 -48
- specpulse/resources/commands/gemini/plan.toml +59 -48
- specpulse/resources/commands/gemini/pulse.toml +21 -39
- specpulse/resources/commands/gemini/spec.toml +40 -28
- specpulse/resources/commands/gemini/task.toml +69 -51
- specpulse/resources/memory/constitution.md +2 -2
- specpulse/resources/scripts/pulse-init.ps1 +186 -0
- specpulse/resources/scripts/pulse-init.py +171 -0
- specpulse/resources/scripts/pulse-init.sh +80 -21
- specpulse/resources/scripts/pulse-plan.ps1 +251 -0
- specpulse/resources/scripts/pulse-plan.py +191 -0
- specpulse/resources/scripts/pulse-plan.sh +113 -12
- specpulse/resources/scripts/pulse-spec.ps1 +185 -0
- specpulse/resources/scripts/pulse-spec.py +167 -0
- specpulse/resources/scripts/pulse-spec.sh +86 -11
- specpulse/resources/scripts/pulse-task.ps1 +263 -0
- specpulse/resources/scripts/pulse-task.py +237 -0
- specpulse/resources/scripts/pulse-task.sh +123 -9
- specpulse/resources/templates/plan.md +142 -287
- specpulse/resources/templates/spec.md +80 -246
- specpulse/resources/templates/task.md +114 -93
- {specpulse-1.0.3.dist-info → specpulse-1.0.5.dist-info}/METADATA +67 -34
- specpulse-1.0.5.dist-info/RECORD +41 -0
- specpulse-1.0.3.dist-info/RECORD +0 -33
- {specpulse-1.0.3.dist-info → specpulse-1.0.5.dist-info}/WHEEL +0 -0
- {specpulse-1.0.3.dist-info → specpulse-1.0.5.dist-info}/entry_points.txt +0 -0
- {specpulse-1.0.3.dist-info → specpulse-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {specpulse-1.0.3.dist-info → specpulse-1.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
# SpecPulse Task Generation Script
|
2
|
+
# Cross-platform PowerShell equivalent of pulse-task.sh
|
3
|
+
|
4
|
+
param(
|
5
|
+
[Parameter(Mandatory=$true)]
|
6
|
+
[string]$FeatureDir,
|
7
|
+
|
8
|
+
[string]$TaskContent = ""
|
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-TaskStructure {
|
81
|
+
param([string]$TaskFile)
|
82
|
+
|
83
|
+
if (-not (Test-Path $TaskFile)) {
|
84
|
+
Exit-WithError "Task file does not exist: $TaskFile"
|
85
|
+
}
|
86
|
+
|
87
|
+
# Required sections
|
88
|
+
$RequiredSections = @(
|
89
|
+
"# Task List:",
|
90
|
+
"## Progress Tracking",
|
91
|
+
"### Parallel Group"
|
92
|
+
)
|
93
|
+
|
94
|
+
$Content = Get-Content $TaskFile -Raw -Encoding UTF8
|
95
|
+
$MissingSections = @()
|
96
|
+
|
97
|
+
foreach ($Section in $RequiredSections) {
|
98
|
+
if ($Content -notmatch [regex]::Escape($Section)) {
|
99
|
+
$MissingSections += $Section
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
if ($MissingSections.Count -gt 0) {
|
104
|
+
Write-Log "WARNING: Missing required sections: $($MissingSections -join ', ')"
|
105
|
+
}
|
106
|
+
|
107
|
+
return $MissingSections.Count
|
108
|
+
}
|
109
|
+
|
110
|
+
function Check-ConstitutionalGates {
|
111
|
+
param([string]$TaskFile)
|
112
|
+
|
113
|
+
if (-not (Test-Path $TaskFile)) {
|
114
|
+
Exit-WithError "Task file does not exist: $TaskFile"
|
115
|
+
}
|
116
|
+
|
117
|
+
$Content = Get-Content $TaskFile -Raw -Encoding UTF8
|
118
|
+
$GatesStatus = @{}
|
119
|
+
|
120
|
+
# Check for constitutional gates compliance
|
121
|
+
$Gates = @{
|
122
|
+
"Simplicity Gate" = "Simplicity Gate"
|
123
|
+
"Test-First Gate" = "Test-First Gate"
|
124
|
+
"Integration-First Gate" = "Integration-First Gate"
|
125
|
+
"Research Gate" = "Research Gate"
|
126
|
+
}
|
127
|
+
|
128
|
+
foreach ($Gate in $Gates.GetEnumerator()) {
|
129
|
+
if ($Content -match $Gate.Value) {
|
130
|
+
$GatesStatus[$Gate.Key] = "COMPLETED"
|
131
|
+
} else {
|
132
|
+
$GatesStatus[$Gate.Key] = "PENDING"
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
return $GatesStatus
|
137
|
+
}
|
138
|
+
|
139
|
+
function Analyze-Tasks {
|
140
|
+
param([string]$TaskFile)
|
141
|
+
|
142
|
+
if (-not (Test-Path $TaskFile)) {
|
143
|
+
Exit-WithError "Task file does not exist: $TaskFile"
|
144
|
+
}
|
145
|
+
|
146
|
+
$Content = Get-Content $TaskFile -Raw -Encoding UTF8
|
147
|
+
|
148
|
+
# Count tasks
|
149
|
+
$TaskMatches = [regex]::Matches($Content, '### T\d{3}:')
|
150
|
+
$TaskCount = $TaskMatches.Count
|
151
|
+
|
152
|
+
# Count parallel tasks
|
153
|
+
$ParallelMatches = [regex]::Matches($Content, '\[P\]')
|
154
|
+
$ParallelCount = $ParallelMatches.Count
|
155
|
+
|
156
|
+
# Check for completion status
|
157
|
+
$CompletionSection = $Content -match "## Progress Tracking"
|
158
|
+
$CompletedTasks = 0
|
159
|
+
$InProgressTasks = 0
|
160
|
+
$BlockedTasks = 0
|
161
|
+
|
162
|
+
if ($CompletionSection) {
|
163
|
+
# Look for progress tracking YAML section
|
164
|
+
if ($Content -match "completed:\s*(\d+)") {
|
165
|
+
$CompletedTasks = [int]$matches[1]
|
166
|
+
}
|
167
|
+
if ($Content -match "in_progress:\s*(\d+)") {
|
168
|
+
$InProgressTasks = [int]$matches[1]
|
169
|
+
}
|
170
|
+
if ($Content -match "blocked:\s*(\d+)") {
|
171
|
+
$BlockedTasks = [int]$matches[1]
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
return @{
|
176
|
+
TotalTasks = $TaskCount
|
177
|
+
ParallelTasks = $ParallelCount
|
178
|
+
CompletedTasks = $CompletedTasks
|
179
|
+
InProgressTasks = $InProgressTasks
|
180
|
+
BlockedTasks = $BlockedTasks
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
# Main execution
|
185
|
+
Write-Log "Processing tasks..."
|
186
|
+
|
187
|
+
# Get and sanitize feature directory
|
188
|
+
$SanitizedDir = Get-CurrentFeatureDir -ProvidedDir $FeatureDir
|
189
|
+
|
190
|
+
# Set file paths
|
191
|
+
$TaskFile = Join-Path $ProjectRoot "tasks" $SanitizedDir "tasks.md"
|
192
|
+
$PlanFile = Join-Path $ProjectRoot "plans" $SanitizedDir "plan.md"
|
193
|
+
$TemplateFile = Join-Path $TemplatesDir "task.md"
|
194
|
+
|
195
|
+
# Ensure tasks directory exists
|
196
|
+
$TaskDir = Split-Path $TaskFile -Parent
|
197
|
+
New-Item -ItemType Directory -Path $TaskDir -Force | Out-Null
|
198
|
+
|
199
|
+
# Handle task content
|
200
|
+
if ($TaskContent) {
|
201
|
+
Write-Log "Updating task file: $TaskFile"
|
202
|
+
try {
|
203
|
+
Set-Content -Path $TaskFile -Value $TaskContent -Encoding UTF8
|
204
|
+
} catch {
|
205
|
+
Exit-WithError "Failed to write task content: $_"
|
206
|
+
}
|
207
|
+
} else {
|
208
|
+
# Ensure task file exists from template
|
209
|
+
if (-not (Test-Path $TaskFile)) {
|
210
|
+
if (-not (Test-Path $TemplateFile)) {
|
211
|
+
Exit-WithError "Template not found: $TemplateFile"
|
212
|
+
}
|
213
|
+
|
214
|
+
Write-Log "Creating task file from template: $TaskFile"
|
215
|
+
try {
|
216
|
+
Copy-Item $TemplateFile $TaskFile -Force
|
217
|
+
} catch {
|
218
|
+
Exit-WithError "Failed to copy task template: $_"
|
219
|
+
}
|
220
|
+
} else {
|
221
|
+
Write-Log "Task file already exists: $TaskFile"
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
# Validate task structure
|
226
|
+
Write-Log "Validating task structure..."
|
227
|
+
$MissingSections = Validate-TaskStructure -TaskFile $TaskFile
|
228
|
+
|
229
|
+
# Check constitutional gates
|
230
|
+
Write-Log "Checking constitutional gates..."
|
231
|
+
$GatesStatus = Check-ConstitutionalGates -TaskFile $TaskFile
|
232
|
+
|
233
|
+
# Analyze tasks
|
234
|
+
Write-Log "Analyzing tasks..."
|
235
|
+
$TaskAnalysis = Analyze-Tasks -TaskFile $TaskFile
|
236
|
+
|
237
|
+
# Calculate completion percentage
|
238
|
+
$CompletionPercentage = 0
|
239
|
+
if ($TaskAnalysis.TotalTasks -gt 0) {
|
240
|
+
$CompletionPercentage = [math]::Round(($TaskAnalysis.CompletedTasks / $TaskAnalysis.TotalTasks) * 100, 2)
|
241
|
+
}
|
242
|
+
|
243
|
+
# Count pending constitutional gates
|
244
|
+
$PendingGates = 0
|
245
|
+
foreach ($Status in $GatesStatus.Values) {
|
246
|
+
if ($Status -eq "PENDING") {
|
247
|
+
$PendingGates++
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
Write-Log "Task processing completed successfully"
|
252
|
+
|
253
|
+
# Output results
|
254
|
+
Write-Host "TASK_FILE=$TaskFile"
|
255
|
+
Write-Host "TOTAL_TASKS=$($TaskAnalysis.TotalTasks)"
|
256
|
+
Write-Host "COMPLETED_TASKS=$($TaskAnalysis.CompletedTasks)"
|
257
|
+
Write-Host "IN_PROGRESS_TASKS=$($TaskAnalysis.InProgressTasks)"
|
258
|
+
Write-Host "BLOCKED_TASKS=$($TaskAnalysis.BlockedTasks)"
|
259
|
+
Write-Host "PARALLEL_TASKS=$($TaskAnalysis.ParallelTasks)"
|
260
|
+
Write-Host "COMPLETION_PERCENTAGE=$CompletionPercentage"
|
261
|
+
Write-Host "MISSING_SECTIONS=$MissingSections"
|
262
|
+
Write-Host "CONSTITUTIONAL_GATES_PENDING=$PendingGates"
|
263
|
+
Write-Host "STATUS=processed"
|
@@ -0,0 +1,237 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
SpecPulse Task Generation Script
|
4
|
+
Cross-platform Python equivalent of pulse-task.sh
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import re
|
10
|
+
from pathlib import Path
|
11
|
+
import datetime
|
12
|
+
|
13
|
+
class SpecPulseTask:
|
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 analyze_tasks(self, task_file):
|
73
|
+
"""Analyze task structure and count statuses"""
|
74
|
+
if not task_file.exists():
|
75
|
+
return {
|
76
|
+
'total': 0,
|
77
|
+
'completed': 0,
|
78
|
+
'pending': 0,
|
79
|
+
'blocked': 0,
|
80
|
+
'parallel': 0
|
81
|
+
}
|
82
|
+
|
83
|
+
content = task_file.read_text(encoding='utf-8')
|
84
|
+
|
85
|
+
# Count different task states
|
86
|
+
total_tasks = len(re.findall(r'^- \[.\]', content, re.MULTILINE))
|
87
|
+
completed_tasks = len(re.findall(r'^- \[x\]', content, re.MULTILINE))
|
88
|
+
pending_tasks = len(re.findall(r'^- \[ \]', content, re.MULTILINE))
|
89
|
+
blocked_tasks = len(re.findall(r'^- \[!\]', content, re.MULTILINE))
|
90
|
+
parallel_tasks = len(re.findall(r'\[P\]', content))
|
91
|
+
|
92
|
+
return {
|
93
|
+
'total': total_tasks,
|
94
|
+
'completed': completed_tasks,
|
95
|
+
'pending': pending_tasks,
|
96
|
+
'blocked': blocked_tasks,
|
97
|
+
'parallel': parallel_tasks
|
98
|
+
}
|
99
|
+
|
100
|
+
def validate_task_structure(self, task_file):
|
101
|
+
"""Validate task structure"""
|
102
|
+
required_sections = [
|
103
|
+
"## Task List:",
|
104
|
+
"## Task Organization",
|
105
|
+
"## Critical Path",
|
106
|
+
"## Execution Schedule"
|
107
|
+
]
|
108
|
+
|
109
|
+
missing_sections = []
|
110
|
+
content = task_file.read_text(encoding='utf-8')
|
111
|
+
|
112
|
+
for section in required_sections:
|
113
|
+
if section not in content:
|
114
|
+
missing_sections.append(section)
|
115
|
+
|
116
|
+
if missing_sections:
|
117
|
+
self.log(f"WARNING: Missing required sections: {', '.join(missing_sections)}")
|
118
|
+
|
119
|
+
return len(missing_sections)
|
120
|
+
|
121
|
+
def check_constitutional_gates(self, task_file):
|
122
|
+
"""Check constitutional gates compliance"""
|
123
|
+
if not task_file.exists():
|
124
|
+
return 0
|
125
|
+
|
126
|
+
content = task_file.read_text(encoding='utf-8')
|
127
|
+
|
128
|
+
# Find constitutional gates section
|
129
|
+
constitutional_section = re.search(
|
130
|
+
r'Constitutional Gates Compliance.*?(?=\n##|\Z)',
|
131
|
+
content,
|
132
|
+
re.DOTALL
|
133
|
+
)
|
134
|
+
|
135
|
+
if constitutional_section:
|
136
|
+
gates_text = constitutional_section.group(0)
|
137
|
+
pending_gates = len(re.findall(r'\[ \]', gates_text))
|
138
|
+
return pending_gates
|
139
|
+
|
140
|
+
return 0
|
141
|
+
|
142
|
+
def check_plan_gates_status(self, plan_file):
|
143
|
+
"""Check if plan has constitutional gates completed"""
|
144
|
+
if not plan_file.exists():
|
145
|
+
return "UNKNOWN"
|
146
|
+
|
147
|
+
content = plan_file.read_text(encoding='utf-8')
|
148
|
+
gate_status_match = re.search(r'Gate Status:\s*\[([^\]]+)\]', content)
|
149
|
+
|
150
|
+
if gate_status_match:
|
151
|
+
return gate_status_match.group(1)
|
152
|
+
else:
|
153
|
+
return "UNKNOWN"
|
154
|
+
|
155
|
+
def main(self, args):
|
156
|
+
"""Main execution function"""
|
157
|
+
if len(args) < 1:
|
158
|
+
self.error_exit("Usage: python pulse-task.py <feature-dir>")
|
159
|
+
|
160
|
+
feature_dir = args[0]
|
161
|
+
|
162
|
+
self.log("Processing task breakdown...")
|
163
|
+
|
164
|
+
# Get and sanitize feature directory
|
165
|
+
sanitized_dir = self.get_current_feature_dir(feature_dir)
|
166
|
+
|
167
|
+
# Set file paths
|
168
|
+
task_file = self.project_root / "tasks" / sanitized_dir / "tasks.md"
|
169
|
+
template_file = self.templates_dir / "task.md"
|
170
|
+
plan_file = self.project_root / "plans" / sanitized_dir / "plan.md"
|
171
|
+
spec_file = self.project_root / "specs" / sanitized_dir / "spec.md"
|
172
|
+
|
173
|
+
# Ensure tasks directory exists
|
174
|
+
task_file.parent.mkdir(parents=True, exist_ok=True)
|
175
|
+
|
176
|
+
# Check if specification and plan exist first
|
177
|
+
if not spec_file.exists():
|
178
|
+
self.error_exit(f"Specification file not found: {spec_file}. Please create specification first.")
|
179
|
+
|
180
|
+
if not plan_file.exists():
|
181
|
+
self.error_exit(f"Implementation plan not found: {plan_file}. Please create plan first.")
|
182
|
+
|
183
|
+
# Ensure task template exists
|
184
|
+
if not template_file.exists():
|
185
|
+
self.error_exit(f"Template not found: {template_file}")
|
186
|
+
|
187
|
+
# Create task file if it doesn't exist
|
188
|
+
if not task_file.exists():
|
189
|
+
self.log(f"Creating task breakdown from template: {task_file}")
|
190
|
+
try:
|
191
|
+
task_file.write_text(template_file.read_text(encoding='utf-8'))
|
192
|
+
except Exception as e:
|
193
|
+
self.error_exit(f"Failed to copy task template: {e}")
|
194
|
+
else:
|
195
|
+
self.log(f"Task breakdown already exists: {task_file}")
|
196
|
+
|
197
|
+
# Validate task structure
|
198
|
+
self.log("Validating task breakdown...")
|
199
|
+
missing_sections = self.validate_task_structure(task_file)
|
200
|
+
|
201
|
+
# Analyze tasks
|
202
|
+
task_stats = self.analyze_tasks(task_file)
|
203
|
+
|
204
|
+
# Check constitutional gates compliance
|
205
|
+
pending_gates = self.check_constitutional_gates(task_file)
|
206
|
+
|
207
|
+
# Check if plan has constitutional gates completed
|
208
|
+
plan_gate_status = self.check_plan_gates_status(plan_file)
|
209
|
+
|
210
|
+
if plan_gate_status != "COMPLETED":
|
211
|
+
self.log(f"WARNING: Implementation plan constitutional gates not completed. Status: {plan_gate_status}")
|
212
|
+
|
213
|
+
# Calculate completion percentage
|
214
|
+
if task_stats['total'] > 0:
|
215
|
+
completion_percentage = int((task_stats['completed'] / task_stats['total']) * 100)
|
216
|
+
else:
|
217
|
+
completion_percentage = 0
|
218
|
+
|
219
|
+
self.log(f"Task analysis completed - Total: {task_stats['total']}, Completed: {task_stats['completed']} ({completion_percentage}%), Parallel: {task_stats['parallel']}")
|
220
|
+
|
221
|
+
# Output comprehensive status
|
222
|
+
print(f"TASK_FILE={task_file}")
|
223
|
+
print(f"SPEC_FILE={spec_file}")
|
224
|
+
print(f"PLAN_FILE={plan_file}")
|
225
|
+
print(f"TOTAL_TASKS={task_stats['total']}")
|
226
|
+
print(f"COMPLETED_TASKS={task_stats['completed']}")
|
227
|
+
print(f"PENDING_TASKS={task_stats['pending']}")
|
228
|
+
print(f"BLOCKED_TASKS={task_stats['blocked']}")
|
229
|
+
print(f"PARALLEL_TASKS={task_stats['parallel']}")
|
230
|
+
print(f"CONSTITUTIONAL_GATES_PENDING={pending_gates}")
|
231
|
+
print(f"COMPLETION_PERCENTAGE={completion_percentage}")
|
232
|
+
print(f"MISSING_SECTIONS={missing_sections}")
|
233
|
+
print("STATUS=generated")
|
234
|
+
|
235
|
+
if __name__ == "__main__":
|
236
|
+
task = SpecPulseTask()
|
237
|
+
task.main(sys.argv[1:])
|
@@ -1,23 +1,137 @@
|
|
1
1
|
#!/bin/bash
|
2
2
|
# Generate task breakdown
|
3
3
|
|
4
|
-
|
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
|
-
|
8
|
-
|
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
|
-
TASK_FILE="tasks/${FEATURE_DIR}/tasks.md"
|
49
|
+
TASK_FILE="$PROJECT_ROOT/tasks/${FEATURE_DIR}/tasks.md"
|
50
|
+
TEMPLATE_FILE="$PROJECT_ROOT/templates/task.md"
|
51
|
+
PLAN_FILE="$PROJECT_ROOT/plans/${FEATURE_DIR}/plan.md"
|
52
|
+
SPEC_FILE="$PROJECT_ROOT/specs/${FEATURE_DIR}/spec.md"
|
53
|
+
|
54
|
+
# Ensure tasks directory exists
|
55
|
+
mkdir -p "$(dirname "$TASK_FILE")"
|
56
|
+
|
57
|
+
# Check if specification and plan exist first
|
58
|
+
if [ ! -f "$SPEC_FILE" ]; then
|
59
|
+
error_exit "Specification file not found: $SPEC_FILE. Please create specification first."
|
60
|
+
fi
|
12
61
|
|
13
|
-
|
62
|
+
if [ ! -f "$PLAN_FILE" ]; then
|
63
|
+
error_exit "Implementation plan not found: $PLAN_FILE. Please create plan first."
|
64
|
+
fi
|
65
|
+
|
66
|
+
# Ensure task template exists
|
67
|
+
if [ ! -f "$TEMPLATE_FILE" ]; then
|
68
|
+
error_exit "Template not found: $TEMPLATE_FILE"
|
69
|
+
fi
|
70
|
+
|
71
|
+
# Create task file if it doesn't exist
|
14
72
|
if [ ! -f "$TASK_FILE" ]; then
|
15
|
-
|
73
|
+
log "Creating task breakdown from template: $TASK_FILE"
|
74
|
+
cp "$TEMPLATE_FILE" "$TASK_FILE" || error_exit "Failed to copy task template"
|
75
|
+
else
|
76
|
+
log "Task breakdown already exists: $TASK_FILE"
|
77
|
+
fi
|
78
|
+
|
79
|
+
# Validate task structure
|
80
|
+
log "Validating task breakdown..."
|
81
|
+
|
82
|
+
# Check for required sections
|
83
|
+
REQUIRED_SECTIONS=("## Task List:" "## Task Organization" "## Critical Path" "## Execution Schedule")
|
84
|
+
MISSING_SECTIONS=()
|
85
|
+
|
86
|
+
for section in "${REQUIRED_SECTIONS[@]}"; do
|
87
|
+
if ! grep -q "$section" "$TASK_FILE"; then
|
88
|
+
MISSING_SECTIONS+=("$section")
|
89
|
+
fi
|
90
|
+
done
|
91
|
+
|
92
|
+
if [ ${#MISSING_SECTIONS[@]} -gt 0 ]; then
|
93
|
+
log "WARNING: Missing required sections: ${MISSING_SECTIONS[*]}"
|
94
|
+
fi
|
95
|
+
|
96
|
+
# Count tasks and analyze structure
|
97
|
+
TOTAL_TASKS=$(grep -c "^- \[.\]" "$TASK_FILE" 2>/dev/null || echo "0")
|
98
|
+
COMPLETED_TASKS=$(grep -c "^- \[x\]" "$TASK_FILE" 2>/dev/null || echo "0")
|
99
|
+
PENDING_TASKS=$(grep -c "^- \[ \]" "$TASK_FILE" 2>/dev/null || echo "0")
|
100
|
+
BLOCKED_TASKS=$(grep -c "^- \[!\]" "$TASK_FILE" 2>/dev/null || echo "0")
|
101
|
+
|
102
|
+
# Check for parallel tasks
|
103
|
+
PARALLEL_TASKS=$(grep -c "\[P\]" "$TASK_FILE" 2>/dev/null || echo "0")
|
104
|
+
|
105
|
+
# Check constitutional gates compliance
|
106
|
+
CONSTITUTIONAL_SECTION=$(grep -A 20 "Constitutional Gates Compliance" "$TASK_FILE" 2>/dev/null || echo "")
|
107
|
+
GATES_COUNT=$(echo "$CONSTITUTIONAL_SECTION" | grep -c "\[ \]" 2>/dev/null || echo "0")
|
108
|
+
|
109
|
+
# Check if plan has constitutional gates completed
|
110
|
+
PLAN_GATE_STATUS=$(grep -A5 "Gate Status:" "$PLAN_FILE" | tail -1 | sed 's/.*\[\(.*\)\].*/\1/' || echo "PENDING")
|
111
|
+
|
112
|
+
if [ "$PLAN_GATE_STATUS" != "COMPLETED" ]; then
|
113
|
+
log "WARNING: Implementation plan constitutional gates not completed. Task generation may be premature."
|
114
|
+
fi
|
115
|
+
|
116
|
+
# Calculate completion percentage
|
117
|
+
if [ "$TOTAL_TASKS" -gt 0 ]; then
|
118
|
+
COMPLETION_PERCENTAGE=$((COMPLETED_TASKS * 100 / TOTAL_TASKS))
|
119
|
+
else
|
120
|
+
COMPLETION_PERCENTAGE=0
|
16
121
|
fi
|
17
122
|
|
18
|
-
|
19
|
-
TASK_COUNT=$(grep -c "^- \[" "$TASK_FILE" 2>/dev/null || echo "0")
|
123
|
+
log "Task analysis completed - Total: $TOTAL_TASKS, Completed: $COMPLETED_TASKS ($COMPLETION_PERCENTAGE%), Parallel: $PARALLEL_TASKS"
|
20
124
|
|
125
|
+
# Output comprehensive status
|
21
126
|
echo "TASK_FILE=$TASK_FILE"
|
22
|
-
echo "
|
127
|
+
echo "SPEC_FILE=$SPEC_FILE"
|
128
|
+
echo "PLAN_FILE=$PLAN_FILE"
|
129
|
+
echo "TOTAL_TASKS=$TOTAL_TASKS"
|
130
|
+
echo "COMPLETED_TASKS=$COMPLETED_TASKS"
|
131
|
+
echo "PENDING_TASKS=$PENDING_TASKS"
|
132
|
+
echo "BLOCKED_TASKS=$BLOCKED_TASKS"
|
133
|
+
echo "PARALLEL_TASKS=$PARALLEL_TASKS"
|
134
|
+
echo "CONSTITUTIONAL_GATES_PENDING=$GATES_COUNT"
|
135
|
+
echo "COMPLETION_PERCENTAGE=$COMPLETION_PERCENTAGE"
|
136
|
+
echo "MISSING_SECTIONS=${#MISSING_SECTIONS[@]}"
|
23
137
|
echo "STATUS=generated"
|