Skill: aipoch-ai/reagent-expiry-alert (version 1.0.0)

--- name: reagent-expiry-alert description: Scan reagent barcodes or IDs, log expiration dates, and generate multi-level alerts before reagent expiry to support laboratory inventory management. license: MIT skill-author: AIPOCH --- # Reagent Expiry Alert Scan reagent bottle barcodes or IDs, log expiration dates, and alert before expiry to support safe laboratory inventory management. ## Quick Check ```bash python -m py_compile scripts/main.py python scripts/main.py --help ``` ## When to Use - Use this skill when logging a new reagent with its expiry date into the inventory. - Use this skill when checking for reagents approaching expiration (30/60/90-day alerts). - Do not use this skill to manage controlled substances, biological hazards requiring special disposal, or reagents subject to regulatory chain-of-custody requirements. ## Workflow 1. Confirm the reagent barcode/ID, expiry date, and action (scan/log or check alerts). 2. Validate that the request is for reagent expiry tracking, not chemical safety assessment or disposal guidance. 3. **Date validation:** If `--expiry` is provided, validate that it is a valid YYYY-MM-DD date. If the date is in the past, emit a warning: "This reagent is already expired as of [date]. It will be logged with an Expired alert status." 4. Log the reagent or run the alert check using the packaged script. 5. Return expiration status, alert level, and reorder recommendations. 6. If inputs are incomplete, state which fields are missing and request only the minimum additional information. ## Usage ```text # Log a new reagent python scripts/main.py --scan "REAGENT-001" --name "Tris Buffer" --expiry 2025-12-31 --location "Shelf A" # Check for upcoming expirations python scripts/main.py --check-alerts --alert-days 30 # Check with custom alert window python scripts/main.py --check-alerts --alert-days 60 ``` ## Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `--scan` | string | No | — | Reagent barcode or ID | | `--name` | string | No | — | Reagent name | | `--expiry` | date | No | — | Expiration date (YYYY-MM-DD) | | `--location` | string | No | — | Storage location | | `--quantity` | string | No | — | Quantity on hand | | `--check-alerts` | flag | No | — | Check for upcoming expirations | | `--alert-days` | integer | No | 30 | Days before expiry to alert | ## Alert Levels - 🔴 Expired — reagent past expiry date - 🟠 Critical — expiring within 30 days - 🟡 Warning — expiring within 60 days - 🟢 OK — expiring beyond 60 days ## Output - Expiration alert report with alert level per reagent - Inventory summary - Reorder recommendations for critical/expired items ## Stress-Case Rules For complex multi-constraint requests, always include these explicit blocks: 1. Assumptions 2. Reagents Checked 3. Alert Report 4. Reorder Recommendations 5. Risks and Manual Checks ## Error Handling - If required inputs are missing, state exactly which fields are missing and request only the minimum additional information. - If the task goes outside the documented scope, stop instead of guessing or silently widening the assignment. - If `scripts/main.py` fails, report the failure point, summarize what still can be completed safely, and provide a manual fallback. - Do not fabricate expiry dates, inventory counts, or reorder thresholds. ## Input Validation This skill accepts: reagent barcode/ID and expiry date for logging, or a check-alerts request for inventory review. If the request does not involve reagent expiry tracking — for example, asking for chemical hazard assessment, waste disposal guidance, or controlled substance management — do not proceed with the workflow. Instead respond: > "reagent-expiry-alert is designed to log reagent expiry dates and generate alerts before expiration. Your request appears to be outside this scope. Please provide a reagent ID and expiry date, or use a more appropriate tool." ## Response Template Use the following fixed structure for non-trivial requests: 1. Objective 2. Inputs Received 3. Assumptions 4. Workflow 5. Deliverable 6. Risks and Limits 7. Next Checks If the request is simple, you may compress the structure, but still keep assumptions and limits explicit when they affect correctness.

path: POLISH_CHANGELOG.md
content: # POLISH_CHANGELOG — reagent-expiry-alert

**Original Score:** 83  
**Polish Date:** 2026-03-19

## Issues Addressed

### P0 / Veto Fixes
- None (no veto failures)

### P1 Fixes
- **Past-date input not handled:** Added step 3 to workflow with explicit date validation. If `--expiry` is in the past, the skill now emits a warning stating the reagent is already expired and will be logged with an Expired alert status.

### P2 Fixes
- None beyond P1 fixes.

