Skill: yoder-bawt/construction-pm (version 1.0.0)

--- name: construction-pm version: 1.0.0 description: "Construction project management toolkit for AI agents. Use when: (1) Tracking construction jobs through the full lifecycle from lead to paid, (2) Generating daily PM briefings on job status and pipeline health, (3) Checking for stale permits or aging jobs that need attention, (4) Parsing emails from project managers for job updates, (5) Monitoring revenue pipeline across all job statuses." metadata: openclaw: requires: bins: ["bash"] env: [] config: [] user-invocable: true --- # Construction PM Production-tested construction project management toolkit. Built from real roofing company workflows managing 36+ active jobs and $1.8M+ pipeline. ## When to Activate Use Construction PM when: 1. **Tracking jobs** through the full lifecycle (lead → estimate → sold → permitted → scheduled → in-progress → complete → invoiced → paid) 2. **Generating daily briefings** for project managers on job status and pipeline health 3. **Checking permits** - identifying permits pending longer than threshold days 4. **Parsing emails** from field staff for job status updates 5. **Monitoring pipeline revenue** across all job statuses to forecast cash flow ## Guardrails / Anti-Patterns **DO:** - ✓ Initialize the database with `init.sh` before using other commands - ✓ Use consistent job numbers that match your accounting/CRM system - ✓ Update job status promptly when conditions change - ✓ Run daily briefings to catch stale jobs before they fall through cracks - ✓ Set permit-check thresholds appropriate for your jurisdiction (14-30 days typical) **DON'T:** - ✗ Delete job history - status changes are append-only audit trail - ✗ Skip the stale check - jobs without updates often indicate problems - ✗ Use generic customer names - be specific for searchability - ✗ Forget to set permit numbers when available - they're required for inspections - ✗ Mix currency formats - use plain integers (49000 not $49,000 or 49k) ## Quick Start ```bash # Initialize the job tracker (creates JSON database) bash scripts/init.sh # Or specify a custom data directory DATA_DIR=/path/to/data bash scripts/init.sh ``` Default data location: `construction-pm-data/` in your workspace. ## Job Pipeline ### Add a job ```bash bash scripts/add-job.sh \ --number "12043" \ --customer "Nichols" \ --address "123 Main St" \ --pm "Greg" \ --value 49000 \ --status "permitted" \ --permit-status "approved" \ --notes "Roof replacement, 30sq" ``` ### Update job status ```bash bash scripts/add-job.sh --number "12043" --status "in-progress" --notes "Crew on site Monday" ``` ### View pipeline ```bash # All jobs bash scripts/pipeline.sh # Filter by status bash scripts/pipeline.sh --status permitted # Filter by PM bash scripts/pipeline.sh --pm Greg # Stale jobs (no update in 14+ days) bash scripts/pipeline.sh --stale 14 # Summary stats bash scripts/pipeline.sh --summary ``` ## Daily Briefing ```bash # Generate today's briefing bash scripts/briefing.sh # Custom stale threshold bash scripts/briefing.sh --stale-days 7 # Output to file bash scripts/briefing.sh > /path/to/briefing.md ``` The briefing includes: - Jobs starting this week - Permits pending > 7 days - Stale jobs (no update in 14+ days) - Pipeline summary by status - Revenue at each stage ## Permit Tracking ```bash # Check all pending permits bash scripts/permit-check.sh # Flag permits older than N days bash scripts/permit-check.sh --threshold 30 ``` ## Email Parsing ```bash # Parse a PM email for job updates bash scripts/parse-email.sh --file /path/to/email.txt # Parse from stdin echo "Job 12043 Nichols - permit approved, scheduling crew for next week" | bash scripts/parse-email.sh ``` Extracts: job numbers, customer names, status keywords, permit mentions, dates. ## Job Statuses | Status | Description | |--------|-------------| | `lead` | Initial inquiry, not yet estimated | | `estimate` | Estimate sent, waiting for approval | | `sold` | Contract signed, not yet permitted | | `permitted` | Permits approved, ready to schedule | | `scheduled` | Crew and materials scheduled | | `in-progress` | Work underway | | `complete` | Work done, pending final inspection | | `invoiced` | Invoice sent | | `paid` | Payment received | | `on-hold` | Paused for any reason | | `cancelled` | Job cancelled | ## Data Format Jobs are stored as JSON in `construction-pm-data/jobs.json`: ```json { "jobs": [ { "number": "12043", "customer": "Nichols", "address": "123 Main St", "pm": "Greg", "value": 49000, "status": "permitted", "permit_status": "approved", "permit_number": "", "notes": "Roof replacement, 30sq", "created": "2026-02-08", "updated": "2026-02-08", "history": [ {"date": "2026-02-08", "from": "", "to": "permitted", "note": "Initial entry"} ] } ] } ``` ## Integration with OpenClaw The briefing script outputs clean Markdown. Pipe it to your preferred delivery: - Telegram: `bash scripts/briefing.sh | your-send-script` - Obsidian vault: `bash scripts/briefing.sh > ~/vault/Inbox/PM-Briefing-$(date +%F).md` - Email: Combine with `gog gmail send` ## Construction-Specific Features - **Permit aging**: Flags permits pending longer than threshold - **Revenue pipeline**: Shows total value at each stage - **Stale detection**: Catches jobs that fell through the cracks - **Status history**: Full audit trail of every status change - **Trade-aware**: Understands roofing, siding, gutters, HVAC, electrical

