DDC_Skills_for_AI_Agents_in_Construction lien-waiver-tracker
Track and manage construction lien waivers. Monitor conditional and unconditional waivers, ensure compliance before payments, and prevent lien exposure.
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/4_DDC_Curated/Contract-Legal/lien-waiver-tracker" ~/.claude/skills/datadrivenconstruction-ddc-skills-for-ai-agents-in-construction-lien-waiver-trac && rm -rf "$T"
manifest:
4_DDC_Curated/Contract-Legal/lien-waiver-tracker/SKILL.mdsource content
Lien Waiver Tracker
Overview
Track and manage lien waivers throughout the construction payment process. Ensure proper waivers are received before releasing payments, monitor waiver status by subcontractor, and minimize lien exposure.
Lien Waiver Types
┌─────────────────────────────────────────────────────────────────┐ │ LIEN WAIVER TYPES │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ CONDITIONAL UNCONDITIONAL │ │ ─────────── ───────────── │ │ 📋 Progress - Conditional ✅ Progress - Unconditional │ │ Effective when paid Immediately effective │ │ Use with payment Use after check clears │ │ │ │ 📋 Final - Conditional ✅ Final - Unconditional │ │ For final payment For final payment │ │ Upon receipt of funds After funds received │ │ │ └─────────────────────────────────────────────────────────────────┘
Technical Implementation
from dataclasses import dataclass, field from typing import List, Dict, Optional from datetime import datetime, timedelta from enum import Enum class WaiverType(Enum): CONDITIONAL_PROGRESS = "conditional_progress" UNCONDITIONAL_PROGRESS = "unconditional_progress" CONDITIONAL_FINAL = "conditional_final" UNCONDITIONAL_FINAL = "unconditional_final" class WaiverStatus(Enum): REQUESTED = "requested" RECEIVED = "received" VERIFIED = "verified" REJECTED = "rejected" MISSING = "missing" class PaymentStatus(Enum): PENDING = "pending" APPROVED = "approved" HELD = "held" RELEASED = "released" @dataclass class Subcontractor: id: str name: str trade: str contract_amount: float contact_name: str contact_email: str tier: int = 1 # 1 = direct, 2 = sub-sub @dataclass class LienWaiver: id: str subcontractor_id: str waiver_type: WaiverType payment_application: int # Pay app number through_date: datetime amount: float status: WaiverStatus = WaiverStatus.REQUESTED requested_date: datetime = field(default_factory=datetime.now) received_date: Optional[datetime] = None verified_by: str = "" file_path: str = "" notes: str = "" @dataclass class PaymentApplication: number: int period_end: datetime subcontractor_id: str amount_requested: float amount_approved: float retainage: float status: PaymentStatus = PaymentStatus.PENDING waivers_complete: bool = False payment_date: Optional[datetime] = None @dataclass class LienExposure: subcontractor_id: str subcontractor_name: str total_paid: float unconditional_waivers: float conditional_pending: float exposure: float class LienWaiverTracker: """Track and manage construction lien waivers.""" def __init__(self, project_id: str, project_name: str): self.project_id = project_id self.project_name = project_name self.subcontractors: Dict[str, Subcontractor] = {} self.waivers: Dict[str, LienWaiver] = {} self.pay_apps: Dict[str, PaymentApplication] = {} def add_subcontractor(self, id: str, name: str, trade: str, contract_amount: float, contact_name: str, contact_email: str, tier: int = 1) -> Subcontractor: """Add subcontractor to tracking.""" sub = Subcontractor( id=id, name=name, trade=trade, contract_amount=contract_amount, contact_name=contact_name, contact_email=contact_email, tier=tier ) self.subcontractors[id] = sub return sub def create_payment_application(self, number: int, period_end: datetime, subcontractor_id: str, amount_requested: float, retainage_rate: float = 0.10) -> PaymentApplication: """Create payment application record.""" if subcontractor_id not in self.subcontractors: raise ValueError(f"Subcontractor {subcontractor_id} not found") retainage = amount_requested * retainage_rate amount_approved = amount_requested - retainage pay_app = PaymentApplication( number=number, period_end=period_end, subcontractor_id=subcontractor_id, amount_requested=amount_requested, amount_approved=amount_approved, retainage=retainage ) key = f"{subcontractor_id}-{number}" self.pay_apps[key] = pay_app # Create waiver request self.request_waiver(subcontractor_id, number, period_end, amount_approved) return pay_app def request_waiver(self, subcontractor_id: str, pay_app_number: int, through_date: datetime, amount: float, waiver_type: WaiverType = WaiverType.CONDITIONAL_PROGRESS) -> LienWaiver: """Request lien waiver from subcontractor.""" waiver_id = f"LW-{subcontractor_id}-{pay_app_number}" waiver = LienWaiver( id=waiver_id, subcontractor_id=subcontractor_id, waiver_type=waiver_type, payment_application=pay_app_number, through_date=through_date, amount=amount ) self.waivers[waiver_id] = waiver return waiver def receive_waiver(self, waiver_id: str, file_path: str, verified_by: str = "") -> LienWaiver: """Record receipt of lien waiver.""" if waiver_id not in self.waivers: raise ValueError(f"Waiver {waiver_id} not found") waiver = self.waivers[waiver_id] waiver.status = WaiverStatus.RECEIVED waiver.received_date = datetime.now() waiver.file_path = file_path waiver.verified_by = verified_by # Check if all waivers for pay app complete self._check_pay_app_waivers(waiver.subcontractor_id, waiver.payment_application) return waiver def verify_waiver(self, waiver_id: str, verified_by: str) -> LienWaiver: """Verify waiver details are correct.""" if waiver_id not in self.waivers: raise ValueError(f"Waiver {waiver_id} not found") waiver = self.waivers[waiver_id] waiver.status = WaiverStatus.VERIFIED waiver.verified_by = verified_by return waiver def reject_waiver(self, waiver_id: str, reason: str) -> LienWaiver: """Reject waiver (incorrect amount, wrong form, etc.).""" if waiver_id not in self.waivers: raise ValueError(f"Waiver {waiver_id} not found") waiver = self.waivers[waiver_id] waiver.status = WaiverStatus.REJECTED waiver.notes = f"Rejected: {reason}" return waiver def convert_to_unconditional(self, waiver_id: str, payment_date: datetime) -> LienWaiver: """Convert conditional waiver to unconditional after payment clears.""" if waiver_id not in self.waivers: raise ValueError(f"Waiver {waiver_id} not found") waiver = self.waivers[waiver_id] # Create new unconditional waiver new_type = (WaiverType.UNCONDITIONAL_PROGRESS if waiver.waiver_type == WaiverType.CONDITIONAL_PROGRESS else WaiverType.UNCONDITIONAL_FINAL) return self.request_waiver( waiver.subcontractor_id, waiver.payment_application, waiver.through_date, waiver.amount, new_type ) def _check_pay_app_waivers(self, subcontractor_id: str, pay_app_number: int): """Check if all waivers for pay app are received.""" key = f"{subcontractor_id}-{pay_app_number}" if key not in self.pay_apps: return pay_app = self.pay_apps[key] # Check all related waivers related_waivers = [ w for w in self.waivers.values() if w.subcontractor_id == subcontractor_id and w.payment_application == pay_app_number ] pay_app.waivers_complete = all( w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED] for w in related_waivers ) def calculate_exposure(self, subcontractor_id: str) -> LienExposure: """Calculate lien exposure for subcontractor.""" if subcontractor_id not in self.subcontractors: raise ValueError(f"Subcontractor {subcontractor_id} not found") sub = self.subcontractors[subcontractor_id] # Sum payments total_paid = sum( pa.amount_approved for pa in self.pay_apps.values() if pa.subcontractor_id == subcontractor_id and pa.status == PaymentStatus.RELEASED ) # Sum unconditional waivers unconditional = sum( w.amount for w in self.waivers.values() if w.subcontractor_id == subcontractor_id and w.waiver_type in [WaiverType.UNCONDITIONAL_PROGRESS, WaiverType.UNCONDITIONAL_FINAL] and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED] ) # Sum conditional pending conditional = sum( w.amount for w in self.waivers.values() if w.subcontractor_id == subcontractor_id and w.waiver_type in [WaiverType.CONDITIONAL_PROGRESS, WaiverType.CONDITIONAL_FINAL] and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED] ) # Exposure = Paid - Unconditional waivers exposure = total_paid - unconditional return LienExposure( subcontractor_id=subcontractor_id, subcontractor_name=sub.name, total_paid=total_paid, unconditional_waivers=unconditional, conditional_pending=conditional, exposure=exposure ) def get_missing_waivers(self) -> List[Dict]: """Get list of missing or pending waivers.""" missing = [] for waiver in self.waivers.values(): if waiver.status in [WaiverStatus.REQUESTED, WaiverStatus.MISSING]: sub = self.subcontractors.get(waiver.subcontractor_id) missing.append({ "waiver_id": waiver.id, "subcontractor": sub.name if sub else waiver.subcontractor_id, "pay_app": waiver.payment_application, "amount": waiver.amount, "type": waiver.waiver_type.value, "requested_date": waiver.requested_date, "days_outstanding": (datetime.now() - waiver.requested_date).days }) return sorted(missing, key=lambda x: -x["days_outstanding"]) def get_waiver_status_by_sub(self, subcontractor_id: str) -> Dict: """Get waiver status summary for subcontractor.""" if subcontractor_id not in self.subcontractors: raise ValueError(f"Subcontractor {subcontractor_id} not found") sub = self.subcontractors[subcontractor_id] waivers = [w for w in self.waivers.values() if w.subcontractor_id == subcontractor_id] by_status = {} for w in waivers: s = w.status.value by_status[s] = by_status.get(s, 0) + 1 return { "subcontractor": sub.name, "total_waivers": len(waivers), "by_status": by_status, "total_amount": sum(w.amount for w in waivers), "verified_amount": sum(w.amount for w in waivers if w.status == WaiverStatus.VERIFIED) } def can_release_payment(self, subcontractor_id: str, pay_app_number: int) -> Dict: """Check if payment can be released.""" key = f"{subcontractor_id}-{pay_app_number}" if key not in self.pay_apps: return {"can_release": False, "reason": "Payment application not found"} pay_app = self.pay_apps[key] # Check for conditional waiver waivers = [ w for w in self.waivers.values() if w.subcontractor_id == subcontractor_id and w.payment_application == pay_app_number and w.waiver_type == WaiverType.CONDITIONAL_PROGRESS ] if not waivers: return {"can_release": False, "reason": "No conditional waiver received"} for w in waivers: if w.status not in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]: return {"can_release": False, "reason": f"Waiver {w.id} not verified"} return {"can_release": True, "reason": "All waivers verified", "amount": pay_app.amount_approved} def generate_report(self) -> str: """Generate lien waiver status report.""" lines = [ "# Lien Waiver Status Report", "", f"**Project:** {self.project_name}", f"**Date:** {datetime.now().strftime('%Y-%m-%d')}", "", "## Summary", "", f"- Total Subcontractors: {len(self.subcontractors)}", f"- Total Waivers Tracked: {len(self.waivers)}", f"- Missing Waivers: {len(self.get_missing_waivers())}", "", "## Exposure by Subcontractor", "", "| Subcontractor | Paid | Unconditional | Exposure |", "|---------------|------|---------------|----------|" ] total_exposure = 0 for sub_id in self.subcontractors: exposure = self.calculate_exposure(sub_id) total_exposure += exposure.exposure lines.append( f"| {exposure.subcontractor_name} | ${exposure.total_paid:,.0f} | " f"${exposure.unconditional_waivers:,.0f} | ${exposure.exposure:,.0f} |" ) lines.extend([ f"| **TOTAL** | | | **${total_exposure:,.0f}** |", "", "## Missing Waivers", "" ]) missing = self.get_missing_waivers() if missing: lines.append("| Subcontractor | Pay App | Amount | Days Outstanding |") lines.append("|---------------|---------|--------|------------------|") for m in missing[:10]: lines.append( f"| {m['subcontractor']} | #{m['pay_app']} | " f"${m['amount']:,.0f} | {m['days_outstanding']} |" ) else: lines.append("*No missing waivers*") return "\n".join(lines)
Quick Start
# Initialize tracker tracker = LienWaiverTracker("PRJ-001", "Office Tower") # Add subcontractors tracker.add_subcontractor( "SUB-001", "ABC Mechanical", "HVAC", contract_amount=500000, contact_name="John Smith", contact_email="john@abcmech.com" ) tracker.add_subcontractor( "SUB-002", "XYZ Electric", "Electrical", contract_amount=350000, contact_name="Jane Doe", contact_email="jane@xyzelectric.com" ) # Create payment applications pa1 = tracker.create_payment_application( number=1, period_end=datetime.now(), subcontractor_id="SUB-001", amount_requested=50000 ) # Receive waiver waiver_id = f"LW-SUB-001-1" tracker.receive_waiver(waiver_id, "/waivers/sub001_pa1.pdf", "PM") # Check if can release payment result = tracker.can_release_payment("SUB-001", 1) print(f"Can release: {result['can_release']} - {result['reason']}") # Calculate exposure exposure = tracker.calculate_exposure("SUB-001") print(f"Lien exposure: ${exposure.exposure:,.2f}") # Generate report print(tracker.generate_report())
Requirements
pip install (no external dependencies)