### QS-1 (Input Validation)
- Already present and well-formed.

### QS-2 (Progressive Disclosure)
- File is 110 lines — within 300-line limit. No content moved to references/.

### QS-3 (Canonical YAML Frontmatter)
- Already present with all four required fields.


path: scripts/main.py
content: #!/usr/bin/env python3
"""
Reagent Expiry Alert
Track reagent expiration dates and send alerts.
"""

import argparse
import json
import os
from datetime import datetime, timedelta
from pathlib import Path


class ReagentTracker:
    """Track reagent expiry dates."""
    
    def __init__(self, data_file="~/.openclaw/reagent_inventory.json"):
        self.data_file = Path(data_file).expanduser()
        self.data_file.parent.mkdir(parents=True, exist_ok=True)
        self.inventory = self._load()
    
    def _load(self):
        if self.data_file.exists():
            with open(self.data_file) as f:
                return json.load(f)
        return {}
    
    def _save(self):
        with open(self.data_file, 'w') as f:
            json.dump(self.inventory, f, indent=2)
    
    def scan_reagent(self, barcode, name, expiry_date, location="", quantity=1):
        """Add or update reagent."""
        self.inventory[barcode] = {
            "name": name,
            "expiry_date": expiry_date,
            "location": location,
            "quantity": quantity,
            "added": datetime.now().isoformat(),
            "scanned": True
        }
        self._save()
        print(f"✓ Scanned: {name} (expires: {expiry_date})")
    
    def check_alerts(self, alert_days=30):
        """Check for upcoming expirations."""
        today = datetime.now()
        alerts = {"expired": [], "soon": [], "warning": []}
        
        for barcode, data in self.inventory.items():
            expiry = datetime.fromisoformat(data["expiry_date"])
            days_until = (expiry - today).days
            
            if days_until < 0:
                alerts["expired"].append((data["name"], barcode, abs(days_until)))
            elif days_until <= 7:
                alerts["expired"].append((data["name"], barcode, days_until))
            elif days_until <= alert_days:
                alerts["soon"].append((data["name"], barcode, days_until))
        
        self._print_alerts(alerts)
        return alerts
    
    def _print_alerts(self, alerts):
        print("\n=== Reagent Expiry Alerts ===")
        
        if alerts["expired"]:
            print("\n🔴 EXPIRED / EXPIRING SOON:")
            for name, barcode, days in alerts["expired"]:
                if days < 0:
                    print(f"  {name}: {days} days OVERDUE ({barcode})")
                else:
                    print(f"  {name}: {days} days left ({barcode})")
        
        if alerts["soon"]:
            print("\n🟡 Expiring within alert period:")
            for name, barcode, days in alerts["soon"]:
                print(f"  {name}: {days} days left ({barcode})")
        
        if not alerts["expired"] and not alerts["soon"]:
            print("\n✅ No expiring reagents in alert period")
    
    def list_inventory(self):
        """List all reagents."""
        print("\n=== Reagent Inventory ===")
        for barcode, data in sorted(self.inventory.items(), 
                                     key=lambda x: x[1]["expiry_date"]):
            print(f"\n{data['name']}")
            print(f"  Barcode: {barcode}")
            print(f"  Expires: {data['expiry_date']}")
            print(f"  Location: {data.get('location', 'N/A')}")
            print(f"  Quantity: {data.get('quantity', 1)}")


def main():
    parser = argparse.ArgumentParser(description="Reagent Expiry Alert")
    parser.add_argument("--scan", "-s", help="Reagent barcode")
    parser.add_argument("--name", "-n", help="Reagent name")
    parser.add_argument("--expiry", "-e", help="Expiry date (YYYY-MM-DD)")
    parser.add_argument("--location", "-l", help="Storage location")
    parser.add_argument("--quantity", type=int, default=1, help="Quantity")
    parser.add_argument("--check-alerts", "-c", action="store_true", help="Check alerts")
    parser.add_argument("--alert-days", type=int, default=30, help="Alert threshold")
    parser.add_argument("--list", action="store_true", help="List inventory")
    
    args = parser.parse_args()
    
    tracker = ReagentTracker()
    
    if args.scan and args.expiry:
        tracker.scan_reagent(args.scan, args.name or args.scan, 
                            args.expiry, args.location, args.quantity)
    elif args.check_alerts:
        tracker.check_alerts(args.alert_days)
    elif args.list:
        tracker.list_inventory()
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)