@4pm/cli 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +289 -16
- package/dist/project-sample/.claude/.autonomous.settings.json +9 -11
- package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +5 -4
- package/dist/project-sample/.claude/AUTONOMOUS.md +26 -26
- package/dist/project-sample/.claude/commands/auto-cycle.md +16 -170
- package/dist/project-sample/.claude/hooks/autonomous-tick.sh +76 -193
- package/dist/project-sample/.claude/settings.json +1 -3
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_QA.sample.md +6 -3
- package/dist/project-sample/.claude/templates/USER_TODO.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_TODO.sample.md +7 -6
- package/dist/project-sample/.claude/templates/project.secrets.sample.json +0 -1
- package/dist/project-sample/AI_PLACEHOLDER.md +5 -5
- package/dist/project-sample/USER_QA.md +13 -6
- package/dist/project-sample/USER_TODO.md +13 -6
- package/dist/project-sample/project.secrets.json.sample +0 -1
- package/package.json +1 -1
|
@@ -1,177 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
3
|
-
allowed-tools: Bash(*), Read(*), Edit(*), Write(*), Glob(*), Grep(*)
|
|
2
|
+
description: (Retired — ADR-0319) The autonomous cycle now runs through the 4PM cli, not `claude -p /auto-cycle`.
|
|
4
3
|
---
|
|
5
4
|
|
|
6
|
-
# /auto-cycle —
|
|
5
|
+
# /auto-cycle — retired (ADR-0319)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
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).
|
|
7
|
+
The autonomous work cycle is **no longer** a `claude -p /auto-cycle` slash command. Under **ADR-0319**
|
|
8
|
+
it runs through the 4PM cli instead:
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
10
|
+
- The cron tick (`.claude/hooks/autonomous-tick.sh`) runs **`4pm auto-run`**, which asks the already
|
|
11
|
+
running **`4pm start`** daemon to run **one** cycle through its live WS session — so the cycle reuses
|
|
12
|
+
the cli's profile/quota failover (ADR-0182), token metering (ADR-0072), folder-scope guard (ADR-0181)
|
|
13
|
+
and AI-run timeout (ADR-0243).
|
|
14
|
+
- The cycle **instructions** now live in the cli (`21-apps/31-cli/src/core/autonomous-cycle.ts` →
|
|
15
|
+
`buildAutonomousCyclePrompt`), not in this file. In short: sync the primary repo's current branch →
|
|
16
|
+
analyse **approved** `USER_TODO` requests into `AI_TODO` tasks (unclear ⇒ ask via `USER_QA`) → fold
|
|
17
|
+
**approved** `USER_QA` answers back → take ONE approved task → implement + test on a `task/TSK-…`
|
|
18
|
+
branch → open a **pull request** into the base branch (no direct merge) → update the books.
|
|
37
19
|
|
|
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).
|
|
20
|
+
Approval is the source of truth in `.claude/.autonomous.approvals.json` (ADR-0152), keyed per row id
|
|
21
|
+
(`REQ-…` / `QA-…` / `TSK-…`), set from the web AI-content grids.
|
|
169
22
|
|
|
170
|
-
|
|
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).
|
|
23
|
+
This file is kept only as a pointer; it is not executed.
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Cron tick for the autonomous workflow — install on WSL.
|
|
3
|
-
# .claude/.autonomous.settings.json (paused,
|
|
4
|
-
#
|
|
2
|
+
# Cron tick for the autonomous workflow (ADR-0152 + ADR-0319) — install on WSL. Control flags live in
|
|
3
|
+
# .claude/.autonomous.settings.json (paused, cron_schedule, quiet_hours, max_ticks_per_day, …); read
|
|
4
|
+
# EVERY tick, so changes take effect next tick (cron_schedule auto-syncs into the crontab).
|
|
5
5
|
#
|
|
6
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
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
8
|
+
# What changed with ADR-0319: the tick no longer runs `claude -p /auto-cycle` directly. It runs
|
|
9
|
+
# `4pm auto-run`, which asks the ALREADY-RUNNING `4pm start` daemon to run one cycle through its live
|
|
10
|
+
# WS session — so the cycle reuses the cli's profile/quota failover (ADR-0182), token metering
|
|
11
|
+
# (ADR-0072), folder-scope (ADR-0181) and AI-run timeout (ADR-0243). Account selection + the token/quota
|
|
12
|
+
# gate now live in the cli (they need the live usage snapshot), NOT in this shell.
|
|
13
|
+
#
|
|
14
|
+
# This tick keeps only the CHEAP local gates (no token spend): a run lock, pause, quiet-hours,
|
|
15
|
+
# max-ticks/day, and a "has-work" check — so the daemon is only woken when there is approved work.
|
|
16
|
+
#
|
|
17
|
+
# "Busy ⇒ wait for the next tick" via the EXISTENCE of `.autonomous.lock` (not flock):
|
|
18
|
+
# - lock file exists → another tick is running → log "skip" and exit.
|
|
19
|
+
# - it doesn't → create the lock → run one cycle via the daemon.
|
|
20
|
+
# The EXIT trap removes the lock whether the cycle succeeds or dies.
|
|
14
21
|
set -euo pipefail
|
|
15
22
|
|
|
16
23
|
# Project root = two levels above this file (.claude/hooks → .claude → root)
|
|
17
24
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
18
25
|
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
26
|
HIST="$PROJECT_DIR/.claude/.autonomous.histories.json"
|
|
21
27
|
HIST_PY="$PROJECT_DIR/.claude/hooks/autonomous-history.py"
|
|
22
28
|
LOG_DIR="$PROJECT_DIR/.claude/logs"
|
|
@@ -30,80 +36,31 @@ if ! ( set -o noclobber; : > "$LOCK" ) 2>/dev/null; then
|
|
|
30
36
|
echo "$(ts) [skip] busy (.autonomous.lock exists) — waiting for the next tick" >> "$LOG"
|
|
31
37
|
exit 0
|
|
32
38
|
fi
|
|
33
|
-
#
|
|
39
|
+
# The lock is released when this wrapper exits, whether the cycle succeeded, errored, or was skipped.
|
|
34
40
|
trap 'rm -f "$LOCK"' EXIT
|
|
35
41
|
|
|
36
42
|
cd "$PROJECT_DIR"
|
|
37
43
|
|
|
38
|
-
#
|
|
39
|
-
# `
|
|
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
|
|
44
|
+
# cron runs with a minimal PATH and does NOT load ~/.bashrc. Add common WSL install dirs so cron can
|
|
45
|
+
# find the `4pm` binary (native install / npm global) + python3.
|
|
43
46
|
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:/usr/local/bin:$PATH"
|
|
44
47
|
|
|
45
|
-
# --- Python (needed for: reading settings + the
|
|
48
|
+
# --- Python (needed for: reading settings + the has-work gate + history) -----------------------
|
|
46
49
|
PY_BIN="$(command -v python3 || command -v python || true)"
|
|
47
50
|
if [ -z "$PY_BIN" ]; then
|
|
48
51
|
echo "$(ts) [error] python/python3 not found — skipping this tick" >> "$LOG"
|
|
49
52
|
exit 0
|
|
50
53
|
fi
|
|
51
54
|
|
|
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
55
|
# '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
56
|
hist() { "$PY_BIN" "$HIST_PY" "$HIST" "$@"; }
|
|
98
57
|
|
|
99
58
|
# --- Read the autonomous config (.autonomous.settings.json) ------------------------------------
|
|
100
|
-
# User-editable; read EVERY tick so changes take effect next tick.
|
|
101
|
-
# lines to eval; a missing/corrupt file -> use defaults (no break).
|
|
59
|
+
# User-editable; read EVERY tick so changes take effect next tick. A missing/corrupt file -> defaults.
|
|
102
60
|
SETTINGS="$PROJECT_DIR/.claude/.autonomous.settings.json"
|
|
103
61
|
eval "$("$PY_BIN" - "$SETTINGS" <<'PY'
|
|
104
62
|
import json, sys, shlex
|
|
105
|
-
defaults = {"paused": False, "
|
|
106
|
-
"cron_schedule": "*/10 * * * *", "command": "/auto-cycle", "model": "",
|
|
63
|
+
defaults = {"paused": False, "cron_schedule": "*/10 * * * *", "command": "auto-run", "profile": "",
|
|
107
64
|
"quiet_hours": "", "max_ticks_per_day": -1, "stop_on_consecutive_failures": 3,
|
|
108
65
|
"log_retention_days": 14, "notify_webhook": ""}
|
|
109
66
|
try:
|
|
@@ -113,11 +70,8 @@ except Exception:
|
|
|
113
70
|
d = {}
|
|
114
71
|
def g(k): return d.get(k, defaults[k])
|
|
115
72
|
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
73
|
print("CFG_CRON_SCHEDULE=" + shlex.quote(str(g("cron_schedule"))))
|
|
119
|
-
print("
|
|
120
|
-
print("CFG_MODEL=" + shlex.quote(str(g("model"))))
|
|
74
|
+
print("CFG_PROFILE=" + shlex.quote(str(g("profile"))))
|
|
121
75
|
print("CFG_QUIET_HOURS=" + shlex.quote(str(g("quiet_hours"))))
|
|
122
76
|
print("CFG_MAX_TICKS_PER_DAY=" + shlex.quote(str(g("max_ticks_per_day"))))
|
|
123
77
|
print("CFG_STOP_ON_CONSEC_FAILURES=" + shlex.quote(str(g("stop_on_consecutive_failures"))))
|
|
@@ -125,14 +79,10 @@ print("CFG_LOG_RETENTION_DAYS=" + shlex.quote(str(g("log_retention_days"))))
|
|
|
125
79
|
print("CFG_NOTIFY_WEBHOOK=" + shlex.quote(str(g("notify_webhook"))))
|
|
126
80
|
PY
|
|
127
81
|
)"
|
|
128
|
-
# Export CFG_* so the history helper (
|
|
129
|
-
export CFG_CRON_SCHEDULE
|
|
130
|
-
CFG_QUIET_HOURS CFG_MAX_TICKS_PER_DAY CFG_STOP_ON_CONSEC_FAILURES CFG_PAUSED
|
|
82
|
+
# Export CFG_* so the history helper ('record') can read the run config from env.
|
|
83
|
+
export CFG_CRON_SCHEDULE CFG_QUIET_HOURS CFG_MAX_TICKS_PER_DAY CFG_STOP_ON_CONSEC_FAILURES CFG_PAUSED
|
|
131
84
|
|
|
132
85
|
# --- 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
86
|
SELF="$PROJECT_DIR/.claude/hooks/autonomous-tick.sh"
|
|
137
87
|
PREV_SCHED="$(hist get-cron 2>/dev/null || true)"
|
|
138
88
|
if [ "$CFG_CRON_SCHEDULE" != "$PREV_SCHED" ]; then
|
|
@@ -160,17 +110,16 @@ if [ "$CFG_PAUSED" = "1" ]; then
|
|
|
160
110
|
exit 0
|
|
161
111
|
fi
|
|
162
112
|
|
|
163
|
-
# --- Prune old logs by log_retention_days
|
|
113
|
+
# --- Prune old logs by log_retention_days -----------------------------------------------------
|
|
164
114
|
if [ "${CFG_LOG_RETENTION_DAYS:-0}" -gt 0 ] 2>/dev/null; then
|
|
165
115
|
find "$LOG_DIR" -maxdepth 1 -type f -name 'autonomous-tick-*.log' -mtime +"$CFG_LOG_RETENTION_DAYS" -delete 2>/dev/null || true
|
|
166
116
|
fi
|
|
167
117
|
|
|
168
118
|
# --- Quiet hours ------------------------------------------------------------------------------
|
|
169
|
-
# Empty = run all day. 'HH:MM-HH:MM' = skip within the window; supports a window crossing midnight.
|
|
170
119
|
if [ -n "$CFG_QUIET_HOURS" ]; then
|
|
171
120
|
if printf '%s' "$CFG_QUIET_HOURS" | grep -Eq '^[0-9]{1,2}:[0-9]{2}-[0-9]{1,2}:[0-9]{2}$'; then
|
|
172
121
|
q_start="${CFG_QUIET_HOURS%%-*}"; q_end="${CFG_QUIET_HOURS##*-}"
|
|
173
|
-
_min() { echo $(( 10#${1%%:*} * 60 + 10#${1##*:} )); }
|
|
122
|
+
_min() { echo $(( 10#${1%%:*} * 60 + 10#${1##*:} )); }
|
|
174
123
|
qs=$(_min "$q_start"); qe=$(_min "$q_end"); qn=$(_min "$(date +%H:%M)")
|
|
175
124
|
in_q=0
|
|
176
125
|
if [ "$qs" -le "$qe" ]; then
|
|
@@ -187,8 +136,7 @@ if [ -n "$CFG_QUIET_HOURS" ]; then
|
|
|
187
136
|
fi
|
|
188
137
|
fi
|
|
189
138
|
|
|
190
|
-
# --- Max ticks/day
|
|
191
|
-
# -1 = unlimited. Counted per local day, stored under ticks{day,count} in .autonomous.histories.json.
|
|
139
|
+
# --- Max ticks/day ----------------------------------------------------------------------------
|
|
192
140
|
TODAY="$(date +%F)"
|
|
193
141
|
tick_count="$(hist get-ticks "$TODAY" 2>/dev/null || echo 0)"; [ -z "$tick_count" ] && tick_count=0
|
|
194
142
|
if [ "${CFG_MAX_TICKS_PER_DAY:--1}" -gt 0 ] 2>/dev/null && [ "${tick_count:-0}" -ge "$CFG_MAX_TICKS_PER_DAY" ]; then
|
|
@@ -196,141 +144,76 @@ if [ "${CFG_MAX_TICKS_PER_DAY:--1}" -gt 0 ] 2>/dev/null && [ "${tick_count:-0}"
|
|
|
196
144
|
exit 0
|
|
197
145
|
fi
|
|
198
146
|
|
|
199
|
-
# --- "Has work" gate
|
|
200
|
-
# Skip
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
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).
|
|
147
|
+
# --- "Has work" gate --------------------------------------------------------------------------
|
|
148
|
+
# Skip WITHOUT waking the daemon unless there is one of:
|
|
149
|
+
# 1) an APPROVED USER_TODO request (REQ-… approved in .autonomous.approvals.json) — Step 2 analyses it;
|
|
150
|
+
# 2) an APPROVED USER_QA answer (QA-… approved) — Step 3 folds it in;
|
|
151
|
+
# 3) an APPROVED AI_TODO task (TSK-… approved) — Step 4 can take it;
|
|
152
|
+
# 4) AI_PROGRESS.md non-empty (leftover work from a previous tick).
|
|
153
|
+
# Approval is the source of truth in .autonomous.approvals.json (ADR-0152/0319), keyed per row id.
|
|
210
154
|
TPL="$PROJECT_DIR/.claude/templates"
|
|
211
|
-
WORK="$(
|
|
212
|
-
AI_PROGRESS="$PROJECT_DIR/AI_PROGRESS.md" \
|
|
155
|
+
WORK="$(USER_TODO="$PROJECT_DIR/USER_TODO.md" USER_QA="$PROJECT_DIR/USER_QA.md" \
|
|
156
|
+
AI_TODO="$PROJECT_DIR/AI_TODO.md" AI_PROGRESS="$PROJECT_DIR/AI_PROGRESS.md" \
|
|
213
157
|
APPROVALS="$PROJECT_DIR/.claude/.autonomous.approvals.json" \
|
|
214
|
-
|
|
158
|
+
AI_PROGRESS_TPL="$TPL/AI_PROGRESS.empty.md" \
|
|
215
159
|
"$PY_BIN" - <<'PY'
|
|
216
160
|
import os, json
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
while lines and not lines[
|
|
224
|
-
while lines and not lines[-1]: lines.pop()
|
|
161
|
+
def read(p):
|
|
162
|
+
try: return open(p, encoding="utf-8").read()
|
|
163
|
+
except Exception: return ""
|
|
164
|
+
def norm(t):
|
|
165
|
+
lines = [ln.rstrip() for ln in t.splitlines()]
|
|
166
|
+
while lines and not lines[0]: lines.pop(0)
|
|
167
|
+
while lines and not lines[-1]: lines.pop()
|
|
225
168
|
return "\n".join(lines)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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")
|
|
169
|
+
try:
|
|
170
|
+
ap = json.load(open(os.environ["APPROVALS"], encoding="utf-8"))
|
|
171
|
+
approved = {k for k, v in ap.items() if isinstance(v, dict) and v.get("approved") is True}
|
|
172
|
+
except Exception:
|
|
173
|
+
approved = set()
|
|
174
|
+
def approved_in(book_env, prefix):
|
|
175
|
+
text = read(os.environ[book_env])
|
|
176
|
+
return any(i.startswith(prefix) and i in text for i in approved)
|
|
177
|
+
user = approved_in("USER_TODO", "REQ-")
|
|
178
|
+
qa = approved_in("USER_QA", "QA-")
|
|
179
|
+
ai = approved_in("AI_TODO", "TSK-")
|
|
180
|
+
prog = norm(read(os.environ["AI_PROGRESS"])) != norm(read(os.environ["AI_PROGRESS_TPL"]))
|
|
181
|
+
print("WORK" if (user or qa or ai or prog) else "EMPTY")
|
|
253
182
|
PY
|
|
254
183
|
)"
|
|
255
184
|
if [ "$WORK" != "WORK" ]; then
|
|
256
|
-
echo "$(ts) [skip] no
|
|
185
|
+
echo "$(ts) [skip] no approved USER_TODO/USER_QA/AI_TODO work and AI_PROGRESS empty — skipping (daemon not woken)" >> "$LOG"
|
|
257
186
|
exit 0
|
|
258
187
|
fi
|
|
259
188
|
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
if [ -z "$
|
|
263
|
-
echo "$(ts) [error] '
|
|
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"
|
|
189
|
+
# --- Locate the `4pm` cli ---------------------------------------------------------------------
|
|
190
|
+
FOURPM_BIN="${FOURPM_BIN:-$(command -v 4pm || true)}"
|
|
191
|
+
if [ -z "$FOURPM_BIN" ]; then
|
|
192
|
+
echo "$(ts) [error] '4pm' not found on PATH — install the 4PM cli in WSL (e.g. ~/.local/bin/4pm)" >> "$LOG"
|
|
280
193
|
exit 0
|
|
281
194
|
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
195
|
|
|
305
|
-
# Record 1 tick that ACTUALLY
|
|
196
|
+
# Record 1 tick that ACTUALLY runs a cycle (for max_ticks_per_day).
|
|
306
197
|
tick_count=$(( ${tick_count:-0} + 1 ))
|
|
307
198
|
hist set-ticks "$TODAY" "$tick_count" 2>>"$LOG" || true
|
|
308
199
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
200
|
+
# Optional profile pin (settings.profile) — else the cli resolves the single linked profile.
|
|
201
|
+
PROFILE_ARGS=()
|
|
202
|
+
[ -n "${CFG_PROFILE:-}" ] && PROFILE_ARGS=(--profile "$CFG_PROFILE")
|
|
312
203
|
|
|
313
|
-
|
|
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")
|
|
204
|
+
echo "$(ts) [run] 4pm auto-run (tick $tick_count/$TODAY)" >> "$LOG"
|
|
317
205
|
set +e
|
|
318
|
-
"$
|
|
319
|
-
${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
|
|
320
|
-
--permission-mode bypassPermissions \
|
|
321
|
-
--dangerously-skip-permissions \
|
|
322
|
-
>> "$LOG" 2>&1
|
|
206
|
+
"$FOURPM_BIN" auto-run ${PROFILE_ARGS[@]+"${PROFILE_ARGS[@]}"} >> "$LOG" 2>&1
|
|
323
207
|
RC=$?
|
|
324
208
|
set -e
|
|
325
209
|
|
|
326
|
-
# --- Record the run + count consecutive failures + auto-stop
|
|
327
|
-
# RC != 0 -> increment; reaching the threshold ->
|
|
328
|
-
# The consecutive-failure count is stored under consecutive_fails in .autonomous.histories.json.
|
|
210
|
+
# --- Record the run + count consecutive failures + auto-stop ----------------------------------
|
|
211
|
+
# RC != 0 (no daemon / dispatch error) -> increment; reaching the threshold -> paused=true. RC == 0 -> reset.
|
|
329
212
|
fails="$(hist get-fails 2>/dev/null || echo 0)"; [ -z "$fails" ] && fails=0
|
|
330
213
|
if [ "$RC" -ne 0 ]; then
|
|
331
|
-
echo "$(ts) [warn]
|
|
214
|
+
echo "$(ts) [warn] 4pm auto-run exited $RC" >> "$LOG"
|
|
332
215
|
fails=$(( ${fails:-0} + 1 )); hist set-fails "$fails" 2>>"$LOG" || true
|
|
333
|
-
hist record "$(ts)" failure "$RC" "
|
|
216
|
+
hist record "$(ts)" failure "$RC" "" "consecutive failure #$fails" 2>>"$LOG" || true
|
|
334
217
|
if [ "${CFG_STOP_ON_CONSEC_FAILURES:-0}" -gt 0 ] 2>/dev/null && [ "$fails" -ge "$CFG_STOP_ON_CONSEC_FAILURES" ]; then
|
|
335
218
|
"$PY_BIN" - "$SETTINGS" <<'PY' 2>>"$LOG" || true
|
|
336
219
|
import json, sys
|
|
@@ -347,10 +230,10 @@ PY
|
|
|
347
230
|
fi
|
|
348
231
|
else
|
|
349
232
|
hist set-fails 0 2>>"$LOG" || true # success → reset the consecutive-failure count
|
|
350
|
-
hist record "$(ts)" success "$RC" "
|
|
233
|
+
hist record "$(ts)" success "$RC" "" "cycle complete" 2>>"$LOG" || true
|
|
351
234
|
fi
|
|
352
235
|
|
|
353
|
-
# notify_webhook: TBD —
|
|
236
|
+
# notify_webhook: TBD — a run summary would be POSTed here if CFG_NOTIFY_WEBHOOK is set.
|
|
354
237
|
|
|
355
238
|
echo "$(ts) [done] tick finished" >> "$LOG"
|
|
356
|
-
# The EXIT trap above removes `.autonomous.lock`
|
|
239
|
+
# The EXIT trap above removes `.autonomous.lock` → the lock is released for the next tick.
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# USER_QA — Questions & answers
|
|
2
2
|
|
|
3
|
-
> When a request is unclear, the AI adds a **row** here (
|
|
4
|
-
>
|
|
5
|
-
> `
|
|
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
|
-
|
|
|
8
|
-
|
|
9
|
-
| | | | |
|
|
14
|
+
| ID | Group | Depends | Original request | Question / options | Answer |
|
|
15
|
+
|----|-------|---------|------------------|--------------------|--------|
|
|
16
|
+
| | | | | | |
|