@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
package/dist/index.js
CHANGED
|
@@ -32916,6 +32916,7 @@ var AI_PROMPT_MAX_LEN = 5e5;
|
|
|
32916
32916
|
var COMMAND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
32917
32917
|
var COMMAND_IMAGE_MAX_COUNT = 10;
|
|
32918
32918
|
var COMMAND_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
32919
|
+
var COMMAND_IMAGE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(png|jpg|webp|gif)$/;
|
|
32919
32920
|
function commandImageExt(mime) {
|
|
32920
32921
|
switch (mime) {
|
|
32921
32922
|
case "image/png":
|
|
@@ -32931,7 +32932,9 @@ function commandImageExt(mime) {
|
|
|
32931
32932
|
}
|
|
32932
32933
|
}
|
|
32933
32934
|
var commandImageRefSchema = external_exports.object({
|
|
32934
|
-
id
|
|
32935
|
+
// Constrained to `<uuidv4>.<ext>` (the only id the upload mints) so a client-supplied id can never
|
|
32936
|
+
// inject `/` or `..` into the `command-images/{orgId}/{id}` storage key (path-traversal guard).
|
|
32937
|
+
id: external_exports.string().regex(COMMAND_IMAGE_ID_RE),
|
|
32935
32938
|
placeholder: external_exports.string().min(1).max(40),
|
|
32936
32939
|
name: external_exports.string().max(255),
|
|
32937
32940
|
mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES)
|
|
@@ -33904,7 +33907,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
33904
33907
|
}
|
|
33905
33908
|
|
|
33906
33909
|
// src/core/ws-client.ts
|
|
33907
|
-
import { existsSync as
|
|
33910
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, rmSync as rmSync5 } from "fs";
|
|
33908
33911
|
import { randomUUID as randomCommandId } from "crypto";
|
|
33909
33912
|
|
|
33910
33913
|
// src/core/update-scheduler.ts
|
|
@@ -37095,6 +37098,7 @@ async function setCommitAuthor(cwd2, machineUsername) {
|
|
|
37095
37098
|
|
|
37096
37099
|
// src/core/scaffold.ts
|
|
37097
37100
|
import { cp, mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
37101
|
+
import { existsSync as existsSync12 } from "fs";
|
|
37098
37102
|
import { execFile as execFile10 } from "child_process";
|
|
37099
37103
|
import { dirname as dirname8, join as join23, resolve as resolve7 } from "path";
|
|
37100
37104
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -37139,7 +37143,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
|
|
|
37139
37143
|
function sampleDir() {
|
|
37140
37144
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
37141
37145
|
const here = dirname8(fileURLToPath3(import.meta.url));
|
|
37142
|
-
|
|
37146
|
+
const bundled = resolve7(here, "project-sample");
|
|
37147
|
+
for (const candidate of [bundled, resolve7(here, "../project-sample"), resolve7(here, "../../project-sample")]) {
|
|
37148
|
+
if (existsSync12(candidate)) return candidate;
|
|
37149
|
+
}
|
|
37150
|
+
return bundled;
|
|
37143
37151
|
}
|
|
37144
37152
|
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
37145
37153
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
@@ -37218,7 +37226,7 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
37218
37226
|
}
|
|
37219
37227
|
|
|
37220
37228
|
// src/ui/slash-commands.ts
|
|
37221
|
-
import { existsSync as
|
|
37229
|
+
import { existsSync as existsSync13 } from "fs";
|
|
37222
37230
|
import { join as join24 } from "path";
|
|
37223
37231
|
var CONFIG_KEYS = [
|
|
37224
37232
|
"aiCli",
|
|
@@ -37281,7 +37289,7 @@ function runConfig(ctx) {
|
|
|
37281
37289
|
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
37282
37290
|
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
37283
37291
|
};
|
|
37284
|
-
if (
|
|
37292
|
+
if (existsSync13(path)) {
|
|
37285
37293
|
ctx.confirm(
|
|
37286
37294
|
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
37287
37295
|
() => writeDefaults("replaced with")
|
|
@@ -37679,7 +37687,7 @@ async function runMemoryCompaction(ai, cwd2, input) {
|
|
|
37679
37687
|
}
|
|
37680
37688
|
|
|
37681
37689
|
// src/core/config-sync.ts
|
|
37682
|
-
import { existsSync as
|
|
37690
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
37683
37691
|
import { join as join25 } from "path";
|
|
37684
37692
|
var SERVER_MANAGED_KEYS = [
|
|
37685
37693
|
"physicPath",
|
|
@@ -37698,7 +37706,7 @@ var SERVER_MANAGED_KEYS = [
|
|
|
37698
37706
|
];
|
|
37699
37707
|
function readConfigText(profileDir2) {
|
|
37700
37708
|
const path = join25(profileDir2, "config.json");
|
|
37701
|
-
if (
|
|
37709
|
+
if (existsSync14(path)) {
|
|
37702
37710
|
try {
|
|
37703
37711
|
return readFileSync15(path, "utf8");
|
|
37704
37712
|
} catch {
|
|
@@ -39464,7 +39472,7 @@ ${guardedPrompt}`;
|
|
|
39464
39472
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
39465
39473
|
ensurePhysicFolderPath(folder) {
|
|
39466
39474
|
try {
|
|
39467
|
-
if (!
|
|
39475
|
+
if (!existsSync15(folder)) {
|
|
39468
39476
|
mkdirSync6(folder, { recursive: true });
|
|
39469
39477
|
this.bus.log(`Created physic project folder: ${folder}`);
|
|
39470
39478
|
}
|
|
@@ -39494,7 +39502,7 @@ ${guardedPrompt}`;
|
|
|
39494
39502
|
projects: [],
|
|
39495
39503
|
version: CLI_VERSION,
|
|
39496
39504
|
physicPath,
|
|
39497
|
-
physicPathExists: physicPath ?
|
|
39505
|
+
physicPathExists: physicPath ? existsSync15(physicPath) : void 0,
|
|
39498
39506
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
39499
39507
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
|
|
39500
39508
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
@@ -45679,7 +45687,7 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
45679
45687
|
var import_react26 = __toESM(require_react(), 1);
|
|
45680
45688
|
|
|
45681
45689
|
// src/core/input-history.ts
|
|
45682
|
-
import { existsSync as
|
|
45690
|
+
import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
|
|
45683
45691
|
import { join as join29 } from "path";
|
|
45684
45692
|
var MAX_HISTORY = 200;
|
|
45685
45693
|
function historyPath(profileDir2) {
|
|
@@ -45687,7 +45695,7 @@ function historyPath(profileDir2) {
|
|
|
45687
45695
|
}
|
|
45688
45696
|
function loadInputHistory(profileDir2) {
|
|
45689
45697
|
const path = historyPath(profileDir2);
|
|
45690
|
-
if (!
|
|
45698
|
+
if (!existsSync17(path)) return [];
|
|
45691
45699
|
try {
|
|
45692
45700
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
45693
45701
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Set up a 10-minute cron for `autonomous-tick.sh`
|
|
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 `claude -p /auto-cycle`
|
|
5
|
+
cycle then exits. A file lock (`.claude/.autonomous.lock`) ensures **no two runs overlap** (a later tick
|
|
6
|
+
that sees the lock skips).
|
|
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>`).
|
|
10
|
+
|
|
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"):
|
|
19
|
+
```bash
|
|
20
|
+
command -v cron || echo "missing cron"
|
|
21
|
+
command -v claude || echo "missing claude (install natively in WSL)"
|
|
22
|
+
command -v git || echo "missing git"
|
|
23
|
+
command -v python3 || echo "missing python3"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Make the tick executable + install the cron line
|
|
27
|
+
```bash
|
|
28
|
+
chmod +x "$PROJECT/.claude/hooks/autonomous-tick.sh"
|
|
29
|
+
crontab -e
|
|
30
|
+
# add (fix the path):
|
|
31
|
+
*/10 * * * * /home/<user>/projects/<your-project>/.claude/hooks/autonomous-tick.sh
|
|
32
|
+
```
|
|
33
|
+
The schedule is also driven by `cron_schedule` in `.claude/.autonomous.settings.json`; once the crontab
|
|
34
|
+
line exists, the tick keeps it in sync on later runs.
|
|
35
|
+
|
|
36
|
+
## 3. Enable / watch
|
|
37
|
+
- Set `"paused": false` in `.claude/.autonomous.settings.json` to enable (or use the web Autonomous tab).
|
|
38
|
+
- Watch: `tail -f .claude/logs/autonomous-tick-$(date +%F).log`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Autonomous mode — how it's assembled
|
|
2
|
+
|
|
3
|
+
> **Sample project — defines the workflow only, does NOT run on its own.** The files below describe an
|
|
4
|
+
> unattended work loop, intended to run on **WSL** (an isolated environment where the AI can be given
|
|
5
|
+
> full permissions).
|
|
6
|
+
|
|
7
|
+
## The pieces
|
|
8
|
+
| File | Role |
|
|
9
|
+
|------|------|
|
|
10
|
+
| `.claude/hooks/autonomous-tick.sh` | Cron tick (every ~10 min); a lock so **busy ⇒ skip, idle ⇒ run**; calls `claude -p /auto-cycle`. |
|
|
11
|
+
| `.claude/commands/auto-cycle.md` | Defines **one cycle**: token gate → sync `${ai_dev_branch}$` → generate tasks → do one task → test → merge into `${ai_dev_branch}$`. |
|
|
12
|
+
| `.claude/skills/check-usage/check_usage.py` | The `check-usage` skill: prints token % and writes `output/last-usage-check.json` (deleted + recreated each run) for the token gate to read. |
|
|
13
|
+
| `.claude/.autonomous.approvals.json` | Approval source of truth (ADR-0152): `{ "<TSK-id>": {approved, by, at} }` — the web VERIFY tab writes it; `/auto-cycle` reads it. |
|
|
14
|
+
| `USER_TODO.md` | The user writes requests here; the cycle reads then clears it. |
|
|
15
|
+
| `AI_TODO.md` / `AI_PROGRESS.md` / `AI_DONE.md` | The task books: queue → in progress → done (ID `TSK-{group:0000}-{task:0000}`). |
|
|
16
|
+
| `.claude/templates/<NAME>.{empty,sample}.md` | Canonical templates for the 5 books. The "has work" gate + `/auto-cycle` **compare against `*.empty.md`** to tell empty/has-work and reset correctly (see `README.md` in that folder). |
|
|
17
|
+
| `.claude/settings.json` | The "bypass all" permission profile for autonomous mode (see the note below). |
|
|
18
|
+
|
|
19
|
+
## Lifecycle (1 tick)
|
|
20
|
+
```
|
|
21
|
+
cron ~10min → autonomous-tick.sh
|
|
22
|
+
├─ locked? → log "skip", exit (wait for the next tick)
|
|
23
|
+
└─ idle → claude -p /auto-cycle
|
|
24
|
+
0. usage: session<80% & weekly<90%? (no → stop)
|
|
25
|
+
1. checkout/fetch/pull the `${ai_dev_branch}$` branch
|
|
26
|
+
2. USER_TODO.md → generate tasks into AI_TODO.md (with IDs) → clear USER_TODO.md
|
|
27
|
+
3. pick one APPROVED task (approvals + dependencies) → AI_PROGRESS.md, remove from AI_TODO.md, commit
|
|
28
|
+
4. task/TSK-… branch → implement → commit → run the project's tests
|
|
29
|
+
5. back to `${ai_dev_branch}$` → merge task (resolve conflicts if any) → AI_DONE.md, remove from AI_PROGRESS.md, commit
|
|
30
|
+
6. report on the `conversation` branch (CONVERSATION.md, ≤50 entries) for later agents
|
|
31
|
+
7. recheck usage → stop
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install on WSL
|
|
35
|
+
```bash
|
|
36
|
+
chmod +x .claude/hooks/autonomous-tick.sh
|
|
37
|
+
crontab -e
|
|
38
|
+
# add the line (fix /path):
|
|
39
|
+
*/10 * * * * /path/to/project/.claude/hooks/autonomous-tick.sh
|
|
40
|
+
# watch:
|
|
41
|
+
tail -f .claude/logs/autonomous-tick-$(date +%F).log
|
|
42
|
+
```
|
|
43
|
+
Requirements: `claude` logged in (has `~/.claude/.credentials.json`), plus `git` and `python3`, and the
|
|
44
|
+
project's test tooling (per `CLAUDE.md`).
|
|
45
|
+
|
|
46
|
+
## Note on permissions (important)
|
|
47
|
+
- Claude Code only auto-loads `.claude/settings.json` and `.claude/settings.local.json`. To make a
|
|
48
|
+
full-permission profile take effect, one of:
|
|
49
|
+
1. The tick already passes `--permission-mode bypassPermissions --dangerously-skip-permissions` (in use).
|
|
50
|
+
2. Or copy the profile into `.claude/settings.local.json`.
|
|
51
|
+
3. Or point `CLAUDE_CONFIG_DIR` at the profile dir when running headless.
|
|
52
|
+
- Only enable full permissions in an isolated environment (WSL/CI). Never on a machine with sensitive data.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
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 in `AI_DONE.md` under "Incidents", **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. Write the question into `USER_QA.md` ("Q&A" section) per
|
|
54
|
+
`.claude/templates/USER_QA.sample.md`: state the original request, what's unclear, and (if possible)
|
|
55
|
+
options to choose from. Each question has a date + an empty answer slot.
|
|
56
|
+
- **Clear `USER_TODO.md`** back to the empty template (Step 2.4) — do NOT generate tasks this cycle.
|
|
57
|
+
The user will read `USER_QA.md`, clarify, and re-post the request into `USER_TODO.md` for a later cycle.
|
|
58
|
+
- Note in `AI_DONE.md` ("Incidents/notes") that this cycle stopped waiting for an answer, then go to
|
|
59
|
+
Step 8 (clean the lock) and **stop**.
|
|
60
|
+
3. If the request is clear enough: split it into small tasks doable in ~1 cycle. Write them into
|
|
61
|
+
`AI_TODO.md` per `.claude/templates/AI_TODO.sample.md` (**7-column table:
|
|
62
|
+
`| ID | Priority | Approved | Depends | Group | Task description | Notes |`**), each with an
|
|
63
|
+
**ID `TSK-{groupid:0000}-{taskid:0000}`** (group = one request/batch, task = a sub-task).
|
|
64
|
+
- **`Priority` column**: judge it — `High` / `Medium` / `Low` (default `Medium`).
|
|
65
|
+
- **`Approved` column**: leave BLANK. This is a display-only mirror — the source of truth for approval
|
|
66
|
+
is `.claude/.autonomous.approvals.json` (the user ticks it in the web VERIFY tab — ADR-0152). Do NOT
|
|
67
|
+
fill it in yourself and do NOT read it to decide (see Step 3).
|
|
68
|
+
- **`Depends` column**: if a task must wait for another, list the `TSK-…` ids here (comma-separated);
|
|
69
|
+
leave empty otherwise. Step 3 skips a task whose dependencies aren't in `AI_DONE.md` yet.
|
|
70
|
+
|
|
71
|
+
**ID rules (MANDATORY):**
|
|
72
|
+
- **Each analysis of `USER_TODO.md` → one NEW `groupid`.** All sub-tasks split from that batch share
|
|
73
|
+
this `groupid`, differing only by `taskid`.
|
|
74
|
+
- **`groupid` must be UNIQUE and INCREASING** across all history (including groups already DONE and
|
|
75
|
+
cleared from `AI_TODO.md`). Since the books are cleared each cycle, **check git history** for the
|
|
76
|
+
largest `groupid` ever used, then take `max + 1`:
|
|
77
|
+
```bash
|
|
78
|
+
MAXG=$( { git log -p -- AI_TODO.md AI_DONE.md AI_PROGRESS.md 2>/dev/null; \
|
|
79
|
+
cat AI_TODO.md AI_DONE.md AI_PROGRESS.md 2>/dev/null; } \
|
|
80
|
+
| grep -oE 'TSK-[0-9]{4}-[0-9]{4}' | sed -E 's/TSK-([0-9]{4}).*/\1/' \
|
|
81
|
+
| sort -rn | head -1 ); MAXG=${MAXG:-0}
|
|
82
|
+
NEWG=$(printf '%04d' $((10#$MAXG + 1)))
|
|
83
|
+
```
|
|
84
|
+
- **`taskid` starts at `0001` and increases WITHIN the group**: `TSK-{NEWG}-0001`, `TSK-{NEWG}-0002`, …
|
|
85
|
+
4. **Clear `USER_TODO.md`** by overwriting with the exact empty template
|
|
86
|
+
(`cp .claude/templates/USER_TODO.empty.md USER_TODO.md`) — so old tasks aren't recreated next cycle AND
|
|
87
|
+
the tick's "has work" gate correctly sees it as empty.
|
|
88
|
+
|
|
89
|
+
## Step 3 — Pick ONE APPROVED task (with satisfied dependencies) and start it
|
|
90
|
+
> **VERIFY gate (MANDATORY):** the source of truth for approval is `.claude/.autonomous.approvals.json`
|
|
91
|
+
> (ADR-0152), NOT the `Approved` column in `AI_TODO.md`. A task is **approved** when approvals has
|
|
92
|
+
> `"<TSK-id>": { "approved": true, … }`. A task not in approvals (or `approved:false`) = NOT permitted →
|
|
93
|
+
> **skip it, leave it queued**.
|
|
94
|
+
|
|
95
|
+
1. Read `.claude/.autonomous.approvals.json` (JSON `{ "<TSK-id>": {approved, by, at}, … }`; missing file
|
|
96
|
+
⇒ nothing approved) and `AI_TODO.md`. **Filter tasks meeting BOTH**:
|
|
97
|
+
- **Approved**: `approved === true` in approvals.
|
|
98
|
+
- **Dependencies met**: every `TSK-…` in the `Depends` column is already in `AI_DONE.md` (done). If a
|
|
99
|
+
dependency isn't done yet → **skip** (wait for a later cycle), even if approved.
|
|
100
|
+
Pick the next task: **run group by group** (smallest group with an eligible task first), **within a
|
|
101
|
+
group prefer `Priority` High → Medium → Low**, then line order.
|
|
102
|
+
- **If NO eligible task** (empty, or all waiting for approval / dependencies — including tasks just
|
|
103
|
+
generated in Step 2): **take no task**. Write one line into `AI_DONE.md` ("Incidents/notes")
|
|
104
|
+
(e.g. "this cycle only generated tasks / waiting for VERIFY approval / waiting for dependencies"),
|
|
105
|
+
then go to Step 8 (clean the lock) and **stop**.
|
|
106
|
+
2. Move that task into `AI_PROGRESS.md` (with a start timestamp, per
|
|
107
|
+
`.claude/templates/AI_PROGRESS.sample.md`), **remove it from `AI_TODO.md`**. If `AI_TODO.md` is now
|
|
108
|
+
empty → `cp .claude/templates/AI_TODO.empty.md AI_TODO.md`.
|
|
109
|
+
3. Commit on `${ai_dev_branch}$`: `git add -A && git commit -m "chore(auto): start TSK-xxxx-xxxx"`.
|
|
110
|
+
|
|
111
|
+
## Step 4 — Implement the task on its own branch
|
|
112
|
+
1. Create the branch: `git checkout -b task/TSK-xxxx-xxxx`.
|
|
113
|
+
2. Implement the task **following the project's architecture + the conventions in `CLAUDE.md`**. Add or
|
|
114
|
+
update tests as appropriate for the change.
|
|
115
|
+
3. **Test** by running the project's test command (see `CLAUDE.md` / the project's scripts — e.g.
|
|
116
|
+
`scripts/test.*`, `npm test`, `pnpm test`, `pytest`, …). If the project defines an integration-test /
|
|
117
|
+
evidence harness, use it and keep the produced report/evidence so it can be reviewed later.
|
|
118
|
+
4. **Wait for the tests to finish** and check the result before continuing.
|
|
119
|
+
5. Commit (include any produced report/evidence so the integration branch carries it):
|
|
120
|
+
`git add -A && git commit -m "feat(TSK-xxxx-xxxx): <short description>"`.
|
|
121
|
+
|
|
122
|
+
## Step 5 — Merge into `${ai_dev_branch}$`, update the books
|
|
123
|
+
1. `git checkout ${ai_dev_branch}$`
|
|
124
|
+
2. `git merge --no-ff task/TSK-xxxx-xxxx`
|
|
125
|
+
- **On CONFLICT** (parallel agents may have moved the integration branch — MEMO #40): `git status`
|
|
126
|
+
shows `UU` files. **Resolve them yourself**: edit each conflicted file into a correct merged result
|
|
127
|
+
(remove every `<<<<<<< ======= >>>>>>>` marker), `git add <file>`, then `git commit --no-edit` to
|
|
128
|
+
finish the merge. If a conflict is too complex to be sure → `git merge --abort`, write a question
|
|
129
|
+
into `USER_QA.md`, go to Step 8 and stop (don't guess).
|
|
130
|
+
3. Record the task in `AI_DONE.md` (ID, description, timestamp — per `.claude/templates/AI_DONE.sample.md`).
|
|
131
|
+
**Remove it from `AI_PROGRESS.md`**: if nothing is in progress after removal, reset with
|
|
132
|
+
`cp .claude/templates/AI_PROGRESS.empty.md AI_PROGRESS.md`. Likewise, if `AI_TODO.md` is now empty →
|
|
133
|
+
`cp .claude/templates/AI_TODO.empty.md AI_TODO.md`.
|
|
134
|
+
4. Commit: `git add -A && git commit -m "chore(auto): finish TSK-xxxx-xxxx, merge into ${ai_dev_branch}$"`.
|
|
135
|
+
5. (Optional) `git push origin ${ai_dev_branch}$`.
|
|
136
|
+
|
|
137
|
+
## Step 6 — Recheck the token budget
|
|
138
|
+
1. Re-run the **check-usage** skill: `python .claude/skills/check-usage/check_usage.py`, then **wait 3s**
|
|
139
|
+
(`sleep 3`) for the result file to be written.
|
|
140
|
+
2. Print a summary: task done, tokens remaining.
|
|
141
|
+
|
|
142
|
+
## Step 7 — Commit & push `${ai_dev_branch}$`
|
|
143
|
+
1. `git checkout ${ai_dev_branch}$`
|
|
144
|
+
2. `git add -A && git commit -m "chore(auto): update books after the autonomous cycle"` (skip if no change).
|
|
145
|
+
3. `git push origin ${ai_dev_branch}$`.
|
|
146
|
+
|
|
147
|
+
## Step 7.5 — Report on the `conversation` branch (share context with later agents — MEMO #40)
|
|
148
|
+
> Multiple Claude instances take different tasks in parallel; a later agent needs to know what an earlier
|
|
149
|
+
> one did. Use a dedicated branch named **`conversation`** holding **only** the file `CONVERSATION.md`
|
|
150
|
+
> (no source or docs), keeping **at most the 50 most recent reports** (trim older ones when over).
|
|
151
|
+
|
|
152
|
+
1. Save the context (task ID + summary + list of files changed this cycle).
|
|
153
|
+
2. `git stash -u` if there are uncommitted changes (usually none — the books were committed in Step 7).
|
|
154
|
+
3. Switch to the conversation branch (create it orphan if missing):
|
|
155
|
+
- `git fetch origin` → `git checkout conversation` (exists) or
|
|
156
|
+
`git checkout --orphan conversation && git rm -rf . 2>/dev/null` (create fresh, clean).
|
|
157
|
+
- `git pull --ff-only origin conversation` (skip if the remote has none).
|
|
158
|
+
4. Append an entry to the **end** of `CONVERSATION.md`:
|
|
159
|
+
```
|
|
160
|
+
## <yyyy-MM-dd HH:mm> · TSK-xxxx-xxxx
|
|
161
|
+
- Did: <short summary>
|
|
162
|
+
- Files: <paths, comma-separated>
|
|
163
|
+
- Merged into: ${ai_dev_branch}$ (<conflict / no conflict>)
|
|
164
|
+
```
|
|
165
|
+
If the number of `##` entries exceeds **50** → drop the oldest ones down to 50.
|
|
166
|
+
5. `git add CONVERSATION.md && git commit -m "chore(conversation): TSK-xxxx-xxxx" && git push origin conversation`.
|
|
167
|
+
6. Return to the integration branch: `git checkout ${ai_dev_branch}$` (and `git stash pop` if you stashed in 2).
|
|
168
|
+
|
|
169
|
+
## Step 8 — Clean the lock (ALWAYS run, even on error/early exit)
|
|
170
|
+
> This is the **final action of every cycle** — run it whether the cycle succeeded, hit an error, or was
|
|
171
|
+
> blocked at the token gate. Goal: never let a stale lock block the next cron tick.
|
|
172
|
+
1. Delete the lock file: `rm -f .claude/.autonomous.lock` (run in the project root).
|
|
173
|
+
- The lock IS the file `.autonomous.lock`: `autonomous-tick.sh` creates it at start and treats "the
|
|
174
|
+
file exists" = a run is in progress. Deleting it here releases the lock for the next tick.
|
|
175
|
+
- This is `/auto-cycle`'s responsibility; the wrapper only has a safety-net trap in case the cycle dies.
|
|
176
|
+
2. **STOP** (the next cron tick will trigger the next cycle).
|
|
Binary file
|
|
@@ -0,0 +1,193 @@
|
|
|
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())
|