path: scripts/add-job.sh
content: #!/usr/bin/env bash
# Add or update a job in the construction PM database
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"
DB="$DATA_DIR/jobs.json"

if [ ! -f "$DB" ]; then
    echo "Error: Database not found. Run init.sh first." >&2
    exit 1
fi

# Parse arguments
NUMBER="" CUSTOMER="" ADDRESS="" PM="" VALUE="" STATUS="" PERMIT_STATUS="" PERMIT_NUMBER="" NOTES="" TRADE=""
while [[ $# -gt 0 ]]; do
    case "$1" in
        --number) NUMBER="$2"; shift 2;;
        --customer) CUSTOMER="$2"; shift 2;;
        --address) ADDRESS="$2"; shift 2;;
        --pm) PM="$2"; shift 2;;
        --value) VALUE="$2"; shift 2;;
        --status) STATUS="$2"; shift 2;;
        --permit-status) PERMIT_STATUS="$2"; shift 2;;
        --permit-number) PERMIT_NUMBER="$2"; shift 2;;
        --notes) NOTES="$2"; shift 2;;
        --trade) TRADE="$2"; shift 2;;
        *) echo "Unknown option: $1" >&2; exit 1;;
    esac
done

if [ -z "$NUMBER" ]; then
    echo "Error: --number is required" >&2
    exit 1
fi

python3 << PYEOF
import json, datetime, sys

db_path = "$DB"
with open(db_path) as f:
    data = json.load(f)

jobs = data.get("jobs", [])
today = datetime.date.today().isoformat()

# Find existing job
existing = None
for i, j in enumerate(jobs):
    if j.get("number") == "$NUMBER":
        existing = i
        break

if existing is not None:
    job = jobs[existing]
    old_status = job.get("status", "")
    
    # Update only provided fields
    if "$CUSTOMER": job["customer"] = "$CUSTOMER"
    if "$ADDRESS": job["address"] = "$ADDRESS"
    if "$PM": job["pm"] = "$PM"
    if "$VALUE": job["value"] = float("$VALUE") if "$VALUE" else job.get("value", 0)
    if "$STATUS": job["status"] = "$STATUS"
    if "$PERMIT_STATUS": job["permit_status"] = "$PERMIT_STATUS"
    if "$PERMIT_NUMBER": job["permit_number"] = "$PERMIT_NUMBER"
    if "$NOTES": job["notes"] = "$NOTES"
    if "$TRADE": job["trade"] = "$TRADE"
    job["updated"] = today
    
    # Log status change
    new_status = job.get("status", "")
    if "$STATUS" and new_status != old_status:
        job.setdefault("history", []).append({
            "date": today,
            "from": old_status,
            "to": new_status,
            "note": "$NOTES" or f"Status changed to {new_status}"
        })
    
    jobs[existing] = job
    print(f"Updated job #{job['number']} - {job.get('customer', 'Unknown')} [{job.get('status', '?')}]")
