guard-heavy-commands の hook が CPU の厳しさで止めた重い処理を、空くまで測り続け、空いたらセッションに知らせる

Claude Code に持たせる skill の置き場。
skill は、決まったコマンドを順に叩くスクリプトではなく、Claude がその場で判断して進める手順として書いている(スクリプトを持つのは restart-sessions で、後戻りできない連鎖と、読むだけの処理(見張り・会話ログの抜き出し)にとどめている)。実際に打つコマンドは、Claude Code のバージョン・権限の設定・CLAUDE.md などで変わる。うまくいかないときは、その場で指示を足すか調べさせる。
使うには、.agents/skills/<skill>/ を使う側のプロジェクトの skill ディレクトリ(Claude Code なら .claude/skills/)に置く。clone した先への symlink にすると、pull した更新がそのまま届く。写したなら、restart-sessions の config.local.json を使う側の .gitignore に足す(このリポジトリの .gitignore は効かない)。restart-sessions のコンテキストの見張りは、mod(mod/)を読み込ませれば各セッションが自分を見張る。mod を使わずに外から見張るなら、使う側の .claude/settings.json に SessionStart の hook を置く(restart-sessions の README)。heavy-task の hook も settings.json に置く(heavy-task の README)。
| skill | できること | 要るもの・実機確認 |
|---|---|---|
| restart-sessions | Herdr 上のセッションを、会話ログを引き継いで再起動する。SessionStart の hook を置いたプロジェクトで起動すると、コンテキストの使用率を見張り、知らせの出たセッションを確認なしで再起動する | Herdr。見張りには、使用率を出すステータスラインと config.local.json。実機確認は macOS・Windows 11 |
| weekly-pace | Claude と Codex の週次枠の使用ペースを概算する。restart-sessions が、Claude がハイペースのとき再起動した先に Codex へ作業を回すよう書き添えるのに使う | Claude 側は Herdr と、Weekly を出すステータスライン。Codex 側は codex CLI |
| heavy-task | 重い処理を始める前に CPU とメモリを測り、始めるか待つかを決める。hook が CPU の厳しいときに重いコマンドを止め、mod が空いたらセッションに知らせる | 何が重いかを書く、使う側の .claude/heavy-commands.json。hook と mod は Claude Code 用(mod は 2.1.289 の function hooks)。実機確認は macOS のみ |
| cleanup | ディスクのゴミの候補を調べてレポートし、選んだものだけ消す。mod が空きを見張り、ラインを切ったら各セッションに自分のリポジトリを片付けさせる | 削除まで通したのは Windows のみ。mod は Claude Code 用で、実機確認は macOS のみ |
| machine-setup | 新しいマシンにアプリ・ツールを入れ、設定ファイルを取り込む | Claude Code。最後まで通したのは Ubuntu(Docker)のみ |
各 skill の前提・仕組み・使い方は、リンク先の README にある。skill に手を入れるときの決まりは AGENTS.md にある。
hooks/register.ts 71 lines1import type { EngineInterface, Register } from 'claude-code'
2import { atom, read, update } from 'claude-code'
3
4import type { HeavyWaitWaiting } from '../types'
5
6// 止められた重い処理と、止められた時刻。空くまで、または15分たつまで持つ。
7const waiting = atom({ plugin: 'heavy-wait', key: 'waiting' } as const, null as HeavyWaitWaiting)
8
9const CPU_IDLE_MIN = 10
10const CHECK_EVERY_MS = 60 * 1000
11const GIVE_UP_AFTER_MS = 15 * 60 * 1000
12
13// guard-heavy-commands.cjs が CPU の厳しさで止めたときの理由の書き出し。
14// 実物の結果の文には、頭に「PreToolUse:Bash hook error: 」が付く。
15export const isHeavyDeny = (text: unknown) => typeof text === 'string' && /^(PreToolUse:\w+ hook error: )?CPU の空きが [\d.]+% で、10% を切っているため、重い処理を止めました/.test(text)
16
17const commandOf = (e: unknown) => {
18 const { command, script } = e as { command?: unknown; script?: unknown }
19 if (typeof command === 'string') return command
20 return typeof script === 'string' ? 'Workflow' : '(コマンド不明)'
21}
22
23// 空きは止めた hook と同じ測り方で測る(このスキルの scripts/guard-heavy-commands.cjs を --idle で呼ぶ)。測れなければ null。
24// mod のフォルダは symlink で読み込まれることがあるので、実体のパスからスキルのフォルダを割り出す。
25async function measureIdle($: EngineInterface) {
26 const { realPath } = await $.fs.stat($.plugin.root, { resolve: true })
27 const { exitCode, stdout } = await $.process.run(['node', `${realPath ?? $.plugin.root}/../scripts/guard-heavy-commands.cjs`, '--idle'])
28 const idle = Number.parseFloat(stdout)
29 return exitCode === 0 && Number.isFinite(idle) ? idle : null
30}
31
32async function check($: EngineInterface) {
33 const current = await read($, waiting)
34 if (current === null) return
35 const idle = await measureIdle($)
36 if (idle === null) return
37 const elapsed = (await $.clock.now()) - current.since
38 if (idle >= CPU_IDLE_MIN) {
39 await update($, waiting, () => null)
40 await $.prompt.submit({
41 text: `[重い処理の待ち] CPU の空きが ${idle.toFixed(1)}% に戻った。止められた次の処理を流し直してよい(heavy-task skill)。\n${current.command}`,
42 })
43 } else if (elapsed >= GIVE_UP_AFTER_MS) {
44 await update($, waiting, () => null)
45 await $.prompt.submit({
46 text: `[重い処理の待ち] 15分たっても CPU の空きが ${idle.toFixed(1)}% のまま。heavy-task skill の手順どおり、待ち続けるか強行するかをユーザーに聞く。\n${current.command}`,
47 })
48 }
49}
50
51export const register: Register = on => {
52 on('session.start', async ($, e, next) => {
53 const started = await next(e)
54 $.clock.every(CHECK_EVERY_MS, () => {
55 void check($)
56 })
57 return started
58 })
59
60 // 止めるのは hook。ここでは止められたのを見て、待ちを始める。
61 on('tool.call', { tool: /^(Bash|PowerShell|Workflow)$/ }, async ($, e, next) => {
62 const result = await next(e)
63 if (result.deny === undefined && result.isError === true && isHeavyDeny(result.text)) {
64 const command = commandOf(e)
65 const since = await $.clock.now()
66 await update($, waiting, current => current ?? { command, since })
67 }
68 return result
69 })
70}
71types/index.d.ts 8 lines1export type HeavyWaitWaiting = { command: string; since: number } | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'heavy-wait': { waiting: HeavyWaitWaiting }
6 }
7}
8