punch-list-manager

SKILL.md

Punch List Manager for Construction Closeout

Complete system for managing construction punch lists from creation through final acceptance.

Business Case

Problem: Punch list management is inefficient:

  • Paper lists get lost or outdated
  • Difficult to track completion status
  • Photos disconnected from items
  • Back-charges delayed due to poor documentation
  • Multiple walks create duplicate items

Solution: Digital punch list system that:

  • Creates items with photos and location markup
  • Assigns to responsible parties with deadlines
  • Tracks completion with before/after photos
  • Generates back-charge documentation
  • Provides real-time completion dashboards

ROI: 50% faster closeout, 80% reduction in disputed back-charges

Punch List Workflow

┌──────────────────────────────────────────────────────────────────────┐
│                      PUNCH LIST WORKFLOW                              │
├──────────────────────────────────────────────────────────────────────┤
│                                                                       │
│   CREATION            ASSIGNMENT          COMPLETION                 │
│   ┌─────────┐         ┌─────────┐         ┌─────────┐               │
│   │ Walk    │────────►│ Assign  │────────►│ Correct │               │
│   │ Site    │         │ Items   │         │ Items   │               │
│   └─────────┘         └─────────┘         └─────────┘               │
│       │                   │                   │                      │
│       ▼                   ▼                   ▼                      │
│   ┌─────────┐         ┌─────────┐         ┌─────────┐               │
│   │ Log     │         │ Notify  │         │ Submit  │               │
│   │ Items   │         │ Parties │         │ Photo   │               │
│   └─────────┘         └─────────┘         └─────────┘               │
│       │                   │                   │                      │
│       ▼                   ▼                   ▼                      │
│   ┌─────────┐         ┌─────────┐         ┌─────────┐               │
│   │ Photo   │         │ Set     │         │ Mark    │               │
│   │ + Tag   │         │ Deadline│         │ Complete│               │
│   └─────────┘         └─────────┘         └─────────┘               │
│                                               │                      │
│                                               ▼                      │
│   VERIFICATION        CLOSEOUT           ┌─────────┐               │
│   ┌─────────┐         ┌─────────┐        │ Verify  │               │
│   │ Re-walk │◄────────│ Accept  │◄───────│ Work    │               │
│   │ Site    │         │ Items   │        └─────────┘               │
│   └─────────┘         └─────────┘                                   │
│       │                   │                                          │
│       ▼                   ▼                                          │
│   ┌─────────┐         ┌─────────┐                                   │
│   │ New     │         │ Final   │                                   │
│   │ Items?  │────NO──►│ Accept  │                                   │
│   └────┬────┘         └─────────┘                                   │
│        │YES                                                          │
│        └──────────────► Back to CREATION                            │
│                                                                       │
└──────────────────────────────────────────────────────────────────────┘

Data Structure

from dataclasses import dataclass, field
from datetime import datetime, date
from enum import Enum
from typing import List, Optional
import uuid

class PunchItemStatus(Enum):
    OPEN = "Open"
    ASSIGNED = "Assigned"
    IN_PROGRESS = "In Progress"
    READY_FOR_VERIFICATION = "Ready for Verification"
    VERIFIED = "Verified"
    REJECTED = "Rejected"
    ACCEPTED = "Accepted"

class PunchItemPriority(Enum):
    CRITICAL = "Critical"     # Life safety / code compliance
    HIGH = "High"             # Affects occupancy
    MEDIUM = "Medium"         # Standard punch
    LOW = "Low"               # Minor / cosmetic
    OBSERVATION = "Observation"

class TradeCategory(Enum):
    GENERAL = "General Contractor"
    ELECTRICAL = "Electrical"
    PLUMBING = "Plumbing"
    HVAC = "HVAC"
    FIRE_PROTECTION = "Fire Protection"
    DRYWALL = "Drywall/Painting"
    FLOORING = "Flooring"
    MILLWORK = "Millwork/Casework"
    GLAZING = "Glazing"
    ROOFING = "Roofing"
    SITEWORK = "Sitework"
    LANDSCAPING = "Landscaping"
    CONTROLS = "Controls/BMS"
    OTHER = "Other"

