loops-routines 0.1.0__tar.gz

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.
Files changed (58) hide show
  1. loops_routines-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +30 -0
  2. loops_routines-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. loops_routines-0.1.0/.github/dependabot.yml +20 -0
  4. loops_routines-0.1.0/.github/pull_request_template.md +9 -0
  5. loops_routines-0.1.0/.github/workflows/ci.yml +39 -0
  6. loops_routines-0.1.0/.github/workflows/release.yml +70 -0
  7. loops_routines-0.1.0/.gitignore +5 -0
  8. loops_routines-0.1.0/CHANGELOG.md +24 -0
  9. loops_routines-0.1.0/CONTRIBUTING.md +39 -0
  10. loops_routines-0.1.0/LICENSE +21 -0
  11. loops_routines-0.1.0/PKG-INFO +220 -0
  12. loops_routines-0.1.0/README.md +194 -0
  13. loops_routines-0.1.0/SECURITY.md +31 -0
  14. loops_routines-0.1.0/docs/screenshot.jpg +0 -0
  15. loops_routines-0.1.0/loops_routines/__init__.py +6 -0
  16. loops_routines-0.1.0/loops_routines/__main__.py +3 -0
  17. loops_routines-0.1.0/loops_routines/cli.py +282 -0
  18. loops_routines-0.1.0/loops_routines/client.py +69 -0
  19. loops_routines-0.1.0/loops_routines/core/__init__.py +0 -0
  20. loops_routines-0.1.0/loops_routines/core/claude_cli.py +61 -0
  21. loops_routines-0.1.0/loops_routines/core/clock.py +19 -0
  22. loops_routines-0.1.0/loops_routines/core/config.py +217 -0
  23. loops_routines-0.1.0/loops_routines/core/deliver.py +50 -0
  24. loops_routines-0.1.0/loops_routines/core/evaluate.py +80 -0
  25. loops_routines-0.1.0/loops_routines/core/integrations.py +368 -0
  26. loops_routines-0.1.0/loops_routines/core/notify.py +124 -0
  27. loops_routines-0.1.0/loops_routines/core/paths.py +51 -0
  28. loops_routines-0.1.0/loops_routines/core/probe.py +61 -0
  29. loops_routines-0.1.0/loops_routines/core/routines.py +213 -0
  30. loops_routines-0.1.0/loops_routines/core/runner.py +489 -0
  31. loops_routines-0.1.0/loops_routines/core/store.py +233 -0
  32. loops_routines-0.1.0/loops_routines/core/triggers.py +178 -0
  33. loops_routines-0.1.0/loops_routines/core/usage.py +119 -0
  34. loops_routines-0.1.0/loops_routines/core/windows.py +111 -0
  35. loops_routines-0.1.0/loops_routines/daemon/__init__.py +0 -0
  36. loops_routines-0.1.0/loops_routines/daemon/app.py +267 -0
  37. loops_routines-0.1.0/loops_routines/daemon/auth.py +90 -0
  38. loops_routines-0.1.0/loops_routines/daemon/main.py +21 -0
  39. loops_routines-0.1.0/loops_routines/daemon/mcp.py +235 -0
  40. loops_routines-0.1.0/loops_routines/defaults/README.md +37 -0
  41. loops_routines-0.1.0/loops_routines/defaults/routines/cleanup.md +23 -0
  42. loops_routines-0.1.0/loops_routines/defaults/routines/monitor-logs.md +29 -0
  43. loops_routines-0.1.0/loops_routines/defaults/routines/weekly-summary.md +21 -0
  44. loops_routines-0.1.0/loops_routines/install.py +144 -0
  45. loops_routines-0.1.0/loops_routines/service.py +810 -0
  46. loops_routines-0.1.0/loops_routines/ui/__init__.py +0 -0
  47. loops_routines-0.1.0/loops_routines/ui/web/index.html +450 -0
  48. loops_routines-0.1.0/pyproject.toml +61 -0
  49. loops_routines-0.1.0/tests/conftest.py +126 -0
  50. loops_routines-0.1.0/tests/test_api.py +80 -0
  51. loops_routines-0.1.0/tests/test_evaluate.py +87 -0
  52. loops_routines-0.1.0/tests/test_install_cli.py +93 -0
  53. loops_routines-0.1.0/tests/test_mcp.py +81 -0
  54. loops_routines-0.1.0/tests/test_notify.py +28 -0
  55. loops_routines-0.1.0/tests/test_routines.py +92 -0
  56. loops_routines-0.1.0/tests/test_service.py +382 -0
  57. loops_routines-0.1.0/tests/test_triggers.py +71 -0
  58. loops_routines-0.1.0/uv.lock +844 -0
