@wonsukchoi/crondex 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ id: estimate-approval-followup-check
2
+ version: 1
3
+ name: Repair Estimate Approval Follow-Up Check
4
+ description: >
5
+ Flags repair estimates sent to a customer that haven't been approved
6
+ yet, so the car doesn't sit untouched while a text goes unanswered. Use
7
+ this if a vehicle has ever taken up a bay for days because nobody
8
+ followed up on an estimate the customer never actually saw or responded
9
+ to.
10
+ category: automotive
11
+ tags: [automotive, repair-shop, estimate, followup]
12
+ schedule: "0 9,15 * * *"
13
+ timezone: "UTC"
14
+ runner: shell
15
+ command: >
16
+ today=$(date +%s);
17
+ awk -F',' -v hours="{{followup_after_hours}}" -v today="$today" '
18
+ NR==1 {next}
19
+ $3 == "pending" {
20
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f \"%Y-%m-%d %H:%M\" \"" $2 "\" +%s 2>/dev/null";
21
+ cmd | getline sent_epoch; close(cmd);
22
+ age_hours = (today - sent_epoch) / 3600;
23
+ if (age_hours >= hours) {
24
+ printf "NO RESPONSE: RO %s — estimate sent %s (%d hrs ago)\n", $1, $2, age_hours;
25
+ }
26
+ }
27
+ ' "{{estimates_csv_path}}"
28
+ variables:
29
+ estimates_csv_path:
30
+ default: "./repair-estimates.csv"
31
+ description: "Path to a CSV with columns: ro_number,sent_at (YYYY-MM-DD HH:MM),status (pending/approved/declined)."
32
+ followup_after_hours:
33
+ default: 24
34
+ description: Hours an estimate can sit with no response before it counts as needing a follow-up call.
35
+ compatible_agents: [generic, claude, codex]
36
+ notes: Follow up by phone or text once flagged — this job only surfaces the gap, it doesn't contact the customer.
@@ -0,0 +1,35 @@
1
+ id: vehicle-pickup-reminder
2
+ version: 1
3
+ name: Finished Vehicle Pickup Reminder
4
+ description: >
5
+ Flags vehicles marked ready for pickup that the customer hasn't
6
+ collected yet, so the lot doesn't fill up with finished cars nobody's
7
+ chasing. Use this if a completed car has ever sat in the lot for a week
8
+ because the "ready" call was made once and then forgotten.
9
+ category: automotive
10
+ tags: [automotive, repair-shop, pickup, customer]
11
+ schedule: "0 9 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ today=$(date +%s);
16
+ awk -F',' -v days="{{warn_days_before}}" -v today="$today" '
17
+ NR==1 {next}
18
+ $3 == "ready" {
19
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
20
+ cmd | getline ready_epoch; close(cmd);
21
+ age_days = (today - ready_epoch) / 86400;
22
+ if (age_days >= days) {
23
+ printf "NOT PICKED UP: RO %s — ready since %s (%d days)\n", $1, $2, age_days;
24
+ }
25
+ }
26
+ ' "{{repair_orders_csv_path}}"
27
+ variables:
28
+ repair_orders_csv_path:
29
+ default: "./repair-orders-status.csv"
30
+ description: "Path to a CSV with columns: ro_number,ready_date (YYYY-MM-DD),status (ready/picked-up)."
31
+ warn_days_before:
32
+ default: 2
33
+ description: Days a finished vehicle can sit before it counts as overdue for pickup.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: A car sitting too long after completion can also mean unpaid storage/labor charges piling up — worth a call, not just a lot walk-through.
@@ -0,0 +1,35 @@
1
+ id: enrollment-waitlist-followup-check
2
+ version: 1
3
+ name: Enrollment Waitlist Follow-Up Check
4
+ description: >
5
+ Flags waitlisted families who haven't been contacted in a while, so a
6
+ spot doesn't go unfilled because nobody worked down the list. Use this
7
+ if an opening has ever sat empty for weeks while a waitlisted family who
8
+ would've taken it never got a call.
9
+ category: childcare
10
+ tags: [childcare, waitlist, enrollment, followup]
11
+ schedule: "0 9 * * 1"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ today=$(date +%s);
16
+ awk -F',' -v days="{{stale_after_days}}" -v today="$today" '
17
+ NR==1 {next}
18
+ $3 == "waiting" {
19
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
20
+ cmd | getline last_contact_epoch; close(cmd);
21
+ age_days = (today - last_contact_epoch) / 86400;
22
+ if (age_days >= days) {
23
+ printf "STALE: %s — last contacted %s (%d days ago)\n", $1, $2, age_days;
24
+ }
25
+ }
26
+ ' "{{waitlist_csv_path}}"
27
+ variables:
28
+ waitlist_csv_path:
29
+ default: "./enrollment-waitlist.csv"
30
+ description: "Path to a CSV with columns: family_name,last_contacted_date (YYYY-MM-DD),status (waiting/enrolled/withdrawn)."
31
+ stale_after_days:
32
+ default: 30
33
+ description: Days since last contact before a waitlisted family counts as due for a check-in.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: A family that's gone quiet on a stale waitlist entry may have already enrolled elsewhere — a check-in also keeps the waitlist itself accurate.
@@ -0,0 +1,35 @@
1
+ id: staff-certification-expiry-check
2
+ version: 1
3
+ name: Childcare Staff Certification Expiry Check
4
+ description: >
5
+ Checks staff CPR/first-aid and other required childcare certifications
6
+ and warns before any expire. Use this if a licensing inspection has ever
7
+ been the first place a lapsed certification got noticed, instead of
8
+ catching it with time to renew.
9
+ category: childcare
10
+ tags: [childcare, staff, certification, compliance]
11
+ schedule: "0 8 * * 1"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ awk -F',' -v days="{{warn_days_before}}" '
16
+ NR==1 {next}
17
+ {
18
+ cmd = "date -d \"" $3 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $3 "\" +%s 2>/dev/null";
19
+ cmd | getline expiry_epoch; close(cmd);
20
+ "date +%s" | getline now_epoch; close("date +%s");
21
+ diff_days = (expiry_epoch - now_epoch) / 86400;
22
+ if (diff_days <= days) {
23
+ printf "EXPIRING: %s — %s expires %s (%d days)\n", $1, $2, $3, diff_days;
24
+ }
25
+ }
26
+ ' "{{staff_certs_csv_path}}"
27
+ variables:
28
+ staff_certs_csv_path:
29
+ default: "./staff-certifications.csv"
30
+ description: "Path to a CSV with columns: staff_name,cert_type (e.g. CPR, first-aid, mandated reporter),expiry_date (YYYY-MM-DD)."
31
+ warn_days_before:
32
+ default: 45
33
+ description: How many days before a certification expires to start warning.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: Required certification types vary by state/licensing body — confirm your jurisdiction's list rather than assuming CPR/first-aid is the whole requirement.
@@ -1,5 +1,5 @@
1
1
  id: error-rate-spike-watch