else:
    job = {
        "number": "$NUMBER",
        "customer": "$CUSTOMER" or "Unknown",
        "address": "$ADDRESS" or "",
        "pm": "$PM" or "",
        "value": float("$VALUE") if "$VALUE" else 0,
        "status": "$STATUS" or "lead",
        "permit_status": "$PERMIT_STATUS" or "",
        "permit_number": "$PERMIT_NUMBER" or "",
        "trade": "$TRADE" or "",
        "notes": "$NOTES" or "",
        "created": today,
        "updated": today,
        "history": [{"date": today, "from": "", "to": "$STATUS" or "lead", "note": "Initial entry"}]
    }
    jobs.append(job)
    print(f"Added job #{job['number']} - {job['customer']} [{job['status']}] \${job['value']:,.0f}")

data["jobs"] = jobs
with open(db_path, "w") as f:
    json.dump(data, f, indent=2)
PYEOF


path: scripts/briefing.sh
content: #!/usr/bin/env bash
# Generate daily construction PM briefing
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"
DB="$DATA_DIR/jobs.json"

if [ ! -f "$DB" ]; then
    echo "Error: Database not found. Run init.sh first." >&2
    exit 1
fi

STALE_DAYS="${1:-14}"

python3 << PYEOF
import json, datetime, sys

with open("$DB") as f:
    data = json.load(f)

jobs = data.get("jobs", [])
today = datetime.date.today()
stale_threshold = int("$STALE_DAYS")
week_end = today + datetime.timedelta(days=7)

active_statuses = {"lead", "estimate", "sold", "permitted", "scheduled", "in-progress", "complete", "invoiced"}
active = [j for j in jobs if j.get("status") in active_statuses]

print(f"# Daily PM Briefing - {today.strftime('%A, %B %d, %Y')}")
print()

# 1. Pipeline snapshot
status_order = ["lead", "estimate", "sold", "permitted", "scheduled", "in-progress", "complete", "invoiced", "paid"]
counts = {}
values = {}
for j in jobs:
    s = j.get("status", "unknown")
    counts[s] = counts.get(s, 0) + 1
    values[s] = values.get(s, 0) + j.get("value", 0)

total_active = sum(counts.get(s, 0) for s in active_statuses)
total_value = sum(values.get(s, 0) for s in active_statuses)
print(f"## Pipeline: {total_active} active jobs | \${total_value:,.0f}")
print()
for s in status_order:
    if s in counts:
        bar = "#" * min(counts[s], 20)
        print(f"  {s:15} {counts[s]:3}  \${values[s]:>10,.0f}  {bar}")
print()

# 2. Needs attention
print("## Needs Attention")
print()

# Stale jobs
stale_cutoff = (today - datetime.timedelta(days=stale_threshold)).isoformat()
stale = [j for j in active if j.get("updated", "") < stale_cutoff]
if stale:
    stale.sort(key=lambda j: j.get("updated", ""))
    print(f"### Stale ({stale_threshold}+ days since update)")
    for j in stale:
        days = (today - datetime.date.fromisoformat(j.get("updated", today.isoformat()))).days
        print(f"  - **#{j['number']}** {j.get('customer', '?')} - {j.get('status', '?')} - {days} days stale")
    print()

# Permits pending
permits_pending = [j for j in active if j.get("permit_status", "").lower() in ("pending", "submitted", "applied", "review")]
if permits_pending:
    print("### Permits Pending")
    for j in permits_pending:
        days = (today - datetime.date.fromisoformat(j.get("updated", today.isoformat()))).days
        print(f"  - **#{j['number']}** {j.get('customer', '?')} - permit {j.get('permit_status', '?')} ({days}d)")
    print()

# Recently sold, not yet permitted
newly_sold = [j for j in active if j.get("status") == "sold"]
if newly_sold:
    print("### Sold - Needs Permit")
    for j in newly_sold:
        print(f"  - **#{j['number']}** {j.get('customer', '?')} \${j.get('value', 0):,.0f}")
    print()

# 3. This week
scheduled = [j for j in active if j.get("status") in ("scheduled", "in-progress")]
if scheduled:
    print("## Active This Week")
    for j in scheduled:
        print(f"  - **#{j['number']}** {j.get('customer', '?')} [{j.get('status')}] \${j.get('value', 0):,.0f}")
        if j.get("notes"):
            print(f"    {j['notes'][:80]}")
    print()

# 4. Revenue outlook
invoiced = sum(j.get("value", 0) for j in active if j.get("status") == "invoiced")
completed = sum(j.get("value", 0) for j in active if j.get("status") == "complete")
in_progress = sum(j.get("value", 0) for j in active if j.get("status") == "in-progress")

