look-ahead-scheduler

SKILL.md

Look-Ahead Scheduler

Overview

Generate rolling look-ahead schedules from the master schedule. Create actionable short-term plans with constraint analysis, crew assignments, and daily coordination.

"Look-ahead planning catches 80% of schedule problems before they occur" — DDC Community

Look-Ahead Hierarchy

┌─────────────────────────────────────────────────────────────────┐
│                    LOOK-AHEAD PLANNING                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Master Schedule (12+ months)                                    │
│       ↓                                                         │
│  Phase Schedule (3-6 months)                                    │
│       ↓                                                         │
│  6-Week Look-Ahead (make-ready)                                 │
│       ↓                                                         │
│  3-Week Look-Ahead (constraint removal)                         │
│       ↓                                                         │
│  Weekly Work Plan (commitment)                                  │
│       ↓                                                         │
│  Daily Coordination                                              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Technical Implementation

from dataclasses import dataclass, field
from typing import List, Dict, Optional, Set
from datetime import datetime, timedelta
from enum import Enum
from collections import defaultdict

class ConstraintType(Enum):
    PREDECESSOR = "predecessor"
    LABOR = "labor"
    MATERIAL = "material"
    EQUIPMENT = "equipment"
    INFORMATION = "information"
    PERMIT = "permit"
    INSPECTION = "inspection"
    SPACE = "space"
    WEATHER = "weather"

class ConstraintStatus(Enum):
    OPEN = "open"
    IN_PROGRESS = "in_progress"
    RESOLVED = "resolved"
    BLOCKED = "blocked"

class ActivityStatus(Enum):
    NOT_STARTED = "not_started"
    IN_PROGRESS = "in_progress"
    COMPLETE = "complete"
    DELAYED = "delayed"

@dataclass
class Constraint:
    id: str
    activity_id: str
    constraint_type: ConstraintType
    description: str
    responsible_party: str
    needed_by: datetime
    status: ConstraintStatus = ConstraintStatus.OPEN
    resolution_notes: str = ""
    resolved_date: Optional[datetime] = None

@dataclass
class LookAheadActivity:
    id: str
    name: str
    trade: str
    location: str
    planned_start: datetime
    planned_finish: datetime
    duration: int
    labor_hours: float
    crew_size: int
    predecessors: List[str] = field(default_factory=list)
    constraints: List[Constraint] = field(default_factory=list)
    status: ActivityStatus = ActivityStatus.NOT_STARTED
    percent_complete: float = 0.0
    notes: str = ""
    can_start: bool = False

@dataclass
class WeeklyWorkPlan:
    week_start: datetime
    week_end: datetime
    activities: List[LookAheadActivity]
    total_labor_hours: float
    trades_involved: List[str]
    constraints_to_resolve: List[Constraint]

@dataclass
class DailyPlan:
    date: datetime
    activities: List[LookAheadActivity]
    labor_by_trade: Dict[str, int]
    equipment_needed: List[str]
    inspections: List[str]
    safety_focus: str

