@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.
@@ -1,39 +1,31 @@
1
- # Set up a 10-minute cron for `autonomous-tick.sh`
1
+ # Set up the cron tick for autonomous mode
2
2
 
3
- How to install the **autonomous cycle** on **WSL (Ubuntu)**: cron calls
4
- [`autonomous-tick.sh`](autonomous-tick.sh) every **10 minutes**; each run does one **`4pm auto-run`**
5
- cycle (the running `4pm start` daemon executes it — ADR-0319) then exits. A file lock
6
- (`.claude/.autonomous.lock`) ensures **no two runs overlap** (a later tick that sees the lock skips).
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
- > All commands below run in a **WSL shell** (Ubuntu) unless noted as PowerShell/Windows. Call the project
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
- ## 0. Set a variable for convenience
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. Make the tick executable + install the cron line
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
- The schedule is also driven by `cron_schedule` in `.claude/.autonomous.settings.json`; once the crontab
35
- line exists, the tick keeps it in sync on later runs.
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. Enable / watch
38
- - Set `"paused": false` in `.claude/.autonomous.settings.json` to enable (or use the web Autonomous tab).
39
- - Watch: `tail -f .claude/logs/autonomous-tick-$(date +%F).log`
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 — defines the workflow only, does NOT run on its own.** The files below describe an
4
- > unattended work loop (ADR-0152 + **ADR-0319**), intended to run on **WSL** (an isolated environment
5
- > where the AI can be given full permissions). Under ADR-0319 the cycle runs **through the 4PM cli**,
6
- > not a raw `claude -p`.
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
- | File | Role |
10
- |------|------|
11
- | `.claude/hooks/autonomous-tick.sh` | Cron tick (every ~5–10 min): a run lock + the CHEAP local gates (pause / quiet-hours / max-ticks / **has-work**), then runs **`4pm auto-run`**. No token spend when there's no approved work. |
12
- | `4pm auto-run` (cli) | Asks the running **`4pm start`** daemon to run **one** cycle over the control socket; the cycle rides the daemon's live WS session (failover ADR-0182, metering ADR-0072, folder-scope ADR-0181, timeout ADR-0243). |
13
- | cli `buildAutonomousCyclePrompt` | The cli-owned cycle instructions (was `.claude/commands/auto-cycle.md`, now retired): sync branch → analyse **approved** USER_TODO → fold **approved** USER_QA → do ONE approved task → **PR** into the base branch. |
14
- | `.claude/.autonomous.approvals.json` | Approval source of truth (ADR-0152): `{ "<REQ/QA/TSK-id>": {approved, by, at} }` — the web AI-content grids write it; the cycle reads it. |
15
- | `USER_TODO.md` / `USER_QA.md` | User requests / the AI's questions back — each row approved before the cycle acts on it (ADR-0319). |
16
- | `AI_TODO.md` / `AI_PROGRESS.md` / `AI_DONE.md` | The task books: queue → in progress → done (ID `TSK-{group:0000}-{task:0000}`). |
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 ~5–10min → autonomous-tick.sh
23
- ├─ locked? / paused? / quiet-hours? / max-ticks? → log "skip", exit
24
- ├─ has-work? (approved USER_TODO/USER_QA/AI_TODO, or AI_PROGRESS non-empty) — no → skip (daemon NOT woken)
25
- └─ 4pm auto-run → the running daemon runs ONE cycle:
26
- 1. sync the primary repo's current branch (base branch — ADR-0292)
27
- 2. analyse APPROVED USER_TODO requests → AI_TODO tasks (unclear ⇒ ask via USER_QA)
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 the line (fix /path):
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 (it
46
- holds the AI credentials + WS session), plus `git`, `python3`, and `gh`/`glab` for the PR step.
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
- ## Note on permissions (important)
49
- - The daemon runs the cycle as a **write-capable agent** (`--permission-mode bypassPermissions`,
50
- ADR-0271) so file + git/`gh`/`glab` writes run headless. It stays bounded by the folder-scope guard
51
- (ADR-0181) + the AI-run timeout (ADR-0243).
52
- - Only enable full permissions in an isolated environment (WSL/CI). Never on a machine with sensitive data.
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
- # Cron tick for the autonomous workflow (ADR-0152 + ADR-0319) — install on WSL. Control flags live in
3
- # .claude/.autonomous.settings.json (paused, cron_schedule, quiet_hours, max_ticks_per_day, …); read
4
- # EVERY tick, so changes take effect next tick (cron_schedule auto-syncs into the crontab).
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
- # Project root = two levels above this file (.claude/hooks → .claude → root)
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
- # --- Python (needed for: reading settings + the has-work gate + history) -----------------------
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; `/auto-cycle` reads that file, never the table.
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
- > `/auto-cycle` only takes tasks that are approved AND have their dependencies met → moves them to
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 tick's "has work" gate + `/auto-cycle` compare a live book
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; `/auto-cycle` reads that file, never the table.
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
- > `/auto-cycle` only takes tasks that are approved AND have their dependencies met → moves them to
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",
@@ -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.