DDC_Skills_for_AI_Agents_in_Construction punch-list-manager
Digital punch list management for construction project closeout. Track deficiencies, assign corrections, photo documentation, and completion verification.
install
source · Clone the upstream repo
git clone https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction "$T" && mkdir -p ~/.claude/skills && cp -r "$T/1_DDC_Toolkit/Field-Operations/punch-list-manager" ~/.claude/skills/datadrivenconstruction-ddc-skills-for-ai-agents-in-construction-punch-list-manag && rm -rf "$T"
manifest:
1_DDC_Toolkit/Field-Operations/punch-list-manager/SKILL.mdsource content
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."