monster-orchestrator 3.2.4 → 3.2.6

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.
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bash
2
+ # monster-stop-hook <monster_id> [event]
3
+ #
4
+ # Claude Code の Stop / Notification hook 共通本体。
5
+ # monster のターン終了時(Stop→idle)や承認プロンプト発生時(Notification→awaiting_input)に発火し、
6
+ # ~/.claude/monsters/events.log に NDJSON で 1 行 append する。
7
+ #
8
+ # 契約:
9
+ # - 第 1 引数: monster_id(省略時は "unknown")
10
+ # - 第 2 引数: event 種別(省略時は "idle"。B2 の呼び出しとの後方互換)
11
+ # 使用値: idle / awaiting_input / spawned / stopped / error
12
+ # / user_prompt / session_end / subagent_stop / spawn_timeout
13
+ # - stdin: Claude Code 提供の JSON。
14
+ # awaiting_input では message を、session_end では reason を読む。
15
+ # それ以外の event では読まない(保存だけして捨てる)。
16
+ # - exit: 常に 0(失敗でも monster を止めない)
17
+ #
18
+ # 注: user_prompt / session_end / subagent_stop は awaiting_input 以外なので
19
+ # subtype 分類は行わず、event 名のみを events.log に記録する。
20
+
21
+ set +e
22
+
23
+ ID="${1:-unknown}"
24
+ EVENT="${2:-idle}"
25
+ # 板 #442 ③: monsters ルートの解決は **全経路で同一の式**にする。
26
+ # server 側(src/paths.ts)は MONSTER_MCP_ROOT を honour しており、hook だけ $HOME 直書きだと
27
+ # 非既定 root で動かしたときに **server と hook が別の台帳を見る**(=イベントが迷子になる)。
28
+ # ⚠️ 共有ファイルを source する形は採らない——この種のスクリプトは ~/.claude/hooks/ 等へ
29
+ # **単体でコピーして使う**運用があり、兄弟ファイルへの依存はコピー時に壊れる。
30
+ # 代わりに式を 1 行に畳み、tests が全経路の同一性を強制する。
31
+ # MONSTER_ROOT は後方互換の別名(monster-deathcheck が従来これを見ていた)。
32
+ ROOT="${MONSTER_MCP_ROOT:-${MONSTER_ROOT:-$HOME/.claude/monsters}}"
33
+ EVENTS="$ROOT/events.log"
34
+ STATE="$ROOT/state.json"
35
+ TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
36
+
37
+ # stdin を一旦 tmpfile に保存(後で JSON パースに使用、空でも問題なし)
38
+ STDIN_TMP="$(mktemp -t monster-hook.XXXXXX)"
39
+ cat > "$STDIN_TMP" 2>/dev/null
40
+ trap 'rm -f "$STDIN_TMP"' EXIT
41
+
42
+ # events.log ディレクトリを保証
43
+ mkdir -p "$ROOT" 2>/dev/null
44
+
45
+ # state.json から tmux session 名を取得(無ければ空)
46
+ SESSION=""
47
+ if [ -f "$STATE" ]; then
48
+ SESSION=$(python3 - "$STATE" "$ID" <<'PY' 2>/dev/null
49
+ import json, sys
50
+ try:
51
+ s = json.load(open(sys.argv[1]))
52
+ m = s.get('monsters', {}).get(sys.argv[2], {})
53
+ print(m.get('tmux_session', ''))
54
+ except Exception:
55
+ print('')
56
+ PY
57
+ )
58
+ fi
59
+
60
+ # tmux 最新 tail を meta.tail に(最大 200 文字、JSON-safe)
61
+ TAIL=""
62
+ if [ -n "$SESSION" ]; then
63
+ TAIL=$(tmux capture-pane -t "$SESSION" -p -S -10 2>/dev/null \
64
+ | tail -3 \
65
+ | tr '\n' ' ' \
66
+ | tr -d '\r' \
67
+ | python3 -c 'import sys,json; s=sys.stdin.read()[:200]; sys.stdout.write(json.dumps(s)[1:-1])' 2>/dev/null)
68
+ fi
69
+
70
+ # Notification hook の場合、stdin JSON から message を抽出して subtype 分類。
71
+ # Stop hook 等の場合は subtype="" のまま。
72
+ # subtype enum:
73
+ # approval - 承認プロンプト("permission" を含む)
74
+ # waiting - ユーザ入力待ち("waiting"/"input" を含む)
75
+ # tool_error - ツールエラー("error"/"failed" を含む)
76
+ # idle_warning - アイドル警告("idle" を含む)
77
+ # other - 上記以外(message はあるが分類外)
78
+ SUBTYPE=""
79
+ MESSAGE_JSON='""'
80
+ if [ "$EVENT" = "awaiting_input" ] && [ -s "$STDIN_TMP" ]; then
81
+ PARSED=$(python3 - "$STDIN_TMP" <<'PY' 2>/dev/null
82
+ import json, sys
83
+ try:
84
+ raw = open(sys.argv[1]).read()
85
+ d = json.loads(raw) if raw.strip() else {}
86
+ except Exception:
87
+ d = {}
88
+ msg = (d.get('message') or '').strip()
89
+ low = msg.lower()
90
+ if not msg:
91
+ sub = ''
92
+ elif 'permission' in low or 'approve' in low or 'allow' in low:
93
+ sub = 'approval'
94
+ elif 'waiting' in low or 'input' in low:
95
+ sub = 'waiting'
96
+ elif 'error' in low or 'failed' in low:
97
+ sub = 'tool_error'
98
+ elif 'idle' in low:
99
+ sub = 'idle_warning'
100
+ else:
101
+ sub = 'other'
102
+ # 200 文字キャップ
103
+ msg = msg[:200]
104
+ print(sub)
105
+ print(json.dumps(msg))
106
+ PY
107
+ )
108
+ SUBTYPE=$(printf '%s' "$PARSED" | sed -n '1p')
109
+ MSG_LINE=$(printf '%s' "$PARSED" | sed -n '2p')
110
+ if [ -n "$MSG_LINE" ]; then
111
+ MESSAGE_JSON="$MSG_LINE"
112
+ fi
113
+ fi
114
+
115
+ # SessionEnd hook は stdin の JSON に reason を載せてくる
116
+ # (clear / logout / prompt_input_exit / other 等)。
117
+ # これが「正常に終わったのか、途中で落ちたのか」の唯一の一次情報だが、従来は読み捨てていた。
118
+ # ⇒ 検出機構を新設する必要はない。既に配達されている信号を読むだけでよい。
119
+ # stopped の latest_event が正常完了も異常死も一律 session_end になっていた原因がこれ。
120
+ REASON=""
121
+ if [ "$EVENT" = "session_end" ] && [ -s "$STDIN_TMP" ]; then
122
+ REASON=$(python3 - "$STDIN_TMP" <<'REASONPY' 2>/dev/null
123
+ import json, sys
124
+ try:
125
+ raw = open(sys.argv[1]).read()
126
+ d = json.loads(raw) if raw.strip() else {}
127
+ except Exception:
128
+ d = {}
129
+ r = d.get('reason')
130
+ # 想定外の型・長大な値を events.log に流し込まない
131
+ print(r[:64] if isinstance(r, str) else '')
132
+ REASONPY
133
+ )
134
+ fi
135
+
136
+ # UserPromptSubmit hook は stdin の JSON に prompt(submit された本文そのもの)を載せてくる。
137
+ # その指紋(sha256 の先頭 16 桁)を meta に置くと、送り手が「自分が送った本文が着弾したか」を
138
+ # **送信単位で**照合できる(BACKLOG #175)。件数の増加だけでは、同じ席へ同時に来た
139
+ # 別の送信と取り違えうるため。
140
+ #
141
+ # 🔑 本文そのものは載せない。指紋だけで照合は足りるうえ、events.log は全レーンが読む共有ログで、
142
+ # 指示本文を撒くと機密がそこへ流れる。
143
+ # 🔑 指紋の対象は **Claude が受け取った本文そのもの**。送り手側は同じ正規化
144
+ # (tmux.ts の sanitizeSendText)を通した文字列を対象にすること(二重情報源を作らない)。
145
+ # ⚠️ python3 が無い / prompt が無い環境では空になり、meta にキーが増えないだけ。
146
+ # 送り手側は従来どおり件数差へ安全に縮退する(fail-safe)。
147
+ PROMPT_SHA=""
148
+ if [ "$EVENT" = "user_prompt" ] && [ -s "$STDIN_TMP" ]; then
149
+ PROMPT_SHA=$(python3 - "$STDIN_TMP" <<'SHAPY' 2>/dev/null
150
+ import json, sys, hashlib
151
+ try:
152
+ raw = open(sys.argv[1]).read()
153
+ d = json.loads(raw) if raw.strip() else {}
154
+ except Exception:
155
+ d = {}
156
+ p = d.get('prompt')
157
+ print(hashlib.sha256(p.encode('utf-8')).hexdigest()[:16] if isinstance(p, str) else '')
158
+ SHAPY
159
+ )
160
+ fi
161
+
162
+ # NDJSON 行構築して append(O_APPEND で atomic)
163
+ # meta に subtype / message を含める(Notification 時のみ意味を持つ)
164
+ if [ -n "$REASON" ]; then
165
+ printf '{"ts":"%s","monster_id":"%s","event":"%s","session":"%s","meta":{"tail":"%s","reason":"%s"}}\n' \
166
+ "$TS" "$ID" "$EVENT" "$SESSION" "$TAIL" "$REASON" >> "$EVENTS" 2>/dev/null
167
+ elif [ -n "$SUBTYPE" ]; then
168
+ printf '{"ts":"%s","monster_id":"%s","event":"%s","session":"%s","meta":{"tail":"%s","subtype":"%s","message":%s}}\n' \
169
+ "$TS" "$ID" "$EVENT" "$SESSION" "$TAIL" "$SUBTYPE" "$MESSAGE_JSON" >> "$EVENTS" 2>/dev/null
170
+ elif [ -n "$PROMPT_SHA" ]; then
171
+ printf '{"ts":"%s","monster_id":"%s","event":"%s","session":"%s","meta":{"tail":"%s","prompt_sha256":"%s"}}\n' \
172
+ "$TS" "$ID" "$EVENT" "$SESSION" "$TAIL" "$PROMPT_SHA" >> "$EVENTS" 2>/dev/null
173
+ else
174
+ printf '{"ts":"%s","monster_id":"%s","event":"%s","session":"%s","meta":{"tail":"%s"}}\n' \
175
+ "$TS" "$ID" "$EVENT" "$SESSION" "$TAIL" >> "$EVENTS" 2>/dev/null
176
+ fi
177
+
178
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # supervisor-notify の 1 tick を回す薄いラッパ(launchd の ProgramArguments から呼ばれる)。
3
+ #
4
+ # 出典: docs/requirements-supervisor-notify.md §3.6(watcher は本パッケージの scripts/ に同梱)
5
+ # 設置手順: docs/supervisor-notify-launchd.md
6
+ #
7
+ # launchd は PATH を /usr/bin:/bin:/usr/sbin:/sbin しか持たないので node を自力で探す。
8
+ # Homebrew の node(/opt/homebrew/bin/node)が本機の実体。MONSTER_NODE_BIN で上書き可。
9
+ set -euo pipefail
10
+
11
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
12
+
13
+ resolve_node() {
14
+ if [ -n "${MONSTER_NODE_BIN:-}" ]; then
15
+ printf '%s\n' "$MONSTER_NODE_BIN"
16
+ return
17
+ fi
18
+ for candidate in /opt/homebrew/bin/node /usr/local/bin/node; do
19
+ [ -x "$candidate" ] && printf '%s\n' "$candidate" && return
20
+ done
21
+ command -v node
22
+ }
23
+
24
+ NODE_BIN="$(resolve_node)"
25
+ if [ ! -x "$NODE_BIN" ]; then
26
+ echo "supervisor-notify: node が見つかりません(MONSTER_NODE_BIN で指定してください)" >&2
27
+ exit 1
28
+ fi
29
+
30
+ # 配布物(dist-obf)を優先し、開発中の tsc 出力(dist)にフォールバックする
31
+ for entry in "$ROOT/dist-obf/supervisor-notify-tick.js" "$ROOT/dist/supervisor-notify-tick.js"; do
32
+ if [ -f "$entry" ]; then
33
+ exec "$NODE_BIN" "$entry"
34
+ fi
35
+ done
36
+
37
+ echo "supervisor-notify: tick 本体が見つかりません(npm run build または npm run build:obfuscated を実行してください)" >&2
38
+ exit 1