@dataclass
class PunchItem:
    item_id: str
    punch_list_id: str
    description: str
    location: str
    trade: TradeCategory
    priority: PunchItemPriority

    # Location details
    building: str = ""
    floor: str = ""
    room: str = ""

    # Assignment
    assigned_to: str = ""
    assigned_date: date = None
    due_date: date = None

    # Documentation
    photo_before: str = ""
    photo_after: str = ""
    drawing_markup: str = ""
    spec_reference: str = ""

    # Status tracking
    status: PunchItemStatus = PunchItemStatus.OPEN
    created_by: str = ""
    created_date: date = field(default_factory=date.today)

    # Completion
    completed_by: str = ""
    completed_date: date = None
    completion_notes: str = ""

    # Verification
    verified_by: str = ""
    verified_date: date = None
    verification_notes: str = ""

    # Back-charge
    back_charge: bool = False
    back_charge_amount: float = 0.0
    back_charge_ref: str = ""

    # History
    history: List[dict] = field(default_factory=list)

@dataclass
class PunchList:
    list_id: str
    project_id: str
    name: str
    walk_date: date
    walk_attendees: List[str]

    items: List[PunchItem] = field(default_factory=list)
    status: str = "Active"  # Active, Complete
    created_by: str = ""
    created_date: date = field(default_factory=date.today)

    area: str = ""  # Building/floor/zone covered
    list_type: str = "Punch"  # Punch, Pre-Punch, Final

Python Implementation

import pandas as pd
from datetime import datetime, date, timedelta
from typing import List, Dict, Optional
from collections import defaultdict