if invoiced or completed or in_progress:
    print("## Revenue Outlook")
    if invoiced: print(f"  Invoiced (awaiting payment): \${invoiced:,.0f}")
    if completed: print(f"  Completed (needs invoice):   \${completed:,.0f}")
    if in_progress: print(f"  In progress:                 \${in_progress:,.0f}")
    print()

# 5. Nothing to report
if not stale and not permits_pending and not newly_sold and not scheduled:
    print("All clear. No items needing immediate attention.")
    print()
PYEOF


path: scripts/init.sh
content: #!/usr/bin/env bash
# Initialize construction PM data directory and job database
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"

mkdir -p "$DATA_DIR"

if [ ! -f "$DATA_DIR/jobs.json" ]; then
    cat > "$DATA_DIR/jobs.json" << 'EOF'
{
  "jobs": [],
  "metadata": {
    "created": "",
    "version": "1.0.0"
  }
}
EOF
    # Set creation date
    if command -v python3 &>/dev/null; then
        python3 -c "
import json, datetime
with open('$DATA_DIR/jobs.json', 'r+') as f:
    d = json.load(f)
    d['metadata']['created'] = datetime.date.today().isoformat()
    f.seek(0); json.dump(d, f, indent=2); f.truncate()
"
    fi
    echo "Initialized construction PM database at $DATA_DIR/jobs.json"
else
    echo "Database already exists at $DATA_DIR/jobs.json"
    python3 -c "
import json
with open('$DATA_DIR/jobs.json') as f:
    d = json.load(f)
    jobs = d.get('jobs', [])
    print(f'  {len(jobs)} jobs tracked')
"
fi


path: scripts/parse-email.sh
content: #!/usr/bin/env bash
# Parse PM emails for job status updates
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"
DB="$DATA_DIR/jobs.json"

# Read from file or stdin
if [ "${1:-}" = "--file" ] && [ -n "${2:-}" ]; then
    EMAIL_TEXT=$(cat "$2")
elif [ ! -t 0 ]; then
    EMAIL_TEXT=$(cat)
else
    echo "Usage: parse-email.sh --file /path/to/email.txt" >&2
    echo "   or: echo 'email text' | parse-email.sh" >&2
    exit 1
fi

python3 << PYEOF
import re, json, sys

email = """$EMAIL_TEXT"""

# Extract job numbers (common formats: #12043, Job 12043, job #12043, 12043)
job_numbers = list(set(re.findall(r'(?:job\s*#?\s*|#)(\d{4,6})', email, re.IGNORECASE)))
# Also look for standalone 5-digit numbers that could be job numbers
standalone = re.findall(r'\b(\d{5})\b', email)
for n in standalone:
    if n not in job_numbers:
        job_numbers.append(n)

# Extract status keywords
status_map = {
    'approved': ['approved', 'permit approved', 'permits approved', 'got the permit'],
    'scheduled': ['scheduled', 'on the schedule', 'crew scheduled', 'start date'],
    'in-progress': ['started', 'in progress', 'crew on site', 'underway', 'working on'],
    'complete': ['complete', 'completed', 'finished', 'done', 'wrapped up', 'final inspection'],
    'invoiced': ['invoiced', 'sent invoice', 'billed'],
    'paid': ['paid', 'payment received', 'check received'],
    'on-hold': ['on hold', 'delayed', 'waiting', 'hold', 'postponed'],
    'cancelled': ['cancelled', 'canceled', 'cancel'],
}

detected_status = None
for status, keywords in status_map.items():
    for kw in keywords:
        if kw.lower() in email.lower():
            detected_status = status
            break
    if detected_status:
        break

# Extract permit info
permit_mentions = re.findall(r'permit\s*(?:#\s*)?(\w+[-\w]*)', email, re.IGNORECASE)
permit_status = None
if any(w in email.lower() for w in ['permit approved', 'permits approved', 'got the permit']):
    permit_status = 'approved'
elif any(w in email.lower() for w in ['permit submitted', 'submitted permit', 'applied for permit']):
    permit_status = 'submitted'
elif any(w in email.lower() for w in ['permit denied', 'permit rejected']):
    permit_status = 'denied'

# Extract customer names (look for capitalized words near job numbers)
customers = re.findall(r'(?:customer|client|homeowner|mr\.?|mrs\.?)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)', email)

