instar 1.2.54 → 1.2.55
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/skills/autonomous/hooks/autonomous-stop-hook.sh +232 -100
- package/.claude/skills/autonomous/scripts/setup-autonomous.sh +6 -0
- package/dist/core/PostUpdateMigrator.d.ts +19 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +45 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +17 -17
- package/upgrades/1.2.55.md +106 -0
- package/upgrades/side-effects/autonomous-topic-keyed-identity.md +105 -0
|
@@ -4,167 +4,306 @@
|
|
|
4
4
|
# Prevents session exit when autonomous mode is active.
|
|
5
5
|
# Feeds the goal and task list back to continue working.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# TOPIC-KEYED OWNERSHIP (primary): the autonomous job is identified by the
|
|
8
|
+
# TOPIC it serves, not by the Claude session UUID. The topic is a stable
|
|
9
|
+
# "street address" — when a session hits the memory limit and restarts, its
|
|
10
|
+
# UUID rotates (a new badge) but instar respawns it into the SAME tmux session
|
|
11
|
+
# name, and the topic-session registry still maps that name to the same topic.
|
|
12
|
+
# So whoever is running in the topic's session is recognized as continuing the
|
|
13
|
+
# job, restart or no restart. This fixes the silent-failure bug where a restart
|
|
14
|
+
# rotated the UUID, mismatched the state file, and let autonomy die unnoticed.
|
|
9
15
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# -
|
|
13
|
-
#
|
|
14
|
-
#
|
|
16
|
+
# LIVENESS-GATED BACKSTOP (rare): when topic resolution is unavailable (no tmux,
|
|
17
|
+
# or the topic isn't in the registry) the hook falls back to session-id matching.
|
|
18
|
+
# A session-id mismatch is then gated by a liveness check on the recorded owner
|
|
19
|
+
# (is its transcript still growing?) — a dead owner is adopted, a live one is
|
|
20
|
+
# left alone. This is the demoted role of the old liveness idea: a thin edge
|
|
21
|
+
# guard, not the main mechanism.
|
|
22
|
+
#
|
|
23
|
+
# RECOVERY NOTE: on an actual restart-and-resume (topic verified, but the
|
|
24
|
+
# session UUID changed) the hook emits ONE user-facing one-line note and an
|
|
25
|
+
# audit record, then records the new UUID so the note never repeats.
|
|
26
|
+
#
|
|
27
|
+
# RESPECTS: emergency stop, duration expiry, genuine completion (promise).
|
|
15
28
|
|
|
16
|
-
set -
|
|
29
|
+
set -uo pipefail # NOTE: -e intentionally omitted; field lookups for optional
|
|
30
|
+
# frontmatter keys are expected to "fail" (grep finds nothing)
|
|
31
|
+
# and must not abort the hook. Each critical step guards itself.
|
|
17
32
|
|
|
18
33
|
# Read hook input from stdin
|
|
19
34
|
HOOK_INPUT=$(cat)
|
|
20
35
|
|
|
21
|
-
# Check if autonomous mode is active
|
|
22
36
|
STATE_FILE=".instar/autonomous-state.local.md"
|
|
37
|
+
REGISTRY_FILE=".instar/topic-session-registry.json"
|
|
38
|
+
RECOVERY_AUDIT=".instar/autonomous-recovery.jsonl"
|
|
39
|
+
LIVENESS_SECS="${INSTAR_AUTONOMOUS_LIVENESS_SECS:-120}"
|
|
23
40
|
|
|
24
41
|
if [[ ! -f "$STATE_FILE" ]]; then
|
|
25
42
|
# No active autonomous session — allow exit
|
|
26
43
|
exit 0
|
|
27
44
|
fi
|
|
28
45
|
|
|
29
|
-
# Parse YAML frontmatter
|
|
46
|
+
# Parse YAML frontmatter (between the first two --- lines)
|
|
30
47
|
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
# Safe frontmatter field reader — never trips pipefail when a key is absent.
|
|
50
|
+
fm_get() {
|
|
51
|
+
local key="$1"
|
|
52
|
+
printf '%s\n' "$FRONTMATTER" | grep "^${key}:" | head -1 | sed "s/^${key}: *//" | tr -d '"' || true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ACTIVE=$(fm_get active)
|
|
33
56
|
if [[ "$ACTIVE" != "true" ]]; then
|
|
34
57
|
exit 0
|
|
35
58
|
fi
|
|
36
59
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
STATE_SESSION=$(echo "$FRONTMATTER" | grep '^session_id:' | sed 's/^session_id: *//' | tr -d '"' || true)
|
|
60
|
+
# ── Inputs from the hook ──────────────────────────────────────────────
|
|
61
|
+
HOOK_SESSION=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
|
62
|
+
TRANSCRIPT_PATH=$(printf '%s' "$HOOK_INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
41
63
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
# If hook has no session_id → fail OPEN (unknown context, don't trap)
|
|
65
|
+
if [[ -z "$HOOK_SESSION" ]]; then
|
|
66
|
+
echo "⚠️ Autonomous mode: No session_id in hook input — fail-open (allowing exit)" >&2
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── State fields ──────────────────────────────────────────────────────
|
|
71
|
+
REPORT_TOPIC=$(fm_get report_topic)
|
|
72
|
+
# Channel that owns this job — recovery note routes here. Default telegram for
|
|
73
|
+
# back-compat (state files written before channel-neutral delivery existed).
|
|
74
|
+
REPORT_CHANNEL=$(fm_get report_channel)
|
|
75
|
+
[[ -z "$REPORT_CHANNEL" ]] && REPORT_CHANNEL="telegram"
|
|
76
|
+
STATE_SESSION=$(fm_get session_id)
|
|
77
|
+
ITERATION=$(fm_get iteration)
|
|
78
|
+
DURATION_SECONDS=$(fm_get duration_seconds)
|
|
79
|
+
STARTED_AT=$(fm_get started_at)
|
|
80
|
+
COMPLETION_PROMISE=$(fm_get completion_promise)
|
|
81
|
+
|
|
82
|
+
# Validate recorded session_id is a real UUID. Claude sometimes writes a custom
|
|
83
|
+
# string instead of $CLAUDE_CODE_SESSION_ID; non-UUID values are treated as
|
|
84
|
+
# empty so the session-match backstop self-bootstraps from the real UUID.
|
|
47
85
|
UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
48
86
|
if [[ -n "$STATE_SESSION" ]] && ! [[ "$STATE_SESSION" =~ $UUID_REGEX ]]; then
|
|
49
|
-
echo "[autonomous] Invalid session_id in state file (not UUID): '$STATE_SESSION' —
|
|
87
|
+
echo "[autonomous] Invalid session_id in state file (not UUID): '$STATE_SESSION' — clearing" >&2
|
|
50
88
|
STATE_SESSION=""
|
|
51
89
|
fi
|
|
52
90
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
# ── Resolve MY topic (the stable address) ─────────────────────────────
|
|
92
|
+
# Test/override seam: INSTAR_HOOK_TMUX_SESSION (if the var is set at all, even
|
|
93
|
+
# empty, it wins — empty means "no tmux"). INSTAR_HOOK_NO_TMUX=1 forces empty.
|
|
94
|
+
resolve_my_tmux() {
|
|
95
|
+
if [[ "${INSTAR_HOOK_NO_TMUX:-}" == "1" ]]; then
|
|
96
|
+
echo ""
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
if [[ -n "${INSTAR_HOOK_TMUX_SESSION+x}" ]]; then
|
|
100
|
+
echo "${INSTAR_HOOK_TMUX_SESSION}"
|
|
101
|
+
return
|
|
102
|
+
fi
|
|
103
|
+
tmux display-message -p '#S' 2>/dev/null || echo ""
|
|
104
|
+
}
|
|
105
|
+
MY_TMUX=$(resolve_my_tmux)
|
|
106
|
+
|
|
107
|
+
# Reverse-lookup: which tmux session owns REPORT_TOPIC per the registry?
|
|
108
|
+
OWNER_TMUX=""
|
|
109
|
+
if [[ -n "$REPORT_TOPIC" ]] && [[ -f "$REGISTRY_FILE" ]]; then
|
|
110
|
+
OWNER_TMUX=$(REPORT_TOPIC="$REPORT_TOPIC" python3 -c "
|
|
111
|
+
import json, os
|
|
112
|
+
try:
|
|
113
|
+
reg = json.load(open('$REGISTRY_FILE'))
|
|
114
|
+
print((reg.get('topicToSession') or {}).get(os.environ['REPORT_TOPIC'], ''))
|
|
115
|
+
except Exception:
|
|
116
|
+
print('')
|
|
117
|
+
" 2>/dev/null || echo "")
|
|
59
118
|
fi
|
|
60
119
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
# ── Ownership decision ────────────────────────────────────────────────
|
|
121
|
+
# OWNER=true means: this session IS the autonomous worker; block its exit.
|
|
122
|
+
# OWNER_METHOD records how we decided (topic | session | bootstrap | adopt-dead).
|
|
123
|
+
OWNER="false"
|
|
124
|
+
OWNER_METHOD=""
|
|
125
|
+
RESTART_DETECTED="false"
|
|
126
|
+
|
|
127
|
+
if [[ -n "$MY_TMUX" ]] && [[ -n "$OWNER_TMUX" ]]; then
|
|
128
|
+
# Topic resolution is conclusive.
|
|
129
|
+
if [[ "$MY_TMUX" == "$OWNER_TMUX" ]]; then
|
|
130
|
+
OWNER="true"; OWNER_METHOD="topic"
|
|
131
|
+
# Restart? Recorded UUID is a real UUID but differs from the live one.
|
|
132
|
+
if [[ -n "$STATE_SESSION" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
133
|
+
RESTART_DETECTED="true"
|
|
134
|
+
fi
|
|
135
|
+
else
|
|
136
|
+
# Registry says topic T is served by a different session → not me.
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
else
|
|
140
|
+
# ── Backstop: topic unresolved (no tmux, or topic not in registry) ──
|
|
141
|
+
if [[ -z "$STATE_SESSION" ]]; then
|
|
142
|
+
# Self-bootstrap: first session to fire claims the job.
|
|
143
|
+
OWNER="true"; OWNER_METHOD="bootstrap"
|
|
144
|
+
elif [[ "$STATE_SESSION" == "$HOOK_SESSION" ]]; then
|
|
145
|
+
OWNER="true"; OWNER_METHOD="session"
|
|
146
|
+
else
|
|
147
|
+
# Session-id mismatch with no topic signal. Gate on recorded owner liveness.
|
|
148
|
+
OWNER_ALIVE="false"
|
|
149
|
+
if [[ -n "$TRANSCRIPT_PATH" ]]; then
|
|
150
|
+
OWNER_TRANSCRIPT="$(dirname "$TRANSCRIPT_PATH")/${STATE_SESSION}.jsonl"
|
|
151
|
+
if [[ -f "$OWNER_TRANSCRIPT" ]]; then
|
|
152
|
+
NOW_E=$(date +%s)
|
|
153
|
+
# GNU stat (-c %Y) first — Linux is the common host (and CI). BSD stat
|
|
154
|
+
# (-f %m) second for macOS. NOTE: GNU `stat -f` is filesystem mode and
|
|
155
|
+
# SUCCEEDS with non-numeric output, so BSD-first would mask the GNU path
|
|
156
|
+
# on Linux and feed garbage into the arithmetic below. The numeric guard
|
|
157
|
+
# is the backstop: a non-numeric mtime is treated as 0 (very old → dead).
|
|
158
|
+
MTIME=$(stat -c %Y "$OWNER_TRANSCRIPT" 2>/dev/null || stat -f %m "$OWNER_TRANSCRIPT" 2>/dev/null || echo 0)
|
|
159
|
+
[[ "$MTIME" =~ ^[0-9]+$ ]] || MTIME=0
|
|
160
|
+
AGE=$(( NOW_E - MTIME ))
|
|
161
|
+
if [[ $MTIME -gt 0 ]] && [[ $AGE -lt $LIVENESS_SECS ]]; then
|
|
162
|
+
OWNER_ALIVE="true"
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
if [[ "$OWNER_ALIVE" == "true" ]]; then
|
|
167
|
+
# A genuinely different, live session owns this — don't steal.
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
# Recorded owner is dead/unknown → adopt the job.
|
|
171
|
+
OWNER="true"; OWNER_METHOD="adopt-dead"; RESTART_DETECTED="true"
|
|
172
|
+
fi
|
|
70
173
|
fi
|
|
71
174
|
|
|
72
|
-
|
|
73
|
-
if [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
175
|
+
if [[ "$OWNER" != "true" ]]; then
|
|
74
176
|
exit 0
|
|
75
177
|
fi
|
|
76
178
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
|
|
80
|
-
DURATION_SECONDS=$(echo "$FRONTMATTER" | grep '^duration_seconds:' | sed 's/duration_seconds: *//')
|
|
81
|
-
STARTED_AT=$(echo "$FRONTMATTER" | grep '^started_at:' | sed 's/started_at: *//' | tr -d '"')
|
|
82
|
-
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | tr -d '"')
|
|
83
|
-
REPORT_TOPIC=$(echo "$FRONTMATTER" | grep '^report_topic:' | sed 's/report_topic: *//' | tr -d '"')
|
|
179
|
+
# ── This IS the autonomous session. Terminal checks first. ────────────
|
|
84
180
|
|
|
85
181
|
# Validate iteration
|
|
86
182
|
if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
|
|
87
183
|
echo "⚠️ Autonomous mode: State file corrupted (bad iteration)" >&2
|
|
88
|
-
rm "$STATE_FILE"
|
|
184
|
+
rm -f "$STATE_FILE"
|
|
89
185
|
exit 0
|
|
90
186
|
fi
|
|
91
187
|
|
|
92
|
-
#
|
|
188
|
+
# Duration expiry. Fail-SAFE: if started_at can't be parsed (START_EPOCH falls
|
|
189
|
+
# back to 0/empty), do NOT expire — an unparseable timestamp must never cause a
|
|
190
|
+
# premature exit (that is the very failure class this hook exists to prevent).
|
|
191
|
+
REMAINING_MIN=""
|
|
93
192
|
if [[ "$DURATION_SECONDS" =~ ^[0-9]+$ ]] && [[ $DURATION_SECONDS -gt 0 ]]; then
|
|
94
|
-
START_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
193
|
+
START_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
194
|
+
if [[ "$START_EPOCH" =~ ^[0-9]+$ ]] && [[ $START_EPOCH -gt 0 ]]; then
|
|
195
|
+
NOW_EPOCH=$(date +%s)
|
|
196
|
+
ELAPSED=$(( NOW_EPOCH - START_EPOCH ))
|
|
197
|
+
if [[ $ELAPSED -ge $DURATION_SECONDS ]]; then
|
|
198
|
+
echo "⏰ Autonomous mode: Duration expired ($ELAPSED seconds elapsed)."
|
|
199
|
+
echo " Session is free to exit."
|
|
200
|
+
rm -f "$STATE_FILE"
|
|
201
|
+
exit 0
|
|
202
|
+
fi
|
|
203
|
+
REMAINING=$(( DURATION_SECONDS - ELAPSED ))
|
|
204
|
+
REMAINING_MIN=$(( REMAINING / 60 ))
|
|
205
|
+
else
|
|
206
|
+
echo "[autonomous] started_at unparseable ('$STARTED_AT') — skipping duration-expiry check (fail-safe: keep running)" >&2
|
|
102
207
|
fi
|
|
103
|
-
REMAINING=$(( DURATION_SECONDS - ELAPSED ))
|
|
104
|
-
REMAINING_MIN=$(( REMAINING / 60 ))
|
|
105
208
|
fi
|
|
106
209
|
|
|
107
|
-
#
|
|
108
|
-
# The MessageSentinel handles this at the messaging layer, but also check here
|
|
210
|
+
# Emergency stop
|
|
109
211
|
if [[ -f ".instar/autonomous-emergency-stop" ]]; then
|
|
110
212
|
echo "🛑 Autonomous mode: Emergency stop detected."
|
|
111
|
-
rm "$STATE_FILE"
|
|
213
|
+
rm -f "$STATE_FILE"
|
|
112
214
|
rm -f ".instar/autonomous-emergency-stop"
|
|
113
215
|
exit 0
|
|
114
216
|
fi
|
|
115
217
|
|
|
116
|
-
#
|
|
117
|
-
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path' 2>/dev/null || echo "")
|
|
118
|
-
|
|
218
|
+
# Completion promise (genuine completion)
|
|
119
219
|
if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
120
|
-
# Check last assistant message for completion promise
|
|
121
220
|
LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 || echo "")
|
|
122
|
-
|
|
123
221
|
if [[ -n "$LAST_LINE" ]]; then
|
|
124
|
-
LAST_OUTPUT=$(
|
|
125
|
-
.message.content |
|
|
126
|
-
map(select(.type == "text")) |
|
|
127
|
-
map(.text) |
|
|
128
|
-
join("\n")
|
|
222
|
+
LAST_OUTPUT=$(printf '%s' "$LAST_LINE" | jq -r '
|
|
223
|
+
.message.content | map(select(.type == "text")) | map(.text) | join("\n")
|
|
129
224
|
' 2>/dev/null || echo "")
|
|
130
|
-
|
|
131
|
-
# Check for completion promise in <promise> tags
|
|
132
225
|
if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
|
|
133
|
-
PROMISE_TEXT=$(
|
|
134
|
-
|
|
226
|
+
PROMISE_TEXT=$(printf '%s' "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
|
|
135
227
|
if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
|
|
136
228
|
echo "✅ Autonomous mode: Completion promise detected — <promise>$COMPLETION_PROMISE</promise>"
|
|
137
229
|
echo " Session is free to exit. Good work!"
|
|
138
|
-
rm "$STATE_FILE"
|
|
230
|
+
rm -f "$STATE_FILE"
|
|
139
231
|
exit 0
|
|
140
232
|
fi
|
|
141
233
|
fi
|
|
142
234
|
fi
|
|
143
235
|
fi
|
|
144
236
|
|
|
145
|
-
# Not
|
|
237
|
+
# ── Not terminal: we are continuing. Handle restart-resume recovery note. ──
|
|
238
|
+
# Always reconcile the recorded session_id to the live one (so the backstop and
|
|
239
|
+
# restart-detection stay accurate). Emit the one-line note exactly once, only
|
|
240
|
+
# when a real restart was detected (topic-verified or dead-owner adoption).
|
|
241
|
+
record_session_id() {
|
|
242
|
+
local new_id="$1"
|
|
243
|
+
local tmp="${STATE_FILE}.sid.$$"
|
|
244
|
+
if grep -q '^session_id:' "$STATE_FILE"; then
|
|
245
|
+
sed "s/^session_id:.*/session_id: \"${new_id}\"/" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
|
|
246
|
+
fi
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Channel-neutral delivery seam. The hook does NOT assume Telegram — it routes
|
|
250
|
+
# the note to whatever channel owns the job. Telegram is wired here; the other
|
|
251
|
+
# channels are owned by the unified notification layer tracked in the Channel
|
|
252
|
+
# Parity initiative. Either way the durable audit record below is the source of
|
|
253
|
+
# truth, so a not-yet-wired channel never silently misfires to Telegram.
|
|
254
|
+
deliver_recovery_note() {
|
|
255
|
+
local channel="$1" target="$2" text="$3"
|
|
256
|
+
[[ -z "$target" ]] && return 0
|
|
257
|
+
case "$channel" in
|
|
258
|
+
telegram)
|
|
259
|
+
if [[ -x ".instar/scripts/telegram-reply.sh" ]]; then
|
|
260
|
+
printf '%s\n' "$text" | .instar/scripts/telegram-reply.sh "$target" >/dev/null 2>&1 || true
|
|
261
|
+
elif [[ -x ".claude/scripts/telegram-reply.sh" ]]; then
|
|
262
|
+
printf '%s\n' "$text" | .claude/scripts/telegram-reply.sh "$target" >/dev/null 2>&1 || true
|
|
263
|
+
fi
|
|
264
|
+
;;
|
|
265
|
+
*)
|
|
266
|
+
# slack | whatsapp | imessage | future — delivered by the unified notify
|
|
267
|
+
# layer (Channel Parity initiative). Recorded to the audit trail above.
|
|
268
|
+
echo "[autonomous] recovery note for channel '$channel' recorded to audit; live delivery pending the Channel Parity initiative" >&2
|
|
269
|
+
;;
|
|
270
|
+
esac
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if [[ "$RESTART_DETECTED" == "true" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
274
|
+
ITER_LABEL="${ITERATION:-?}"
|
|
275
|
+
NOTE="Heads up — my session restarted mid-run and I've picked the autonomous job back up (topic ${REPORT_TOPIC:-?}, iteration ${ITER_LABEL}). No action needed."
|
|
276
|
+
# Durable, channel-neutral audit record (always).
|
|
277
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
278
|
+
printf '{"ts":"%s","event":"restart-resume","channel":"%s","topic":"%s","oldSession":"%s","newSession":"%s","method":"%s","iteration":"%s"}\n' \
|
|
279
|
+
"$TS" "$REPORT_CHANNEL" "${REPORT_TOPIC:-}" "$STATE_SESSION" "$HOOK_SESSION" "$OWNER_METHOD" "$ITER_LABEL" >> "$RECOVERY_AUDIT" 2>/dev/null || true
|
|
280
|
+
# Best-effort user-facing delivery via the channel that owns the job.
|
|
281
|
+
deliver_recovery_note "$REPORT_CHANNEL" "$REPORT_TOPIC" "$NOTE"
|
|
282
|
+
echo "[autonomous] restart-resume: channel=$REPORT_CHANNEL topic=${REPORT_TOPIC:-?} old=$STATE_SESSION new=$HOOK_SESSION method=$OWNER_METHOD" >&2
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# Reconcile recorded session_id to live (covers restart, bootstrap, adopt).
|
|
286
|
+
if [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
287
|
+
record_session_id "$HOOK_SESSION"
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# ── Continue the job: increment iteration, feed the task back. ────────
|
|
146
291
|
NEXT_ITERATION=$((ITERATION + 1))
|
|
147
292
|
|
|
148
|
-
# Extract prompt (everything after closing ---)
|
|
149
293
|
PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE")
|
|
150
|
-
|
|
151
294
|
if [[ -z "$PROMPT_TEXT" ]]; then
|
|
152
295
|
echo "⚠️ Autonomous mode: State file has no task content" >&2
|
|
153
|
-
rm "$STATE_FILE"
|
|
296
|
+
rm -f "$STATE_FILE"
|
|
154
297
|
exit 0
|
|
155
298
|
fi
|
|
156
299
|
|
|
157
|
-
|
|
158
|
-
TEMP_FILE
|
|
159
|
-
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE"
|
|
160
|
-
mv "$TEMP_FILE" "$STATE_FILE"
|
|
300
|
+
TEMP_FILE="${STATE_FILE}.iter.$$"
|
|
301
|
+
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE" && mv "$TEMP_FILE" "$STATE_FILE"
|
|
161
302
|
|
|
162
303
|
# ── Progress Report Check ──
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
LAST_REPORT_AT=$(echo "$FRONTMATTER" | grep '^last_report_at:' | sed 's/last_report_at: *//' | tr -d '"')
|
|
304
|
+
REPORT_INTERVAL=$(fm_get report_interval)
|
|
305
|
+
LAST_REPORT_AT=$(fm_get last_report_at)
|
|
166
306
|
|
|
167
|
-
# Convert report interval to seconds
|
|
168
307
|
REPORT_INTERVAL_SECS=1800 # default 30 minutes
|
|
169
308
|
if [[ "$REPORT_INTERVAL" =~ ^([0-9]+)m$ ]]; then
|
|
170
309
|
REPORT_INTERVAL_SECS=$(( ${BASH_REMATCH[1]} * 60 ))
|
|
@@ -174,37 +313,30 @@ fi
|
|
|
174
313
|
|
|
175
314
|
REPORT_DUE="false"
|
|
176
315
|
NOW_EPOCH=$(date +%s)
|
|
177
|
-
|
|
178
|
-
if [[ -z "$LAST_REPORT_AT" ]] || [[ "$LAST_REPORT_AT" == "null" ]] || [[ "$LAST_REPORT_AT" == '""' ]]; then
|
|
179
|
-
# No report sent yet — due if we've been running for at least one interval
|
|
316
|
+
if [[ -z "$LAST_REPORT_AT" ]] || [[ "$LAST_REPORT_AT" == "null" ]]; then
|
|
180
317
|
if [[ -n "$STARTED_AT" ]]; then
|
|
181
|
-
START_EPOCH_R=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
318
|
+
START_EPOCH_R=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
182
319
|
ELAPSED_SINCE_START=$(( NOW_EPOCH - START_EPOCH_R ))
|
|
183
320
|
if [[ $ELAPSED_SINCE_START -ge $REPORT_INTERVAL_SECS ]]; then
|
|
184
321
|
REPORT_DUE="true"
|
|
185
322
|
fi
|
|
186
323
|
fi
|
|
187
324
|
else
|
|
188
|
-
|
|
189
|
-
LAST_REPORT_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_REPORT_AT" +%s 2>/dev/null || date -d "$LAST_REPORT_AT" +%s 2>/dev/null || echo "0")
|
|
325
|
+
LAST_REPORT_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_REPORT_AT" +%s 2>/dev/null || date -d "$LAST_REPORT_AT" +%s 2>/dev/null || echo "0")
|
|
190
326
|
ELAPSED_SINCE_REPORT=$(( NOW_EPOCH - LAST_REPORT_EPOCH ))
|
|
191
327
|
if [[ $ELAPSED_SINCE_REPORT -ge $REPORT_INTERVAL_SECS ]]; then
|
|
192
328
|
REPORT_DUE="true"
|
|
193
329
|
fi
|
|
194
330
|
fi
|
|
195
331
|
|
|
196
|
-
# If report is due, update last_report_at in state file
|
|
197
332
|
REPORT_DIRECTIVE=""
|
|
198
333
|
if [[ "$REPORT_DUE" == "true" ]]; then
|
|
199
334
|
REPORT_NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
200
|
-
# Update or add last_report_at in frontmatter
|
|
201
335
|
if grep -q '^last_report_at:' "$STATE_FILE"; then
|
|
202
|
-
TEMP_FILE2="${STATE_FILE}.
|
|
203
|
-
sed "s/^last_report_at: .*/last_report_at: \"$REPORT_NOW\"/" "$STATE_FILE" > "$TEMP_FILE2"
|
|
204
|
-
mv "$TEMP_FILE2" "$STATE_FILE"
|
|
336
|
+
TEMP_FILE2="${STATE_FILE}.rpt.$$"
|
|
337
|
+
sed "s/^last_report_at: .*/last_report_at: \"$REPORT_NOW\"/" "$STATE_FILE" > "$TEMP_FILE2" && mv "$TEMP_FILE2" "$STATE_FILE"
|
|
205
338
|
else
|
|
206
|
-
|
|
207
|
-
TEMP_FILE2="${STATE_FILE}.tmp2.$$"
|
|
339
|
+
TEMP_FILE2="${STATE_FILE}.rpt.$$"
|
|
208
340
|
sed "0,/^---$/! { /^---$/i\\
|
|
209
341
|
last_report_at: \"$REPORT_NOW\"
|
|
210
342
|
}" "$STATE_FILE" > "$TEMP_FILE2" 2>/dev/null && mv "$TEMP_FILE2" "$STATE_FILE" || true
|
|
@@ -10,6 +10,7 @@ set -euo pipefail
|
|
|
10
10
|
GOAL=""
|
|
11
11
|
DURATION="4h"
|
|
12
12
|
REPORT_TOPIC=""
|
|
13
|
+
REPORT_CHANNEL="telegram" # channel that owns this job; recovery note routes here (telegram|slack|whatsapp|imessage)
|
|
13
14
|
LEVEL_UP="false"
|
|
14
15
|
TASKS=""
|
|
15
16
|
COMPLETION_PROMISE=""
|
|
@@ -29,6 +30,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
29
30
|
REPORT_TOPIC="$2"
|
|
30
31
|
shift 2
|
|
31
32
|
;;
|
|
33
|
+
--report-channel)
|
|
34
|
+
REPORT_CHANNEL="$2"
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
32
37
|
--level-up)
|
|
33
38
|
LEVEL_UP="true"
|
|
34
39
|
shift
|
|
@@ -102,6 +107,7 @@ duration_seconds: $DURATION_SECONDS
|
|
|
102
107
|
started_at: "$STARTED_AT"
|
|
103
108
|
end_at: "$END_AT"
|
|
104
109
|
report_topic: "$REPORT_TOPIC"
|
|
110
|
+
report_channel: "$REPORT_CHANNEL"
|
|
105
111
|
report_interval: "$REPORT_INTERVAL"
|
|
106
112
|
last_report_at: ""
|
|
107
113
|
level_up: $LEVEL_UP
|
|
@@ -157,6 +157,25 @@ export declare class PostUpdateMigrator {
|
|
|
157
157
|
* longer matches the stock fingerprint is left untouched.
|
|
158
158
|
*/
|
|
159
159
|
private migrateBuildSkillMethodology;
|
|
160
|
+
/**
|
|
161
|
+
* Update the deployed autonomous stop hook to the topic-keyed version.
|
|
162
|
+
*
|
|
163
|
+
* The old hook keyed autonomous-session ownership on the Claude session UUID;
|
|
164
|
+
* a memory-limit restart rotated the UUID, mismatched the state file, and let
|
|
165
|
+
* the still-running session exit — autonomy died silently. The new hook keys
|
|
166
|
+
* on the TOPIC (a stable address that survives restarts), demotes session-id
|
|
167
|
+
* matching to a liveness-gated backstop, and emits a one-line recovery note.
|
|
168
|
+
*
|
|
169
|
+
* installAutonomousSkill() is install-if-missing, so existing agents never get
|
|
170
|
+
* this through init — a dedicated migration is the only path (Migration Parity
|
|
171
|
+
* Standard, "updating existing skill content").
|
|
172
|
+
*
|
|
173
|
+
* Idempotent + conservative: only re-copies the bundled hook when the installed
|
|
174
|
+
* copy (a) lacks the new "topic-session-registry" marker AND (b) still looks
|
|
175
|
+
* like the stock hook (contains "Autonomous Mode Stop Hook"). A customized hook
|
|
176
|
+
* that no longer matches the stock fingerprint is left untouched.
|
|
177
|
+
*/
|
|
178
|
+
private migrateAutonomousStopHookTopicKeyed;
|
|
160
179
|
/**
|
|
161
180
|
* Deploy any missing built-in skills (e.g., guardian job skills added after initial setup).
|
|
162
181
|
* Non-destructive — only writes SKILL.md files that don't already exist.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAiCH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kCAAkC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAiB;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU,CAAiC;gBAEvC,MAAM,EAAE,cAAc;IAIlC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAItC;;;;;;OAMG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,qBAAqB,CAAC;IAIjC,OAAO,CAAC,aAAa;IAOrB;;;OAGG;IACH,OAAO,IAAI,eAAe;
|
|
1
|
+
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -142,6 +142,7 @@ export class PostUpdateMigrator {
|
|
|
142
142
|
this.autoMigrateLegacyJobsJson(result);
|
|
143
143
|
this.migrateSkillPortHardcoding(result);
|
|
144
144
|
this.migrateBuildSkillMethodology(result);
|
|
145
|
+
this.migrateAutonomousStopHookTopicKeyed(result);
|
|
145
146
|
this.migrateSelfKnowledgeTree(result);
|
|
146
147
|
this.migrateSoulMd(result);
|
|
147
148
|
this.migrateAgentMdSections(result);
|
|
@@ -1103,6 +1104,50 @@ export class PostUpdateMigrator {
|
|
|
1103
1104
|
result.errors.push(`skills/build/SKILL.md methodology migration: ${err instanceof Error ? err.message : String(err)}`);
|
|
1104
1105
|
}
|
|
1105
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Update the deployed autonomous stop hook to the topic-keyed version.
|
|
1109
|
+
*
|
|
1110
|
+
* The old hook keyed autonomous-session ownership on the Claude session UUID;
|
|
1111
|
+
* a memory-limit restart rotated the UUID, mismatched the state file, and let
|
|
1112
|
+
* the still-running session exit — autonomy died silently. The new hook keys
|
|
1113
|
+
* on the TOPIC (a stable address that survives restarts), demotes session-id
|
|
1114
|
+
* matching to a liveness-gated backstop, and emits a one-line recovery note.
|
|
1115
|
+
*
|
|
1116
|
+
* installAutonomousSkill() is install-if-missing, so existing agents never get
|
|
1117
|
+
* this through init — a dedicated migration is the only path (Migration Parity
|
|
1118
|
+
* Standard, "updating existing skill content").
|
|
1119
|
+
*
|
|
1120
|
+
* Idempotent + conservative: only re-copies the bundled hook when the installed
|
|
1121
|
+
* copy (a) lacks the new "topic-session-registry" marker AND (b) still looks
|
|
1122
|
+
* like the stock hook (contains "Autonomous Mode Stop Hook"). A customized hook
|
|
1123
|
+
* that no longer matches the stock fingerprint is left untouched.
|
|
1124
|
+
*/
|
|
1125
|
+
migrateAutonomousStopHookTopicKeyed(result) {
|
|
1126
|
+
try {
|
|
1127
|
+
const deployed = path.join(this.config.projectDir, '.claude', 'skills', 'autonomous', 'hooks', 'autonomous-stop-hook.sh');
|
|
1128
|
+
if (!fs.existsSync(deployed))
|
|
1129
|
+
return; // installAutonomousSkill handles fresh installs
|
|
1130
|
+
const current = fs.readFileSync(deployed, 'utf8');
|
|
1131
|
+
if (current.includes('topic-session-registry'))
|
|
1132
|
+
return; // already topic-keyed — idempotent
|
|
1133
|
+
if (!current.includes('Autonomous Mode Stop Hook')) {
|
|
1134
|
+
result.skipped.push('skills/autonomous/hooks/autonomous-stop-hook.sh: customized — left untouched');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const bundled = path.join(__dirname, '..', '..', '.claude', 'skills', 'autonomous', 'hooks', 'autonomous-stop-hook.sh');
|
|
1138
|
+
if (!fs.existsSync(bundled))
|
|
1139
|
+
return;
|
|
1140
|
+
const next = fs.readFileSync(bundled, 'utf8');
|
|
1141
|
+
if (next.includes('topic-session-registry')) {
|
|
1142
|
+
fs.writeFileSync(deployed, next);
|
|
1143
|
+
fs.chmodSync(deployed, 0o755);
|
|
1144
|
+
result.upgraded.push('skills/autonomous/hooks/autonomous-stop-hook.sh (topic-keyed ownership + recovery note)');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
result.errors.push(`autonomous stop hook topic-keying migration: ${err instanceof Error ? err.message : String(err)}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1106
1151
|
/**
|
|
1107
1152
|
* Deploy any missing built-in skills (e.g., guardian job skills added after initial setup).
|
|
1108
1153
|
* Non-destructive — only writes SKILL.md files that don't already exist.
|