class PunchListManager:
    """Construction punch list management system"""

    def __init__(self, project_id: str, storage_path: str = None):
        self.project_id = project_id
        self.storage_path = storage_path or f"punch_{project_id}"
        self.punch_lists: Dict[str, PunchList] = {}
        self.items: Dict[str, PunchItem] = {}

    def create_punch_list(
        self,
        name: str,
        walk_date: date,
        attendees: List[str],
        area: str = "",
        list_type: str = "Punch",
        created_by: str = ""
    ) -> PunchList:
        """Create new punch list from walk"""

        list_id = f"PL-{datetime.now().strftime('%Y%m%d%H%M%S')}"

        punch_list = PunchList(
            list_id=list_id,
            project_id=self.project_id,
            name=name,
            walk_date=walk_date,
            walk_attendees=attendees,
            area=area,
            list_type=list_type,
            created_by=created_by
        )

        self.punch_lists[list_id] = punch_list
        return punch_list

    def add_item(
        self,
        punch_list_id: str,
        description: str,
        location: str,
        trade: TradeCategory,
        priority: PunchItemPriority = PunchItemPriority.MEDIUM,
        building: str = "",
        floor: str = "",
        room: str = "",
        photo_before: str = "",
        drawing_markup: str = "",
        spec_reference: str = "",
        created_by: str = ""
    ) -> PunchItem:
        """Add item to punch list"""

        if punch_list_id not in self.punch_lists:
            raise ValueError(f"Punch list {punch_list_id} not found")

        # Generate item ID
        punch_list = self.punch_lists[punch_list_id]
        item_num = len(punch_list.items) + 1
        item_id = f"{punch_list_id}-{item_num:04d}"

        item = PunchItem(
            item_id=item_id,
            punch_list_id=punch_list_id,
            description=description,
            location=location,
            trade=trade,
            priority=priority,
            building=building,
            floor=floor,
            room=room,
            photo_before=photo_before,
            drawing_markup=drawing_markup,
            spec_reference=spec_reference,
            created_by=created_by
        )

        # Add history entry
        item.history.append({
            'date': datetime.now(),
            'action': 'Created',
            'by': created_by,
            'notes': ''
        })

        self.items[item_id] = item
        punch_list.items.append(item)

        return item

    def assign_item(
        self,
        item_id: str,
        assigned_to: str,
        due_date: date = None,
        assigned_by: str = ""
    ) -> PunchItem:
        """Assign item to responsible party"""

        item = self.items.get(item_id)
        if not item:
            raise ValueError(f"Item {item_id} not found")

        if due_date is None:
            # Default due dates by priority
            days = {
                PunchItemPriority.CRITICAL: 1,
                PunchItemPriority.HIGH: 3,
                PunchItemPriority.MEDIUM: 7,
                PunchItemPriority.LOW: 14,
                PunchItemPriority.OBSERVATION: 30
            }
            due_date = date.today() + timedelta(days=days.get(item.priority, 7))

        item.assigned_to = assigned_to
        item.assigned_date = date.today()
        item.due_date = due_date
        item.status = PunchItemStatus.ASSIGNED

        item.history.append({
            'date': datetime.now(),
            'action': 'Assigned',
            'by': assigned_by,
            'notes': f'Assigned to {assigned_to}, due {due_date}'
        })

        # Trigger notification
        self._notify_assignment(item)

        return item

    def mark_complete(
        self,
        item_id: str,
        completed_by: str,
        photo_after: str = "",
        completion_notes: str = ""
    ) -> PunchItem:
        """Mark item as completed by trade"""

        item = self.items.get(item_id)
        if not item:
            raise ValueError(f"Item {item_id} not found")

        item.completed_by = completed_by
        item.completed_date = date.today()
        item.photo_after = photo_after
        item.completion_notes = completion_notes
        item.status = PunchItemStatus.READY_FOR_VERIFICATION

        item.history.append({
            'date': datetime.now(),
            'action': 'Completed',
            'by': completed_by,
            'notes': completion_notes
        })

        return item

    def verify_item(
        self,
        item_id: str,
        verified_by: str,
        accepted: bool,
        notes: str = ""
    ) -> PunchItem:
        """Verify completed item"""

        item = self.items.get(item_id)
        if not item:
            raise ValueError(f"Item {item_id} not found")

        item.verified_by = verified_by
        item.verified_date = date.today()
        item.verification_notes = notes

        if accepted:
            item.status = PunchItemStatus.ACCEPTED
            action = 'Accepted'
        else:
            item.status = PunchItemStatus.REJECTED
            action = 'Rejected'
            # Re-assign for rework
            item.assigned_date = date.today()
            item.due_date = date.today() + timedelta(days=3)

        item.history.append({
            'date': datetime.now(),
            'action': action,
            'by': verified_by,
            'notes': notes
        })

        return item

    def add_back_charge(
        self,
        item_id: str,
        amount: float,
        reference: str = ""
    ) -> PunchItem:
        """Add back-charge to item"""

        item = self.items.get(item_id)
        if not item:
            raise ValueError(f"Item {item_id} not found")

        item.back_charge = True
        item.back_charge_amount = amount
        item.back_charge_ref = reference

        item.history.append({
            'date': datetime.now(),
            'action': 'Back Charge',
            'by': '',
            'notes': f'Amount: ${amount:.2f}, Ref: {reference}'
        })

        return item

    def get_items_by_trade(self, trade: TradeCategory) -> List[PunchItem]:
        """Get all items for a specific trade"""
        return [i for i in self.items.values() if i.trade == trade]

    def get_items_by_status(self, status: PunchItemStatus) -> List[PunchItem]:
        """Get items by status"""
        return [i for i in self.items.values() if i.status == status]

    def get_overdue_items(self) -> List[PunchItem]:
        """Get overdue items"""
        today = date.today()
        return [
            i for i in self.items.values()
            if i.status in [PunchItemStatus.OPEN, PunchItemStatus.ASSIGNED, PunchItemStatus.IN_PROGRESS]
            and i.due_date and i.due_date < today
        ]

    def get_statistics(self) -> dict:
        """Get punch list statistics"""

        all_items = list(self.items.values())
        if not all_items:
            return {'total': 0}

        by_status = defaultdict(int)
        by_trade = defaultdict(lambda: {'total': 0, 'open': 0})
        by_priority = defaultdict(int)

        for item in all_items:
            by_status[item.status.value] += 1
            by_trade[item.trade.value]['total'] += 1
            if item.status not in [PunchItemStatus.ACCEPTED, PunchItemStatus.VERIFIED]:
                by_trade[item.trade.value]['open'] += 1
            by_priority[item.priority.value] += 1

        # Calculate completion rate
        accepted = len([i for i in all_items if i.status == PunchItemStatus.ACCEPTED])
        completion_rate = accepted / len(all_items) * 100 if all_items else 0

        # Back charges
        back_charge_items = [i for i in all_items if i.back_charge]
        total_back_charges = sum(i.back_charge_amount for i in back_charge_items)

        return {
            'total': len(all_items),
            'by_status': dict(by_status),
            'by_trade': dict(by_trade),
            'by_priority': dict(by_priority),
            'completion_rate': round(completion_rate, 1),
            'overdue': len(self.get_overdue_items()),
            'back_charge_count': len(back_charge_items),
            'back_charge_total': total_back_charges
        }

    def generate_trade_report(self, trade: TradeCategory) -> str:
        """Generate report for specific trade"""

        items = self.get_items_by_trade(trade)

        report = f"""
╔══════════════════════════════════════════════════════════════╗
║           PUNCH LIST - {trade.value.upper():<30}║   Project: {self.project_id:<40}║   Date: {date.today().strftime('%d.%m.%Y'):<43}╠══════════════════════════════════════════════════════════════╣

Total Items: {len(items)}
Open: {len([i for i in items if i.status not in [PunchItemStatus.ACCEPTED]])}
Due Today: {len([i for i in items if i.due_date == date.today()])}
Overdue: {len([i for i in items if i.due_date and i.due_date < date.today() and i.status not in [PunchItemStatus.ACCEPTED]])}

ITEMS REQUIRING ACTION
───────────────────────────────────────────────────────────────
"""
        for item in items:
            if item.status not in [PunchItemStatus.ACCEPTED]:
                overdue_flag = "🔴" if item.due_date and item.due_date < date.today() else ""
                report += f"""
{overdue_flag} [{item.item_id}] {item.priority.value}
   Location: {item.location}
   Description: {item.description}
   Status: {item.status.value}
   Due: {item.due_date}
"""

        report += """
╚══════════════════════════════════════════════════════════════╝
"""
        return report

    def generate_summary_dashboard(self) -> str:
        """Generate overall punch list dashboard"""

        stats = self.get_statistics()

        report = f"""
╔══════════════════════════════════════════════════════════════════╗
║                    PUNCH LIST DASHBOARD                           ║
║   Project: {self.project_id:<40}║   Date: {date.today().strftime('%d.%m.%Y'):<43}╠══════════════════════════════════════════════════════════════════╣

📊 OVERALL STATUS
───────────────────────────────────────────────────────────────────
   Total Items:        {stats['total']}
   Completion Rate:    {stats['completion_rate']}%
   Overdue Items:      {stats['overdue']}

📈 BY STATUS
───────────────────────────────────────────────────────────────────
"""
        for status, count in stats['by_status'].items():
            bar = "█" * int(count / max(stats['by_status'].values()) * 20) if stats['by_status'] else ""
            report += f"   {status:<25} {count:>5}  {bar}\n"

        report += """
🔧 BY TRADE (Open Items)
───────────────────────────────────────────────────────────────────
"""
        for trade, data in sorted(stats['by_trade'].items(), key=lambda x: x[1]['open'], reverse=True):
            if data['open'] > 0:
                report += f"   {trade:<25} {data['open']:>5} open / {data['total']} total\n"

        report += f"""
💰 BACK CHARGES
───────────────────────────────────────────────────────────────────
   Items with Back Charges:  {stats['back_charge_count']}
   Total Back Charges:       ${stats['back_charge_total']:,.2f}

╚══════════════════════════════════════════════════════════════════╝
"""
        return report

    def _notify_assignment(self, item: PunchItem):
        """Send notification for assigned item"""
        print(f"📋 Punch item assigned: {item.item_id}")
        print(f"   To: {item.assigned_to}")
        print(f"   Due: {item.due_date}")
        print(f"   Location: {item.location}")

    def export_to_excel(self, output_path: str) -> str:
        """Export punch list to Excel"""

        records = []
        for item in self.items.values():
            records.append({
                'Item ID': item.item_id,
                'Description': item.description,
                'Location': item.location,
                'Building': item.building,
                'Floor': item.floor,
                'Room': item.room,
                'Trade': item.trade.value,
                'Priority': item.priority.value,
                'Status': item.status.value,
                'Assigned To': item.assigned_to,
                'Due Date': item.due_date,
                'Completed By': item.completed_by,
                'Completed Date': item.completed_date,
                'Back Charge': 'Yes' if item.back_charge else 'No',
                'Back Charge Amount': item.back_charge_amount if item.back_charge else '',
                'Photo Before': item.photo_before,
                'Photo After': item.photo_after
            })

        df = pd.DataFrame(records)
        df.to_excel(output_path, index=False)
        return output_path