@@ -0,0 +1,30 @@
1
+ name: Bug report
2
+ description: Something doesn't work as described
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened
9
+ description: What you did, what you expected, what happened instead.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: routine
14
+ attributes:
15
+ label: The routine
16
+ description: The routine file's header (between the +++ lines), if a routine is involved. Remove anything private.
17
+ render: toml
18
+ - type: textarea
19
+ id: logs
20
+ attributes:
21
+ label: Logs
22
+ description: "`loops-routines status`, `loops-routines get RUN`, the end of ~/Library/Logs/loops_routines/daemon.log. Remove tokens and private data."
23
+ render: text
24
+ - type: input
25
+ id: versions
26
+ attributes:
27
+ label: Versions
28
+ description: loops-routines (`loops-routines status`), `claude --version`, macOS
29
+ validations:
30
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security vulnerability
4
+ url: https://github.com/Melevir/loops-routines/security/advisories/new
5
+ about: Report it privately, not as an issue.
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ cooldown:
8
+ default-days: 7
9
+ groups:
10
+ python:
11
+ patterns: ["*"]
12
+ - package-ecosystem: github-actions
13
+ directory: /
14
+ schedule:
15
+ interval: weekly
16
+ cooldown:
17
+ default-days: 7
18
+ groups:
19
+ actions:
20
+ patterns: ["*"]
@@ -0,0 +1,9 @@
1
+ ## What and why
2
+
3
+ <!-- What changes for a user, and the problem it solves. Link the issue if there is one. -->
4
+
5
+ ## Checks
6
+
7
+ - [ ] A test that fails without this change
8
+ - [ ] `uv run pytest` and `uv run ruff check .` pass
9
+ - [ ] A line in `CHANGELOG.md` under "Unreleased", if a user would notice
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ lint:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+ - uses: astral-sh/setup-uv@v6
23
+ - run: uv run --frozen ruff check .
24
+
25
+ test:
26
+ runs-on: macos-latest
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python: ["3.12", "3.13", "3.14"]
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 0 # hatch-vcs reads the version from git tags
35
+ - uses: astral-sh/setup-uv@v6
36
+ with:
37
+ python-version: ${{ matrix.python }}
38
+ enable-cache: true
39
+ - run: uv run --frozen pytest -q
@@ -0,0 +1,70 @@
1
+ name: Release
2
+
3
+ # Push a tag vX.Y.Z on main: the tests run, the package is built with that version, published to PyPI
4
+ # through Trusted Publishing (no API token) with attestations, and a GitHub Release is created.
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*.*.*"]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: macos-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+ - uses: astral-sh/setup-uv@v6
21
+ with:
22
+ python-version: "3.14"
23
+ - run: uv run --frozen ruff check .
24
+ - run: uv run --frozen pytest -q
25
+
26
+ build:
27
+ needs: test
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 0 # hatch-vcs reads the version from the tag
33
+ - uses: astral-sh/setup-uv@v6
34
+ - name: Check the tag matches the built version
35
+ run: |
36
+ uv build
37
+ ls dist/*-"${GITHUB_REF_NAME#v}"-py3-none-any.whl
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment:
47
+ name: pypi
48
+ url: https://pypi.org/project/loops-routines/
49
+ permissions:
50
+ id-token: write # Trusted Publishing
51
+ steps:
52
+ - uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+ - uses: pypa/gh-action-pypi-publish@release/v1
57
+
58
+ github-release:
59
+ needs: publish
60
+ runs-on: ubuntu-latest
61
+ permissions:
62
+ contents: write
63
+ steps:
64
+ - uses: actions/download-artifact@v4
65
+ with:
66
+ name: dist
67
+ path: dist/
68
+ - env:
69
+ GH_TOKEN: ${{ github.token }}
70
+ run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ dist/
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-08
10
+
11
+ The first release.
12
+
13
+ ### Added
14
+
15
+ - The daemon: routines on a cron line or an interval, within the plan's 5-hour and 7-day usage limits,
16
+ with priorities, parallel slots, grace periods and time windows.
17
+ - REST API with live events, an MCP server for Claude Code sessions, a CLI and a web page.
18
+ - `run_routine_now` (MCP) and `loops-routines run --now`: start a routine outside the queue.
19
+ - Agent tools: `report_status`, `report_blocker`, `notify` (macOS banners) and `ask_user` (dialogs with
20
+ the agent's own buttons).
21
+ - `loops-routines install --launchd --mcp` and `loops-routines restart`.
22
+
23
+ [Unreleased]: https://github.com/Melevir/loops-routines/compare/v0.1.0...HEAD
24
+ [0.1.0]: https://github.com/Melevir/loops-routines/releases/tag/v0.1.0
@@ -0,0 +1,39 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. Issues with a clear "what I did / what I expected / what happened" are as welcome as code.
4
+
5
+ ## Setup
6
+
7
+ Needs macOS, [uv](https://docs.astral.sh/uv/) and Python 3.12+.
8
+
9
+ ```bash
10
+ uv sync
11
+ uv run pytest
12
+ uv run ruff check .
13
+ ```
14
+
15
+ The tests never touch your real setup: they run in temporary folders, start the real runner against a fake
16
+ `claude` script (`tests/conftest.py`), and replace notifications with a fake notifier. Keep it that way: a test
17
+ must not call `launchctl`, `osascript` or a real `claude`.
18
+
19
+ To try a change against a real `claude` without disturbing an installed daemon, run a separate instance on
20
+ another port:
21
+
22
+ ```bash
23
+ uv run loops-routines --root /tmp/ct install
24
+ # set [daemon] port in /tmp/ct/home/config.toml, then:
25
+ uv run loops-routines --root /tmp/ct daemon
26
+ ```
27
+
28
+ ## Changes
29
+
30
+ - One change per pull request, with a test that fails without it.
31
+ - `service.py` holds the behaviour; REST (`daemon/app.py`), MCP (`daemon/mcp.py`) and the CLI stay thin
32
+ over it. A new action goes into `Service` first, then into whichever transports need it.
33
+ - MCP tools are `async def`: the MCP SDK runs sync tools in a worker thread, where the daemon's event loop
34
+ isn't available.
35
+ - Add a line under "Unreleased" in `CHANGELOG.md` for anything a user would notice.
36
+
37
+ ## Releases
38
+
39
+ Maintainers tag `vX.Y.Z` on `main`; CI builds and publishes to PyPI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilia (Melevir)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.5
2
+ Name: loops-routines
3
+ Version: 0.1.0
4
+ Summary: A daemon that runs Claude Code routines on a schedule and within the plan's usage limits, with an API, MCP and CLI
5
+ Project-URL: Homepage, https://github.com/Melevir/loops-routines
6
+ Project-URL: Issues, https://github.com/Melevir/loops-routines/issues
7
+ Project-URL: Changelog, https://github.com/Melevir/loops-routines/blob/main/CHANGELOG.md
8
+ Author-email: Ilia <melevir@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude-code,cron,launchd,macos,mcp,scheduler
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: httpx>=0.28
24
+ Requires-Dist: mcp>=2.3.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # loops-routines
28
+
29
+ [![CI](https://github.com/Melevir/loops-routines/actions/workflows/ci.yml/badge.svg)](https://github.com/Melevir/loops-routines/actions/workflows/ci.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/loops-routines.svg)](https://pypi.org/project/loops-routines/)
31
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
32
+
33
+ > An independent project, not made or endorsed by Anthropic. "Claude" and "Claude Code" are Anthropic's
34
+ > trademarks; loops-routines only starts the `claude` command you already have.
35
+
36
+ A daemon that runs Claude Code **routines** (on a cron line or an interval) **within your plan's usage
37
+ limits**. One process serves everything: the scheduler, the REST API with live events, the MCP server
38
+ for your Claude sessions, and a small web page. Agents tell you things with **macOS notifications** and
39
+ ask you questions in **system dialogs with their own buttons**, waiting for your click.
40
+
41
+ Examples it is made for:
42
+
43
+ - a weekly summary of the changes in several repositories (`0 9 * * mon`);
44
+ - a cleanup every 2 hours, but only with tokens the 5-hour window would lose anyway (`profile = "spare"`);
45
+ - a log monitor every 30 minutes that stays quiet until something new breaks (`notify = "on_alert"`).
46
+
47
+ ![The web page: the plan's 5-hour and 7-day windows, and the routines with their schedules](docs/screenshot.jpg)
48
+
49
+ ## Install
50
+
51
+ Needs macOS, [uv](https://docs.astral.sh/uv/) and [Claude Code](https://claude.com/claude-code) (`claude`
52
+ on your `PATH`, signed in).
53
+
54
+ ```bash
55
+ uv tool install loops-routines # from PyPI: the `loops-routines` command in ~/.local/bin
56
+ loops-routines install --launchd --mcp # ~/.loops_routines, the daemon as a LaunchAgent, the MCP server in Claude Code
57
+ loops-routines status
58
+ ```
59
+
60
+ `--launchd` writes `~/Library/LaunchAgents/com.loops-routines.daemon.plist` and loads it: the daemon starts now
61
+ and at every login, and launchd restarts it if it stops. It runs as you (a LaunchAgent, not a system
62
+ LaunchDaemon), so agents use your Claude sign-in, keychain and tools. Run the install from a terminal
63
+ whose `PATH` finds `claude` and whatever your routines call: the LaunchAgent keeps that `PATH`.
64
+
65
+ Upgrade, then restart the daemon so it runs the new code:
66
+
67
+ ```bash
68
+ uv tool upgrade loops-routines
69
+ loops-routines restart
70
+ ```
71
+
72
+ Remove: `loops-routines uninstall` (the LaunchAgent; `~/.loops_routines` is kept), then `uv tool uninstall loops-routines`.
73
+
74
+ ### Before you start
75
+
76
+ - **macOS only.** Notifications, dialogs and the LaunchAgent are macOS's; the scheduler itself is not, but
77
+ nothing else is supported yet.
78
+ - **Routines spend your plan.** Every run is a `claude -p` session on your own sign-in and counts against
79
+ your 5-hour and 7-day windows. Limit profiles (`spare`, `keep_free`) decide how much a routine may take;
80
+ start with `profile = "spare"` for anything that doesn't have to run.
81
+ - **Agents act as you.** They run with your files, keychain and `gh` login, limited by the allowed tools in
82
+ `config.toml` and the routine. Read [SECURITY.md](SECURITY.md) before switching on a routine that pushes,
83
+ posts or reads untrusted text.
84
+ - The example routines in `~/.loops_routines/routines/` are switched off and name placeholder repositories and
85
+ services: edit them first.
86
+
87
+ ## Quick start (from a checkout)
88
+
89
+ ```bash
90
+ uv sync
91
+ uv run loops-routines install # ~/.loops_routines: config.toml, README, three example routines (off)
92
+ uv run loops-routines daemon # or: loops-routines install --launchd (keeps it running, starts at login)
93
+ uv run loops-routines status
94
+ uv run loops-routines open # the web page in the browser
95
+ uv run loops-routines install --mcp # let your Claude Code sessions see and steer the routines
96
+ ```
97
+
98
+ A routine is a file `~/.loops_routines/routines/<name>.md`: a TOML header between `+++` lines, then the
99
+ prompt. See `loops_routines/defaults/README.md` (copied to `~/.loops_routines/README.md`) for every key, and
100
+ `loops-routines check <file>` to validate one and see its next three runs.
101
+
102
+ ## Architecture
103
+
104
+ ```
105
+ launchd (KeepAlive)
106
+ └─ loops-routines daemon ───────────────────────────────────────────────────────────┐
107
+ Service.tick (every 30 s): reload files → wind down runs past keep_free → │
108
+ evaluate each routine → expire / wait / launch by priority → probe usage │
109
+ Run × max_parallel → `claude -p` stream-json, own process group, own folder │
110
+ Store (SQLite, the daemon's only) deliver: reports/*.md │
111
+ REST /api/* + SSE /api/events MCP /mcp (user) MCP /agent/mcp (runs) │
112
+ /ui/ — a web page on the same API │
113
+ ────────────────────────────────────────────────────────────────────────────────────┘
114
+ ▲ REST + SSE ▲ MCP ▲ REST
115
+ browser Claude Code CLI
116
+
117
+ run ── /agent/mcp notify ──▶ daemon ──▶ banner (Notification Center)
118
+ run ── /agent/mcp ask_user ─▶ daemon ──▶ dialog with buttons ──┐ first answer wins:
119
+ ◀── answer ── web page / CLI / MCP ┘ the dialog, or any of these
120
+ ```
121
+
122
+ | Module | What it does |
123
+ |---|---|
124
+ | `core/triggers.py` | cron (5 fields, names, steps, `@daily`) and `every 30m`; `next_after(moment)` |
125
+ | `core/windows.py` | time windows `mon-fri 09:00-19:00` (from Loops) |
126
+ | `core/routines.py` | routine files → `Routine`; broken files are kept with their error |
127
+ | `core/evaluate.py` | the pure decision: idle / wait (why) / launch / expire for one routine |
128
+ | `core/usage.py`, `core/probe.py` | 5-hour and 7-day snapshots, the limit gates, the one-request probe (from Loops) |
129
+ | `core/integrations.py` | plugins, MCP servers, skills a routine names → `claude` flags (from Loops) |
130
+ | `core/runner.py` | one run: clones, per-run config files, stream-json in/out, stop, wrap-up, result file |
131
+ | `core/store.py` | SQLite: routine state, runs (and skipped occurrences), messages from agents |
132
+ | `service.py` | the scheduler tick and every query and action; REST, MCP and CLI are thin over it |
133
+ | `daemon/app.py`, `daemon/mcp.py`, `daemon/auth.py` | HTTP transports and the token check |
134
+ | `core/notify.py` | macOS banners and button dialogs through osascript; the prompt rules per `notify` |
135
+ | `ui/web/index.html` | the web page (`loops-routines open`) |
136
+
137
+ ### Scheduling rules
138
+
139
+ - Occurrences come from the trigger; only the **latest** due one counts, so a Mac that slept for a day
140
+ runs a routine once, not 48 times.
141
+ - An occurrence may wait for its window and limits until its deadline: `grace` after it, or (without
142
+ `grace`) until the next occurrence. Past it, it is recorded as **skipped** with the reason it waited for.
143
+ - Limits: `min_free` and `before_reset` decide the start; `keep_free` / `keep_free_weekly` also wind a
144
+ running run down (it gets `wind_down_minutes` to write its result, then it is stopped).
145
+ - When numbers are older than `max_snapshot_age_minutes`, the daemon spends one haiku request on a
146
+ probe (at most every `probe_cooldown_minutes`). Every run also refreshes them from its own events.
147
+ - Several due routines: higher `priority` first, up to `max_parallel` at once; a routine never overlaps
148
+ itself. "Run now" does not use up an occurrence.
149
+ - A routine switched off (in its file or the UI) owes nothing for the time it was off.
150
+
151
+ ### Results
152
+
153
+ The agent ends by writing `{"status": "ok | alert | nothing", "summary", "report"}` to its result file.
154
+ The daemon stores it and writes `reports/<routine>/<run>.md`. Without a result file, the agent's last
155
+ words become the summary with status `unknown`. While it works, the agent may post `report_status`
156
+ lines through `/agent/mcp`, and you can message it (`loops-routines say RUN TEXT`).
157
+
158
+ ### Notifications and questions
159
+
160
+ Agents use two tools of `/agent/mcp`, as their routine's `notify` allows (`always`: the outcome when it
161
+ finishes; `on_alert`: only an alert; `never`: neither tool):
162
+
163
+ - `notify(text, kind)` — a Notification Center banner titled with the routine; `kind` is info, success,
164
+ warning or error (error and warning play `[notify] error_sound`).
165
+ - `ask_user(question, buttons, timeout_minutes)` — a system dialog with 1–3 buttons of the agent's wording
166
+ (the recommended one is the default, on the right) and waits. The first answer wins: a click in the
167
+ dialog, the web page, `loops-routines answer`, or `answer_question` over MCP; the dialog then closes. No
168
+ answer in time, or the run ends: the agent gets `timed_out` / `cancelled` and takes the safe path.
169
+
170
+ The daemon notifies by itself only what no agent can: a run that failed or timed out, an alert the agent
171
+ did not send, and a `report_blocker`. Everything is kept with the run.
172
+
173
+ Why a dialog and not buttons on the banner: macOS gives notification actions only to a signed app with its
174
+ own notification permission; osascript banners can't have buttons. The first banner may make macOS ask
175
+ whether "Script Editor" may notify — allow it in System Settings → Notifications. `loops-routines
176
+ notify-test` (and `--ask`) checks both.
177
+
178
+ ### Security
179
+
180
+ The daemon listens on 127.0.0.1 only and checks the Host header. `/api` and `/mcp` need the token in
181
+ `~/Library/Application Support/loops_routines/token` (0600); `/agent/mcp` accepts only a live run's own token,
182
+ so an agent can't post as another run. The UI page gets the token from its URL fragment (never sent to
183
+ the server) and keeps it for the tab only.
184
+
185
+ ## API
186
+
187
+ ```
188
+ GET /health GET /api/summary
189
+ GET /api/routines GET /api/routines/{name}
190
+ POST /api/routines/{name}/run {force, out_of_queue}
191
+ POST /api/routines/{name}/enabled {enabled: true|false|null}
192
+ GET /api/runs?routine=&limit=&before=&skipped=0
193
+ GET /api/runs/{id} GET /api/runs/{id}/log?lines=
194
+ POST /api/runs/{id}/stop POST /api/runs/{id}/message {text}
195
+ POST /api/runs/seen {ids}
196
+ GET /api/usage POST /api/usage/probe
197
+ GET /api/integrations POST /api/integrations/scan
198
+ GET /api/questions POST /api/questions/{id}/answer {answer}
199
+ POST /api/notify/test {kind, ask}
200
+ POST /api/reload GET /api/events (SSE)
201
+ ```
202
+
203
+ MCP `/mcp`: `list_routines`, `routine_status`, `run_routine`, `run_routine_now` (outside the queue: on top
204
+ of `max_parallel`, whatever its limits and window say), `set_routine_enabled`, `list_runs`,
205
+ `get_run`, `run_log`, `stop_run`, `message_run`, `usage`, `pending_questions`, `answer_question`.
206
+ MCP `/agent/mcp` (runs only): `report_status`, `report_blocker`, `notify`, `ask_user`.
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ uv run pytest
212
+ uv run loops-routines --root /tmp/ct daemon # a separate instance: /tmp/ct/home, state, logs
213
+ ```
214
+
215
+ Tests run the real runner against a fake `claude` script (`tests/conftest.py`). See
216
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the rest, and [CHANGELOG.md](CHANGELOG.md) for what changed.
217
+
218
+ ## License
219
+
220
+ [MIT](LICENSE).