class LookAheadScheduler:
    """Generate rolling look-ahead schedules."""

    def __init__(self, project_name: str):
        self.project_name = project_name
        self.activities: Dict[str, LookAheadActivity] = {}
        self.constraints: Dict[str, Constraint] = {}
        self.weekly_plans: List[WeeklyWorkPlan] = []

    def import_from_master(self, master_activities: List[Dict],
                          look_ahead_start: datetime,
                          look_ahead_weeks: int = 6) -> int:
        """Import activities from master schedule for look-ahead period."""
        look_ahead_end = look_ahead_start + timedelta(weeks=look_ahead_weeks)
        count = 0

        for act in master_activities:
            start = datetime.fromisoformat(act['planned_start']) if isinstance(act['planned_start'], str) else act['planned_start']
            finish = datetime.fromisoformat(act['planned_finish']) if isinstance(act['planned_finish'], str) else act['planned_finish']

            # Include if overlaps look-ahead period
            if start <= look_ahead_end and finish >= look_ahead_start:
                activity = LookAheadActivity(
                    id=act['id'],
                    name=act['name'],
                    trade=act.get('trade', ''),
                    location=act.get('location', ''),
                    planned_start=start,
                    planned_finish=finish,
                    duration=act.get('duration', (finish - start).days),
                    labor_hours=act.get('labor_hours', 0),
                    crew_size=act.get('crew_size', 0),
                    predecessors=act.get('predecessors', [])
                )
                self.activities[activity.id] = activity
                count += 1

        return count

    def add_constraint(self, activity_id: str, constraint_type: ConstraintType,
                      description: str, responsible_party: str,
                      needed_by: datetime) -> Constraint:
        """Add constraint to activity."""
        constraint_id = f"CON-{len(self.constraints)+1:04d}"

        constraint = Constraint(
            id=constraint_id,
            activity_id=activity_id,
            constraint_type=constraint_type,
            description=description,
            responsible_party=responsible_party,
            needed_by=needed_by
        )

        self.constraints[constraint_id] = constraint

        if activity_id in self.activities:
            self.activities[activity_id].constraints.append(constraint)

        return constraint

    def update_constraint(self, constraint_id: str, status: ConstraintStatus,
                         notes: str = "") -> Constraint:
        """Update constraint status."""
        if constraint_id not in self.constraints:
            raise ValueError(f"Constraint {constraint_id} not found")

        constraint = self.constraints[constraint_id]
        constraint.status = status
        constraint.resolution_notes = notes

        if status == ConstraintStatus.RESOLVED:
            constraint.resolved_date = datetime.now()

        return constraint

    def analyze_make_ready(self) -> Dict[str, List[str]]:
        """Analyze which activities are 'make-ready' (constraints resolved)."""
        ready = []
        not_ready = []
        blocked = []

        for act in self.activities.values():
            # Check predecessors complete
            pred_complete = all(
                self.activities.get(p, {}).status == ActivityStatus.COMPLETE
                for p in act.predecessors if p in self.activities
            )

            # Check constraints resolved
            open_constraints = [c for c in act.constraints
                              if c.status != ConstraintStatus.RESOLVED]

            if not pred_complete:
                blocked.append(act.id)
                act.can_start = False
            elif open_constraints:
                not_ready.append(act.id)
                act.can_start = False
            else:
                ready.append(act.id)
                act.can_start = True

        return {
            "ready": ready,
            "not_ready": not_ready,
            "blocked": blocked
        }

    def generate_weekly_plan(self, week_start: datetime) -> WeeklyWorkPlan:
        """Generate weekly work plan."""
        week_end = week_start + timedelta(days=6)

        # Get activities for this week
        week_activities = [
            act for act in self.activities.values()
            if act.planned_start <= week_end and act.planned_finish >= week_start
        ]

        # Calculate totals
        total_hours = sum(act.labor_hours for act in week_activities)
        trades = list(set(act.trade for act in week_activities if act.trade))

        # Get constraints needing resolution this week
        constraints_due = [
            c for c in self.constraints.values()
            if c.status == ConstraintStatus.OPEN and c.needed_by <= week_end
        ]

        plan = WeeklyWorkPlan(
            week_start=week_start,
            week_end=week_end,
            activities=week_activities,
            total_labor_hours=total_hours,
            trades_involved=trades,
            constraints_to_resolve=constraints_due
        )

        self.weekly_plans.append(plan)
        return plan

    def generate_daily_plan(self, date: datetime) -> DailyPlan:
        """Generate daily coordination plan."""
        # Get activities for this day
        day_activities = [
            act for act in self.activities.values()
            if act.planned_start <= date <= act.planned_finish
            and act.can_start
        ]

        # Labor by trade
        labor_by_trade = defaultdict(int)
        for act in day_activities:
            labor_by_trade[act.trade] += act.crew_size

        # Equipment needed
        equipment = []
        for act in day_activities:
            for c in act.constraints:
                if c.constraint_type == ConstraintType.EQUIPMENT:
                    equipment.append(c.description)

        # Inspections
        inspections = [
            c.description for c in self.constraints.values()
            if c.constraint_type == ConstraintType.INSPECTION
            and c.needed_by.date() == date.date()
        ]

        # Safety focus based on activities
        safety_focus = self._determine_safety_focus(day_activities)

        return DailyPlan(
            date=date,
            activities=day_activities,
            labor_by_trade=dict(labor_by_trade),
            equipment_needed=equipment,
            inspections=inspections,
            safety_focus=safety_focus
        )

    def _determine_safety_focus(self, activities: List[LookAheadActivity]) -> str:
        """Determine daily safety focus based on activities."""
        # Keywords to safety topics
        keywords = {
            "excavation": "Excavation Safety - Shoring, sloping, access",
            "steel": "Steel Erection - Fall protection, crane safety",
            "concrete": "Concrete Pour - Silica, formwork, PPE",
            "roof": "Fall Protection - 100% tie-off, guardrails",
            "electrical": "Electrical Safety - LOTO, qualified personnel",
            "crane": "Crane Safety - Rigging, load charts, signaling",
            "welding": "Hot Work - Fire watch, permits, ventilation"
        }

        for act in activities:
            name_lower = act.name.lower()
            for keyword, safety in keywords.items():
                if keyword in name_lower:
                    return safety

        return "General Site Safety - PPE, housekeeping, awareness"

    def generate_look_ahead_report(self, weeks: int = 3) -> str:
        """Generate look-ahead schedule report."""
        self.analyze_make_ready()
        today = datetime.now()

        lines = [
            f"# {weeks}-Week Look-Ahead Schedule",
            f"",
            f"**Project:** {self.project_name}",
            f"**Generated:** {today.strftime('%Y-%m-%d')}",
            f"**Period:** {today.strftime('%Y-%m-%d')} to {(today + timedelta(weeks=weeks)).strftime('%Y-%m-%d')}",
            f"",
        ]

        # Summary
        make_ready = self.analyze_make_ready()
        lines.extend([
            f"## Summary",
            f"",
            f"- **Total Activities:** {len(self.activities)}",
            f"- **Ready to Start:** {len(make_ready['ready'])}",
            f"- **Awaiting Constraints:** {len(make_ready['not_ready'])}",
            f"- **Blocked:** {len(make_ready['blocked'])}",
            f""
        ])

        # Open constraints
        open_constraints = [c for c in self.constraints.values()
                          if c.status == ConstraintStatus.OPEN]
        if open_constraints:
            lines.extend([
                f"## Open Constraints ({len(open_constraints)})",
                f"",
                f"| Activity | Type | Description | Responsible | Needed By |",
                f"|----------|------|-------------|-------------|-----------|"
            ])
            for c in sorted(open_constraints, key=lambda x: x.needed_by):
                lines.append(
                    f"| {c.activity_id} | {c.constraint_type.value} | {c.description} | "
                    f"{c.responsible_party} | {c.needed_by.strftime('%Y-%m-%d')} |"
                )
            lines.append("")

        # Weekly breakdown
        for week_num in range(weeks):
            week_start = today + timedelta(weeks=week_num)
            week_start = week_start - timedelta(days=week_start.weekday())  # Monday

            plan = self.generate_weekly_plan(week_start)

            lines.extend([
                f"## Week {week_num + 1}: {plan.week_start.strftime('%b %d')} - {plan.week_end.strftime('%b %d')}",
                f"",
                f"**Activities:** {len(plan.activities)} | **Labor:** {plan.total_labor_hours:.0f} hrs | **Trades:** {', '.join(plan.trades_involved)}",
                f""
            ])

            if plan.activities:
                lines.append("| Activity | Trade | Location | Start | Duration | Status |")
                lines.append("|----------|-------|----------|-------|----------|--------|")
                for act in sorted(plan.activities, key=lambda a: a.planned_start):
                    status = "Ready" if act.can_start else "Constraint"
                    lines.append(
                        f"| {act.name} | {act.trade} | {act.location} | "
                        f"{act.planned_start.strftime('%m/%d')} | {act.duration}d | {status} |"
                    )
                lines.append("")

        return "\n".join(lines)

    def get_constraint_log(self) -> str:
        """Generate constraint log."""
        lines = [
            "# Constraint Log",
            "",
            "| ID | Activity | Type | Description | Responsible | Status | Needed By |",
            "|----|----------|------|-------------|-------------|--------|-----------|"
        ]

        for c in sorted(self.constraints.values(), key=lambda x: x.needed_by):
            lines.append(
                f"| {c.id} | {c.activity_id} | {c.constraint_type.value} | "
                f"{c.description} | {c.responsible_party} | {c.status.value} | "
                f"{c.needed_by.strftime('%Y-%m-%d')} |"
            )

        return "\n".join(lines)