# Usage Example
if __name__ == "__main__":
    # Initialize manager
    manager = PunchListManager(project_id="PROJECT-2026-001")

    # Create punch list from walk
    punch_list = manager.create_punch_list(
        name="Floor 5 Pre-Final Walk",
        walk_date=date.today(),
        attendees=["PM", "Architect", "GC Super"],
        area="Building A, Floor 5",
        list_type="Pre-Final",
        created_by="PM"
    )

    # Add items
    item1 = manager.add_item(
        punch_list_id=punch_list.list_id,
        description="Touch up paint at door frame Room 501",
        location="Room 501, door frame",
        trade=TradeCategory.DRYWALL,
        priority=PunchItemPriority.LOW,
        building="A",
        floor="5",
        room="501",
        created_by="PM"
    )

    item2 = manager.add_item(
        punch_list_id=punch_list.list_id,
        description="Missing cover plate on electrical outlet",
        location="Room 502, east wall",
        trade=TradeCategory.ELECTRICAL,
        priority=PunchItemPriority.MEDIUM,
        building="A",
        floor="5",
        room="502",
        created_by="PM"
    )

    # Assign items
    manager.assign_item(
        item_id=item1.item_id,
        assigned_to="ABC Painting",
        assigned_by="GC Super"
    )

    manager.assign_item(
        item_id=item2.item_id,
        assigned_to="XYZ Electric",
        due_date=date.today() + timedelta(days=2),
        assigned_by="GC Super"
    )

    # Mark complete
    manager.mark_complete(
        item_id=item1.item_id,
        completed_by="ABC Painting",
        completion_notes="Paint touched up"
    )

    # Verify
    manager.verify_item(
        item_id=item1.item_id,
        verified_by="PM",
        accepted=True,
        notes="Looks good"
    )

    # Generate reports
    print(manager.generate_summary_dashboard())
    print(manager.generate_trade_report(TradeCategory.ELECTRICAL))

Telegram Bot Integration

name: Punch List Bot
commands:
  /newitem:
    steps:
      - Ask: Photo of deficiency
      - Ask: Location (Building/Floor/Room)
      - Ask: Description
      - Ask: Trade (show buttons)
      - Ask: Priority (show buttons)
      - Confirm and create item

  /myitems:
    - Show open items assigned to user
    - Buttons: [Mark Complete] [View Details]

  /complete:
    - Select item from list
    - Ask for completion photo
    - Ask for notes
    - Submit for verification

  /dashboard:
    - Show summary statistics
    - Open items by trade
    - Overdue items

"The last 10% of punch takes 50% of the time. Start early, stay organized."

Weekly Installs
3
GitHub Stars
51
First Seen
8 days ago
Installed on
opencode3
gemini-cli3
antigravity3
claude-code3
github-copilot3
codex3