sleep_cycles
Plan bedtimes for a wake-up time, or wake-up times for a bedtime, in whole sleep cycles (90 minutes by default, configurable) plus the time it takes to fall asleep, across midnight, marking the options whose total sleep falls in a recommended range (default 7-9 hours for adults). Every output carries a disclaimer: not medical advice.
Install
/tools install sleep_cycles /tools install sleep_cycles@1.0.0
In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · sleep_cycles 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool sleep_cycles; each run is approved as a local_exec.
Arguments
One JSON object on stdin, validated against the schema before the program starts.
| name | type | required | default | meaning |
|---|---|---|---|---|
mode | string | yes | wake_at lists bedtimes; bed_at lists wake-up times. one of "wake_at", "bed_at" | |
time | string | yes | pattern ^([01]?\d|2[0-3]):[0-5]\d$ | |
cycle_minutes | integer | no | 90 | ≥ 30; ≤ 150 |
latency_minutes | integer | no | 15 | ≥ 0; ≤ 120 |
min_cycles | integer | no | 3 | ≥ 1; ≤ 12 |
max_cycles | integer | no | 6 | ≥ 1; ≤ 12 |
recommended_hours | array of number | no | [7, 9] | items ≥ 2; items ≤ 2 |
Input schema (JSON)
{
"type": "object",
"required": [
"mode",
"time"
],
"additionalProperties": false,
"properties": {
"mode": {
"type": "string",
"enum": [
"wake_at",
"bed_at"
],
"description": "wake_at lists bedtimes; bed_at lists wake-up times"
},
"time": {
"type": "string",
"pattern": "^([01]?\\d|2[0-3]):[0-5]\\d$"
},
"cycle_minutes": {
"type": "integer",
"minimum": 30,
"maximum": 150,
"default": 90
},
"latency_minutes": {
"type": "integer",
"minimum": 0,
"maximum": 120,
"default": 15
},
"min_cycles": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 3
},
"max_cycles": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"default": 6
},
"recommended_hours": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"minimum": 0,
"maximum": 24
},
"default": [
7,
9
]
}
}
}
Example
Input
{
"mode": "wake_at",
"time": "07:00",
"min_cycles": 5,
"max_cycles": 6
}
Output (stdout)
{
"options": [
{
"cycles": 6,
"bedtime": "21:45",
"day": "previous day"
},
{
"cycles": 5,
"bedtime": "23:15",
"day": "previous day"
}
]
}
Files
The command the install card registers is python3 sleep_cycles.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.
sleep_cycles.py · sha256 c0b71cb647e59e00cb83bdd189c886438ab7198325596e99c4b96027b5ab2244
#!/usr/bin/env python3
"""Bedtime and wake-time planner in whole sleep cycles, across midnight.
Input (JSON on stdin):
mode "wake_at" (time is the wake-up time: list bedtimes)
| "bed_at" (time is when you go to bed: list wake-up times)
time "HH:MM" (24-hour)
cycle_minutes default 90 (30-150)
latency_minutes time to fall asleep, default 15 (0-120)
min_cycles default 3
max_cycles default 6
recommended_hours [low, high] total sleep counted as recommended, default [7, 9]
(adults 18-64: National Sleep Foundation, Hirshkowitz et al. 2015)
"""
import json
import re
import sys
DISCLAIMER = ("Not medical advice: the 90-minute cycle is an average and real cycles vary between people and "
"through the night. Persistent sleep problems need a qualified clinician.")
DAY = 1440
def fail(message):
print(json.dumps({"error": message, "disclaimer": DISCLAIMER}))
sys.exit(2)
def integer(request, field, default, low, high):
value = request.get(field, default)
if isinstance(value, bool) or not isinstance(value, int) or not low <= value <= high:
fail(f"{field} must be an integer from {low} to {high}")
return value
def clock(minutes):
offset, rest = divmod(minutes, DAY)
return f"{rest // 60:02d}:{rest % 60:02d}", offset
def duration(minutes):
return f"{minutes // 60}:{minutes % 60:02d}"
def main():
try:
request = json.load(sys.stdin)
except ValueError as error:
fail(f"stdin is not JSON: {error}")
if not isinstance(request, dict):
fail("input must be a JSON object")
extra = sorted(set(request) - {"mode", "time", "cycle_minutes", "latency_minutes", "min_cycles", "max_cycles",
"recommended_hours"})
if extra:
fail(f"unknown field(s): {', '.join(extra)}")
mode = request.get("mode")
if mode not in ("wake_at", "bed_at"):
fail("mode must be wake_at or bed_at")
raw = request.get("time")
match = re.fullmatch(r"([01]?\d|2[0-3]):([0-5]\d)", raw) if isinstance(raw, str) else None
if not match:
fail("time must be HH:MM, 24-hour")
base = int(match.group(1)) * 60 + int(match.group(2))
cycle = integer(request, "cycle_minutes", 90, 30, 150)
latency = integer(request, "latency_minutes", 15, 0, 120)
low_c = integer(request, "min_cycles", 3, 1, 12)
high_c = integer(request, "max_cycles", 6, 1, 12)
if low_c > high_c:
fail("min_cycles must not exceed max_cycles")
hours = request.get("recommended_hours", [7, 9])
if (not isinstance(hours, list) or len(hours) != 2
or not all(isinstance(h, (int, float)) and not isinstance(h, bool) for h in hours)
or not 0 <= hours[0] <= hours[1] <= 24):
fail("recommended_hours must be [low, high] with 0 <= low <= high <= 24")
options = []
for n in range(high_c, low_c - 1, -1) if mode == "wake_at" else range(low_c, high_c + 1):
sleep = n * cycle
if mode == "wake_at":
asleep = base - sleep
target = asleep - latency
else:
asleep = base + latency
target = asleep + sleep
time_text, offset = clock(target)
asleep_text, asleep_offset = clock(asleep)
options.append({
"cycles": n, "sleep_minutes": sleep, "sleep_duration": duration(sleep),
("bedtime" if mode == "wake_at" else "wake_time"): time_text, "day_offset": offset,
"day": {-1: "previous day", 0: "same day", 1: "next day"}.get(offset, f"{offset:+d} days"),
"fall_asleep_at": asleep_text, "fall_asleep_day_offset": asleep_offset,
"time_in_bed_minutes": sleep + latency,
"recommended": hours[0] * 60 <= sleep <= hours[1] * 60,
})
result = {"mode": mode, "time": f"{base // 60:02d}:{base % 60:02d}", "cycle_minutes": cycle,
"latency_minutes": latency, "recommended_hours": hours, "options": options,
"notes": ["day_offset is relative to the day of the given time.",
"Recommended range default: 7-9 h for adults 18-64 (National Sleep Foundation, Hirshkowitz et al., "
"Sleep Health 2015); give recommended_hours for other ages."],
"disclaimer": DISCLAIMER}
print(json.dumps(result))
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except Exception as error: # last resort: a usage error, never a traceback
fail(f"invalid input ({type(error).__name__})")
Where it is served
The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/sleep_cycles/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/sleep_cycles/1.0.0/sleep_cycles.py