# Extract dollar amounts
amounts = re.findall(r'\$[\d,]+(?:\.\d{2})?', email)

result = {
    "job_numbers": job_numbers,
    "detected_status": detected_status,
    "permit_status": permit_status,
    "permit_numbers": [p for p in permit_mentions if not p.lower() in ('approved', 'submitted', 'denied', 'pending')],
    "customers": customers,
    "amounts": amounts,
    "raw_length": len(email),
}

print(json.dumps(result, indent=2))

# Summary
if job_numbers or detected_status:
    print(f"\n--- Extracted ---")
    if job_numbers: print(f"Jobs: {', '.join(f'#{n}' for n in job_numbers)}")
    if detected_status: print(f"Status: {detected_status}")
    if permit_status: print(f"Permit: {permit_status}")
    if customers: print(f"Customer: {', '.join(customers)}")
    if amounts: print(f"Amounts: {', '.join(amounts)}")
PYEOF


path: scripts/permit-check.sh
content: #!/usr/bin/env bash
# Check permit statuses and flag aging permits
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"
DB="$DATA_DIR/jobs.json"

if [ ! -f "$DB" ]; then
    echo "Error: Database not found. Run init.sh first." >&2
    exit 1
fi

THRESHOLD="${1:-30}"

python3 << PYEOF
import json, datetime

with open("$DB") as f:
    data = json.load(f)

jobs = data.get("jobs", [])
today = datetime.date.today()
threshold = int("$THRESHOLD")

pending_statuses = {"pending", "submitted", "applied", "review", "in-review"}
approved_statuses = {"approved", "issued", "granted"}

pending = []
approved = []
missing = []

for j in jobs:
    ps = j.get("permit_status", "").lower()
    status = j.get("status", "").lower()
    
    # Skip cancelled/paid/lead jobs
    if status in ("cancelled", "paid", "lead"):
        continue
    
    if ps in pending_statuses:
        days = (today - datetime.date.fromisoformat(j.get("updated", today.isoformat()))).days
        pending.append((j, days))
    elif ps in approved_statuses:
        approved.append(j)
    elif status in ("sold", "estimate") and not ps:
        missing.append(j)

print("# Permit Status Report")
print()

if pending:
    pending.sort(key=lambda x: -x[1])  # Oldest first
    print(f"## Pending ({len(pending)})")
    for j, days in pending:
        flag = " ⚠️ OVERDUE" if days > threshold else ""
        print(f"  #{j['number']:>6} | {j.get('customer', '?'):20} | {j.get('permit_status', '?'):10} | {days}d{flag}")
    print()

overdue = [p for p in pending if p[1] > threshold]
if overdue:
    print(f"## ⚠️ Overdue ({len(overdue)} permits pending > {threshold} days)")
    for j, days in overdue:
        print(f"  - **#{j['number']}** {j.get('customer', '?')} - {days} days pending")
    print()

if missing:
    print(f"## Missing Permit Info ({len(missing)})")
    for j in missing:
        print(f"  #{j['number']:>6} | {j.get('customer', '?'):20} | Status: {j.get('status', '?')} | No permit data")
    print()

if approved:
    print(f"## Approved ({len(approved)})")
    for j in approved:
        pn = f" #{j.get('permit_number', '')}" if j.get("permit_number") else ""
        print(f"  #{j['number']:>6} | {j.get('customer', '?'):20} | Approved{pn}")
    print()

if not pending and not missing:
    print("All permits accounted for. No action needed.")
PYEOF


path: scripts/pipeline.sh
content: #!/usr/bin/env bash
# View construction job pipeline
set -euo pipefail

DATA_DIR="${DATA_DIR:-${OPENCLAW_WORKSPACE:-.}/construction-pm-data}"
DB="$DATA_DIR/jobs.json"

if [ ! -f "$DB" ]; then
    echo "Error: Database not found. Run init.sh first." >&2
    exit 1
fi

# Parse arguments
FILTER_STATUS="" FILTER_PM="" STALE_DAYS="" SUMMARY=false
while [[ $# -gt 0 ]]; do
    case "$1" in
        --status) FILTER_STATUS="$2"; shift 2;;
        --pm) FILTER_PM="$2"; shift 2;;
        --stale) STALE_DAYS="$2"; shift 2;;
        --summary) SUMMARY=true; shift;;
        *) echo "Unknown option: $1" >&2; exit 1;;
    esac
done

