@4pm/cli 1.17.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.
Files changed (24) hide show
  1. package/dist/index.js +848 -241
  2. package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +16 -23
  3. package/dist/project-sample/.claude/AUTONOMOUS.md +31 -36
  4. package/dist/project-sample/.claude/hooks/autonomous-tick.sh +8 -349
  5. package/dist/project-sample/.claude/settings.json +1 -3
  6. package/dist/project-sample/.claude/templates/AI_TODO.empty.md +2 -2
  7. package/dist/project-sample/.claude/templates/README.md +1 -1
  8. package/dist/project-sample/.claude/templates/USER_QA.empty.md +13 -6
  9. package/dist/project-sample/.claude/templates/USER_QA.sample.md +6 -3
  10. package/dist/project-sample/.claude/templates/USER_TODO.empty.md +13 -6
  11. package/dist/project-sample/.claude/templates/USER_TODO.sample.md +7 -6
  12. package/dist/project-sample/.claude/templates/project.secrets.sample.json +0 -1
  13. package/dist/project-sample/AI_PLACEHOLDER.md +5 -5
  14. package/dist/project-sample/AI_TODO.md +2 -2
  15. package/dist/project-sample/USER_QA.md +13 -6
  16. package/dist/project-sample/USER_TODO.md +13 -6
  17. package/dist/project-sample/project.secrets.json.sample +0 -1
  18. package/package.json +1 -1
  19. package/dist/project-sample/.claude/.autonomous.settings.json +0 -24
  20. package/dist/project-sample/.claude/commands/auto-cycle.md +0 -177
  21. package/dist/project-sample/.claude/hooks/__pycache__/autonomous-history.cpython-312.pyc +0 -0
  22. package/dist/project-sample/.claude/hooks/autonomous-history.py +0 -193
  23. package/dist/project-sample/.claude/skills/check-usage/SKILL.md +0 -40
  24. package/dist/project-sample/.claude/skills/check-usage/check_usage.py +0 -161
@@ -1,5 +1,4 @@
1
1
  {
2
- "ai_dev_branch": "main",
3
2
  "api_base_url": "https://api.example.com",
4
3
  "api_key": "sk-demo-0000…zzzz"
5
4
  }
@@ -7,13 +7,13 @@
7
7
  > file holds only keys + explanations, so it is **safe to commit** and the AI can read it for context.
8
8
  >
9
9
  > **Why two files:**
10
- > - `AI_PLACEHOLDER.md` (this file) — keys + explanations. Safe to commit; not gitignored/denied.
11
- > - `project.secrets.json` — key → real value. **Gitignored** and **denied** in `.claude/settings.json`,
12
- > so secret values never reach git and the AI can't read them directly. Manage values from the web
13
- > (Placeholder tab) — they are write-only (never shown back).
10
+ > - `AI_PLACEHOLDER.md` (this file) — keys + explanations. Safe to commit; not gitignored.
11
+ > - `project.secrets.json` — key → real value. **Gitignored** (never committed / pushed), so secret
12
+ > values stay on the worker. The AI **can** read this file to use the values while running/testing a
13
+ > task — so enter **test values only**; do **not** put high-sensitivity secrets here. Manage values
14
+ > from the web (Placeholder tab) — they are write-only (never shown back).
14
15
 
15
16
  | Key | Explanation |
16
17
  |-----|-------------|
17
- | ai_dev_branch | The shared dev branch the AI merges tasks into before the main branch. |
18
18
  | api_base_url | Base URL of the internal API the AI calls during integration tests. |
19
19
  | api_key | Key for the internal API (real value in project.secrets.json, NOT committed). |
@@ -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 |
@@ -1,9 +1,16 @@
1
1
  # USER_QA — Questions & answers
2
2
 