Quick Start

from datetime import datetime, timedelta

# Initialize scheduler
scheduler = LookAheadScheduler("Office Tower Construction")

# Import activities from master schedule
master_activities = [
    {
        "id": "A100",
        "name": "Level 5 MEP Rough-in",
        "trade": "Mechanical",
        "location": "Level 5",
        "planned_start": datetime.now(),
        "planned_finish": datetime.now() + timedelta(days=10),
        "labor_hours": 400,
        "crew_size": 5
    },
    {
        "id": "A101",
        "name": "Level 5 Framing",
        "trade": "Carpentry",
        "location": "Level 5",
        "planned_start": datetime.now() + timedelta(days=5),
        "planned_finish": datetime.now() + timedelta(days=15),
        "labor_hours": 320,
        "crew_size": 4,
        "predecessors": ["A100"]
    }
]

scheduler.import_from_master(master_activities, datetime.now(), look_ahead_weeks=6)

# Add constraints
scheduler.add_constraint(
    "A100",
    ConstraintType.MATERIAL,
    "Ductwork delivery",
    "ABC Mechanical",
    datetime.now() + timedelta(days=2)
)

scheduler.add_constraint(
    "A101",
    ConstraintType.INFORMATION,
    "Framing layout drawings",
    "Architect",
    datetime.now() + timedelta(days=4)
)

# Analyze make-ready status
status = scheduler.analyze_make_ready()
print(f"Ready: {len(status['ready'])}, Not Ready: {len(status['not_ready'])}")

# Generate look-ahead report
print(scheduler.generate_look_ahead_report(weeks=3))

# Generate daily plan
daily = scheduler.generate_daily_plan(datetime.now())
print(f"Today's safety focus: {daily.safety_focus}")

Requirements

pip install (no external dependencies)
Weekly Installs
2
GitHub Stars
51
First Seen
8 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2