@4pm/cli 1.5.16 → 1.5.17-b
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 +19 -11
- package/dist/project-sample/.4pm +0 -0
- package/dist/project-sample/.claude/.autonomous.approvals.json +1 -0
- package/dist/project-sample/.claude/.autonomous.settings.json +24 -0
- package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +38 -0
- package/dist/project-sample/.claude/AUTONOMOUS.md +52 -0
- package/dist/project-sample/.claude/agents/.gitkeep +0 -0
- package/dist/project-sample/.claude/agents/long-memory/.gitkeep +0 -0
- package/dist/project-sample/.claude/agents/short-memory/.gitkeep +0 -0
- package/dist/project-sample/.claude/commands/auto-cycle.md +176 -0
- package/dist/project-sample/.claude/hooks/__pycache__/autonomous-history.cpython-312.pyc +0 -0
- package/dist/project-sample/.claude/hooks/autonomous-history.py +193 -0
- package/dist/project-sample/.claude/hooks/autonomous-tick.sh +356 -0
- package/dist/project-sample/.claude/rag/.gitkeep +0 -0
- package/dist/project-sample/.claude/settings.json +17 -0
- package/dist/project-sample/.claude/skills/check-usage/SKILL.md +40 -0
- package/dist/project-sample/.claude/skills/check-usage/check_usage.py +161 -0
- package/dist/project-sample/.claude/templates/AI_DONE.empty.md +9 -0
- package/dist/project-sample/.claude/templates/AI_DONE.sample.md +7 -0
- package/dist/project-sample/.claude/templates/AI_PLACEHOLDER.empty.md +11 -0
- package/dist/project-sample/.claude/templates/AI_PLACEHOLDER.sample.md +9 -0
- package/dist/project-sample/.claude/templates/AI_PROGRESS.empty.md +5 -0
- package/dist/project-sample/.claude/templates/AI_PROGRESS.sample.md +3 -0
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +14 -0
- package/dist/project-sample/.claude/templates/AI_TODO.sample.md +10 -0
- package/dist/project-sample/.claude/templates/README.md +14 -0
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +6 -0
- package/dist/project-sample/.claude/templates/USER_QA.sample.md +7 -0
- package/dist/project-sample/.claude/templates/USER_TODO.empty.md +6 -0
- package/dist/project-sample/.claude/templates/USER_TODO.sample.md +6 -0
- package/dist/project-sample/.claude/templates/project.secrets.sample.json +5 -0
- package/dist/project-sample/.vscode/extensions.json +6 -0
- package/dist/project-sample/.vscode/settings.json +18 -0
- package/dist/project-sample/AI_DONE.md +9 -0
- package/dist/project-sample/AI_PLACEHOLDER.md +19 -0
- package/dist/project-sample/AI_PROGRESS.md +5 -0
- package/dist/project-sample/AI_SECURITY.md +30 -0
- package/dist/project-sample/AI_TODO.md +14 -0
- package/dist/project-sample/CLAUDE.md +0 -0
- package/dist/project-sample/USER_QA.md +6 -0
- package/dist/project-sample/USER_TODO.md +6 -0
- package/dist/project-sample/docs/.gitkeep +0 -0
- package/dist/project-sample/project.secrets.json.sample +5 -0
- package/dist/project-sample/project.settings.json +8 -0
- package/dist/project-sample/reports/.gitkeep +0 -0
- package/dist/project-sample/scripts/.gitkeep +0 -0
- package/dist/project-sample/src/.gitkeep +0 -0
- package/dist/project-sample/tests/IT/README.md +25 -0
- package/dist/project-sample/tests/IT/senarios/.gitkeep +0 -0
- package/dist/project-sample/tests/IT/tools/.gitkeep +0 -0
- package/dist/project-sample/tests/README.md +22 -0
- package/dist/project-sample/tests/UT/README.md +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cron tick for the autonomous workflow — install on WSL. The schedule & control flags live in
|
|
3
|
+
# .claude/.autonomous.settings.json (paused, max_session_pct, max_weekly_pct, cron_schedule, …);
|
|
4
|
+
# read EVERY tick, so changes take effect on the next tick (cron_schedule auto-syncs into the crontab).
|
|
5
|
+
#
|
|
6
|
+
# Install once: crontab -e → */10 * * * * /path/to/project/.claude/hooks/autonomous-tick.sh
|
|
7
|
+
# After that, change the cadence/priority by editing cron_schedule in .autonomous.settings.json.
|
|
8
|
+
#
|
|
9
|
+
# "Busy ⇒ wait for the next tick" via the EXISTENCE of the file `.autonomous.lock` (not flock):
|
|
10
|
+
# - If the lock file exists → another tick is running → log "skip" and exit.
|
|
11
|
+
# - If it doesn't → create the lock → call Claude headless to run /auto-cycle.
|
|
12
|
+
# Removing the lock is /auto-cycle's job (on success or error). The trap below is only a SAFETY NET:
|
|
13
|
+
# if the process dies before /auto-cycle removes it, the wrapper cleans it up on exit.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
# Project root = two levels above this file (.claude/hooks → .claude → root)
|
|
17
|
+
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
18
|
+
LOCK="$PROJECT_DIR/.claude/.autonomous.lock"
|
|
19
|
+
# State + cron history COMBINED into ONE JSON file (replacing the 3 old cron-applied/fails/ticks files).
|
|
20
|
+
HIST="$PROJECT_DIR/.claude/.autonomous.histories.json"
|
|
21
|
+
HIST_PY="$PROJECT_DIR/.claude/hooks/autonomous-history.py"
|
|
22
|
+
LOG_DIR="$PROJECT_DIR/.claude/logs"
|
|
23
|
+
mkdir -p "$LOG_DIR"
|
|
24
|
+
LOG="$LOG_DIR/autonomous-tick-$(date +%F).log" # per-DAY log (so log_retention_days is meaningful)
|
|
25
|
+
ts() { date '+%F %T'; }
|
|
26
|
+
|
|
27
|
+
# Create the lock atomically: `noclobber` makes `> file` FAIL if the file exists, closing the race
|
|
28
|
+
# where two ticks both pass a separate "does it exist?" check.
|
|
29
|
+
if ! ( set -o noclobber; : > "$LOCK" ) 2>/dev/null; then
|
|
30
|
+
echo "$(ts) [skip] busy (.autonomous.lock exists) — waiting for the next tick" >> "$LOG"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
# Safety net: ensure the lock is cleaned when the wrapper exits, even if Claude dies silently.
|
|
34
|
+
trap 'rm -f "$LOCK"' EXIT
|
|
35
|
+
|
|
36
|
+
cd "$PROJECT_DIR"
|
|
37
|
+
|
|
38
|
+
# IMPORTANT: cron runs with a minimal PATH (/usr/bin:/bin) and does NOT load ~/.bashrc/~/.profile.
|
|
39
|
+
# `claude` is installed NATIVELY in WSL. Add common WSL install dirs so cron can find the claude binary:
|
|
40
|
+
# - $HOME/.local/bin : native installer (curl … | sh)
|
|
41
|
+
# - $HOME/.npm-global/bin : npm global with a custom prefix
|
|
42
|
+
# - /usr/local/bin : default npm global when installed with sudo
|
|
43
|
+
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:$PATH"
|
|
44
|
+
|
|
45
|
+
# --- Python (needed for: reading settings + the token gate) ------------------------------------
|
|
46
|
+
PY_BIN="$(command -v python3 || command -v python || true)"
|
|
47
|
+
if [ -z "$PY_BIN" ]; then
|
|
48
|
+
echo "$(ts) [error] python/python3 not found — skipping this tick" >> "$LOG"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Claude config dir (CLAUDE_CONFIG_DIR) from project.settings.json (MULTIPLE accounts) --------
|
|
53
|
+
# claudeConfigDir is a LIST of absolute paths, tried in fallback order. Pick the FIRST account whose
|
|
54
|
+
# token is still valid; if none is valid, pick the first account WITH credentials and mark it for
|
|
55
|
+
# refresh (calling 'claude /usage' once CLAUDE_BIN is known). Empty/missing -> keep the default (~/.claude).
|
|
56
|
+
SEL="$("$PY_BIN" - "$PROJECT_DIR/project.settings.json" <<'PY'
|
|
57
|
+
import json, os, sys, time
|
|
58
|
+
def exp_at(d):
|
|
59
|
+
try:
|
|
60
|
+
o = json.load(open(os.path.join(d, ".credentials.json"), encoding="utf-8")).get("claudeAiOauth", {})
|
|
61
|
+
return o.get("expiresAt")
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
s = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
66
|
+
dirs = s.get("claudeConfigDir", []) if isinstance(s, dict) else []
|
|
67
|
+
except Exception:
|
|
68
|
+
dirs = []
|
|
69
|
+
if isinstance(dirs, str):
|
|
70
|
+
dirs = [dirs]
|
|
71
|
+
dirs = [str(x).strip() for x in dirs if str(x).strip().startswith("/")]
|
|
72
|
+
now = time.time() * 1000
|
|
73
|
+
chosen, expired = "", "0"
|
|
74
|
+
for d in dirs: # prefer an account whose token is still valid
|
|
75
|
+
e = exp_at(d)
|
|
76
|
+
if e and e > now:
|
|
77
|
+
chosen = d
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
for d in dirs: # none valid -> first account with credentials (will refresh)
|
|
81
|
+
if exp_at(d) is not None:
|
|
82
|
+
chosen, expired = d, "1"
|
|
83
|
+
break
|
|
84
|
+
print(chosen)
|
|
85
|
+
print(expired)
|
|
86
|
+
PY
|
|
87
|
+
)"
|
|
88
|
+
CFG_CLAUDE_DIR="$(printf '%s\n' "$SEL" | sed -n 1p)"
|
|
89
|
+
CFG_CLAUDE_EXPIRED="$(printf '%s\n' "$SEL" | sed -n 2p)"
|
|
90
|
+
if [ -n "$CFG_CLAUDE_DIR" ]; then
|
|
91
|
+
export CLAUDE_CONFIG_DIR="$CFG_CLAUDE_DIR"
|
|
92
|
+
echo "$(ts) [cfg] CLAUDE_CONFIG_DIR=$CFG_CLAUDE_DIR (expired=$CFG_CLAUDE_EXPIRED)" >> "$LOG"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# 'hist' wrapper: every read/write of .autonomous.histories.json goes through the Python helper.
|
|
96
|
+
# (get-cron/set-cron, get-fails/set-fails, get-ticks/set-ticks, record — see autonomous-history.py)
|
|
97
|
+
hist() { "$PY_BIN" "$HIST_PY" "$HIST" "$@"; }
|
|
98
|
+
|
|
99
|
+
# --- Read the autonomous config (.autonomous.settings.json) ------------------------------------
|
|
100
|
+
# User-editable; read EVERY tick so changes take effect next tick. Python prints CFG_*=<shlex-quoted>
|
|
101
|
+
# lines to eval; a missing/corrupt file -> use defaults (no break).
|
|
102
|
+
SETTINGS="$PROJECT_DIR/.claude/.autonomous.settings.json"
|
|
103
|
+
eval "$("$PY_BIN" - "$SETTINGS" <<'PY'
|
|
104
|
+
import json, sys, shlex
|
|
105
|
+
defaults = {"paused": False, "max_session_pct": 80, "max_weekly_pct": 90,
|
|
106
|
+
"cron_schedule": "*/10 * * * *", "command": "/auto-cycle", "model": "",
|
|
107
|
+
"quiet_hours": "", "max_ticks_per_day": -1, "stop_on_consecutive_failures": 3,
|
|
108
|
+
"log_retention_days": 14, "notify_webhook": ""}
|
|
109
|
+
try:
|
|
110
|
+
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
111
|
+
if not isinstance(d, dict): d = {}
|
|
112
|
+
except Exception:
|
|
113
|
+
d = {}
|
|
114
|
+
def g(k): return d.get(k, defaults[k])
|
|
115
|
+
print("CFG_PAUSED=" + ("1" if bool(g("paused")) else "0"))
|
|
116
|
+
print("CFG_MAX_SESSION_PCT=" + shlex.quote(str(g("max_session_pct"))))
|
|
117
|
+
print("CFG_MAX_WEEKLY_PCT=" + shlex.quote(str(g("max_weekly_pct"))))
|
|
118
|
+
print("CFG_CRON_SCHEDULE=" + shlex.quote(str(g("cron_schedule"))))
|
|
119
|
+
print("CFG_COMMAND=" + shlex.quote(str(g("command"))))
|
|
120
|
+
print("CFG_MODEL=" + shlex.quote(str(g("model"))))
|
|
121
|
+
print("CFG_QUIET_HOURS=" + shlex.quote(str(g("quiet_hours"))))
|
|
122
|
+
print("CFG_MAX_TICKS_PER_DAY=" + shlex.quote(str(g("max_ticks_per_day"))))
|
|
123
|
+
print("CFG_STOP_ON_CONSEC_FAILURES=" + shlex.quote(str(g("stop_on_consecutive_failures"))))
|
|
124
|
+
print("CFG_LOG_RETENTION_DAYS=" + shlex.quote(str(g("log_retention_days"))))
|
|
125
|
+
print("CFG_NOTIFY_WEBHOOK=" + shlex.quote(str(g("notify_webhook"))))
|
|
126
|
+
PY
|
|
127
|
+
)"
|
|
128
|
+
# Export CFG_* so the history helper (autonomous-history.py 'record') can read the run config from env.
|
|
129
|
+
export CFG_CRON_SCHEDULE CFG_COMMAND CFG_MODEL CFG_MAX_SESSION_PCT CFG_MAX_WEEKLY_PCT \
|
|
130
|
+
CFG_QUIET_HOURS CFG_MAX_TICKS_PER_DAY CFG_STOP_ON_CONSEC_FAILURES CFG_PAUSED
|
|
131
|
+
|
|
132
|
+
# --- Sync the crontab when cron_schedule changes ----------------------------------------------
|
|
133
|
+
# Compare with the last-applied schedule (cron_applied in .autonomous.histories.json); if different,
|
|
134
|
+
# rewrite the crontab LINE pointing at this script. Safe: only self-edit when the crontab already has
|
|
135
|
+
# the autonomous line (or one was applied before) -> avoid creating a crontab during a manual run.
|
|
136
|
+
SELF="$PROJECT_DIR/.claude/hooks/autonomous-tick.sh"
|
|
137
|
+
PREV_SCHED="$(hist get-cron 2>/dev/null || true)"
|
|
138
|
+
if [ "$CFG_CRON_SCHEDULE" != "$PREV_SCHED" ]; then
|
|
139
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
140
|
+
CUR="$(crontab -l 2>/dev/null || true)"
|
|
141
|
+
if printf '%s\n' "$CUR" | grep -qF "$SELF" || [ -n "$PREV_SCHED" ]; then
|
|
142
|
+
NEWTAB="$( { printf '%s\n' "$CUR" | grep -vF "$SELF" || true; echo "$CFG_CRON_SCHEDULE $SELF"; } )"
|
|
143
|
+
if printf '%s\n' "$NEWTAB" | crontab - 2>>"$LOG"; then
|
|
144
|
+
hist set-cron "$CFG_CRON_SCHEDULE" 2>>"$LOG" || true
|
|
145
|
+
echo "$(ts) [cron] synced schedule -> '$CFG_CRON_SCHEDULE'" >> "$LOG"
|
|
146
|
+
else
|
|
147
|
+
echo "$(ts) [warn] could not write crontab (keeping the old schedule)" >> "$LOG"
|
|
148
|
+
fi
|
|
149
|
+
else
|
|
150
|
+
echo "$(ts) [cron] crontab has no autonomous line — skipping auto-install (install it once manually first)" >> "$LOG"
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
echo "$(ts) [cron] no 'crontab' on PATH — skipping schedule sync" >> "$LOG"
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# --- Pause flag -------------------------------------------------------------------------------
|
|
158
|
+
if [ "$CFG_PAUSED" = "1" ]; then
|
|
159
|
+
echo "$(ts) [skip] paused=true in .autonomous.settings.json — skipping this tick" >> "$LOG"
|
|
160
|
+
exit 0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# --- Prune old logs by log_retention_days (logs are per-day in $LOG_DIR) -----------------------
|
|
164
|
+
if [ "${CFG_LOG_RETENTION_DAYS:-0}" -gt 0 ] 2>/dev/null; then
|
|
165
|
+
find "$LOG_DIR" -maxdepth 1 -type f -name 'autonomous-tick-*.log' -mtime +"$CFG_LOG_RETENTION_DAYS" -delete 2>/dev/null || true
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# --- Quiet hours ------------------------------------------------------------------------------
|
|
169
|
+
# Empty = run all day. 'HH:MM-HH:MM' = skip within the window; supports a window crossing midnight.
|
|
170
|
+
if [ -n "$CFG_QUIET_HOURS" ]; then
|
|
171
|
+
if printf '%s' "$CFG_QUIET_HOURS" | grep -Eq '^[0-9]{1,2}:[0-9]{2}-[0-9]{1,2}:[0-9]{2}$'; then
|
|
172
|
+
q_start="${CFG_QUIET_HOURS%%-*}"; q_end="${CFG_QUIET_HOURS##*-}"
|
|
173
|
+
_min() { echo $(( 10#${1%%:*} * 60 + 10#${1##*:} )); } # "HH:MM" -> minutes (10# forces base 10)
|
|
174
|
+
qs=$(_min "$q_start"); qe=$(_min "$q_end"); qn=$(_min "$(date +%H:%M)")
|
|
175
|
+
in_q=0
|
|
176
|
+
if [ "$qs" -le "$qe" ]; then
|
|
177
|
+
{ [ "$qn" -ge "$qs" ] && [ "$qn" -lt "$qe" ]; } && in_q=1
|
|
178
|
+
else
|
|
179
|
+
{ [ "$qn" -ge "$qs" ] || [ "$qn" -lt "$qe" ]; } && in_q=1 # window crossing midnight
|
|
180
|
+
fi
|
|
181
|
+
if [ "$in_q" = 1 ]; then
|
|
182
|
+
echo "$(ts) [skip] within quiet_hours ($CFG_QUIET_HOURS) — skipping this tick" >> "$LOG"
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
echo "$(ts) [warn] quiet_hours has a bad format ('$CFG_QUIET_HOURS') — ignoring the check" >> "$LOG"
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# --- Max ticks/day (max_ticks_per_day) --------------------------------------------------------
|
|
191
|
+
# -1 = unlimited. Counted per local day, stored under ticks{day,count} in .autonomous.histories.json.
|
|
192
|
+
TODAY="$(date +%F)"
|
|
193
|
+
tick_count="$(hist get-ticks "$TODAY" 2>/dev/null || echo 0)"; [ -z "$tick_count" ] && tick_count=0
|
|
194
|
+
if [ "${CFG_MAX_TICKS_PER_DAY:--1}" -gt 0 ] 2>/dev/null && [ "${tick_count:-0}" -ge "$CFG_MAX_TICKS_PER_DAY" ]; then
|
|
195
|
+
echo "$(ts) [skip] reached max_ticks_per_day=$CFG_MAX_TICKS_PER_DAY ($tick_count ticks today) — skipping" >> "$LOG"
|
|
196
|
+
exit 0
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# --- "Has work" gate (USER_TODO / APPROVED AI_TODO / AI_PROGRESS) -------------------------------
|
|
200
|
+
# Skip IMMEDIATELY (WITHOUT calling Claude — /auto-cycle would still cost tokens just to start + stop)
|
|
201
|
+
# UNLESS there is one of three kinds of work:
|
|
202
|
+
# 1) USER_TODO.md has a new request → /auto-cycle will generate tasks (Step 2);
|
|
203
|
+
# 2) AI_TODO.md has AT LEAST 1 APPROVED task (approved in .autonomous.approvals.json) → Step 3 can take
|
|
204
|
+
# it. Tasks not approved DON'T count as work (VERIFY gate: wait for the user to approve them first);
|
|
205
|
+
# 3) AI_PROGRESS.md is non-empty → work left over from a previous tick to continue.
|
|
206
|
+
#
|
|
207
|
+
# "EMPTY" (USER_TODO / AI_PROGRESS) = COMPARE TO THE TEMPLATE: the content (normalized: trim trailing
|
|
208
|
+
# whitespace + leading/trailing blank lines) EQUALS the empty template in .claude/templates/. AI_TODO is
|
|
209
|
+
# judged separately via approvals (a full AI_TODO of unapproved tasks != the empty template but has NO work).
|
|
210
|
+
TPL="$PROJECT_DIR/.claude/templates"
|
|
211
|
+
WORK="$(AI_TODO="$PROJECT_DIR/AI_TODO.md" USER_TODO="$PROJECT_DIR/USER_TODO.md" \
|
|
212
|
+
AI_PROGRESS="$PROJECT_DIR/AI_PROGRESS.md" \
|
|
213
|
+
APPROVALS="$PROJECT_DIR/.claude/.autonomous.approvals.json" \
|
|
214
|
+
USER_TODO_TPL="$TPL/USER_TODO.empty.md" AI_PROGRESS_TPL="$TPL/AI_PROGRESS.empty.md" \
|
|
215
|
+
"$PY_BIN" - <<'PY'
|
|
216
|
+
import os, json
|
|
217
|
+
def norm(p):
|
|
218
|
+
# Read + normalize; return None if the file can't be read.
|
|
219
|
+
try:
|
|
220
|
+
lines = [ln.rstrip() for ln in open(p, encoding="utf-8").read().splitlines()]
|
|
221
|
+
except Exception:
|
|
222
|
+
return None
|
|
223
|
+
while lines and not lines[0]: lines.pop(0)
|
|
224
|
+
while lines and not lines[-1]: lines.pop()
|
|
225
|
+
return "\n".join(lines)
|
|
226
|
+
def has_work(live, tpl):
|
|
227
|
+
nv = norm(live)
|
|
228
|
+
if nv is None: # no live file -> treat as NO work (safe, don't call Claude)
|
|
229
|
+
return False
|
|
230
|
+
nt = norm(tpl)
|
|
231
|
+
if nt is None: # no template -> can't compare -> any non-empty content counts as work
|
|
232
|
+
return bool(nv)
|
|
233
|
+
return nv != nt # differs from the empty template = has work
|
|
234
|
+
def has_approved(aitodo_p, approvals_p):
|
|
235
|
+
# At least 1 APPROVED task still in the queue (ADR-0152): the approval source is
|
|
236
|
+
# .autonomous.approvals.json ("<TSK-id>": {approved:true,...}) — the ✓ column is no longer read.
|
|
237
|
+
try:
|
|
238
|
+
ap = json.load(open(approvals_p, encoding="utf-8"))
|
|
239
|
+
approved = {k for k, v in ap.items() if isinstance(v, dict) and v.get("approved") is True}
|
|
240
|
+
except Exception:
|
|
241
|
+
return False
|
|
242
|
+
if not approved:
|
|
243
|
+
return False
|
|
244
|
+
try:
|
|
245
|
+
todo = open(aitodo_p, encoding="utf-8").read()
|
|
246
|
+
except Exception:
|
|
247
|
+
return False
|
|
248
|
+
return any(tid in todo for tid in approved) # approved AND still present in AI_TODO
|
|
249
|
+
user = has_work(os.environ["USER_TODO"], os.environ["USER_TODO_TPL"])
|
|
250
|
+
prog = has_work(os.environ["AI_PROGRESS"], os.environ["AI_PROGRESS_TPL"])
|
|
251
|
+
ai = has_approved(os.environ["AI_TODO"], os.environ["APPROVALS"])
|
|
252
|
+
print("WORK" if (user or prog or ai) else "EMPTY")
|
|
253
|
+
PY
|
|
254
|
+
)"
|
|
255
|
+
if [ "$WORK" != "WORK" ]; then
|
|
256
|
+
echo "$(ts) [skip] no new request (USER_TODO), approved task (AI_TODO), or in-progress work (AI_PROGRESS) — skipping (no Claude call)" >> "$LOG"
|
|
257
|
+
exit 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# Allow an override via env; if still not found, log a clear error and skip.
|
|
261
|
+
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude || true)}"
|
|
262
|
+
if [ -z "$CLAUDE_BIN" ]; then
|
|
263
|
+
echo "$(ts) [error] 'claude' not found on PATH — check the claude install in WSL (e.g. ~/.local/bin/claude)" >> "$LOG"
|
|
264
|
+
exit 0
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# The chosen account's token expired -> try to refresh with 'claude /usage' (startup refresh).
|
|
268
|
+
# (Switching accounts was done in the CLAUDE_CONFIG_DIR selection above.) Best-effort, doesn't block.
|
|
269
|
+
if [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ "${CFG_CLAUDE_EXPIRED:-0}" = "1" ]; then
|
|
270
|
+
echo "$(ts) [cfg] token expired -> refreshing with 'claude /usage'" >> "$LOG"
|
|
271
|
+
"$CLAUDE_BIN" /usage >/dev/null 2>&1 || true
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# --- Token gate (moved from /auto-cycle Step 0 to here) ---------------------------------------
|
|
275
|
+
# Reason: check quota BEFORE calling Claude so we don't spend tokens just to start + self-stop when
|
|
276
|
+
# quota is already high. Thresholds from settings: session_pct < max_session_pct AND weekly_pct < max_weekly_pct.
|
|
277
|
+
USAGE_JSON="$PROJECT_DIR/.claude/skills/check-usage/output/last-usage-check.json"
|
|
278
|
+
if ! "$PY_BIN" "$PROJECT_DIR/.claude/skills/check-usage/check_usage.py" >> "$LOG" 2>&1; then
|
|
279
|
+
echo "$(ts) [skip] check_usage.py failed (can't read quota) — skipping to be safe" >> "$LOG"
|
|
280
|
+
exit 0
|
|
281
|
+
fi
|
|
282
|
+
GATE="$(MAXS="$CFG_MAX_SESSION_PCT" MAXW="$CFG_MAX_WEEKLY_PCT" "$PY_BIN" - "$USAGE_JSON" <<'PY'
|
|
283
|
+
import json, os, sys
|
|
284
|
+
try:
|
|
285
|
+
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
286
|
+
s = float(d.get("session_pct", 100))
|
|
287
|
+
w = float(d.get("weekly_pct", 100))
|
|
288
|
+
maxs = float(os.environ.get("MAXS", "80"))
|
|
289
|
+
maxw = float(os.environ.get("MAXW", "90"))
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"ERR {e}")
|
|
292
|
+
sys.exit(0)
|
|
293
|
+
print(f"{'OK' if (s < maxs and w < maxw) else 'HIGH'} session={s:.0f}% weekly={w:.0f}% (max {maxs:.0f}/{maxw:.0f})")
|
|
294
|
+
PY
|
|
295
|
+
)"
|
|
296
|
+
case "$GATE" in
|
|
297
|
+
OK*) echo "$(ts) [gate] tokens ok ($GATE) — continue" >> "$LOG" ;;
|
|
298
|
+
HIGH*) echo "$(ts) [skip] tokens high ($GATE) — skipping this tick" >> "$LOG"
|
|
299
|
+
hist record "$(ts)" skip "" "$GATE" "tokens high — skipped" 2>>"$LOG" || true; exit 0 ;;
|
|
300
|
+
*) echo "$(ts) [skip] can't read usage ($GATE) — skipping to be safe" >> "$LOG"
|
|
301
|
+
hist record "$(ts)" skip "" "$GATE" "can't read usage" 2>>"$LOG" || true; exit 0 ;;
|
|
302
|
+
esac
|
|
303
|
+
# -----------------------------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
# Record 1 tick that ACTUALLY calls Claude (for max_ticks_per_day) — stored in ticks{day,count}.
|
|
306
|
+
tick_count=$(( ${tick_count:-0} + 1 ))
|
|
307
|
+
hist set-ticks "$TODAY" "$tick_count" 2>>"$LOG" || true
|
|
308
|
+
|
|
309
|
+
echo "$(ts) [run] starting $CFG_COMMAND (tick $tick_count/$TODAY)" >> "$LOG"
|
|
310
|
+
# No GUI window (cron has no desktop session). Everything goes to "$LOG"; watch it live with:
|
|
311
|
+
# tail -f .claude/logs/autonomous-tick-$(date +%F).log
|
|
312
|
+
|
|
313
|
+
# Headless: -p runs one cycle then exits. No permission prompts (unattended on WSL).
|
|
314
|
+
# Force the model if settings has 'model' (empty array is safe under set -u via ${arr[@]+...}).
|
|
315
|
+
MODEL_ARGS=()
|
|
316
|
+
[ -n "$CFG_MODEL" ] && MODEL_ARGS=(--model "$CFG_MODEL")
|
|
317
|
+
set +e
|
|
318
|
+
"$CLAUDE_BIN" -p "$CFG_COMMAND" \
|
|
319
|
+
${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
|
|
320
|
+
--permission-mode bypassPermissions \
|
|
321
|
+
--dangerously-skip-permissions \
|
|
322
|
+
>> "$LOG" 2>&1
|
|
323
|
+
RC=$?
|
|
324
|
+
set -e
|
|
325
|
+
|
|
326
|
+
# --- Record the run + count consecutive failures + auto-stop (stop_on_consecutive_failures) ----
|
|
327
|
+
# RC != 0 -> increment; reaching the threshold -> set paused=true (needs a manual resume). RC == 0 -> reset.
|
|
328
|
+
# The consecutive-failure count is stored under consecutive_fails in .autonomous.histories.json.
|
|
329
|
+
fails="$(hist get-fails 2>/dev/null || echo 0)"; [ -z "$fails" ] && fails=0
|
|
330
|
+
if [ "$RC" -ne 0 ]; then
|
|
331
|
+
echo "$(ts) [warn] $CFG_COMMAND exited $RC" >> "$LOG"
|
|
332
|
+
fails=$(( ${fails:-0} + 1 )); hist set-fails "$fails" 2>>"$LOG" || true
|
|
333
|
+
hist record "$(ts)" failure "$RC" "$GATE" "consecutive failure #$fails" 2>>"$LOG" || true
|
|
334
|
+
if [ "${CFG_STOP_ON_CONSEC_FAILURES:-0}" -gt 0 ] 2>/dev/null && [ "$fails" -ge "$CFG_STOP_ON_CONSEC_FAILURES" ]; then
|
|
335
|
+
"$PY_BIN" - "$SETTINGS" <<'PY' 2>>"$LOG" || true
|
|
336
|
+
import json, sys
|
|
337
|
+
p = sys.argv[1]
|
|
338
|
+
try:
|
|
339
|
+
d = json.load(open(p, encoding="utf-8"))
|
|
340
|
+
d["paused"] = True
|
|
341
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
342
|
+
json.dump(d, f, ensure_ascii=False, indent=2); f.write("\n")
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
PY
|
|
346
|
+
echo "$(ts) [stop] $fails consecutive failures >= $CFG_STOP_ON_CONSEC_FAILURES → set paused=true (resume manually)" >> "$LOG"
|
|
347
|
+
fi
|
|
348
|
+
else
|
|
349
|
+
hist set-fails 0 2>>"$LOG" || true # success → reset the consecutive-failure count
|
|
350
|
+
hist record "$(ts)" success "$RC" "$GATE" "cycle complete" 2>>"$LOG" || true
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# notify_webhook: TBD — this is where a run summary would be POSTed to a webhook if CFG_NOTIFY_WEBHOOK is set.
|
|
354
|
+
|
|
355
|
+
echo "$(ts) [done] tick finished" >> "$LOG"
|
|
356
|
+
# The EXIT trap above removes `.autonomous.lock` if /auto-cycle didn't → the lock is released.
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"model": "claude-opus-4-8",
|
|
4
|
+
"cleanupPeriodDays": 30,
|
|
5
|
+
"env": {
|
|
6
|
+
"XNDP_ENV": "development"
|
|
7
|
+
},
|
|
8
|
+
"permissions": {
|
|
9
|
+
"defaultMode": "bypassPermissions",
|
|
10
|
+
"allow": [],
|
|
11
|
+
"ask": [],
|
|
12
|
+
"deny": [
|
|
13
|
+
"Read(./project.secrets.json)"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"enableAllProjectMcpServers": true
|
|
17
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-usage
|
|
3
|
+
description: Show Claude Code plan usage limits — current session % and reset time, weekly % and reset time (same data as the built-in /usage screen). Use when the user asks "how much usage left", "khi nao reset", "check usage/quota/limit", or invokes /check-usage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# check-usage — Claude Code plan usage limits
|
|
7
|
+
|
|
8
|
+
Fetches the same data the built-in `/usage` screen shows (session + weekly utilization and
|
|
9
|
+
reset times), by calling the Anthropic OAuth usage endpoint with the local credentials.
|
|
10
|
+
|
|
11
|
+
## How to run
|
|
12
|
+
|
|
13
|
+
Run the bundled script with the Bash tool. It prints a compact bullet list to the console;
|
|
14
|
+
relay that output to the user.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python .claude/skills/check-usage/check_usage.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
On Windows if `python` is missing, try `py .claude/skills/check-usage/check_usage.py`.
|
|
21
|
+
|
|
22
|
+
Add `--json` to dump the raw API response instead of the formatted view:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python .claude/skills/check-usage/check_usage.py --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Output file
|
|
29
|
+
Every run (the formatted view, not `--json`) rewrites `./output/last-usage-check.json` next to
|
|
30
|
+
the script: it deletes the previous file then creates a fresh one with this run's result. The
|
|
31
|
+
record has machine-readable fields the autonomous workflow reads back:
|
|
32
|
+
`session_pct`, `weekly_pct`, `session_resets_at`, `weekly_resets_at`, `checked_at`, `plan`,
|
|
33
|
+
plus the human `summary` and the full `raw` API payload.
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
- Reads the OAuth access token from `~/.claude/.credentials.json` (Claude Code keeps it refreshed).
|
|
37
|
+
- If the call returns HTTP 401, the token expired — tell the user to run the real `/usage` once
|
|
38
|
+
inside Claude Code (or re-login) to refresh credentials, then retry.
|
|
39
|
+
- Reset times are printed in the machine's local timezone with a "con Xh Ym" countdown.
|
|
40
|
+
- Do not print the access token.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch Claude Code plan-usage limits (same data as the built-in /usage screen).
|
|
3
|
+
|
|
4
|
+
Reads the OAuth access token from ~/.claude/.credentials.json and queries the
|
|
5
|
+
Anthropic usage endpoint, then prints session + weekly utilization and reset times
|
|
6
|
+
in the local timezone.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.error
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
|
|
16
|
+
# Windows consoles default to cp1252 which can't encode the emoji/bar glyphs.
|
|
17
|
+
for _s in (sys.stdout, sys.stderr):
|
|
18
|
+
try:
|
|
19
|
+
_s.reconfigure(encoding="utf-8")
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
CRED_PATH = os.path.expanduser("~/.claude/.credentials.json")
|
|
24
|
+
URL = "https://api.anthropic.com/api/oauth/usage"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UsageError(Exception):
|
|
28
|
+
"""Recoverable failure while loading the token or calling the usage API.
|
|
29
|
+
Raised (instead of exiting) so the caller can refresh the token and retry once."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_token():
|
|
33
|
+
try:
|
|
34
|
+
with open(CRED_PATH, "r", encoding="utf-8") as f:
|
|
35
|
+
data = json.load(f)
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
raise UsageError(f"Credentials not found: {CRED_PATH} (log in to Claude Code first).")
|
|
38
|
+
oauth = data.get("claudeAiOauth", data)
|
|
39
|
+
token = oauth.get("accessToken")
|
|
40
|
+
if not token:
|
|
41
|
+
raise UsageError("No accessToken in credentials.")
|
|
42
|
+
exp = oauth.get("expiresAt")
|
|
43
|
+
if exp and datetime.now(timezone.utc).timestamp() * 1000 > exp:
|
|
44
|
+
print("Warning: token may be expired — run /usage once in Claude Code to refresh.\n",
|
|
45
|
+
file=sys.stderr)
|
|
46
|
+
return token, oauth.get("subscriptionType", "?")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fetch(token):
|
|
50
|
+
req = urllib.request.Request(URL, headers={
|
|
51
|
+
"Authorization": f"Bearer {token}",
|
|
52
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
53
|
+
"User-Agent": "claude-cli/2.0.0 (external, cli)",
|
|
54
|
+
})
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(req, timeout=20) as r:
|
|
57
|
+
return json.load(r)
|
|
58
|
+
except urllib.error.HTTPError as e:
|
|
59
|
+
body = e.read().decode("utf-8", "replace")[:300]
|
|
60
|
+
raise UsageError(f"API error HTTP {e.code}: {body}")
|
|
61
|
+
except urllib.error.URLError as e:
|
|
62
|
+
raise UsageError(f"Could not reach API: {e.reason}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_usage():
|
|
66
|
+
"""Load the token and query the usage API. Raises UsageError on any failure."""
|
|
67
|
+
token, sub = load_token()
|
|
68
|
+
return fetch(token), sub
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def refresh_token():
|
|
72
|
+
"""Best-effort token refresh: run `claude /usage`, which refreshes the OAuth
|
|
73
|
+
token on startup. On Windows we go through `wsl` (the credentials this script
|
|
74
|
+
reads live in WSL); on POSIX we call `claude` directly. Output is discarded and
|
|
75
|
+
the call is bounded by a timeout so it can never hang the usage check."""
|
|
76
|
+
cmd = ["wsl", "claude", "/usage"] if sys.platform == "win32" else ["claude", "/usage"]
|
|
77
|
+
print(f"Refreshing token via: {' '.join(cmd)}", file=sys.stderr)
|
|
78
|
+
try:
|
|
79
|
+
subprocess.run(
|
|
80
|
+
cmd, timeout=60,
|
|
81
|
+
stdin=subprocess.DEVNULL,
|
|
82
|
+
stdout=subprocess.DEVNULL,
|
|
83
|
+
stderr=subprocess.DEVNULL,
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Token refresh attempt failed: {e}", file=sys.stderr)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fmt_reset(iso):
|
|
90
|
+
if not iso:
|
|
91
|
+
return "?"
|
|
92
|
+
dt = datetime.fromisoformat(iso).astimezone() # -> local tz
|
|
93
|
+
return f"{dt:%a %d/%m %H:%M}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def save_last_result(record):
|
|
97
|
+
"""Delete the previous ./output/last-usage-check.json, then write a fresh one
|
|
98
|
+
holding this run's result. Console output is still printed by the caller."""
|
|
99
|
+
out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
|
100
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
101
|
+
out_path = os.path.join(out_dir, "last-usage-check.json")
|
|
102
|
+
# Remove the stale file first, then recreate it from scratch.
|
|
103
|
+
try:
|
|
104
|
+
os.remove(out_path)
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
pass
|
|
107
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
108
|
+
json.dump(record, f, ensure_ascii=False, indent=2)
|
|
109
|
+
return out_path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
try:
|
|
114
|
+
data, sub = get_usage()
|
|
115
|
+
except UsageError as e:
|
|
116
|
+
# First attempt failed — try to refresh the token once, then retry once.
|
|
117
|
+
print(f"Usage check failed: {e}", file=sys.stderr)
|
|
118
|
+
refresh_token()
|
|
119
|
+
try:
|
|
120
|
+
data, sub = get_usage()
|
|
121
|
+
except UsageError as e2:
|
|
122
|
+
print(f"Still failing after token refresh — skipping: {e2}", file=sys.stderr)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
if "--json" in sys.argv:
|
|
126
|
+
print(json.dumps(data, indent=2))
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
fh = data.get("five_hour") or {}
|
|
130
|
+
sd = data.get("seven_day") or {}
|
|
131
|
+
|
|
132
|
+
lines = [
|
|
133
|
+
f"- Plan: {sub}",
|
|
134
|
+
f"- Session: {fh.get('utilization', 0):.0f}% — reset {fmt_reset(fh.get('resets_at'))}",
|
|
135
|
+
f"- Weekly: {sd.get('utilization', 0):.0f}% — reset {fmt_reset(sd.get('resets_at'))}",
|
|
136
|
+
]
|
|
137
|
+
extra = data.get("extra_usage") or {}
|
|
138
|
+
if extra.get("is_enabled") and extra.get("used_credits"):
|
|
139
|
+
dp = extra.get("decimal_places", 2)
|
|
140
|
+
lines.append(f"- Extra: {extra['used_credits'] / (10 ** dp):.2f} {extra.get('currency', 'USD')}")
|
|
141
|
+
|
|
142
|
+
summary = "\n".join(lines)
|
|
143
|
+
print(summary) # keep the console output
|
|
144
|
+
|
|
145
|
+
# Persist this run's result so the autonomous workflow can read it back.
|
|
146
|
+
record = {
|
|
147
|
+
"checked_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
148
|
+
"plan": sub,
|
|
149
|
+
"session_pct": fh.get("utilization", 0),
|
|
150
|
+
"weekly_pct": sd.get("utilization", 0),
|
|
151
|
+
"session_resets_at": fh.get("resets_at"),
|
|
152
|
+
"weekly_resets_at": sd.get("resets_at"),
|
|
153
|
+
"summary": summary,
|
|
154
|
+
"raw": data,
|
|
155
|
+
}
|
|
156
|
+
out_path = save_last_result(record)
|
|
157
|
+
print(f"\nSaved result to: {out_path}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# AI_PLACEHOLDER — Placeholder table (key + explanation)
|
|
2
|
+
|
|
3
|
+
> Lists **placeholders** the AI understands: each has a **key** and an **explanation**. Reference them in
|
|
4
|
+
> docs/config as `${key}$` (e.g. `${ai_dev_branch}$`); the tool substitutes the real **value** at runtime.
|
|
5
|
+
>
|
|
6
|
+
> **Where values live:** the real value of each key is stored in `project.secrets.json` (same folder). This
|
|
7
|
+
> file holds only keys + explanations, so it is **safe to commit** and the AI can read it for context.
|
|
8
|
+
|
|
9
|
+
| Key | Explanation |
|
|
10
|
+
|-----|-------------|
|
|
11
|
+
| ai_dev_branch | The shared dev branch the AI merges tasks into before the main branch. |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# AI_PLACEHOLDER — Placeholder table (key + explanation)
|
|
2
|
+
|
|
3
|
+
> Reference placeholders as `${key}$`. Real values live in `project.secrets.json` (not committed).
|
|
4
|
+
|
|
5
|
+
| Key | Explanation |
|
|
6
|
+
|-----|-------------|
|
|
7
|
+
| ai_dev_branch | The shared dev branch the AI merges tasks into before the main branch. |
|
|
8
|
+
| api_base_url | Base URL of the internal API the AI calls during integration tests. |
|
|
9
|
+
| api_key | Key for the internal API (real value in project.secrets.json, NOT committed). |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# AI_TODO — Task queue (generated by the AI)
|
|
2
|
+
|
|
3
|
+
> Tasks are generated from `USER_TODO.md`. ID format `TSK-{groupid:0000}-{taskid:0000}`
|
|
4
|
+
> (group = one request/batch, task = a small sub-task doable in ~1 cycle).
|
|
5
|
+
> **Priority** = High / Medium / Low.
|
|
6
|
+
> **Approved**: the source of truth is `.claude/.autonomous.approvals.json` (the user ticks it in the
|
|
7
|
+
> web VERIFY tab — ADR-0152); the Approved column here is display-only, not where the decision is made.
|
|
8
|
+
> **Depends** = the `TSK-…` ids that must be DONE (present in `AI_DONE.md`) first.
|
|
9
|
+
> `/auto-cycle` only takes tasks that are approved AND have their dependencies met → moves them to
|
|
10
|
+
> `AI_PROGRESS.md`; runs group by group, within a group High → Medium → Low.
|
|
11
|
+
|
|
12
|
+
| ID | Priority | Approved | Depends | Group | Task description | Notes |
|
|
13
|
+
|----|----------|----------|---------|-------|------------------|-------|
|
|
14
|
+
| | | | | | | |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# AI_TODO — Task queue (generated by the AI)
|
|
2
|
+
|
|
3
|
+
> Tasks are generated from `USER_TODO.md`. ID format `TSK-{groupid:0000}-{taskid:0000}`.
|
|
4
|
+
> **Priority** = High / Medium / Low. **Approved** source of truth = `.claude/.autonomous.approvals.json`
|
|
5
|
+
> (web VERIFY tab). **Depends** = `TSK-…` ids that must be in `AI_DONE.md` first.
|
|
6
|
+
|
|
7
|
+
| ID | Priority | Approved | Depends | Group | Task description | Notes |
|
|
8
|
+
|----|----------|----------|---------|-------|------------------|-------|
|
|
9
|
+
| TSK-0001-0001 | High | ✓ | | Auth | Add login-form validation per docs/auth.md | |
|
|
10
|
+
| TSK-0001-0002 | Medium | | TSK-0001-0001 | Auth | Wire the login API call + error handling | After 0001 |
|