python3 << PYEOF
import json, datetime, sys

with open("$DB") as f:
    data = json.load(f)

jobs = data.get("jobs", [])
today = datetime.date.today()
filter_status = "$FILTER_STATUS"
filter_pm = "$FILTER_PM"
stale_days = int("$STALE_DAYS") if "$STALE_DAYS" else 0
summary = $( [ "$SUMMARY" = "true" ] && echo "True" || echo "False" )

# Filter
filtered = jobs
if filter_status:
    filtered = [j for j in filtered if j.get("status", "").lower() == filter_status.lower()]
if filter_pm:
    filtered = [j for j in filtered if filter_pm.lower() in j.get("pm", "").lower()]
if stale_days:
    cutoff = (today - datetime.timedelta(days=stale_days)).isoformat()
    filtered = [j for j in filtered if j.get("updated", "") < cutoff and j.get("status") not in ("paid", "cancelled", "complete")]

if summary:
    # Pipeline summary
    status_order = ["lead", "estimate", "sold", "permitted", "scheduled", "in-progress", "complete", "invoiced", "paid", "on-hold", "cancelled"]
    counts = {}
    values = {}
    for j in jobs:
        s = j.get("status", "unknown")
        counts[s] = counts.get(s, 0) + 1
        values[s] = values.get(s, 0) + j.get("value", 0)
    
    print("## Pipeline Summary")
    print(f"**Total:** {len(jobs)} jobs | \${sum(j.get('value', 0) for j in jobs):,.0f}")
    print()
    for s in status_order:
        if s in counts:
            print(f"  {s:15} {counts[s]:3} jobs  \${values[s]:>12,.0f}")
    # Check for unknown statuses
    for s in counts:
        if s not in status_order:
            print(f"  {s:15} {counts[s]:3} jobs  \${values.get(s, 0):>12,.0f}")
else:
    if not filtered:
        if stale_days:
            print(f"No stale jobs (threshold: {stale_days} days)")
        else:
            print("No jobs found matching criteria")
        sys.exit(0)
    
    # Sort by value descending
    filtered.sort(key=lambda j: j.get("value", 0), reverse=True)
    
    if stale_days:
        print(f"## Stale Jobs (no update in {stale_days}+ days)")
    elif filter_status:
        print(f"## Jobs - {filter_status}")
    else:
        print("## All Jobs")
    print()
    
    for j in filtered:
        updated = j.get("updated", "?")
        days_old = (today - datetime.date.fromisoformat(updated)).days if updated != "?" else "?"
        permit = f" | Permit: {j.get('permit_status', 'n/a')}" if j.get("permit_status") else ""
        print(f"  #{j.get('number', '?'):>6} | {j.get('customer', '?'):20} | {j.get('status', '?'):12} | \${j.get('value', 0):>10,.0f} | {days_old}d ago{permit}")
    
    print(f"\n  **{len(filtered)} jobs** | \${sum(j.get('value', 0) for j in filtered):,.0f} total")
PYEOF


path: skill.json
content: {
  "name": "construction-pm",
  "version": "1.0.0",
  "description": "Construction project management toolkit for AI agents. Use when: (1) Tracking construction jobs through the full lifecycle from lead to paid, (2) Generating daily PM briefings on job status and pipeline health, (3) Checking for stale permits or aging jobs that need attention, (4) Parsing emails from project managers for job updates, (5) Monitoring revenue pipeline across all job statuses.",
  "author": "yoder-bawt",
  "license": "MIT",
  "keywords": [
    "construction",
    "project-management",
    "briefing",
    "pipeline",
    "permits",
    "jobs",
    "email",
    "roofing",
    "contractor",
    "trades",
    "job-tracking"
  ],
  "requires": {
    "bins": ["bash"]
  },
  "scripts": {
    "init": "bash scripts/init.sh",
    "briefing": "bash scripts/briefing.sh",
    "add-job": "bash scripts/add-job.sh",
    "pipeline": "bash scripts/pipeline.sh",
    "parse-email": "bash scripts/parse-email.sh",
    "permit-check": "bash scripts/permit-check.sh"
  },
  "files": [
    "SKILL.md",
    "scripts/init.sh",
    "scripts/briefing.sh",
    "scripts/add-job.sh",
    "scripts/pipeline.sh",
    "scripts/parse-email.sh",
    "scripts/permit-check.sh"
  ]
}


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)