2
- version: 1
2
+ version: 2
3
3
  name: Error Rate Spike Watch
4
4
  description: >
5
5
  Checks your error tracker's recent error count against a threshold and
@@ -11,7 +11,7 @@ schedule: "*/15 * * * *"
11
11
  timezone: "UTC"
12
12
  runner: hybrid
13
13
  command: >
14
- if [ -n "$SENTRY_AUTH_TOKEN" ] && [ -n "{{sentry_project}}" ]; then
14
+ if [ -n "${SENTRY_AUTH_TOKEN:-}" ] && [ -n "{{sentry_project}}" ]; then
15
15
  count=$(curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" "https://sentry.io/api/0/projects/{{sentry_org}}/{{sentry_project}}/events/?statsPeriod=15m" | grep -o '"id"' | wc -l | tr -d ' ');
16
16
  if [ "$count" -ge "{{threshold}}" ]; then echo "WARNING: $count errors in last 15m (threshold {{threshold}})"; exit 1;
17
17
  else echo "OK: $count errors in last 15m"; fi
@@ -0,0 +1,36 @@
1
+ id: facility-cleaning-log-check
2
+ version: 1
3
+ name: Facility Cleaning Log Check
4
+ description: >
5
+ Checks that scheduled cleaning tasks (equipment wipe-down, locker rooms,
6
+ floors) were actually logged as done today. Use this if a cleaning
7
+ checklist has ever gone unfilled for a shift because it lived on a
8
+ clipboard nobody was checking.
9
+ category: fitness
10
+ tags: [fitness, cleaning, facility, compliance]
11
+ schedule: "0 21 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ today=$(date +%Y-%m-%d);
16
+ missing=$(awk -F',' -v today="$today" -v tasks="{{required_tasks}}" '
17
+ BEGIN { n = split(tasks, req, ","); }
18
+ $2 == today { done[$1] = 1 }
19
+ END {
20
+ for (i = 1; i <= n; i++) {
21
+ t = req[i];
22
+ gsub(/^ +| +$/, "", t);
23
+ if (!(t in done)) print "NOT LOGGED TODAY: " t;
24
+ }
25
+ }
26
+ ' "{{cleaning_log_csv_path}}");
27
+ if [ -n "$missing" ]; then echo "$missing"; exit 1; else echo "OK: all required cleaning tasks logged today"; fi
28
+ variables:
29
+ cleaning_log_csv_path:
30
+ default: "./cleaning-log.csv"
31
+ description: "Path to a CSV with columns: task_name,completed_date (YYYY-MM-DD)."
32
+ required_tasks:
33
+ default: "equipment wipe-down, locker rooms, floors, restrooms"
34
+ description: Comma-separated list of tasks that must be logged each day.
35
+ compatible_agents: [generic, claude, codex]
36
+ notes: Only as reliable as whoever fills in the log — pair with a physical walk-through periodically to spot-check that logged-done actually means done.
@@ -0,0 +1,35 @@
1
+ id: trainer-session-package-expiry-check
2
+ version: 1
3
+ name: Trainer Session Package Expiry Check
4
+ description: >
5
+ Flags members with a paid personal-training session package that's
6
+ expiring soon with sessions still unused. Use this if a client has ever
7
+ paid for ten sessions, used three, and let the rest expire because
8
+ nobody nudged them to book before the window closed.
9
+ category: fitness
10
+ tags: [fitness, personal-training, sessions, expiry]
11
+ schedule: "0 9 * * 1"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ awk -F',' -v days="{{warn_days_before}}" '
16
+ NR==1 {next}
17
+ $4 > 0 {
18
+ cmd = "date -d \"" $3 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $3 "\" +%s 2>/dev/null";
19
+ cmd | getline expiry_epoch; close(cmd);
20
+ "date +%s" | getline now_epoch; close("date +%s");
21
+ diff_days = (expiry_epoch - now_epoch) / 86400;
22
+ if (diff_days <= days && diff_days >= 0) {
23
+ printf "EXPIRING: %s — %d session(s) left, expires %s (%d days)\n", $1, $4, $3, diff_days;
24
+ }
25
+ }
26
+ ' "{{packages_csv_path}}"
27
+ variables:
28
+ packages_csv_path:
29
+ default: "./training-packages.csv"
30
+ description: "Path to a CSV with columns: member_name,package_size,expiry_date (YYYY-MM-DD),sessions_remaining."
31
+ warn_days_before:
32
+ default: 14
33
+ description: How many days before expiry to start warning, while there's still time to book remaining sessions.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: An expiring package with several sessions left is also a good moment to offer an extension or renewal, not just a "use it or lose it" notice.
@@ -0,0 +1,35 @@
1
+ id: budget-appropriation-deadline-check
2
+ version: 1
3
+ name: Budget Appropriation Submission Deadline Check
4
+ description: >
5
+ Warns you before an agency's budget appropriation submission is due.
6
+ Use this if a department has ever missed its window to request funding
7
+ for the next cycle because the internal deadline snuck up during budget
8
+ season chaos.
9
+ category: government
10
+ tags: [government, budget, appropriation, deadline]
11
+ schedule: "0 8 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ awk -F',' -v days="{{warn_days_before}}" '
16
+ NR==1 {next}
17
+ {
18
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
19
+ cmd | getline due_epoch; close(cmd);
20
+ "date +%s" | getline now_epoch; close("date +%s");
21
+ diff_days = (due_epoch - now_epoch) / 86400;
22
+ if (diff_days <= days && diff_days >= 0) {
23
+ printf "SUBMISSION DUE: %s — due %s (%d days)\n", $1, $2, diff_days;
24
+ }
25
+ }
26
+ ' "{{budget_deadlines_csv_path}}"
27
+ variables:
28
+ budget_deadlines_csv_path:
29
+ default: "./budget-deadlines.csv"
30
+ description: "Path to a CSV with columns: department_name,submission_due_date (YYYY-MM-DD)."
31
+ warn_days_before:
32
+ default: 21
33
+ description: How many days before the submission deadline to start warning.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: Keep department names distinct if you're tracking multiple agencies' cycles in one CSV — otherwise run one instance of this job per department.
@@ -0,0 +1,39 @@
1
+ id: public-meeting-notice-compliance-check
2
+ version: 1
3
+ name: Public Meeting Notice Compliance Check
4
+ description: >
5
+ Checks that an upcoming public meeting's notice was posted with enough
6
+ lead time to meet open-meetings law requirements. Use this if a meeting
7
+ has ever gotten challenged, or had to be redone, because notice went out
8
+ a day late.
9
+ category: government
10
+ tags: [government, open-meetings, notice, compliance]
11
+ schedule: "0 8 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ meeting_epoch=$(date -d "{{meeting_date}}" +%s 2>/dev/null || date -jf "%Y-%m-%d" "{{meeting_date}}" +%s);
16
+ now_epoch=$(date +%s);
17
+ days_until=$(( (meeting_epoch - now_epoch) / 86400 ));
18
+ if [ ! -f "{{notice_posted_marker}}" ]; then
19
+ if [ "$days_until" -le "{{required_notice_days}}" ]; then
20
+ echo "COMPLIANCE RISK: meeting on {{meeting_date}} is in $days_until days with no notice-posted marker found (requires {{required_notice_days}} days notice)";
21
+ exit 1;
22
+ else
23
+ echo "OK: meeting is $days_until days out, still within the notice window";
24
+ fi
25
+ else
26
+ echo "OK: notice marker found for meeting on {{meeting_date}}";
27
+ fi
28
+ variables:
29
+ meeting_date:
30
+ default: "YYYY-MM-DD"
31
+ description: Date of the upcoming public meeting.
32
+ required_notice_days:
33
+ default: 3
34
+ description: Minimum days of advance notice required by your jurisdiction's open-meetings law.
35
+ notice_posted_marker:
36
+ default: "./notice-posted.marker"
37
+ description: Path to a file you create once notice has actually been posted (physically and/or online) — its existence is the "done" signal.
38
+ compatible_agents: [generic, claude, codex]
39
+ notes: Required notice periods and posting locations vary significantly by state/locality — confirm your specific statute rather than relying on the example default.
@@ -0,0 +1,36 @@
1
+ id: gift-card-liability-reconciliation-check
2
+ version: 1
3
+ name: Gift Card Liability Reconciliation Check
4
+ description: >
5
+ Compares your POS's outstanding gift card balance against your
6
+ accounting ledger and flags a mismatch. Use this if outstanding gift
7
+ card liability has ever drifted out of sync with your books for months
8
+ before anyone reconciled it at year-end and found a gap.
9
+ category: retail
10
+ tags: [retail, gift-cards, reconciliation, finance]
11
+ schedule: "0 7 1 * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ pos="{{pos_outstanding_balance_usd}}";
16
+ ledger="{{ledger_outstanding_balance_usd}}";
17
+ tolerance="{{tolerance_usd}}";
18
+ diff=$(awk -v p="$pos" -v l="$ledger" 'BEGIN{d=p-l; if (d<0) d=-d; printf "%.2f", d}');
19
+ if awk -v d="$diff" -v t="$tolerance" 'BEGIN{exit !(d > t)}'; then
20
+ echo "MISMATCH: POS shows \$$pos outstanding, ledger shows \$$ledger (diff \$$diff)";
21
+ exit 1;
22
+ else
23
+ echo "OK: POS and ledger outstanding gift card balances match within tolerance";
24
+ fi
25
+ variables:
26
+ pos_outstanding_balance_usd:
27
+ default: 0
28
+ description: Total outstanding gift card balance per your POS system's report.
29
+ ledger_outstanding_balance_usd:
30
+ default: 0
31
+ description: Total outstanding gift card liability per your accounting ledger.
32
+ tolerance_usd:
33
+ default: 25.00
34
+ description: Acceptable discrepancy in dollars before flagging.
35
+ compatible_agents: [generic, claude, codex]
36
+ notes: A growing gap over several months usually points at a systemic sync issue between POS and accounting, not a one-time error — worth escalating rather than just re-running this each month.
@@ -0,0 +1,39 @@
1
+ id: return-fraud-pattern-check
2
+ version: 1
3
+ name: Return Fraud Pattern Check
4
+ description: >
5
+ Checks recent returns for patterns that look like abuse — the same
6
+ customer returning unusually often, or a spike in receiptless returns —
7
+ instead of treating every return as an isolated, no-questions-asked
8
+ event. Use this if return fraud has ever quietly eaten margin for
9
+ months because nobody was looking at returns in aggregate.
10
+ category: retail
11
+ tags: [retail, returns, fraud, loss-prevention]
12
+ schedule: "0 7 * * 1"
13
+ timezone: "UTC"
14
+ runner: shell
15
+ command: >
16
+ awk -F',' -v max_returns="{{max_returns_per_customer}}" '
17
+ NR==1 {next}
18
+ { count[$2]++; if ($3 == "no-receipt") noreceipt++; total++ }
19
+ END {
20
+ for (cust in count) {
21
+ if (count[cust] >= max_returns) {
22
+ printf "FREQUENT RETURNER: %s — %d returns in the period\n", cust, count[cust];
23
+ }
24
+ }
25
+ if (total > 0) {
26
+ pct = (noreceipt / total) * 100;
27
+ printf "no-receipt returns: %.0f%% of %d total\n", pct, total;
28
+ }
29
+ }
30
+ ' "{{returns_csv_path}}"
31
+ variables:
32
+ returns_csv_path:
33
+ default: "./returns-log.csv"
34
+ description: "Path to a CSV with columns: return_id,customer_identifier,receipt_status (receipt/no-receipt) — one row per return in the period."
35
+ max_returns_per_customer:
36
+ default: 5
37
+ description: Number of returns from the same customer in the period that counts as worth a closer look.
38
+ compatible_agents: [generic, claude, codex]
39
+ notes: A flagged pattern is a prompt to look closer, not proof of fraud — plenty of frequent returners are just picky shoppers, not scammers.
@@ -0,0 +1,35 @@
1
+ id: diagnostic-equipment-calibration-check
2
+ version: 1
3
+ name: Diagnostic Equipment Calibration Check
4
+ description: >
5
+ Tracks calibration due dates for in-house diagnostic equipment (x-ray,
6
+ in-house lab analyzers, ultrasound) and warns before any lapse. Use this
7
+ if a lab result has ever been called into question because equipment
8
+ calibration had quietly lapsed and nobody noticed until an audit.
9
+ category: veterinary
10
+ tags: [veterinary, equipment, calibration, compliance]
11
+ schedule: "0 7 * * 1"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ awk -F',' -v days="{{warn_days_before}}" '
16
+ NR==1 {next}
17
+ {
18
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
19
+ cmd | getline due_epoch; close(cmd);
20
+ "date +%s" | getline now_epoch; close("date +%s");
21
+ diff_days = (due_epoch - now_epoch) / 86400;
22
+ if (diff_days <= days) {
23
+ printf "CALIBRATION DUE: %s — due %s (%d days)\n", $1, $2, diff_days;
24
+ }
25
+ }
26
+ ' "{{equipment_csv_path}}"
27
+ variables:
28
+ equipment_csv_path:
29
+ default: "./diagnostic-equipment-calibration.csv"
30
+ description: "Path to a CSV with columns: equipment_name,calibration_due_date (YYYY-MM-DD)."
31
+ warn_days_before:
32
+ default: 14
33
+ description: How many days before calibration is due to start warning.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: Take equipment out of clinical use once calibration lapses rather than continuing to trust its readings until the service tech arrives.
@@ -0,0 +1,30 @@
1
+ id: surgery-prep-check
2
+ version: 1
3
+ name: Surgery Prep Check
4
+ description: >
5
+ Checks tomorrow's scheduled surgeries for missing pre-op bloodwork or
6
+ signed consent forms. Use this if a surgery has ever had to be
7
+ rescheduled the morning of because consent wasn't signed or bloodwork
8
+ hadn't been run, and nobody checked the night before.
9
+ category: veterinary
10
+ tags: [veterinary, surgery, pre-op, consent]
11
+ schedule: "0 17 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ tomorrow=$(date -d "+1 day" +%Y-%m-%d 2>/dev/null || date -v+1d +%Y-%m-%d);
16
+ awk -F',' -v tomorrow="$tomorrow" '
17
+ NR==1 {next}
18
+ $2 == tomorrow {
19
+ missing = "";
20
+ if ($3 != "done") missing = missing " bloodwork";
21
+ if ($4 != "signed") missing = missing " consent";
22
+ if (missing != "") printf "NOT READY: %s (%s) — missing:%s\n", $1, $5, missing;
23
+ }
24
+ ' "{{surgery_schedule_csv_path}}"
25
+ variables:
26
+ surgery_schedule_csv_path:
27
+ default: "./surgery-schedule.csv"
28
+ description: "Path to a CSV with columns: pet_name,surgery_date (YYYY-MM-DD),bloodwork_status (done/pending),consent_status (signed/pending),owner_name."
29
+ compatible_agents: [generic, claude, codex]
30
+ notes: Run this the evening before so there's still time to call the owner and get bloodwork done or consent signed before the surgery slot.
@@ -0,0 +1,35 @@
1
+ id: cycle-count-schedule-check
2
+ version: 1
3
+ name: Cycle Count Schedule Adherence Check
4
+ description: >
5
+ Checks whether scheduled cycle counts actually happened on time, zone by
6
+ zone. Use this if a cycle count program has ever quietly stopped
7
+ happening for a whole section of the warehouse because nobody was
8
+ tracking adherence to the schedule itself, only the count results.
9
+ category: warehousing
10
+ tags: [warehousing, cycle-count, schedule, inventory]
11
+ schedule: "0 8 * * 1"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ today=$(date +%s);
16
+ awk -F',' -v days="{{overdue_after_days}}" -v today="$today" '
17
+ NR==1 {next}
18
+ {
19
+ cmd = "date -d \"" $2 "\" +%s 2>/dev/null || date -j -f %Y-%m-%d \"" $2 "\" +%s 2>/dev/null";
20
+ cmd | getline last_count_epoch; close(cmd);
21
+ age_days = (today - last_count_epoch) / 86400;
22
+ if (age_days >= days) {
23
+ printf "OVERDUE: zone %s — last counted %s (%d days ago)\n", $1, $2, age_days;
24
+ }
25
+ }
26
+ ' "{{cycle_count_log_csv_path}}"
27
+ variables:
28
+ cycle_count_log_csv_path:
29
+ default: "./cycle-count-log.csv"
30
+ description: "Path to a CSV with columns: zone_name,last_count_date (YYYY-MM-DD)."
31
+ overdue_after_days:
32
+ default: 30
33
+ description: Days since the last cycle count before a zone counts as overdue for its next one.
34
+ compatible_agents: [generic, claude, codex]
35
+ notes: This checks schedule adherence only — pair with `inventory-count-discrepancy-check` for what the counts themselves turn up.
@@ -0,0 +1,36 @@
1
+ id: forklift-inspection-check
2
+ version: 1
3
+ name: Forklift Daily Safety Inspection Check
4
+ description: >
5
+ Checks that each forklift's required daily pre-use inspection was
6
+ actually logged today. Use this if a forklift has ever been operated
7
+ on a shift with no inspection logged, which is exactly what an OSHA
8
+ audit will ask to see records of.
9
+ category: warehousing
10
+ tags: [warehousing, forklift, safety, compliance]
11
+ schedule: "0 10 * * *"
12
+ timezone: "UTC"
13
+ runner: shell
14
+ command: >
15
+ today=$(date +%Y-%m-%d);
16
+ missing=$(awk -F',' -v today="$today" -v fleet="{{forklift_ids}}" '
17
+ BEGIN { n = split(fleet, ids, ","); }
18
+ $2 == today { logged[$1] = 1 }
19
+ END {
20
+ for (i = 1; i <= n; i++) {
21
+ id = ids[i];
22
+ gsub(/^ +| +$/, "", id);
23
+ if (!(id in logged)) print "NOT INSPECTED TODAY: " id;
24
+ }
25
+ }
26
+ ' "{{inspection_log_csv_path}}");
27
+ if [ -n "$missing" ]; then echo "$missing"; exit 1; else echo "OK: all forklifts inspected today"; fi
28
+ variables:
29
+ inspection_log_csv_path:
30
+ default: "./forklift-inspection-log.csv"
31
+ description: "Path to a CSV with columns: forklift_id,inspection_date (YYYY-MM-DD)."
32
+ forklift_ids:
33
+ default: "FL-1,FL-2"
34
+ description: Comma-separated list of forklift IDs that must each have a logged inspection today.
35
+ compatible_agents: [generic, claude, codex]
36
+ notes: A forklift with a failed inspection item should be pulled from service immediately — this job only checks that the inspection was logged, not what it found.
package/lib/deploy.js ADDED
@@ -0,0 +1,84 @@
1
+ // Turns a resolved crondex job into a deployment artifact — pulled out of
2
+ // bin/crondex.js's `deploy` command so it's unit testable (see test/deploy.test.js).
3
+
4
+ const PLACEHOLDER_RE = /\{\{\s*(\w+)\s*\}\}/g;
5
+
6
+ // Merges a job's `variables` defaults with any user-supplied overrides.
7
+ export function resolveVariables(job, overrides = {}) {
8
+ const values = {};
9
+ for (const [name, spec] of Object.entries(job.variables ?? {})) {
10
+ values[name] = name in overrides ? overrides[name] : spec.default;
11
+ }
12
+ return values;
13
+ }
14
+
15
+ // crondex substitutes {{placeholders}} as literal text — this mirrors that exactly,
16
+ // leaving a placeholder untouched if no value was resolved for it (rather than
17
+ // silently dropping it, which would produce a broken script with no warning).
18
+ export function substitutePlaceholders(text, values) {
19
+ return text.replace(PLACEHOLDER_RE, (match, name) => (name in values ? String(values[name]) : match));
20
+ }
21
+
22
+ // hybrid jobs support both — shell/agent-prompt jobs only support their one mode.
23
+ export function pickMode(job, requestedMode) {
24
+ if (job.runner === "shell") return "script";
25
+ if (job.runner === "agent-prompt") return "prompt";
26
+ return requestedMode === "prompt" ? "prompt" : "script";
27
+ }
28
+
29
+ function escapeSingleQuotes(text) {
30
+ return text.replace(/'/g, `'"'"'`);
31
+ }
32
+
33
+ // Flattens a (possibly multi-line) resolved command/prompt into one crontab line.
34
+ // isPrompt jobs can't assume any particular agent CLI syntax, so the line defers to
35
+ // a CRONDEX_AGENT_CLI env var the user sets themselves (e.g. `export
36
+ // CRONDEX_AGENT_CLI="claude -p"`) rather than guessing wrong.
37
+ export function buildCrontabLine(job, resolvedText, isPrompt) {
38
+ const flat = resolvedText.trim().replace(/\s*\n\s*/g, " ");
39
+ const escaped = escapeSingleQuotes(flat);
40
+ const body = isPrompt
41
+ ? `\${CRONDEX_AGENT_CLI:?set CRONDEX_AGENT_CLI to your agent CLI invocation, e.g. "claude -p"} '${escaped}'`
42
+ : `bash -lc '${escaped}'`;
43
+ return `${job.schedule} ${body} # crondex:${job.id}`;
44
+ }
45
+
46
+ // Builds a ready-to-commit GitHub Actions workflow file for the job. GitHub Actions
47
+ // schedules always run in UTC regardless of the `cron:` string's intent, so a
48
+ // non-UTC job gets a visible warning comment rather than silently firing at the
49
+ // wrong hour.
50
+ export function buildGithubActionsWorkflow(job, { command, prompt, mode }) {
51
+ const lines = [];
52
+ lines.push(`name: ${job.name}`);
53
+ lines.push(`# Generated by \`crondex deploy ${job.id} --target github-actions\` from ${job.path}.`);
54
+ if (job.timezone && job.timezone !== "UTC") {
55
+ lines.push(`# NOTE: this job's schedule ("${job.schedule}") is defined in ${job.timezone}, but GitHub`);
56
+ lines.push(`# Actions cron always runs in UTC — adjust the hour field(s) below if timing matters.`);
57
+ }
58
+ lines.push("");
59
+ lines.push("on:");
60
+ lines.push(" schedule:");
61
+ lines.push(` - cron: "${job.schedule}"`);
62
+ lines.push(" workflow_dispatch: {}");
63
+ lines.push("");
64
+ lines.push("jobs:");
65
+ lines.push(" run:");
66
+ lines.push(" runs-on: ubuntu-latest");
67
+ lines.push(" steps:");
68
+ lines.push(" - uses: actions/checkout@v4");
69
+ if (mode === "script") {
70
+ lines.push(" - name: run job");
71
+ lines.push(" run: |");
72
+ for (const l of command.trimEnd().split("\n")) lines.push(` ${l}`);
73
+ } else {
74
+ lines.push(" - name: run job (agent-prompt)");
75
+ lines.push(
76
+ " # TODO: wire up your agent CLI/action here — this step only prints the resolved prompt."
77
+ );
78
+ lines.push(" run: |");
79
+ lines.push(" cat <<'CRONDEX_PROMPT'");
80
+ for (const l of prompt.trimEnd().split("\n")) lines.push(` ${l}`);
81
+ lines.push(" CRONDEX_PROMPT");
82
+ }
83
+ return lines.join("\n") + "\n";
84
+ }
@@ -0,0 +1,18 @@
1
+ // Job pairs that score above the near-duplicate thresholds but were reviewed and
2
+ // confirmed genuinely distinct — recorded here so scripts/check-duplicates.js stops
3
+ // re-flagging (and CI stops failing on) the same reviewed pair every run. Order of
4
+ // ids within a pair doesn't matter.
5
+ //
6
+ // Add an entry only after actually comparing the two jobs — this is a record of a
7
+ // human decision, not a way to silence the check.
8
+ export const ALLOWED_DUPLICATE_PAIRS = [
9
+ [
10
+ "food-cost-percentage-watch",
11
+ "labor-cost-percentage-watch",
12
+ "Same benchmark-against-sales pattern, different cost categories (food vs. labor) — intentionally parallel jobs, not a copy-paste duplicate.",
13
+ ],
14
+ ];
15
+
16
+ export function isAllowedPair(idA, idB) {
17
+ return ALLOWED_DUPLICATE_PAIRS.some(([x, y]) => (x === idA && y === idB) || (x === idB && y === idA));
18
+ }