Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 97 additions & 26 deletions script.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ def count_requirements(text: str) -> int:
return len(set(re.findall(r'REQ-\d{3}', text)))


def strip_code_blocks(text: str) -> str:
"""Strip markdown fenced and inline code blocks to prevent false positive linting."""
if not text:
return ""
# Remove fenced code blocks
text = re.sub(r'```.*?```', '', text, flags=re.DOTALL)
# Remove inline code blocks
text = re.sub(r'`[^`\n]+`', '', text)
return text


def has_section(text: str, heading: str) -> bool:
"""Check if markdown heading exists (case-insensitive)."""
pattern = r'^#{1,3}\s+.*' + re.escape(heading) + r'.*$'
Expand All @@ -77,19 +88,51 @@ def get_section_content(text: str, heading: str) -> str:
level = 0
content = []
heading_re = re.compile(r'^(#{1,6})\s+(.*)')

target = heading.strip().lower()
# Step 1: Try exact matching on cleaned headers
for line in lines:
heading_match = heading_re.match(line)
if heading_match:
h_text = heading_match.group(2).strip().lower()
cleaned_h_text = re.sub(r'^(?:step\s+\d+[:\s]|\d+(?:\.\d+)*[:\s]|-)\s*', '', h_text).strip()
if cleaned_h_text == target:
capturing = True
level = len(heading_match.group(1))
content = []
continue
elif capturing and len(heading_match.group(1)) <= level:
break
elif capturing:
content.append(line)

if content:
return '\n'.join(content).strip()

# Step 2: Fallback to substring search (protecting against non-functional overlap)
capturing = False
content = []
for line in lines:
heading_match = heading_re.match(line)
if heading_match and heading.lower() in heading_match.group(2).lower():
capturing = True
level = len(heading_match.group(1))
continue
if capturing:
if heading_match and len(heading_match.group(1)) <= level:
if heading_match:
h_text = heading_match.group(2).strip().lower()
cleaned_h_text = re.sub(r'^(?:step\s+\d+[:\s]|\d+(?:\.\d+)*[:\s]|-)\s*', '', h_text).strip()
if target in cleaned_h_text:
if target == "functional requirements" and "non-functional" in cleaned_h_text:
continue
capturing = True
level = len(heading_match.group(1))
content = []
continue
elif capturing and len(heading_match.group(1)) <= level:
break
elif capturing:
content.append(line)

return '\n'.join(content).strip()



# ─── VAGUE_PATTERNS for validation ───────────────────────────────────────────