3
- > When a request is unclear, the AI adds a **row** here (date, the original request, what's unclear +
4
- > options) instead of guessing. Fill the **Answer** column, then re-post the clarified request into
5
- > `USER_TODO.md` — per `.claude/templates/USER_QA.sample.md`.
3
+ > When a request is unclear, the AI adds a **row** here (instead of guessing) with an
4
+ > **ID `QA-{groupid:0000}-{qaid:0000}`**: the original request and what's unclear + options, leaving the
5
+ > `Answer` blank. You fill the `Answer`; the autonomous cycle folds it back into a re-analysis **only after
6
+ > the row is approved**. Approval + authorship (who answered / who approved, with dates) live in JSON
7
+ > sidecars, not in this table (ADR-0320): `.claude/.autonomous.approvals.json` +
8
+ > `.claude/.autonomous.authors.json` — the web USER_QA grid shows them and enforces four-eyes (the person
9
+ > who answered can't approve their own answer, except ADMIN). See `.claude/templates/USER_QA.sample.md`.
10
+ >
11
+ > **Columns (content only):** `ID` · `Group` · `Depends` (comma-separated `QA-…`) · `Original request` ·
12
+ > `Question / options` · `Answer`.
6
13
 
7
- | Date | Original request | Question / options | Answer |
8
- |------|------------------|--------------------|--------|
9
- | | | | |
14
+ | ID | Group | Depends | Original request | Question / options | Answer |
15
+ |----|-------|---------|------------------|--------------------|--------|
16
+ | | | | | | |
@@ -1,9 +1,16 @@
1
1
  # USER_TODO — User requests
2
2
 
3
- > Write what you want the AI to do (in the project's language — default English), **one request per row**
4
- > in the table below. The autonomous cycle reads this, generates tasks into `AI_TODO.md`, then clears this
5
- > file back to the empty template — per `.claude/templates/USER_TODO.sample.md`.
3
+ > Write what you want the AI to do (in the project's language — default English), **one request per row**.
4
+ > Each row has an **ID `REQ-{groupid:0000}-{reqid:0000}`** (group = one batch of related requests) used to
5
+ > approve it and to reference it from `Depends`. The autonomous cycle analyses a request into tasks in
6
+ > `AI_TODO.md` **only after the row is approved**. Approval + authorship are kept in JSON sidecars, not in
7
+ > this table (ADR-0320): `.claude/.autonomous.approvals.json` (who/when approved) and
8
+ > `.claude/.autonomous.authors.json` (who/when wrote the row) — the web USER_TODO grid shows them and
9
+ > enforces four-eyes (the writer can't approve their own row, except ADMIN). See
10
+ > `.claude/templates/USER_TODO.sample.md`.
11
+ >
12
+ > **Columns (content only):** `ID` · `Group` · `Depends` (comma-separated `REQ-…`) · `Request`.
6
13
 
7
- | # | Request | Notes |
8
- |---|---------|-------|
9
- | | | |
14
+ | ID | Group | Depends | Request |
15
+ |----|-------|---------|---------|
16
+ | | | | |
@@ -1,5 +1,4 @@
1
1
  {
2
- "ai_dev_branch": "main",
3
2
  "api_base_url": "https://api.example.com",
4
3
  "api_key": "sk-demo-0000…zzzz"
5
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.17.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,24 +0,0 @@
1
- {
2
- "_about": "Autonomous workflow config. 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 invoking Claude. A quick brake without removing the crontab entry.",
5
- "max_session_pct": 80,
6
- "max_weekly_pct": 90,
7
- "_quota": "Only run when session_pct < max_session_pct AND weekly_pct < max_weekly_pct (read from the usage skill).",
8
- "cron_schedule": "*/5 * * * *",
9
- "_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).",
10
- "command": "/auto-cycle",
11
- "_command": "The headless slash-command each tick invokes.",
12
- "model": "",
13
- "_model": "Empty = Claude Code's default model. Or set an id (e.g. 'claude-sonnet-4-6') to force a cheaper/faster model for the autonomous loop.",
14
- "quiet_hours": "",
15
- "_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').",
16
- "max_ticks_per_day": -1,
17
- "_max_ticks_per_day": "-1 = NO limit. >0 = cap on the number of ticks that actually invoke Claude in one (local) day. The count is stored under ticks{day,count} in .claude/.autonomous.histories.json.",
18
- "stop_on_consecutive_failures": 3,
19
- "_stop_on_consecutive_failures": "After N consecutive FAILED command runs -> set paused=true to stop safely. 0 = off. The count is stored under consecutive_fails in .claude/.autonomous.histories.json (reset on success).",
20
- "log_retention_days": 14,
21
- "_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.",
22
- "notify_webhook": "",
23
- "_notify_webhook": "TBD — a webhook URL (Slack/Discord) to notify on finish/error. NOT wired yet (read but unused)."
24
- }
@@ -1,177 +0,0 @@
1
- ---
2
- description: Autonomous cycle — pull a request from USER_TODO, do ONE small approved task, test it, merge into ${ai_dev_branch}$. (The token gate is checked by autonomous-tick.sh BEFORE Claude is invoked.)
3
- allowed-tools: Bash(*), Read(*), Edit(*), Write(*), Glob(*), Grep(*)
4
- ---
5
-
6
- # /auto-cycle — one autonomous work cycle
7
-
8
- You are running **unattended** (headless, triggered by cron every ~10 min via
9
- `.claude/hooks/autonomous-tick.sh`). Do **exactly one full cycle** with the steps below, then **stop**.
10
- The integration branch is named **`${ai_dev_branch}$`** (the placeholder is resolved at runtime — see
11
- `AI_PLACEHOLDER.md`); a task branch is **merged straight into `${ai_dev_branch}$`** (no PR).
12
-
13
- > Survival rule: **each cycle does exactly ONE small task** to avoid running out of tokens mid-way. If a
14
- > step fails, log it as a row in the **Incidents** table of `AI_DONE.md`, **clean the lock (Step 8)**, then stop — don't
15
- > push on.
16
- >
17
- > **Lock (`.claude/.autonomous.lock`):** whether the cycle ends normally or on error, you MUST delete
18
- > the lock file before stopping so the next cron tick isn't blocked by a stale lock (Step 8) —
19
- > mandatory, even on an early error exit.
20
- >
21
- > **Token gate:** the quota check (session/weekly) moved to the `autonomous-tick.sh` wrapper — it runs
22
- > first and only invokes Claude when quota is sufficient. So by the time `/auto-cycle` starts, the gate
23
- > has already passed; don't re-check at the top.
24
- >
25
- > **Books & templates (MUST compare):** the 5 book files at the repo root — `USER_TODO.md`,
26
- > `AI_TODO.md`, `AI_PROGRESS.md`, `AI_DONE.md`, `USER_QA.md` — have canonical templates in
27
- > `.claude/templates/` (`<NAME>.empty.md` = EMPTY state, `<NAME>.sample.md` = example WITH DATA; see
28
- > `.claude/templates/README.md`). Rules:
29
- > - **Is a file empty / has work?** → compare against `<NAME>.empty.md` (empty ⇔ equal to the empty
30
- > template after trimming trailing whitespace + leading/trailing blank lines). Don't guess by skimming.
31
- > - **When clearing / resetting** a file → overwrite with the **exact** `<NAME>.empty.md`
32
- > (`cp .claude/templates/<NAME>.empty.md <NAME>`). The tick's "has work" gate relies on this match.
33
- > - **When adding data** → keep the header/blockquote, fill in per `<NAME>.sample.md`.
34
- >
35
- > **Language:** write books/docs/code in the language the project's `CLAUDE.md` specifies (default
36
- > English). Don't switch languages on your own.
37
-
38
- ---
39
-
40
- ## Step 1 — Sync the `${ai_dev_branch}$` branch
41
- 1. If not on `${ai_dev_branch}$`:
42
- - `git fetch origin`
43
- - If it doesn't exist yet: `git checkout -b ${ai_dev_branch}$ origin/${ai_dev_branch}$` (if the remote
44
- has it) or `git checkout -b ${ai_dev_branch}$ main` (create from main).
45
- - Otherwise: `git checkout ${ai_dev_branch}$`.
46
- 2. `git pull --ff-only origin ${ai_dev_branch}$` (skip if the remote has no such branch yet).
47
-
48
- ## Step 2 — Intake user requests → generate tasks
49
- 1. Read `USER_TODO.md` and **compare with `.claude/templates/USER_TODO.empty.md`**. If **equal** (only
50
- the empty template remains) → **no** new request → skip task generation.
51
- 2. **If anything is unclear / needs a user decision** (ambiguous, missing info, contradictory, or the
52
- user said "ask if unclear, don't decide on your own"):
53
- - **Do NOT guess** and generate tasks. Append a **row** to `USER_QA.md` per
54
- `.claude/templates/USER_QA.sample.md` (columns `Date | Original request | Question / options | Answer`):
55
- fill the date, the original request, and what's unclear + (if possible) options to choose from; leave
56
- the **Answer** column blank for the user.
57
- - **Clear `USER_TODO.md`** back to the empty template (Step 2.4) — do NOT generate tasks this cycle.
58
- The user will read `USER_QA.md`, clarify, and re-post the request into `USER_TODO.md` for a later cycle.
59
- - Append a row to the **Incidents** table in `AI_DONE.md` that this cycle stopped waiting for an answer, then go to
60
- Step 8 (clean the lock) and **stop**.
61
- 3. If the request is clear enough: split it into small tasks doable in ~1 cycle. Write them into
62
- `AI_TODO.md` per `.claude/templates/AI_TODO.sample.md` (**7-column table:
63
- `| ID | Priority | Approved | Depends | Group | Task description | Notes |`**), each with an
64
- **ID `TSK-{groupid:0000}-{taskid:0000}`** (group = one request/batch, task = a sub-task).
65
- - **`Priority` column**: judge it — `High` / `Medium` / `Low` (default `Medium`).
66
- - **`Approved` column**: leave BLANK. This is a display-only mirror — the source of truth for approval
67
- is `.claude/.autonomous.approvals.json` (the user ticks it in the web VERIFY tab — ADR-0152). Do NOT
68
- fill it in yourself and do NOT read it to decide (see Step 3).
69
- - **`Depends` column**: if a task must wait for another, list the `TSK-…` ids here (comma-separated);
70
- leave empty otherwise. Step 3 skips a task whose dependencies aren't in `AI_DONE.md` yet.
71
-
72
- **ID rules (MANDATORY):**
73
- - **Each analysis of `USER_TODO.md` → one NEW `groupid`.** All sub-tasks split from that batch share
74
- this `groupid`, differing only by `taskid`.
75
- - **`groupid` must be UNIQUE and INCREASING** across all history (including groups already DONE and
76
- cleared from `AI_TODO.md`). Since the books are cleared each cycle, **check git history** for the
77
- largest `groupid` ever used, then take `max + 1`:
78
- ```bash
79
- MAXG=$( { git log -p -- AI_TODO.md AI_DONE.md AI_PROGRESS.md 2>/dev/null; \
80
- cat AI_TODO.md AI_DONE.md AI_PROGRESS.md 2>/dev/null; } \
81
- | grep -oE 'TSK-[0-9]{4}-[0-9]{4}' | sed -E 's/TSK-([0-9]{4}).*/\1/' \
82
- | sort -rn | head -1 ); MAXG=${MAXG:-0}
83
- NEWG=$(printf '%04d' $((10#$MAXG + 1)))
84
- ```
85
- - **`taskid` starts at `0001` and increases WITHIN the group**: `TSK-{NEWG}-0001`, `TSK-{NEWG}-0002`, …
86
- 4. **Clear `USER_TODO.md`** by overwriting with the exact empty template
87
- (`cp .claude/templates/USER_TODO.empty.md USER_TODO.md`) — so old tasks aren't recreated next cycle AND
88
- the tick's "has work" gate correctly sees it as empty.
89
-
90
- ## Step 3 — Pick ONE APPROVED task (with satisfied dependencies) and start it
91
- > **VERIFY gate (MANDATORY):** the source of truth for approval is `.claude/.autonomous.approvals.json`
92
- > (ADR-0152), NOT the `Approved` column in `AI_TODO.md`. A task is **approved** when approvals has
93
- > `"<TSK-id>": { "approved": true, … }`. A task not in approvals (or `approved:false`) = NOT permitted →
94
- > **skip it, leave it queued**.
95
-
96
- 1. Read `.claude/.autonomous.approvals.json` (JSON `{ "<TSK-id>": {approved, by, at}, … }`; missing file
97
- ⇒ nothing approved) and `AI_TODO.md`. **Filter tasks meeting BOTH**:
98
- - **Approved**: `approved === true` in approvals.
99
- - **Dependencies met**: every `TSK-…` in the `Depends` column is already in `AI_DONE.md` (done). If a
100
- dependency isn't done yet → **skip** (wait for a later cycle), even if approved.
101
- Pick the next task: **run group by group** (smallest group with an eligible task first), **within a
102
- group prefer `Priority` High → Medium → Low**, then line order.
103
- - **If NO eligible task** (empty, or all waiting for approval / dependencies — including tasks just
104
- generated in Step 2): **take no task**. Append a row to the **Incidents** table in `AI_DONE.md`
105
- (e.g. "this cycle only generated tasks / waiting for VERIFY approval / waiting for dependencies"),
106
- then go to Step 8 (clean the lock) and **stop**.
107
- 2. Move that task into `AI_PROGRESS.md` (with a start timestamp, per
108
- `.claude/templates/AI_PROGRESS.sample.md`), **remove it from `AI_TODO.md`**. If `AI_TODO.md` is now
109
- empty → `cp .claude/templates/AI_TODO.empty.md AI_TODO.md`.
110
- 3. Commit on `${ai_dev_branch}$`: `git add -A && git commit -m "chore(auto): start TSK-xxxx-xxxx"`.
111
-
112
- ## Step 4 — Implement the task on its own branch
113
- 1. Create the branch: `git checkout -b task/TSK-xxxx-xxxx`.
114
- 2. Implement the task **following the project's architecture + the conventions in `CLAUDE.md`**. Add or
115
- update tests as appropriate for the change.
116
- 3. **Test** by running the project's test command (see `CLAUDE.md` / the project's scripts — e.g.
117
- `scripts/test.*`, `npm test`, `pnpm test`, `pytest`, …). If the project defines an integration-test /
118
- evidence harness, use it and keep the produced report/evidence so it can be reviewed later.
119
- 4. **Wait for the tests to finish** and check the result before continuing.
120
- 5. Commit (include any produced report/evidence so the integration branch carries it):
121
- `git add -A && git commit -m "feat(TSK-xxxx-xxxx): <short description>"`.
122
-
123
- ## Step 5 — Merge into `${ai_dev_branch}$`, update the books
124
- 1. `git checkout ${ai_dev_branch}$`
125
- 2. `git merge --no-ff task/TSK-xxxx-xxxx`
126
- - **On CONFLICT** (parallel agents may have moved the integration branch — MEMO #40): `git status`
127
- shows `UU` files. **Resolve them yourself**: edit each conflicted file into a correct merged result
128
- (remove every `<<<<<<< ======= >>>>>>>` marker), `git add <file>`, then `git commit --no-edit` to
129
- finish the merge. If a conflict is too complex to be sure → `git merge --abort`, write a question
130
- into `USER_QA.md`, go to Step 8 and stop (don't guess).
131
- 3. Record the task by **appending a row to the Done table** in `AI_DONE.md` (Timestamp, ID, Task description, Files, Notes — per `.claude/templates/AI_DONE.sample.md`; keep the table header intact).
132
- **Remove it from `AI_PROGRESS.md`**: if nothing is in progress after removal, reset with
133
- `cp .claude/templates/AI_PROGRESS.empty.md AI_PROGRESS.md`. Likewise, if `AI_TODO.md` is now empty →
134
- `cp .claude/templates/AI_TODO.empty.md AI_TODO.md`.
135
- 4. Commit: `git add -A && git commit -m "chore(auto): finish TSK-xxxx-xxxx, merge into ${ai_dev_branch}$"`.
136
- 5. (Optional) `git push origin ${ai_dev_branch}$`.
137
-
138
- ## Step 6 — Recheck the token budget
139
- 1. Re-run the **check-usage** skill: `python .claude/skills/check-usage/check_usage.py`, then **wait 3s**
140
- (`sleep 3`) for the result file to be written.
141
- 2. Print a summary: task done, tokens remaining.
142
-
143
- ## Step 7 — Commit & push `${ai_dev_branch}$`
144
- 1. `git checkout ${ai_dev_branch}$`
145
- 2. `git add -A && git commit -m "chore(auto): update books after the autonomous cycle"` (skip if no change).
146
- 3. `git push origin ${ai_dev_branch}$`.
147
-
148
- ## Step 7.5 — Report on the `conversation` branch (share context with later agents — MEMO #40)
149
- > Multiple Claude instances take different tasks in parallel; a later agent needs to know what an earlier
150
- > one did. Use a dedicated branch named **`conversation`** holding **only** the file `CONVERSATION.md`
151
- > (no source or docs), keeping **at most the 50 most recent reports** (trim older ones when over).
152
-
153
- 1. Save the context (task ID + summary + list of files changed this cycle).
154
- 2. `git stash -u` if there are uncommitted changes (usually none — the books were committed in Step 7).
155
- 3. Switch to the conversation branch (create it orphan if missing):
156
- - `git fetch origin` → `git checkout conversation` (exists) or
157
- `git checkout --orphan conversation && git rm -rf . 2>/dev/null` (create fresh, clean).
158
- - `git pull --ff-only origin conversation` (skip if the remote has none).
159
- 4. Append an entry to the **end** of `CONVERSATION.md`:
160
- ```
161
- ## <yyyy-MM-dd HH:mm> · TSK-xxxx-xxxx
162
- - Did: <short summary>
163
- - Files: <paths, comma-separated>
164
- - Merged into: ${ai_dev_branch}$ (<conflict / no conflict>)
165
- ```
166
- If the number of `##` entries exceeds **50** → drop the oldest ones down to 50.
167
- 5. `git add CONVERSATION.md && git commit -m "chore(conversation): TSK-xxxx-xxxx" && git push origin conversation`.
168
- 6. Return to the integration branch: `git checkout ${ai_dev_branch}$` (and `git stash pop` if you stashed in 2).
169
-
170
- ## Step 8 — Clean the lock (ALWAYS run, even on error/early exit)
171
- > This is the **final action of every cycle** — run it whether the cycle succeeded, hit an error, or was
172
- > blocked at the token gate. Goal: never let a stale lock block the next cron tick.
173
- 1. Delete the lock file: `rm -f .claude/.autonomous.lock` (run in the project root).
174
- - The lock IS the file `.autonomous.lock`: `autonomous-tick.sh` creates it at start and treats "the
175
- file exists" = a run is in progress. Deleting it here releases the lock for the next tick.
176
- - This is `/auto-cycle`'s responsibility; the wrapper only has a safety-net trap in case the cycle dies.
177
- 2. **STOP** (the next cron tick will trigger the next cycle).
@@ -1,193 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Manage .claude/.autonomous.histories.json — the SINGLE file combining the autonomous workflow's state + history.
5
-
6
- Structure:
7
- {
8
- "cron_applied": "*/10 * * * *", # the cron schedule applied last time (to detect a schedule change)
9
- "consecutive_fails": 0, # number of CONSECUTIVE failed command runs (reset on success)
10
- "ticks": {"day": "YYYY-MM-DD", "count": N}, # count of ticks that actually invoked Claude today
11
- "records": [ {...}, ... ] # cron run history, NEWEST LAST, at most 50 records
12
- }
13
-
14
- Each record:
15
- {
16
- "ts": "YYYY-MM-DD HH:MM:SS",
17
- "status": "success" | "failure" | "skip",
18
- "rc": <int|null>, # the command's exit code (null if it didn't run)
19
- "gate": "<token-gate string>",
20
- "note": "<short note>",
21
- "command": "/auto-cycle",
22
- "tick": "YYYY-MM-DD #N", # day + tick number within the day
23
- "config": { cron_schedule, model, max_session_pct, max_weekly_pct, quiet_hours,
24
- max_ticks_per_day, stop_on_consecutive_failures, paused }
25
- }
26
-
27
- Usage (the file path is the first arg):
28
- history.py <file> migrate <cron-applied> <fails> <ticks> # one-time load from 3 old files (paths)
29
- history.py <file> get-cron
30
- history.py <file> set-cron <schedule>
31
- history.py <file> get-fails
32
- history.py <file> set-fails <n>
33
- history.py <file> get-ticks <today> # print the count for 'today' (0 if a different day/missing)
34
- history.py <file> set-ticks <day> <count>
35
- history.py <file> record <ts> <status> <rc> <gate> <note> # config is read from the CFG_* env vars
36
-
37
- Philosophy: NEVER break on a missing/corrupt file — always initialize the default skeleton.
38
- """
39
- import json
40
- import os
41
- import sys
42
-
43
- MAX_RECORDS = 50
44
-
45
- SKELETON = {
46
- "cron_applied": "",
47
- "consecutive_fails": 0,
48
- "ticks": {"day": "", "count": 0},
49
- "records": [],
50
- }
51
-
52
-
53
- def load(path):
54
- try:
55
- with open(path, encoding="utf-8") as f:
56
- d = json.load(f)
57
- if not isinstance(d, dict):
58
- raise ValueError("not an object")
59
- except Exception:
60
- d = {}
61
- out = dict(SKELETON)
62
- out.update({k: d[k] for k in SKELETON if k in d})
63
- if not isinstance(out.get("ticks"), dict):
64
- out["ticks"] = dict(SKELETON["ticks"])
65
- if not isinstance(out.get("records"), list):
66
- out["records"] = []
67
- return out
68
-
69
-
70
- def save(path, d):
71
- tmp = path + ".tmp"
72
- with open(tmp, "w", encoding="utf-8") as f:
73
- json.dump(d, f, ensure_ascii=False, indent=2)
74
- f.write("\n")
75
- os.replace(tmp, path)
76
-
77
-
78
- def cfg_from_env():
79
- def g(k, default=""):
80
- return os.environ.get(k, default)
81
- return {
82
- "cron_schedule": g("CFG_CRON_SCHEDULE"),
83
- "model": g("CFG_MODEL"),
84
- "max_session_pct": g("CFG_MAX_SESSION_PCT"),
85
- "max_weekly_pct": g("CFG_MAX_WEEKLY_PCT"),
86
- "quiet_hours": g("CFG_QUIET_HOURS"),
87
- "max_ticks_per_day": g("CFG_MAX_TICKS_PER_DAY"),
88
- "stop_on_consecutive_failures": g("CFG_STOP_ON_CONSEC_FAILURES"),
89
- "paused": g("CFG_PAUSED"),
90
- }
91
-
92
-
93
- def main():
94
- if len(sys.argv) < 3:
95
- sys.stderr.write("missing args: <file> <op> [...]\n")
96
- return 2
97
- path = sys.argv[1]
98
- op = sys.argv[2]
99
- args = sys.argv[3:]
100
- d = load(path)
101
-
102
- if op == "get-cron":
103
- sys.stdout.write(str(d.get("cron_applied", "")))
104
- return 0
105
-
106
- if op == "set-cron":
107
- d["cron_applied"] = args[0] if args else ""
108
- save(path, d)
109
- return 0
110
-
111
- if op == "get-fails":
112
- sys.stdout.write(str(int(d.get("consecutive_fails", 0) or 0)))
113
- return 0
114
-
115
- if op == "set-fails":
116
- d["consecutive_fails"] = int(args[0]) if args else 0
117
- save(path, d)
118
- return 0
119
-
120
- if op == "get-ticks":
121
- today = args[0] if args else ""
122
- t = d.get("ticks", {})
123
- sys.stdout.write(str(int(t.get("count", 0) or 0) if t.get("day") == today else 0))
124
- return 0
125
-
126
- if op == "set-ticks":
127
- day = args[0] if len(args) > 0 else ""
128
- count = int(args[1]) if len(args) > 1 else 0
129
- d["ticks"] = {"day": day, "count": count}
130
- save(path, d)
131
- return 0
132
-
133
- if op == "record":
134
- ts = args[0] if len(args) > 0 else ""
135
- status = args[1] if len(args) > 1 else ""
136
- rc_raw = args[2] if len(args) > 2 else ""
137
- gate = args[3] if len(args) > 3 else ""
138
- note = args[4] if len(args) > 4 else ""
139
- try:
140
- rc = int(rc_raw)
141
- except (ValueError, TypeError):
142
- rc = None
143
- t = d.get("ticks", {})
144
- rec = {
145
- "ts": ts,
146
- "status": status,
147
- "rc": rc,
148
- "gate": gate,
149
- "note": note,
150
- "command": os.environ.get("CFG_COMMAND", ""),
151
- "tick": "%s #%s" % (t.get("day", ""), t.get("count", 0)),
152
- "config": cfg_from_env(),
153
- }
154
- d["records"].append(rec)
155
- if len(d["records"]) > MAX_RECORDS:
156
- d["records"] = d["records"][-MAX_RECORDS:]
157
- save(path, d)
158
- return 0
159
-
160
- if op == "migrate":
161
- # Only load when histories.json has no real data yet (don't overwrite live data).
162
- cron_p = args[0] if len(args) > 0 else ""
163
- fails_p = args[1] if len(args) > 1 else ""
164
- ticks_p = args[2] if len(args) > 2 else ""
165
-
166
- def read_txt(p):
167
- try:
168
- with open(p, encoding="utf-8") as f:
169
- return f.read().strip()
170
- except Exception:
171
- return ""
172
-
173
- if cron_p and not d.get("cron_applied"):
174
- v = read_txt(cron_p)
175
- if v:
176
- d["cron_applied"] = v
177
- if fails_p and not d.get("consecutive_fails"):
178
- v = read_txt(fails_p)
179
- if v.isdigit():
180
- d["consecutive_fails"] = int(v)
181
- if ticks_p and not d.get("ticks", {}).get("day"):
182
- parts = read_txt(ticks_p).split()
183
- if len(parts) == 2 and parts[1].isdigit():
184
- d["ticks"] = {"day": parts[0], "count": int(parts[1])}
185
- save(path, d)
186
- return 0
187
-
188
- sys.stderr.write("invalid op: %s\n" % op)
189
- return 2
190
-
191
-
192
- if __name__ == "__main__":
193
- sys.exit(main())
@@ -1,40 +0,0 @@
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.