cc-session-recover 0.1.4 → 0.1.5
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/.claude/hooks/remind-on-prompt.sh +13 -0
- package/.claude/settings.example.json +10 -0
- package/.claude/standing-instructions.md +8 -8
- package/CHANGELOG.md +7 -0
- package/README.md +3 -3
- package/bin/cli.js +64 -5
- package/package.json +1 -1
- package/scripts/install-into-project.sh +65 -6
- package/scripts/test-fake-quota-flow.sh +64 -1
- package/scripts/verify-claude-loop-workflow.sh +6 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# UserPromptSubmit hook.
|
|
4
|
+
# Injects an unconditional recovery order next to every prompt. Earlier
|
|
5
|
+
# softer wording ("if this is a multi-step task...") left the model
|
|
6
|
+
# discretion, and in long sessions it deferred forever. No judgment calls.
|
|
7
|
+
# Keep this output short: it is added to context on every single prompt.
|
|
8
|
+
|
|
9
|
+
set -u
|
|
10
|
+
|
|
11
|
+
printf 'Recovery check (do this before the request itself): if no recurring auto-continue schedule exists in this session, create one now — every 45 minutes, prompt: "Read .claude/auto-continue.md and follow it". Never create a duplicate. On multi-step tasks, update HANDOFF.md after each work step.\n'
|
|
12
|
+
|
|
13
|
+
exit 0
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Standing quota-recovery instructions for this project:
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
At the start of any task in this session — coding, analysis, research, document work, audits, or skill-driven and automated runs:
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
5
|
+
1. Before doing the task itself, check whether this session already has a recurring auto-continue schedule.
|
|
6
|
+
2. If none exists, create one now with Claude Code's scheduler (`CronCreate` or `/loop`). Do this even for short-looking requests; an unused schedule is harmless and you can cancel it when done.
|
|
7
|
+
3. Use this scheduled prompt exactly: "Read .claude/auto-continue.md and follow it."
|
|
8
|
+
4. Prefer a 45-minute recurring interval. If the scheduler cannot represent that cleanly, use a 30-minute recurring interval.
|
|
9
|
+
5. Keep `HANDOFF.md` updated after every small safe step on multi-step tasks.
|
|
10
|
+
6. Cancel the auto-continue schedule when the task is fully complete.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Do not create the schedule for quick questions or single-step requests.
|
|
12
|
-
Do not create a second schedule if one already exists.
|
|
12
|
+
Never create a second schedule if one already exists in this session.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 — 2026-06-13
|
|
4
|
+
|
|
5
|
+
- Add a `UserPromptSubmit` recovery reminder hook so the schedule instruction is repeated next to every prompt, not just at session start (it was getting buried in long sessions).
|
|
6
|
+
- Make the schedule rule unconditional and consistent across both injection points: always ensure one recurring auto-continue schedule exists, instead of leaving the model a "is this a long task?" judgment call that silently skipped audits and skill-driven runs. An explicit user instruction not to schedule still wins.
|
|
7
|
+
- Preserve existing `.claude/settings.local.json` on install by merging the recovery hooks in, rather than skipping when the file exists.
|
|
8
|
+
- Use the quoted `"$CLAUDE_PROJECT_DIR"` form in example hook commands (safe for paths with spaces).
|
|
9
|
+
|
|
3
10
|
## 0.1.4 — 2026-06-12
|
|
4
11
|
|
|
5
12
|
- Expand standing quota-recovery instructions to cover long analysis, research, document, and automated workflow tasks, not only coding tasks.
|
package/README.md
CHANGED
|
@@ -42,6 +42,6 @@ If quota dies mid-task, work resumes automatically after the reset.
|
|
|
42
42
|
|
|
43
43
|
## Docs
|
|
44
44
|
|
|
45
|
-
- [Simple flow](docs/simple-flow.md) — how it works, told as a story (notebook, alarm, watchman).
|
|
46
|
-
- [FAQ](docs/faq.md) — reliability, hook approval, what still needs a human.
|
|
47
|
-
- [Full details](docs/claude-code-auto-resume.md) — closed-terminal watcher, precise reset-time resume, all limits.
|
|
45
|
+
- [Simple flow](https://github.com/softcane/cc-session-recover/blob/main/docs/simple-flow.md) — how it works, told as a story (notebook, alarm, watchman).
|
|
46
|
+
- [FAQ](https://github.com/softcane/cc-session-recover/blob/main/docs/faq.md) — reliability, hook approval, what still needs a human.
|
|
47
|
+
- [Full details](https://github.com/softcane/cc-session-recover/blob/main/docs/claude-code-auto-resume.md) — closed-terminal watcher, precise reset-time resume, all limits.
|
package/bin/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ const FILES = [
|
|
|
23
23
|
'.claude/statusline-quota-cache.sh',
|
|
24
24
|
'.claude/hooks/log-stop-failure.sh',
|
|
25
25
|
'.claude/hooks/inject-standing-instructions.sh',
|
|
26
|
+
'.claude/hooks/remind-on-prompt.sh',
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
const COPY_IF_MISSING = ['HANDOFF.md'];
|
|
@@ -38,6 +39,66 @@ const IGNORE_ENTRIES = [
|
|
|
38
39
|
'.claude/quota-blocked.json',
|
|
39
40
|
];
|
|
40
41
|
const IGNORE_HEADER = '# Claude Code session-recovery runtime state';
|
|
42
|
+
const RECOVERY_HOOK_SCRIPTS = [
|
|
43
|
+
'inject-standing-instructions.sh',
|
|
44
|
+
'remind-on-prompt.sh',
|
|
45
|
+
'log-stop-failure.sh',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function readJson(file) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addMissingHookGroups(existingGroups, incomingGroups) {
|
|
57
|
+
const merged = Array.isArray(existingGroups) ? existingGroups.slice() : [];
|
|
58
|
+
for (const group of incomingGroups || []) {
|
|
59
|
+
const encoded = JSON.stringify(group);
|
|
60
|
+
if (!merged.some((existing) => JSON.stringify(existing) === encoded || hasSameRecoveryHook(existing, group))) {
|
|
61
|
+
merged.push(group);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function recoveryHookScripts(group) {
|
|
68
|
+
const handlers = Array.isArray(group && group.hooks) ? group.hooks : [];
|
|
69
|
+
const commands = handlers
|
|
70
|
+
.filter((handler) => handler && handler.type === 'command' && typeof handler.command === 'string')
|
|
71
|
+
.map((handler) => handler.command);
|
|
72
|
+
return RECOVERY_HOOK_SCRIPTS.filter((script) => commands.some((command) => command.includes(script)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasSameRecoveryHook(existingGroup, incomingGroup) {
|
|
76
|
+
const incomingScripts = recoveryHookScripts(incomingGroup);
|
|
77
|
+
if (!incomingScripts.length) return false;
|
|
78
|
+
|
|
79
|
+
const existingScripts = recoveryHookScripts(existingGroup);
|
|
80
|
+
return incomingScripts.some((script) => existingScripts.includes(script));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mergeHookSettings(localPath, templatePath) {
|
|
84
|
+
if (!fs.existsSync(localPath)) {
|
|
85
|
+
fs.copyFileSync(templatePath, localPath);
|
|
86
|
+
return 'created';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const localSettings = readJson(localPath);
|
|
90
|
+
const templateSettings = readJson(templatePath);
|
|
91
|
+
const incomingHooks = templateSettings.hooks || {};
|
|
92
|
+
const mergedHooks = { ...(localSettings.hooks || {}) };
|
|
93
|
+
|
|
94
|
+
for (const [event, groups] of Object.entries(incomingHooks)) {
|
|
95
|
+
mergedHooks[event] = addMissingHookGroups(mergedHooks[event], groups);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
localSettings.hooks = mergedHooks;
|
|
99
|
+
fs.writeFileSync(localPath, `${JSON.stringify(localSettings, null, 2)}\n`);
|
|
100
|
+
return 'merged';
|
|
101
|
+
}
|
|
41
102
|
|
|
42
103
|
function usage() {
|
|
43
104
|
console.error('Usage: cc-session-recover init [--no-hooks] [target-dir]');
|
|
@@ -109,11 +170,9 @@ function main() {
|
|
|
109
170
|
|
|
110
171
|
if (enableHook) {
|
|
111
172
|
const local = path.join(target, '.claude', 'settings.local.json');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.error('
|
|
115
|
-
} else {
|
|
116
|
-
fs.copyFileSync(path.join(TEMPLATE_ROOT, '.claude', 'settings.example.json'), local);
|
|
173
|
+
const result = mergeHookSettings(local, path.join(TEMPLATE_ROOT, '.claude', 'settings.example.json'));
|
|
174
|
+
if (result === 'merged') {
|
|
175
|
+
console.error('Merged hook settings into existing .claude/settings.local.json.');
|
|
117
176
|
}
|
|
118
177
|
}
|
|
119
178
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-session-recover",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Quota-resume workflow for Claude Code: handoff notebook, heartbeat auto-continue, StopFailure hooks, and an unattended watcher that resumes after quota resets.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "node --check bin/cli.js && bash -n scripts/*.sh .claude/hooks/*.sh .claude/statusline-quota-cache.sh && bash scripts/verify-claude-loop-workflow.sh",
|
|
@@ -27,12 +27,75 @@ TEMPLATE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
|
27
27
|
exit 1
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
merge_hook_settings() {
|
|
31
|
+
local local_settings=$1
|
|
32
|
+
local template_settings=$2
|
|
33
|
+
local tmp
|
|
34
|
+
|
|
35
|
+
if [ ! -f "$local_settings" ]; then
|
|
36
|
+
cp "$template_settings" "$local_settings"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
41
|
+
printf 'Cannot merge hooks into existing .claude/settings.local.json because jq is not on PATH.\n' >&2
|
|
42
|
+
printf 'Install jq, then rerun this installer.\n' >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
tmp=$(mktemp "${local_settings}.tmp.XXXXXX")
|
|
47
|
+
if jq -s '
|
|
48
|
+
def recovery_scripts: [
|
|
49
|
+
"inject-standing-instructions.sh",
|
|
50
|
+
"remind-on-prompt.sh",
|
|
51
|
+
"log-stop-failure.sh"
|
|
52
|
+
];
|
|
53
|
+
def as_array: if type == "array" then . else [] end;
|
|
54
|
+
def as_object: if type == "object" then . else {} end;
|
|
55
|
+
def recovery_hook_scripts:
|
|
56
|
+
[(.hooks // [])[]? |
|
|
57
|
+
select(.type == "command" and (.command | type == "string")) |
|
|
58
|
+
.command as $command |
|
|
59
|
+
recovery_scripts[] |
|
|
60
|
+
select($command | contains(.))
|
|
61
|
+
];
|
|
62
|
+
def same_recovery_hook($existing; $incoming):
|
|
63
|
+
(recovery_hook_scripts as $existing_scripts |
|
|
64
|
+
($incoming | recovery_hook_scripts) as $incoming_scripts |
|
|
65
|
+
(($incoming_scripts | length) > 0) and
|
|
66
|
+
any($incoming_scripts[]; . as $script | any($existing_scripts[]; . == $script))
|
|
67
|
+
);
|
|
68
|
+
def add_missing($incoming):
|
|
69
|
+
as_array as $existing |
|
|
70
|
+
reduce (($incoming // []) | as_array)[] as $item ($existing;
|
|
71
|
+
if any(.[]; (. == $item) or same_recovery_hook(.; $item)) then . else . + [$item] end
|
|
72
|
+
);
|
|
73
|
+
def merge_hooks($incoming):
|
|
74
|
+
as_object as $base |
|
|
75
|
+
reduce (($incoming // {}) | as_object | keys_unsorted[]) as $event ($base;
|
|
76
|
+
.[$event] = (.[$event] | add_missing($incoming[$event]))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
.[0] as $local |
|
|
80
|
+
.[1] as $template |
|
|
81
|
+
$local + {hooks: (($local.hooks // {}) | merge_hooks($template.hooks // {}))}
|
|
82
|
+
' "$local_settings" "$template_settings" > "$tmp"; then
|
|
83
|
+
mv "$tmp" "$local_settings"
|
|
84
|
+
printf 'Merged hook settings into existing .claude/settings.local.json.\n' >&2
|
|
85
|
+
else
|
|
86
|
+
rm -f "$tmp"
|
|
87
|
+
printf 'Could not merge hooks into .claude/settings.local.json.\n' >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
30
92
|
mkdir -p "$TARGET/.claude/hooks"
|
|
31
93
|
|
|
32
94
|
cp "$TEMPLATE_ROOT/.claude/auto-continue.md" "$TARGET/.claude/auto-continue.md"
|
|
33
95
|
cp "$TEMPLATE_ROOT/.claude/standing-instructions.md" "$TARGET/.claude/standing-instructions.md"
|
|
34
96
|
cp "$TEMPLATE_ROOT/.claude/statusline-quota-cache.sh" "$TARGET/.claude/statusline-quota-cache.sh"
|
|
35
97
|
cp "$TEMPLATE_ROOT/.claude/hooks/inject-standing-instructions.sh" "$TARGET/.claude/hooks/inject-standing-instructions.sh"
|
|
98
|
+
cp "$TEMPLATE_ROOT/.claude/hooks/remind-on-prompt.sh" "$TARGET/.claude/hooks/remind-on-prompt.sh"
|
|
36
99
|
cp "$TEMPLATE_ROOT/.claude/settings.example.json" "$TARGET/.claude/settings.example.json"
|
|
37
100
|
cp "$TEMPLATE_ROOT/.claude/hooks/log-stop-failure.sh" "$TARGET/.claude/hooks/log-stop-failure.sh"
|
|
38
101
|
if [ ! -f "$TARGET/HANDOFF.md" ]; then
|
|
@@ -41,6 +104,7 @@ fi
|
|
|
41
104
|
|
|
42
105
|
chmod +x "$TARGET/.claude/hooks/log-stop-failure.sh"
|
|
43
106
|
chmod +x "$TARGET/.claude/hooks/inject-standing-instructions.sh"
|
|
107
|
+
chmod +x "$TARGET/.claude/hooks/remind-on-prompt.sh"
|
|
44
108
|
chmod +x "$TARGET/.claude/statusline-quota-cache.sh"
|
|
45
109
|
|
|
46
110
|
# Keep runtime state out of the target's git history. HANDOFF.md must stay in
|
|
@@ -72,12 +136,7 @@ for entry in HANDOFF.md .claude/settings.local.json .claude/rate-limit-state.jso
|
|
|
72
136
|
done
|
|
73
137
|
|
|
74
138
|
if [ "$ENABLE_LOCAL_HOOK" -eq 1 ]; then
|
|
75
|
-
|
|
76
|
-
printf 'Skipped hook enablement because .claude/settings.local.json already exists.\n' >&2
|
|
77
|
-
printf 'Merge .claude/settings.example.json into it manually if wanted.\n' >&2
|
|
78
|
-
else
|
|
79
|
-
cp "$TEMPLATE_ROOT/.claude/settings.example.json" "$TARGET/.claude/settings.local.json"
|
|
80
|
-
fi
|
|
139
|
+
merge_hook_settings "$TARGET/.claude/settings.local.json" "$TEMPLATE_ROOT/.claude/settings.example.json"
|
|
81
140
|
fi
|
|
82
141
|
|
|
83
142
|
printf 'Installed Claude Code workflow into %s\n' "$TARGET"
|
|
@@ -44,6 +44,67 @@ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" --enable-local-hook "$DUMM
|
|
|
44
44
|
[ -f "$DUMMY/.claude/settings.local.json" ] || fail "hook settings not installed"
|
|
45
45
|
[ ! -d "$DUMMY/scripts" ] || fail "install must not create a scripts folder in the target"
|
|
46
46
|
[ ! -d "$DUMMY/docs" ] || fail "install must not create a docs folder in the target"
|
|
47
|
+
jq -e '.hooks.SessionStart and .hooks.UserPromptSubmit and .hooks.StopFailure' "$DUMMY/.claude/settings.local.json" >/dev/null \
|
|
48
|
+
|| fail "installed local settings missing recovery hooks"
|
|
49
|
+
printf 'ok: installer activated recovery hooks in local settings\n'
|
|
50
|
+
|
|
51
|
+
step "Existing settings.local should keep settings and receive hooks"
|
|
52
|
+
MERGE_DUMMY="$WORK/merge-repo"
|
|
53
|
+
mkdir -p "$MERGE_DUMMY/.claude"
|
|
54
|
+
printf '{"permissions":{"allow":["Bash(npm test *)"]},"hooks":{"PostToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"true"}]}]}}\n' \
|
|
55
|
+
> "$MERGE_DUMMY/.claude/settings.local.json"
|
|
56
|
+
bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$MERGE_DUMMY" >/dev/null
|
|
57
|
+
jq -e '
|
|
58
|
+
(.permissions.allow[0] == "Bash(npm test *)") and
|
|
59
|
+
(.hooks.PostToolUse[0].matcher == "Write") and
|
|
60
|
+
(.hooks.SessionStart | length > 0) and
|
|
61
|
+
(.hooks.UserPromptSubmit | length > 0) and
|
|
62
|
+
(.hooks.StopFailure | length > 0)
|
|
63
|
+
' "$MERGE_DUMMY/.claude/settings.local.json" >/dev/null || fail "bash installer did not merge hooks into existing settings"
|
|
64
|
+
bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$MERGE_DUMMY" >/dev/null
|
|
65
|
+
DUPES=$(jq '[.hooks.SessionStart[], .hooks.UserPromptSubmit[], .hooks.StopFailure[]] | length' "$MERGE_DUMMY/.claude/settings.local.json")
|
|
66
|
+
[ "$DUPES" -eq 3 ] || fail "bash installer duplicated recovery hooks on rerun"
|
|
67
|
+
printf 'ok: bash installer preserved existing settings and merged hooks once\n'
|
|
68
|
+
|
|
69
|
+
step "Older shell-form recovery hooks should not be duplicated"
|
|
70
|
+
OLD_DUMMY="$WORK/old-hook-repo"
|
|
71
|
+
mkdir -p "$OLD_DUMMY/.claude"
|
|
72
|
+
jq -n '{
|
|
73
|
+
hooks: {
|
|
74
|
+
SessionStart: [
|
|
75
|
+
{
|
|
76
|
+
hooks: [
|
|
77
|
+
{
|
|
78
|
+
type: "command",
|
|
79
|
+
command: "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-standing-instructions.sh"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}' > "$OLD_DUMMY/.claude/settings.local.json"
|
|
86
|
+
bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" "$OLD_DUMMY" >/dev/null
|
|
87
|
+
SESSION_START_COUNT=$(jq '.hooks.SessionStart | length' "$OLD_DUMMY/.claude/settings.local.json")
|
|
88
|
+
[ "$SESSION_START_COUNT" -eq 1 ] || fail "installer duplicated old SessionStart recovery hook"
|
|
89
|
+
jq -e '(.hooks.UserPromptSubmit | length > 0) and (.hooks.StopFailure | length > 0)' \
|
|
90
|
+
"$OLD_DUMMY/.claude/settings.local.json" >/dev/null || fail "installer failed to add missing hooks next to old hook"
|
|
91
|
+
printf 'ok: installer recognized old recovery hook commands and added only missing hooks\n'
|
|
92
|
+
|
|
93
|
+
step "Node installer should also merge existing settings.local"
|
|
94
|
+
NODE_DUMMY="$WORK/node-merge-repo"
|
|
95
|
+
mkdir -p "$NODE_DUMMY/.claude"
|
|
96
|
+
printf '{"permissions":{"allow":["Bash(npm test *)"]}}\n' > "$NODE_DUMMY/.claude/settings.local.json"
|
|
97
|
+
node "$TEMPLATE_ROOT/bin/cli.js" init "$NODE_DUMMY" >/dev/null
|
|
98
|
+
jq -e '
|
|
99
|
+
(.permissions.allow[0] == "Bash(npm test *)") and
|
|
100
|
+
(.hooks.SessionStart | length > 0) and
|
|
101
|
+
(.hooks.UserPromptSubmit | length > 0) and
|
|
102
|
+
(.hooks.StopFailure | length > 0)
|
|
103
|
+
' "$NODE_DUMMY/.claude/settings.local.json" >/dev/null || fail "node installer did not merge hooks into existing settings"
|
|
104
|
+
node "$TEMPLATE_ROOT/bin/cli.js" init "$NODE_DUMMY" >/dev/null
|
|
105
|
+
DUPES=$(jq '[.hooks.SessionStart[], .hooks.UserPromptSubmit[], .hooks.StopFailure[]] | length' "$NODE_DUMMY/.claude/settings.local.json")
|
|
106
|
+
[ "$DUPES" -eq 3 ] || fail "node installer duplicated recovery hooks on rerun"
|
|
107
|
+
printf 'ok: node installer preserved existing settings and merged hooks once\n'
|
|
47
108
|
|
|
48
109
|
step "Fake SessionStart: standing instructions should be injected"
|
|
49
110
|
INJECTED=$(printf '{"session_id":"fake-session-123","hook_event_name":"SessionStart","source":"startup"}' \
|
|
@@ -60,13 +121,15 @@ printf '{"workspace":{"project_dir":"%s"},"model":{"display_name":"Test"},"rate_
|
|
|
60
121
|
printf 'ok: status line wrapper cached the reset time\n'
|
|
61
122
|
|
|
62
123
|
step "Fake quota stop: StopFailure(rate_limit) should log and write the marker"
|
|
63
|
-
printf '{"session_id":"fake-session-123","hook_event_name":"StopFailure","
|
|
124
|
+
printf '{"session_id":"fake-session-123","hook_event_name":"StopFailure","error":"rate_limit","last_assistant_message":"API Error: Rate limit reached"}' \
|
|
64
125
|
| CLAUDE_PROJECT_DIR="$DUMMY" bash "$DUMMY/.claude/hooks/log-stop-failure.sh"
|
|
65
126
|
[ -f "$DUMMY/.claude/stop-failure-events.jsonl" ] || fail "missing stop-failure log"
|
|
66
127
|
[ -f "$DUMMY/.claude/quota-blocked.json" ] || fail "missing quota-blocked marker"
|
|
67
128
|
grep -Fq "hit a rate limit" "$DUMMY/HANDOFF.md" || fail "missing handoff note"
|
|
68
129
|
SESSION=$(jq -r '.hook_input.session_id // empty' "$DUMMY/.claude/quota-blocked.json")
|
|
69
130
|
[ "$SESSION" = "fake-session-123" ] || fail "marker has wrong session_id: $SESSION"
|
|
131
|
+
ERROR=$(jq -r '.hook_input.error // empty' "$DUMMY/.claude/quota-blocked.json")
|
|
132
|
+
[ "$ERROR" = "rate_limit" ] || fail "marker has wrong error field: $ERROR"
|
|
70
133
|
RESETS=$(jq -r '.rate_limit_state.five_hour_resets_at // empty' "$DUMMY/.claude/quota-blocked.json")
|
|
71
134
|
[ -n "$RESETS" ] || fail "marker missing cached reset time"
|
|
72
135
|
printf 'ok: hook wrote log, handoff note, and marker with session_id and reset time\n'
|
|
@@ -25,6 +25,7 @@ require_file "HANDOFF.md"
|
|
|
25
25
|
require_file ".claude/settings.example.json"
|
|
26
26
|
require_file ".claude/hooks/log-stop-failure.sh"
|
|
27
27
|
require_file ".claude/hooks/inject-standing-instructions.sh"
|
|
28
|
+
require_file ".claude/hooks/remind-on-prompt.sh"
|
|
28
29
|
require_file ".claude/standing-instructions.md"
|
|
29
30
|
require_file "scripts/test-fake-quota-flow.sh"
|
|
30
31
|
require_file "README.md"
|
|
@@ -62,6 +63,10 @@ require_text ".claude/settings.example.json" "SessionStart"
|
|
|
62
63
|
require_text ".claude/settings.example.json" "inject-standing-instructions.sh"
|
|
63
64
|
require_text ".claude/standing-instructions.md" "auto-continue.md"
|
|
64
65
|
require_text ".claude/standing-instructions.md" "HANDOFF.md"
|
|
66
|
+
require_text ".claude/settings.example.json" "UserPromptSubmit"
|
|
67
|
+
require_text ".claude/settings.example.json" "remind-on-prompt.sh"
|
|
68
|
+
require_text ".claude/hooks/remind-on-prompt.sh" "HANDOFF.md"
|
|
69
|
+
require_text ".claude/hooks/remind-on-prompt.sh" "auto-continue.md"
|
|
65
70
|
require_text ".claude/settings.example.json" "StopFailure"
|
|
66
71
|
require_text ".claude/settings.example.json" "rate_limit"
|
|
67
72
|
require_text ".claude/settings.example.json" "log-stop-failure.sh"
|
|
@@ -78,6 +83,7 @@ require_text "scripts/quota-watcher.sh" "session_id"
|
|
|
78
83
|
[ -x "$ROOT/scripts/install-into-project.sh" ] || fail "scripts/install-into-project.sh must be executable"
|
|
79
84
|
[ -x "$ROOT/scripts/quota-watcher.sh" ] || fail "scripts/quota-watcher.sh must be executable"
|
|
80
85
|
[ -x "$ROOT/.claude/hooks/inject-standing-instructions.sh" ] || fail ".claude/hooks/inject-standing-instructions.sh must be executable"
|
|
86
|
+
[ -x "$ROOT/.claude/hooks/remind-on-prompt.sh" ] || fail ".claude/hooks/remind-on-prompt.sh must be executable"
|
|
81
87
|
[ -x "$ROOT/scripts/test-fake-quota-flow.sh" ] || fail "scripts/test-fake-quota-flow.sh must be executable"
|
|
82
88
|
|
|
83
89
|
for forbidden in "claude -p" "tmux" "screen" "expect" "TIOCSTI"; do
|