VAGUE_WORDS = [
Expand Down Expand Up @@ -136,9 +179,9 @@ def cmd_preflight(args: argparse.Namespace) -> None:
tasks = data.get("tasks", data) if isinstance(data, dict) else data
if isinstance(tasks, list):
task_count = len(tasks)
tasks_completed = sum(1 for t in tasks if t.get("status") == "done")
tasks_completed = sum(1 for t in tasks if isinstance(t, dict) and t.get("status") == "done")
tasks_pending = task_count - tasks_completed
except (json.JSONDecodeError, KeyError):
except Exception:
pass

# Detect taskmaster method
Expand Down Expand Up @@ -214,6 +257,8 @@ def _read_execution_state() -> dict:
try:
with open(state_file) as f:
state = json.load(f)
if not isinstance(state, dict):
return {"has_incomplete": False}
return {
"has_incomplete": state.get("status") == "in_progress",
"last_task": state.get("current_task"),
Expand All @@ -222,7 +267,7 @@ def _read_execution_state() -> dict:
"last_updated": state.get("last_updated"),
"checkpoint": state.get("last_checkpoint"),
}
except (json.JSONDecodeError, KeyError):
except Exception:
return {"has_incomplete": False}


Expand Down Expand Up @@ -264,9 +309,13 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# ─── Required Elements (9 checks, 5 points each = 45 points) ─────────

# Pre-process text to strip code blocks (avoids false matches in code blocks)
clean_text = strip_code_blocks(text)
clean_text_no_toc = re.sub(r'^\s*[\d\.\-]+\s*\[.*?\]\(#.*?\)\s*$', '', clean_text, flags=re.MULTILINE)

# Check 1: Executive summary exists and is 50-200 words
exec_summary = get_section_content(text, "Executive Summary")
wc = word_count(exec_summary)
wc = word_count(strip_code_blocks(exec_summary))
checks.append({
"id": 1,
"category": "required",
Expand All @@ -278,8 +327,9 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 2: Problem statement includes user impact
problem = get_section_content(text, "Problem Statement")
clean_problem = strip_code_blocks(problem)
has_user_impact = bool(
re.search(r'user\s+impact|who\s+is\s+affected|pain\s+point', problem, re.IGNORECASE)
re.search(r'user\s+impact|who\s+is\s+affected|pain\s+point', clean_problem, re.IGNORECASE)
or has_section(text, "User Impact")
)
checks.append({
Expand All @@ -293,7 +343,7 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 3: Problem statement includes business impact
has_biz_impact = bool(
re.search(r'business\s+impact|revenue|cost|strategic', problem, re.IGNORECASE)
re.search(r'business\s+impact|revenue|cost|strategic', clean_problem, re.IGNORECASE)
or has_section(text, "Business Impact")
)
checks.append({
Expand All @@ -307,9 +357,10 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 4: Goals have SMART metrics
goals_section = get_section_content(text, "Goals")
clean_goals = strip_code_blocks(goals_section)
has_smart = bool(re.search(
r'(metric|baseline|target|timeframe|measurement)',
goals_section, re.IGNORECASE
clean_goals, re.IGNORECASE
))
checks.append({
"id": 4,
Expand All @@ -322,10 +373,13 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 5: User stories have acceptance criteria (min 3 per story)
stories_section = get_section_content(text, "User Stories")
story_blocks = re.split(r'###\s+Story\s+\d+', stories_section)
clean_stories = strip_code_blocks(stories_section)
# Split story blocks robustly on level 3 headings
story_blocks = re.split(r'^###\s+.*$', clean_stories, flags=re.MULTILINE)
ac_counts = []
for block in story_blocks[1:]: # skip pre-heading text
ac_matches = re.findall(r'- \[[ x]\]', block)
# Count checkboxes and lists robustly
ac_matches = re.findall(r'^\s*[-*+]\s+(?:\[[ x]\]\s+)?\S+|^\s*\d+\.\s+\S+', block, flags=re.MULTILINE)
ac_counts.append(len(ac_matches))
stories_ok = all(c >= 3 for c in ac_counts) if ac_counts else False
checks.append({
Expand All @@ -341,7 +395,8 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:
reqs_section = get_section_content(text, "Functional Requirements")
if not reqs_section:
reqs_section = get_section_content(text, "Requirements")
vague_in_reqs = VAGUE_PATTERN.findall(reqs_section)
clean_reqs = strip_code_blocks(reqs_section)
vague_in_reqs = VAGUE_PATTERN.findall(clean_reqs)
checks.append({
"id": 6,
"category": "required",
Expand All @@ -354,7 +409,7 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:
# Check 7: Each requirement has priority (Must/Should/Could or P0/P1/P2)
has_priority = bool(re.search(
r'(must\s+have|should\s+have|could\s+have|nice\s+to\s+have|P0|P1|P2)',
reqs_section, re.IGNORECASE
clean_reqs, re.IGNORECASE
))
checks.append({
"id": 7,
Expand All @@ -366,7 +421,7 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:
})

# Check 8: Requirements are numbered (REQ-NNN)
req_count = count_requirements(text)
req_count = count_requirements(clean_text)
checks.append({
"id": 8,
"category": "required",
Expand All @@ -378,9 +433,10 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 9: Technical considerations address architecture
tech_section = get_section_content(text, "Technical")
clean_tech = strip_code_blocks(tech_section)
has_arch = bool(re.search(
r'(architecture|system\s+design|component|integration|diagram)',
tech_section, re.IGNORECASE
clean_tech, re.IGNORECASE
))
checks.append({
"id": 9,
Expand All @@ -395,9 +451,10 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 10: Non-functional requirements have specific targets
nfr_section = get_section_content(text, "Non-Functional")
clean_nfr = strip_code_blocks(nfr_section)
has_nfr_targets = bool(re.search(
r'\d+\s*(ms|seconds?|minutes?|%|MB|GB|requests?/s)',
nfr_section, re.IGNORECASE
clean_nfr, re.IGNORECASE
))
checks.append({
"id": 10,
Expand All @@ -410,8 +467,8 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 11: Requirements have task breakdown hints
has_task_hints = bool(re.search(
r'task\s+breakdown|implementation\s+step|~\d+h',
text, re.IGNORECASE
r'~\s*\d+(?:\.\d+)?\s*[hm]\b|estimate\s*:\s*\d+|breakdown\s*:\s*\S+',
clean_text_no_toc, re.IGNORECASE
))
checks.append({
"id": 11,
Expand All @@ -424,8 +481,8 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:

# Check 12: Dependencies identified
has_deps = bool(re.search(
r'(dependenc|depends\s+on|blocked\s+by|prerequisite|REQ-\d{3}.*depends)',
text, re.IGNORECASE
r'depends\s+on\s+REQ-\d{3}|blocked\s+by\s+REQ-\d{3}|prerequisite\s*:\s*REQ-\d{3}|REQ-\d{3}\s+depends\s+on',
clean_text_no_toc, re.IGNORECASE
))
checks.append({
"id": 12,
Expand All @@ -449,7 +506,7 @@ def cmd_validate_prd(args: argparse.Namespace) -> None:
})

# ─── Vague language warnings ─────────────────────────────────────────
all_vague = VAGUE_PATTERN.findall(text)
all_vague = VAGUE_PATTERN.findall(clean_text)
vague_penalty = min(len(all_vague), 5)
for match in set(all_vague):
warnings.append({
Expand Down Expand Up @@ -766,10 +823,18 @@ def now_iso():
set -euo pipefail

TASK_NUM="${1:?Usage: rollback.sh <task_number>}"
TAG="checkpoint-task-$(printf '%03d' "$TASK_NUM")"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_BRANCH="rollback-backup-${TIMESTAMP}"

# Support both padded (005) and unpadded (5) tags
if git rev-parse "checkpoint-task-$TASK_NUM" >/dev/null 2>&1; then
TAG="checkpoint-task-$TASK_NUM"
elif git rev-parse "checkpoint-task-$(printf '%03d' "$TASK_NUM")" >/dev/null 2>&1; then
TAG="checkpoint-task-$(printf '%03d' "$TASK_NUM")"
else
TAG="checkpoint-task-$TASK_NUM"
fi

echo "Checking for checkpoint tag: $TAG"

if ! git rev-parse "$TAG" >/dev/null 2>&1; then
Expand All @@ -778,6 +843,12 @@ def now_iso():
exit 1
fi

# Check for uncommitted changes to prevent data loss
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "WARNING: You have uncommitted changes. Stashing them before rollback..."
git stash save "rollback-stash-${TIMESTAMP}"
fi

echo "Creating backup branch: $BACKUP_BRANCH"
git branch "$BACKUP_BRANCH"

Expand Down
35 changes: 20 additions & 15 deletions scripts/setup-taskmaster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,31 @@ echo "✅ Created subdirectories: docs/, tasks/, reports/"
# Update .gitignore to exclude taskmaster state files
GITIGNORE_FILE=".gitignore"

if [ -f "$GITIGNORE_FILE" ]; then
if ! grep -q ".taskmaster/state.json" "$GITIGNORE_FILE" 2>/dev/null; then
echo "" >> "$GITIGNORE_FILE"
echo "# Taskmaster AI state files" >> "$GITIGNORE_FILE"
echo ".taskmaster/state.json" >> "$GITIGNORE_FILE"
echo ".taskmaster/tasks/" >> "$GITIGNORE_FILE"
echo ".taskmaster/reports/" >> "$GITIGNORE_FILE"
echo "✅ Updated .gitignore with taskmaster exclusions"
add_to_gitignore() {
local entry="$1"
if [ -f "$GITIGNORE_FILE" ]; then
if ! grep -Fq "$entry" "$GITIGNORE_FILE" 2>/dev/null; then
echo "$entry" >> "$GITIGNORE_FILE"
echo "✅ Added $entry to .gitignore"
fi
else
echo "ℹ️ .gitignore already configured for taskmaster"
echo "$entry" >> "$GITIGNORE_FILE"
fi
else
# Create .gitignore if it doesn't exist
}

if [ ! -f "$GITIGNORE_FILE" ]; then
echo "# Taskmaster AI state files" > "$GITIGNORE_FILE"
echo ".taskmaster/state.json" >> "$GITIGNORE_FILE"
echo ".taskmaster/tasks/" >> "$GITIGNORE_FILE"
echo ".taskmaster/reports/" >> "$GITIGNORE_FILE"
echo "✅ Created .gitignore with taskmaster exclusions"
echo "✅ Created .gitignore"
fi

# Ensure all taskmaster paths are ignored
add_to_gitignore ".taskmaster/state.json"
add_to_gitignore ".taskmaster/tasks/"
add_to_gitignore ".taskmaster/reports/"
add_to_gitignore ".taskmaster/state/"
add_to_gitignore "local.zsh"


# Create placeholder README in docs/
if [ ! -f ".taskmaster/docs/README.md" ]; then
cat > .taskmaster/docs/README.md <<'EOF'
Expand Down
20 changes: 20 additions & 0 deletions templates/taskmaster-prd-comprehensive.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
---
prd_version: "1.0"
project: "[Feature/Product Name]"
author: "[Name]"
date: "[YYYY-MM-DD]"
status: "Draft"
complexity_target: "Medium"
requirements:
- id: "REQ-001"
title: "[Requirement 1 Title]"
priority: "Must"
dependencies: []
estimated_hours: 8
- id: "REQ-002"
title: "[Requirement 2 Title]"
priority: "Should"
dependencies: ["REQ-001"]
estimated_hours: 12
---

# PRD: [Feature/Product Name]

**Author:** [Name]
Expand Down
9 changes: 9 additions & 0 deletions templates/taskmaster-prd-minimal.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
---
prd_version: "1.0"
project: "[Feature Name]"
requirements:
- id: "REQ-001"
title: "[Requirement 1 Title]"
priority: "Must"
---

# PRD: [Feature Name]

**Author:** [Name]
Expand Down
Loading