@4pm/cli 1.18.0 → 1.19.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.
- package/dist/index.js +578 -244
- package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +14 -22
- package/dist/project-sample/.claude/AUTONOMOUS.md +31 -36
- package/dist/project-sample/.claude/hooks/autonomous-tick.sh +8 -232
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +2 -2
- package/dist/project-sample/.claude/templates/README.md +1 -1
- package/dist/project-sample/AI_TODO.md +2 -2
- package/package.json +1 -1
- package/dist/project-sample/.claude/.autonomous.settings.json +0 -22
- package/dist/project-sample/.claude/commands/auto-cycle.md +0 -23
- package/dist/project-sample/.claude/hooks/__pycache__/autonomous-history.cpython-312.pyc +0 -0
- package/dist/project-sample/.claude/hooks/autonomous-history.py +0 -193
- package/dist/project-sample/.claude/skills/check-usage/SKILL.md +0 -40
- package/dist/project-sample/.claude/skills/check-usage/check_usage.py +0 -161
|
@@ -1,39 +1,31 @@
|
|
|
1
|
-
# Set up
|
|
1
|
+
# Set up the cron tick for autonomous mode
|
|
2
2
|
|
|
3
|
-
How to install the **autonomous
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
How to install the **autonomous cron tick** on **WSL (Ubuntu)**. Under **ADR-0321** the tick is a **dumb**
|
|
4
|
+
one-liner (`exec 4pm auto-run`) — all logic lives in the cli/daemon, and all config lives in
|
|
5
|
+
`~/.4pm/profiles/<name>/autonomous.config.json` (edited from the web **Autonomous → Settings** tab), so
|
|
6
|
+
nothing about the loop is configured in this file or the repo.
|
|
7
7
|
|
|
8
|
-
>
|
|
9
|
-
> root `$PROJECT` (e.g. `~/projects/<your-project>`).
|
|
8
|
+
> Run in a **WSL shell** (Ubuntu). Call the project root `$PROJECT` (e.g. `~/projects/<your-project>`).
|
|
10
9
|
|
|
11
|
-
##
|
|
12
|
-
```bash
|
|
13
|
-
PROJECT="$HOME/projects/<your-project>" # fix to your path
|
|
14
|
-
cd "$PROJECT"
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## 1. Check the required tools
|
|
18
|
-
Every line must print a path/version (not "not found"):
|
|
10
|
+
## 1. Check the tools
|
|
19
11
|
```bash
|
|
20
12
|
command -v cron || echo "missing cron"
|
|
21
13
|
command -v 4pm || echo "missing 4pm cli (install natively in WSL; a '4pm start' daemon must be running)"
|
|
22
14
|
command -v git || echo "missing git"
|
|
23
|
-
command -v python3 || echo "missing python3"
|
|
24
15
|
command -v gh || command -v glab || echo "missing gh/glab (needed for the PR step)"
|
|
25
16
|
```
|
|
26
17
|
|
|
27
|
-
## 2.
|
|
18
|
+
## 2. Install the tick
|
|
28
19
|
```bash
|
|
29
20
|
chmod +x "$PROJECT/.claude/hooks/autonomous-tick.sh"
|
|
30
21
|
crontab -e
|
|
31
22
|
# add (fix the path):
|
|
32
23
|
*/10 * * * * /home/<user>/projects/<your-project>/.claude/hooks/autonomous-tick.sh
|
|
33
24
|
```
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
After that, set the **schedule** and every other knob from the web **Autonomous → Settings** tab — the
|
|
26
|
+
cli keeps this crontab line's schedule in sync with `cronSchedule`, and **Start/Pause** + **Install/
|
|
27
|
+
Uninstall cron** are driven from the **Overview** tab.
|
|
36
28
|
|
|
37
|
-
## 3.
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
## 3. Watch
|
|
30
|
+
The daemon writes a per-day tick log; view it from the web **Autonomous → Logs** tab (or on the worker
|
|
31
|
+
at `.claude/logs/autonomous-tick-$(date +%F).log`).
|
|
@@ -1,52 +1,47 @@
|
|
|
1
1
|
# Autonomous mode — how it's assembled
|
|
2
2
|
|
|
3
|
-
> **Sample project —
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
> not
|
|
3
|
+
> **Sample project — describes the workflow only; it does NOT run on its own.** Intended for an isolated
|
|
4
|
+
> environment (WSL / a per-project container) where the AI can be given full permissions.
|
|
5
|
+
>
|
|
6
|
+
> **ADR-0321: the autonomous LOGIC lives in the 4PM cli, not in this repo.** The project repo carries
|
|
7
|
+
> only a **dumb** cron tick and the **data** books — never the algorithm — so the logic can't be read
|
|
8
|
+
> from, or tampered with in, a checkout. The **config** is not in the repo either: it lives in the
|
|
9
|
+
> **profile dir** (`~/.4pm/profiles/<name>/autonomous.config.json`) and is edited from the web
|
|
10
|
+
> **Autonomous → Settings** tab.
|
|
7
11
|
|
|
8
12
|
## The pieces
|
|
9
|
-
|
|
|
10
|
-
|
|
11
|
-
| `.claude/hooks/autonomous-tick.sh` |
|
|
12
|
-
| `4pm auto-run` (cli) | Asks the running **`4pm start`** daemon to run **one** cycle over the control socket
|
|
13
|
-
| cli `
|
|
14
|
-
|
|
|
15
|
-
| `USER_TODO.md` / `USER_QA.md`
|
|
16
|
-
|
|
|
17
|
-
| `.claude/templates/<NAME>.{empty,sample}.md` | Canonical templates for the books. The has-work gate + the cycle **compare against `*.empty.md`** to tell empty/has-work and reset correctly. |
|
|
18
|
-
| `.claude/settings.json` | The "bypass all" permission profile for autonomous mode (see the note below). |
|
|
13
|
+
| Where | Role |
|
|
14
|
+
|-------|------|
|
|
15
|
+
| `.claude/hooks/autonomous-tick.sh` | **Dumb** cron tick — its only job is `exec 4pm auto-run`. No gates, no schedule sync, no lock, no settings. |
|
|
16
|
+
| `4pm auto-run` (cli) | Asks the running **`4pm start`** daemon to run **one** cycle over the control socket (token-authenticated — ADR-0320/0321). |
|
|
17
|
+
| cli daemon (`runAutonomousCycle`) | Owns **all** logic: reads `autonomous.config.json`; the gates (paused / quiet-hours / max-ticks / **has-work** / **quota**); cron schedule sync; run histories + auto-pause; serialize one cycle at a time; model; usage via the live snapshot (ADR-0072). Runs the cycle as a write-capable agent (bypass — ADR-0271: branch + PR). |
|
|
18
|
+
| `~/.4pm/profiles/<name>/autonomous.config.json` | The config knobs (paused, cronSchedule, quietHours, maxTicksPerDay, stopOnConsecutiveFailures, logRetentionDays, model, **maxSessionPct**, **maxWeeklyPct**). Clean JSON — the web Settings Form labels + explains each field. **Outside the repo.** |
|
|
19
|
+
| `USER_TODO.md` / `USER_QA.md` / `AI_TODO.md` / `AI_PROGRESS.md` / `AI_DONE.md` | The data books (content-only tables — ADR-0320). |
|
|
20
|
+
| `.claude/.autonomous.approvals.json` · `.autonomous.authors.json` | Per-row approver / writer (ADR-0320) — project data. `.autonomous.histories.json` = runtime state (gitignored). |
|
|
19
21
|
|
|
20
22
|
## Lifecycle (1 tick)
|
|
21
23
|
```
|
|
22
|
-
cron
|
|
23
|
-
├─
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
3. fold APPROVED USER_QA answers back into AI_TODO
|
|
29
|
-
4. take ONE approved task (deps met) → AI_PROGRESS, commit
|
|
30
|
-
5. implement + test on task/TSK-… branch
|
|
31
|
-
6. rebase + push + open a PULL REQUEST into the base branch (no direct merge)
|
|
32
|
-
7. update the books (AI_DONE), commit + push the base branch
|
|
33
|
-
└─ record success/failure (N consecutive failures → pause); the EXIT trap releases the lock
|
|
24
|
+
cron → autonomous-tick.sh → `4pm auto-run` → the running daemon:
|
|
25
|
+
├─ paused / quiet-hours / max-ticks / has-work / quota over caps? → log "skip", done
|
|
26
|
+
└─ run ONE cycle (write-capable agent):
|
|
27
|
+
sync the primary repo's branch → analyse APPROVED USER_TODO → fold APPROVED USER_QA →
|
|
28
|
+
one approved task → implement + test → open a PULL REQUEST into the base branch → update books
|
|
29
|
+
└─ record history (N consecutive failures → auto-pause); serialized (one cycle at a time)
|
|
34
30
|
```
|
|
35
31
|
|
|
36
32
|
## Install on WSL
|
|
37
33
|
```bash
|
|
38
34
|
chmod +x .claude/hooks/autonomous-tick.sh
|
|
39
35
|
crontab -e
|
|
40
|
-
# add
|
|
36
|
+
# add (fix /path):
|
|
41
37
|
*/10 * * * * /path/to/project/.claude/hooks/autonomous-tick.sh
|
|
42
|
-
# watch:
|
|
43
|
-
tail -f .claude/logs/autonomous-tick-$(date +%F).log
|
|
44
38
|
```
|
|
45
|
-
Requirements: the **`4pm`** cli on PATH with a **running `4pm start` daemon** serving this project
|
|
46
|
-
|
|
39
|
+
Requirements: the **`4pm`** cli on PATH with a **running `4pm start` daemon** serving this project, plus
|
|
40
|
+
`git` and `gh`/`glab` for the PR step. The **schedule** and every other knob are set from the web
|
|
41
|
+
Autonomous → Settings tab; the cli keeps the crontab line in sync with `cronSchedule`.
|
|
47
42
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
## Permissions
|
|
44
|
+
The daemon runs the cycle as a write-capable agent (`--permission-mode bypassPermissions`, ADR-0271),
|
|
45
|
+
bounded by the folder-scope guard (ADR-0181) + the AI-run timeout (ADR-0243). Only enable full
|
|
46
|
+
permissions in an isolated environment (WSL / a per-project container). Never on a machine with
|
|
47
|
+
sensitive data.
|
|
@@ -1,239 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# .
|
|
4
|
-
#
|
|
2
|
+
# Autonomous cron tick (ADR-0321). Its ONLY job is to invoke the 4PM cli — there is NO autonomous logic
|
|
3
|
+
# here. All decisions (config, paused/quiet-hours/max-ticks/has-work/quota gates, cron schedule sync,
|
|
4
|
+
# run histories, serialization, model, usage checks) live in the cli/daemon, so the logic can't be read
|
|
5
|
+
# from or tampered with in the project repo. `4pm auto-run` asks the running `4pm start` daemon to run
|
|
6
|
+
# one cycle through its live session.
|
|
5
7
|
#
|
|
6
8
|
# Install once: crontab -e → */10 * * * * /path/to/project/.claude/hooks/autonomous-tick.sh
|
|
7
|
-
#
|
|
8
|
-
# What changed with ADR-0319: the tick no longer runs `claude -p /auto-cycle` directly. It runs
|
|
9
|
-
# `4pm auto-run`, which asks the ALREADY-RUNNING `4pm start` daemon to run one cycle through its live
|
|
10
|
-
# WS session — so the cycle reuses the cli's profile/quota failover (ADR-0182), token metering
|
|
11
|
-
# (ADR-0072), folder-scope (ADR-0181) and AI-run timeout (ADR-0243). Account selection + the token/quota
|
|
12
|
-
# gate now live in the cli (they need the live usage snapshot), NOT in this shell.
|
|
13
|
-
#
|
|
14
|
-
# This tick keeps only the CHEAP local gates (no token spend): a run lock, pause, quiet-hours,
|
|
15
|
-
# max-ticks/day, and a "has-work" check — so the daemon is only woken when there is approved work.
|
|
16
|
-
#
|
|
17
|
-
# "Busy ⇒ wait for the next tick" via the EXISTENCE of `.autonomous.lock` (not flock):
|
|
18
|
-
# - lock file exists → another tick is running → log "skip" and exit.
|
|
19
|
-
# - it doesn't → create the lock → run one cycle via the daemon.
|
|
20
|
-
# The EXIT trap removes the lock whether the cycle succeeds or dies.
|
|
9
|
+
# (the cli keeps this crontab line's schedule in sync with autonomous.config.json).
|
|
21
10
|
set -euo pipefail
|
|
22
11
|
|
|
23
|
-
#
|
|
24
|
-
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
25
|
-
LOCK="$PROJECT_DIR/.claude/.autonomous.lock"
|
|
26
|
-
HIST="$PROJECT_DIR/.claude/.autonomous.histories.json"
|
|
27
|
-
HIST_PY="$PROJECT_DIR/.claude/hooks/autonomous-history.py"
|
|
28
|
-
LOG_DIR="$PROJECT_DIR/.claude/logs"
|
|
29
|
-
mkdir -p "$LOG_DIR"
|
|
30
|
-
LOG="$LOG_DIR/autonomous-tick-$(date +%F).log" # per-DAY log (so log_retention_days is meaningful)
|
|
31
|
-
ts() { date '+%F %T'; }
|
|
32
|
-
|
|
33
|
-
# Create the lock atomically: `noclobber` makes `> file` FAIL if the file exists, closing the race
|
|
34
|
-
# where two ticks both pass a separate "does it exist?" check.
|
|
35
|
-
if ! ( set -o noclobber; : > "$LOCK" ) 2>/dev/null; then
|
|
36
|
-
echo "$(ts) [skip] busy (.autonomous.lock exists) — waiting for the next tick" >> "$LOG"
|
|
37
|
-
exit 0
|
|
38
|
-
fi
|
|
39
|
-
# The lock is released when this wrapper exits, whether the cycle succeeded, errored, or was skipped.
|
|
40
|
-
trap 'rm -f "$LOCK"' EXIT
|
|
41
|
-
|
|
42
|
-
cd "$PROJECT_DIR"
|
|
43
|
-
|
|
44
|
-
# cron runs with a minimal PATH and does NOT load ~/.bashrc. Add common WSL install dirs so cron can
|
|
45
|
-
# find the `4pm` binary (native install / npm global) + python3.
|
|
12
|
+
# cron has a minimal PATH and does not load ~/.bashrc; add common WSL install dirs so `4pm` is found.
|
|
46
13
|
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:$PATH"
|
|
47
14
|
|
|
48
|
-
|
|
49
|
-
PY_BIN="$(command -v python3 || command -v python || true)"
|
|
50
|
-
if [ -z "$PY_BIN" ]; then
|
|
51
|
-
echo "$(ts) [error] python/python3 not found — skipping this tick" >> "$LOG"
|
|
52
|
-
exit 0
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
# 'hist' wrapper: every read/write of .autonomous.histories.json goes through the Python helper.
|
|
56
|
-
hist() { "$PY_BIN" "$HIST_PY" "$HIST" "$@"; }
|
|
57
|
-
|
|
58
|
-
# --- Read the autonomous config (.autonomous.settings.json) ------------------------------------
|
|
59
|
-
# User-editable; read EVERY tick so changes take effect next tick. A missing/corrupt file -> defaults.
|
|
60
|
-
SETTINGS="$PROJECT_DIR/.claude/.autonomous.settings.json"
|
|
61
|
-
eval "$("$PY_BIN" - "$SETTINGS" <<'PY'
|
|
62
|
-
import json, sys, shlex
|
|
63
|
-
defaults = {"paused": False, "cron_schedule": "*/10 * * * *", "command": "auto-run", "profile": "",
|
|
64
|
-
"quiet_hours": "", "max_ticks_per_day": -1, "stop_on_consecutive_failures": 3,
|
|
65
|
-
"log_retention_days": 14, "notify_webhook": ""}
|
|
66
|
-
try:
|
|
67
|
-
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
68
|
-
if not isinstance(d, dict): d = {}
|
|
69
|
-
except Exception:
|
|
70
|
-
d = {}
|
|
71
|
-
def g(k): return d.get(k, defaults[k])
|
|
72
|
-
print("CFG_PAUSED=" + ("1" if bool(g("paused")) else "0"))
|
|
73
|
-
print("CFG_CRON_SCHEDULE=" + shlex.quote(str(g("cron_schedule"))))
|
|
74
|
-
print("CFG_PROFILE=" + shlex.quote(str(g("profile"))))
|
|
75
|
-
print("CFG_QUIET_HOURS=" + shlex.quote(str(g("quiet_hours"))))
|
|
76
|
-
print("CFG_MAX_TICKS_PER_DAY=" + shlex.quote(str(g("max_ticks_per_day"))))
|
|
77
|
-
print("CFG_STOP_ON_CONSEC_FAILURES=" + shlex.quote(str(g("stop_on_consecutive_failures"))))
|
|
78
|
-
print("CFG_LOG_RETENTION_DAYS=" + shlex.quote(str(g("log_retention_days"))))
|
|
79
|
-
print("CFG_NOTIFY_WEBHOOK=" + shlex.quote(str(g("notify_webhook"))))
|
|
80
|
-
PY
|
|
81
|
-
)"
|
|
82
|
-
# Export CFG_* so the history helper ('record') can read the run config from env.
|
|
83
|
-
export CFG_CRON_SCHEDULE CFG_QUIET_HOURS CFG_MAX_TICKS_PER_DAY CFG_STOP_ON_CONSEC_FAILURES CFG_PAUSED
|
|
84
|
-
|
|
85
|
-
# --- Sync the crontab when cron_schedule changes ----------------------------------------------
|
|
86
|
-
SELF="$PROJECT_DIR/.claude/hooks/autonomous-tick.sh"
|
|
87
|
-
PREV_SCHED="$(hist get-cron 2>/dev/null || true)"
|
|
88
|
-
if [ "$CFG_CRON_SCHEDULE" != "$PREV_SCHED" ]; then
|
|
89
|
-
if command -v crontab >/dev/null 2>&1; then
|
|
90
|
-
CUR="$(crontab -l 2>/dev/null || true)"
|
|
91
|
-
if printf '%s\n' "$CUR" | grep -qF "$SELF" || [ -n "$PREV_SCHED" ]; then
|
|
92
|
-
NEWTAB="$( { printf '%s\n' "$CUR" | grep -vF "$SELF" || true; echo "$CFG_CRON_SCHEDULE $SELF"; } )"
|
|
93
|
-
if printf '%s\n' "$NEWTAB" | crontab - 2>>"$LOG"; then
|
|
94
|
-
hist set-cron "$CFG_CRON_SCHEDULE" 2>>"$LOG" || true
|
|
95
|
-
echo "$(ts) [cron] synced schedule -> '$CFG_CRON_SCHEDULE'" >> "$LOG"
|
|
96
|
-
else
|
|
97
|
-
echo "$(ts) [warn] could not write crontab (keeping the old schedule)" >> "$LOG"
|
|
98
|
-
fi
|
|
99
|
-
else
|
|
100
|
-
echo "$(ts) [cron] crontab has no autonomous line — skipping auto-install (install it once manually first)" >> "$LOG"
|
|
101
|
-
fi
|
|
102
|
-
else
|
|
103
|
-
echo "$(ts) [cron] no 'crontab' on PATH — skipping schedule sync" >> "$LOG"
|
|
104
|
-
fi
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
# --- Pause flag -------------------------------------------------------------------------------
|
|
108
|
-
if [ "$CFG_PAUSED" = "1" ]; then
|
|
109
|
-
echo "$(ts) [skip] paused=true in .autonomous.settings.json — skipping this tick" >> "$LOG"
|
|
110
|
-
exit 0
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
# --- Prune old logs by log_retention_days -----------------------------------------------------
|
|
114
|
-
if [ "${CFG_LOG_RETENTION_DAYS:-0}" -gt 0 ] 2>/dev/null; then
|
|
115
|
-
find "$LOG_DIR" -maxdepth 1 -type f -name 'autonomous-tick-*.log' -mtime +"$CFG_LOG_RETENTION_DAYS" -delete 2>/dev/null || true
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
# --- Quiet hours ------------------------------------------------------------------------------
|
|
119
|
-
if [ -n "$CFG_QUIET_HOURS" ]; then
|
|
120
|
-
if printf '%s' "$CFG_QUIET_HOURS" | grep -Eq '^[0-9]{1,2}:[0-9]{2}-[0-9]{1,2}:[0-9]{2}$'; then
|
|
121
|
-
q_start="${CFG_QUIET_HOURS%%-*}"; q_end="${CFG_QUIET_HOURS##*-}"
|
|
122
|
-
_min() { echo $(( 10#${1%%:*} * 60 + 10#${1##*:} )); }
|
|
123
|
-
qs=$(_min "$q_start"); qe=$(_min "$q_end"); qn=$(_min "$(date +%H:%M)")
|
|
124
|
-
in_q=0
|
|
125
|
-
if [ "$qs" -le "$qe" ]; then
|
|
126
|
-
{ [ "$qn" -ge "$qs" ] && [ "$qn" -lt "$qe" ]; } && in_q=1
|
|
127
|
-
else
|
|
128
|
-
{ [ "$qn" -ge "$qs" ] || [ "$qn" -lt "$qe" ]; } && in_q=1 # window crossing midnight
|
|
129
|
-
fi
|
|
130
|
-
if [ "$in_q" = 1 ]; then
|
|
131
|
-
echo "$(ts) [skip] within quiet_hours ($CFG_QUIET_HOURS) — skipping this tick" >> "$LOG"
|
|
132
|
-
exit 0
|
|
133
|
-
fi
|
|
134
|
-
else
|
|
135
|
-
echo "$(ts) [warn] quiet_hours has a bad format ('$CFG_QUIET_HOURS') — ignoring the check" >> "$LOG"
|
|
136
|
-
fi
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# --- Max ticks/day ----------------------------------------------------------------------------
|
|
140
|
-
TODAY="$(date +%F)"
|
|
141
|
-
tick_count="$(hist get-ticks "$TODAY" 2>/dev/null || echo 0)"; [ -z "$tick_count" ] && tick_count=0
|
|
142
|
-
if [ "${CFG_MAX_TICKS_PER_DAY:--1}" -gt 0 ] 2>/dev/null && [ "${tick_count:-0}" -ge "$CFG_MAX_TICKS_PER_DAY" ]; then
|
|
143
|
-
echo "$(ts) [skip] reached max_ticks_per_day=$CFG_MAX_TICKS_PER_DAY ($tick_count ticks today) — skipping" >> "$LOG"
|
|
144
|
-
exit 0
|
|
145
|
-
fi
|
|
146
|
-
|
|
147
|
-
# --- "Has work" gate --------------------------------------------------------------------------
|
|
148
|
-
# Skip WITHOUT waking the daemon unless there is one of:
|
|
149
|
-
# 1) an APPROVED USER_TODO request (REQ-… approved in .autonomous.approvals.json) — Step 2 analyses it;
|
|
150
|
-
# 2) an APPROVED USER_QA answer (QA-… approved) — Step 3 folds it in;
|
|
151
|
-
# 3) an APPROVED AI_TODO task (TSK-… approved) — Step 4 can take it;
|
|
152
|
-
# 4) AI_PROGRESS.md non-empty (leftover work from a previous tick).
|
|
153
|
-
# Approval is the source of truth in .autonomous.approvals.json (ADR-0152/0319), keyed per row id.
|
|
154
|
-
TPL="$PROJECT_DIR/.claude/templates"
|
|
155
|
-
WORK="$(USER_TODO="$PROJECT_DIR/USER_TODO.md" USER_QA="$PROJECT_DIR/USER_QA.md" \
|
|
156
|
-
AI_TODO="$PROJECT_DIR/AI_TODO.md" AI_PROGRESS="$PROJECT_DIR/AI_PROGRESS.md" \
|
|
157
|
-
APPROVALS="$PROJECT_DIR/.claude/.autonomous.approvals.json" \
|
|
158
|
-
AI_PROGRESS_TPL="$TPL/AI_PROGRESS.empty.md" \
|
|
159
|
-
"$PY_BIN" - <<'PY'
|
|
160
|
-
import os, json
|
|
161
|
-
def read(p):
|
|
162
|
-
try: return open(p, encoding="utf-8").read()
|
|
163
|
-
except Exception: return ""
|
|
164
|
-
def norm(t):
|
|
165
|
-
lines = [ln.rstrip() for ln in t.splitlines()]
|
|
166
|
-
while lines and not lines[0]: lines.pop(0)
|
|
167
|
-
while lines and not lines[-1]: lines.pop()
|
|
168
|
-
return "\n".join(lines)
|
|
169
|
-
try:
|
|
170
|
-
ap = json.load(open(os.environ["APPROVALS"], encoding="utf-8"))
|
|
171
|
-
approved = {k for k, v in ap.items() if isinstance(v, dict) and v.get("approved") is True}
|
|
172
|
-
except Exception:
|
|
173
|
-
approved = set()
|
|
174
|
-
def approved_in(book_env, prefix):
|
|
175
|
-
text = read(os.environ[book_env])
|
|
176
|
-
return any(i.startswith(prefix) and i in text for i in approved)
|
|
177
|
-
user = approved_in("USER_TODO", "REQ-")
|
|
178
|
-
qa = approved_in("USER_QA", "QA-")
|
|
179
|
-
ai = approved_in("AI_TODO", "TSK-")
|
|
180
|
-
prog = norm(read(os.environ["AI_PROGRESS"])) != norm(read(os.environ["AI_PROGRESS_TPL"]))
|
|
181
|
-
print("WORK" if (user or qa or ai or prog) else "EMPTY")
|
|
182
|
-
PY
|
|
183
|
-
)"
|
|
184
|
-
if [ "$WORK" != "WORK" ]; then
|
|
185
|
-
echo "$(ts) [skip] no approved USER_TODO/USER_QA/AI_TODO work and AI_PROGRESS empty — skipping (daemon not woken)" >> "$LOG"
|
|
186
|
-
exit 0
|
|
187
|
-
fi
|
|
188
|
-
|
|
189
|
-
# --- Locate the `4pm` cli ---------------------------------------------------------------------
|
|
190
|
-
FOURPM_BIN="${FOURPM_BIN:-$(command -v 4pm || true)}"
|
|
191
|
-
if [ -z "$FOURPM_BIN" ]; then
|
|
192
|
-
echo "$(ts) [error] '4pm' not found on PATH — install the 4PM cli in WSL (e.g. ~/.local/bin/4pm)" >> "$LOG"
|
|
193
|
-
exit 0
|
|
194
|
-
fi
|
|
195
|
-
|
|
196
|
-
# Record 1 tick that ACTUALLY runs a cycle (for max_ticks_per_day).
|
|
197
|
-
tick_count=$(( ${tick_count:-0} + 1 ))
|
|
198
|
-
hist set-ticks "$TODAY" "$tick_count" 2>>"$LOG" || true
|
|
199
|
-
|
|
200
|
-
# Optional profile pin (settings.profile) — else the cli resolves the single linked profile.
|
|
201
|
-
PROFILE_ARGS=()
|
|
202
|
-
[ -n "${CFG_PROFILE:-}" ] && PROFILE_ARGS=(--profile "$CFG_PROFILE")
|
|
203
|
-
|
|
204
|
-
echo "$(ts) [run] 4pm auto-run (tick $tick_count/$TODAY)" >> "$LOG"
|
|
205
|
-
set +e
|
|
206
|
-
"$FOURPM_BIN" auto-run ${PROFILE_ARGS[@]+"${PROFILE_ARGS[@]}"} >> "$LOG" 2>&1
|
|
207
|
-
RC=$?
|
|
208
|
-
set -e
|
|
209
|
-
|
|
210
|
-
# --- Record the run + count consecutive failures + auto-stop ----------------------------------
|
|
211
|
-
# RC != 0 (no daemon / dispatch error) -> increment; reaching the threshold -> paused=true. RC == 0 -> reset.
|
|
212
|
-
fails="$(hist get-fails 2>/dev/null || echo 0)"; [ -z "$fails" ] && fails=0
|
|
213
|
-
if [ "$RC" -ne 0 ]; then
|
|
214
|
-
echo "$(ts) [warn] 4pm auto-run exited $RC" >> "$LOG"
|
|
215
|
-
fails=$(( ${fails:-0} + 1 )); hist set-fails "$fails" 2>>"$LOG" || true
|
|
216
|
-
hist record "$(ts)" failure "$RC" "" "consecutive failure #$fails" 2>>"$LOG" || true
|
|
217
|
-
if [ "${CFG_STOP_ON_CONSEC_FAILURES:-0}" -gt 0 ] 2>/dev/null && [ "$fails" -ge "$CFG_STOP_ON_CONSEC_FAILURES" ]; then
|
|
218
|
-
"$PY_BIN" - "$SETTINGS" <<'PY' 2>>"$LOG" || true
|
|
219
|
-
import json, sys
|
|
220
|
-
p = sys.argv[1]
|
|
221
|
-
try:
|
|
222
|
-
d = json.load(open(p, encoding="utf-8"))
|
|
223
|
-
d["paused"] = True
|
|
224
|
-
with open(p, "w", encoding="utf-8") as f:
|
|
225
|
-
json.dump(d, f, ensure_ascii=False, indent=2); f.write("\n")
|
|
226
|
-
except Exception:
|
|
227
|
-
pass
|
|
228
|
-
PY
|
|
229
|
-
echo "$(ts) [stop] $fails consecutive failures >= $CFG_STOP_ON_CONSEC_FAILURES → set paused=true (resume manually)" >> "$LOG"
|
|
230
|
-
fi
|
|
231
|
-
else
|
|
232
|
-
hist set-fails 0 2>>"$LOG" || true # success → reset the consecutive-failure count
|
|
233
|
-
hist record "$(ts)" success "$RC" "" "cycle complete" 2>>"$LOG" || true
|
|
234
|
-
fi
|
|
235
|
-
|
|
236
|
-
# notify_webhook: TBD — a run summary would be POSTed here if CFG_NOTIFY_WEBHOOK is set.
|
|
237
|
-
|
|
238
|
-
echo "$(ts) [done] tick finished" >> "$LOG"
|
|
239
|
-
# The EXIT trap above removes `.autonomous.lock` → the lock is released for the next tick.
|
|
15
|
+
exec 4pm auto-run
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
> **Tag** = optional catalog tag(s) (e.g. `UpdateSpecFromDB`) whose action runs from the server down to
|
|
7
7
|
> the project when the task is approved (approval is committed on Save — ADR-0311).
|
|
8
8
|
> **Approval** is NOT a table column — it lives in `.claude/.autonomous.approvals.json` (ADR-0152), set
|
|
9
|
-
> from the web AI Todo grid;
|
|
9
|
+
> from the web AI Todo grid; the autonomous cli reads that file, never the table.
|
|
10
10
|
> **Depends** = the `TSK-…` ids that must be DONE (present in `AI_DONE.md`) first.
|
|
11
|
-
>
|
|
11
|
+
> the autonomous cli only takes tasks that are approved AND have their dependencies met → moves them to
|
|
12
12
|
> `AI_PROGRESS.md`; runs group by group, within a group High → Medium → Low.
|
|
13
13
|
|
|
14
14
|
| ID | Priority | Tag | Depends | Group | Task description | Notes |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical templates for the 5 "book" files used by the autonomous loop. Each book has two templates:
|
|
4
4
|
|
|
5
|
-
- `<NAME>.empty.md` — the EMPTY state. The
|
|
5
|
+
- `<NAME>.empty.md` — the EMPTY state. The cli's "has-work" gate + the autonomous cli compare a live book
|
|
6
6
|
against this (equal ⇒ empty). When clearing/resetting a book, overwrite it with **exactly** this file
|
|
7
7
|
(`cp .claude/templates/<NAME>.empty.md <NAME>`).
|
|
8
8
|
- `<NAME>.sample.md` — an example WITH DATA, showing the expected format when adding entries.
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
> **Tag** = optional catalog tag(s) (e.g. `UpdateSpecFromDB`) whose action runs from the server down to
|
|
7
7
|
> the project when the task is approved (approval is committed on Save — ADR-0311).
|
|
8
8
|
> **Approval** is NOT a table column — it lives in `.claude/.autonomous.approvals.json` (ADR-0152), set
|
|
9
|
-
> from the web AI Todo grid;
|
|
9
|
+
> from the web AI Todo grid; the autonomous cli reads that file, never the table.
|
|
10
10
|
> **Depends** = the `TSK-…` ids that must be DONE (present in `AI_DONE.md`) first.
|
|
11
|
-
>
|
|
11
|
+
> the autonomous cli only takes tasks that are approved AND have their dependencies met → moves them to
|
|
12
12
|
> `AI_PROGRESS.md`; runs group by group, within a group High → Medium → Low.
|
|
13
13
|
|
|
14
14
|
| ID | Priority | Tag | Depends | Group | Task description | Notes |
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_about": "Autonomous workflow config (ADR-0152 + ADR-0319). autonomous-tick.sh READS this file EVERY cron tick, so edits take effect from the next tick. Keys starting with '_' are comments and are ignored by the script.",
|
|
3
|
-
"paused": true,
|
|
4
|
-
"_paused": "true = PAUSED: the tick still runs but skips immediately, without waking the daemon. A quick brake without removing the crontab entry.",
|
|
5
|
-
"cron_schedule": "*/5 * * * *",
|
|
6
|
-
"_cron_schedule": "Standard 5-field cron. When you change this, the tick rewrites the crontab line pointing at autonomous-tick.sh on the next run (only if the crontab already has the autonomous line — install it once manually first).",
|
|
7
|
-
"command": "auto-run",
|
|
8
|
-
"_command": "The cli subcommand each tick runs. `4pm auto-run` asks the running `4pm start` daemon to run one cycle through its live session (ADR-0319).",
|
|
9
|
-
"profile": "",
|
|
10
|
-
"_profile": "Optional profile name for `4pm auto-run --profile <name>`. Empty = the cli resolves the single linked profile (set this only on a worker serving multiple projects).",
|
|
11
|
-
"_quota_moved": "The session/weekly token gate + account selection moved OUT of this shell into the cli (ADR-0319) — they need the daemon's live usage snapshot. Configure quota-based profile failover in the cli's own config, not here.",
|
|
12
|
-
"quiet_hours": "",
|
|
13
|
-
"_quiet_hours": "Empty = run ALL DAY. Or 'HH:MM-HH:MM' (e.g. '00:00-06:00') to SKIP within that window; a window crossing midnight is valid (e.g. '22:00-06:00').",
|
|
14
|
-
"max_ticks_per_day": -1,
|
|
15
|
-
"_max_ticks_per_day": "-1 = NO limit. >0 = cap on the number of ticks that actually wake the daemon in one (local) day. The count is stored under ticks{day,count} in .claude/.autonomous.histories.json.",
|
|
16
|
-
"stop_on_consecutive_failures": 3,
|
|
17
|
-
"_stop_on_consecutive_failures": "After N consecutive FAILED runs (no daemon / dispatch error) -> set paused=true to stop safely. 0 = off. The count is stored under consecutive_fails in .claude/.autonomous.histories.json (reset on success).",
|
|
18
|
-
"log_retention_days": 14,
|
|
19
|
-
"_log_retention_days": "Logs are written per DAY: .claude/logs/autonomous-tick-YYYY-MM-DD.log. Delete log files older than N days. 0 = keep forever.",
|
|
20
|
-
"notify_webhook": "",
|
|
21
|
-
"_notify_webhook": "TBD — a webhook URL (Slack/Discord) to notify on finish/error. NOT wired yet (read but unused)."
|
|
22
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: (Retired — ADR-0319) The autonomous cycle now runs through the 4PM cli, not `claude -p /auto-cycle`.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# /auto-cycle — retired (ADR-0319)
|
|
6
|
-
|
|
7
|
-
The autonomous work cycle is **no longer** a `claude -p /auto-cycle` slash command. Under **ADR-0319**
|
|
8
|
-
it runs through the 4PM cli instead:
|
|
9
|
-
|
|
10
|
-
- The cron tick (`.claude/hooks/autonomous-tick.sh`) runs **`4pm auto-run`**, which asks the already
|
|
11
|
-
running **`4pm start`** daemon to run **one** cycle through its live WS session — so the cycle reuses
|
|
12
|
-
the cli's profile/quota failover (ADR-0182), token metering (ADR-0072), folder-scope guard (ADR-0181)
|
|
13
|
-
and AI-run timeout (ADR-0243).
|
|
14
|
-
- The cycle **instructions** now live in the cli (`21-apps/31-cli/src/core/autonomous-cycle.ts` →
|
|
15
|
-
`buildAutonomousCyclePrompt`), not in this file. In short: sync the primary repo's current branch →
|
|
16
|
-
analyse **approved** `USER_TODO` requests into `AI_TODO` tasks (unclear ⇒ ask via `USER_QA`) → fold
|
|
17
|
-
**approved** `USER_QA` answers back → take ONE approved task → implement + test on a `task/TSK-…`
|
|
18
|
-
branch → open a **pull request** into the base branch (no direct merge) → update the books.
|
|
19
|
-
|
|
20
|
-
Approval is the source of truth in `.claude/.autonomous.approvals.json` (ADR-0152), keyed per row id
|
|
21
|
-
(`REQ-…` / `QA-…` / `TSK-…`), set from the web AI-content grids.
|
|
22
|
-
|
|
23
|
-
This file is kept only as a pointer; it is not executed.
|