@trygocode/notify 0.1.6 → 0.3.1

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.
@@ -1,21 +1,341 @@
1
1
  # gocode-notify — Ralph/Homer loop opt-in snippet (PRD §5.6, trigger C)
2
2
  #
3
3
  # OPT-IN. This snippet is NOT auto-injected by `gocode-notify setup`; the
4
- # installer never edits your loop scripts without consent. Paste these two
5
- # lines into the completion/halt path of any loop you control (this repo's
6
- # `ralph`/`homer` skills, Geoffrey Huntley's `while :; do … done` one-liner,
7
- # or your own driver) to get a phone push when the loop finishes or halts.
4
+ # installer never edits your loop scripts without consent. Paste the relevant
5
+ # stanzas into the completion/halt/stall paths of any loop you control (this
6
+ # repo's `ralph`/`homer` skills, Geoffrey Huntley's `while :; do … done`
7
+ # one-liner, or your own driver) to get phone pushes for the right events.
8
8
  #
9
9
  # Prereq: you've already paired this machine once with
10
10
  # npx @trygocode/notify@latest login --code <CODE>
11
11
  # (get <CODE> from the GoCode app → "Connect a coding agent").
12
12
  #
13
- # Both lines are fire-and-forget: the `|| true` guarantees a failed/slow push
13
+ # All calls are fire-and-forget: the `|| true` guarantees a failed/slow push
14
14
  # can never block or fail your loop (the CLI also self-times-out in 5s).
15
+ #
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ # KIND CONTRACT (PRD §4 "Notify Human-Gating")
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+ #
20
+ # The *kind* is the contract. The GoCode server classifies every kind into
21
+ # exactly one push-worthiness bucket. Callers only need to emit the RIGHT kind;
22
+ # the server handles foreground-suppression, stall-dedup, and FCM delivery.
23
+ #
24
+ # Kind Push-worthy? When to emit
25
+ # ───────────────── ──────────── ────────────────────────────────────────
26
+ # loop_completed ✅ YES Loop finished all work cleanly.
27
+ # ralph_completed ✅ YES Ralph-specific alias for loop_completed.
28
+ # loop_halted ✅ YES Loop paused — a HUMAN is needed.
29
+ # ralph_halted ✅ YES Ralph-specific alias for loop_halted.
30
+ # Covers: user_blocking question, max
31
+ # consecutive failures.
32
+ # ralph_synth_question ✅ YES Setup question raised BEFORE the loop
33
+ # starts — genuinely needs a human.
34
+ # ralph_waiting ✅ EDGE ONLY Offline / quota stall. Emit ONLY on the
35
+ # stall EDGE (first failure). Server drops
36
+ # repeats until a resumed/completed/halted
37
+ # re-arms the edge. See pattern below.
38
+ # ralph_resumed 🔕 NO push Stall recovered; loop running again.
39
+ # Silent control event: its only effect is
40
+ # to reset the server-side stall state
41
+ # machine back to ARMED so the NEXT genuine
42
+ # stall edge will push again.
43
+ # ralph_question 🔕 NO push In-loop question the Oracle answers
44
+ # autonomously. Server logs it for the
45
+ # dashboard but NEVER sends to FCM. Do NOT
46
+ # emit this on oracle-answerable paths.
47
+ # ralph_advanced 🔕 NO push Queue advanced to next PRD (info only).
48
+ # finished ✅ YES Generic agent-finished (non-loop hooks).
49
+ # awaiting_input ✅ YES Generic agent needs the human (non-loop).
50
+ # error ✅ YES Generic agent hit an error.
51
+ #
52
+ # Rule: only emit push-worthy kinds on the paths where a HUMAN is genuinely
53
+ # needed. Oracle-answerable questions and routine loop events must be silent.
54
+ #
55
+ # ─────────────────────────────────────────────────────────────────────────────
56
+ # PATTERN 1 — Loop completion (all work done cleanly)
57
+ # ─────────────────────────────────────────────────────────────────────────────
58
+
59
+ # Use loop_completed (or ralph_completed for Ralph-specific loops).
60
+ gocode-notify send --kind loop_completed --source ralph \
61
+ --project "$(basename "$PWD")" || true
62
+
63
+ # ─────────────────────────────────────────────────────────────────────────────
64
+ # PATTERN 2 — Loop halt (a HUMAN is needed — user_blocking / max-failures)
65
+ # ─────────────────────────────────────────────────────────────────────────────
66
+ #
67
+ # Emit ralph_halted ONLY when the loop is genuinely paused awaiting human action:
68
+ # - A user_blocking question was raised (the Oracle cannot answer it).
69
+ # - The loop hit max consecutive failures and stopped.
70
+ #
71
+ # Do NOT emit ralph_halted for oracle-answerable (blocking) questions — the
72
+ # Oracle handles those autonomously; emitting halted would be a false alarm.
73
+
74
+ gocode-notify send --kind ralph_halted --source ralph \
75
+ --project "$(basename "$PWD")" \
76
+ --title "Ralph halted — needs you" || true
77
+
78
+ # ─────────────────────────────────────────────────────────────────────────────
79
+ # PATTERN 3 — Offline / quota stall (edge-triggered, NOT every retry)
80
+ # ─────────────────────────────────────────────────────────────────────────────
81
+ #
82
+ # A loop that polls every ~60s on a network/quota outage must push ONCE when
83
+ # it first stalls, stay SILENT while still stalled, and push AGAIN only after
84
+ # it recovers and then stalls again. Never spam on every retry.
85
+ #
86
+ # Caller-side responsibility: only fire ralph_waiting on the EDGE (first stall),
87
+ # not on every subsequent retry. The server also deduplicates repeats as a
88
+ # defense-in-depth backstop.
89
+ #
90
+ # Shell pattern (adapt to your loop's retry logic):
91
+
92
+ _stall_notified=0 # reset to 0 at loop start or after recovery
93
+
94
+ # Inside your retry / offline-backoff loop:
95
+ if [[ "$_stall_notified" -eq 0 ]]; then
96
+ # First time we're stalled — notify on the edge.
97
+ gocode-notify send --kind ralph_waiting --source ralph \
98
+ --project "$(basename "$PWD")" \
99
+ --title "Ralph waiting — offline or quota" || true
100
+ _stall_notified=1
101
+ fi
102
+ sleep 60
103
+
104
+ # When the endpoint recovers — emit ralph_resumed to reset the server edge state.
105
+ # This re-arms the next genuine stall edge so it will push again.
106
+ # Then reset the caller-side flag.
107
+ _stall_notified=0
108
+ gocode-notify send --kind ralph_resumed --source ralph \
109
+ --project "$(basename "$PWD")" || true
110
+
111
+ # ─────────────────────────────────────────────────────────────────────────────
112
+ # PATTERN 4 — Oracle-answerable question (SILENT — do NOT push)
113
+ # ─────────────────────────────────────────────────────────────────────────────
114
+ #
115
+ # When the loop raises a question that the Oracle will answer autonomously
116
+ # (a normal "blocking" question, not "user_blocking"), emit NOTHING. The
117
+ # Oracle will handle it and the loop will continue with no human in the picture.
118
+ #
119
+ # Previously: some loops emitted `ralph_question` here. DO NOT do this —
120
+ # ralph_question is classified as a non-push info kind server-side (it is
121
+ # logged for the dashboard but never sent to FCM), and the caller should stop
122
+ # emitting it entirely on oracle-answerable paths to minimize noise.
123
+ #
124
+ # Summary:
125
+ # user_blocking halt → push (ralph_halted)
126
+ # oracle-answerable → push NOTHING (or at most ralph_question, which is
127
+ # server-side demoted to a silent info event)
128
+
129
+ # ─────────────────────────────────────────────────────────────────────────────
130
+ # PATTERN 5 — Crash-safe terminal notify via trap (PRD §8.6)
131
+ # ─────────────────────────────────────────────────────────────────────────────
132
+ #
133
+ # Problem: a loop killed by an unexpected signal (SIGTERM, SIGKILL, SIGINT),
134
+ # an unhandled error, or a shell `exit` on a non-zero status will terminate
135
+ # WITHOUT ever reaching the explicit halt/completed path — so the user's phone
136
+ # is never notified, leaving them unaware the loop stopped.
137
+ #
138
+ # Solution: register a bash `trap` at the TOP of your loop script that fires a
139
+ # best-effort `loop_halted` push on ANY unexpected exit. The `|| true` guard
140
+ # ensures the trap itself can never cause a secondary failure.
141
+ #
142
+ # HOW IT WORKS
143
+ # ─────────────
144
+ # 1. Set _LOOP_EXITED=0 at the top of your script (before the main loop).
145
+ # 2. At every NORMAL exit path (completed / halted / stall) flip the flag to 1
146
+ # BEFORE sending the final push, so the trap knows a push already went out.
147
+ # 3. The trap only fires a push when _LOOP_EXITED == 0 (i.e. the script reached
148
+ # the EXIT signal without a controlled terminal notification).
149
+ #
150
+ # SIGNALS COVERED
151
+ # ────────────────
152
+ # Bash's EXIT pseudo-signal fires on:
153
+ # • Normal exit (fall-through past the last line) ← covered by flag
154
+ # • `exit N` from any point in the script ← covered by flag
155
+ # • SIGTERM (graceful kill, e.g. `kill <pid>`) ← ✅ caught
156
+ # • SIGINT (Ctrl-C / terminal close) ← ✅ caught
157
+ # • Unhandled ERR (set -e + unset command) ← ✅ caught
158
+ #
159
+ # NOTE: SIGKILL (kill -9) cannot be trapped by any process — that's a kernel
160
+ # hard-kill. Nothing can be done for SIGKILL; document this for your users.
161
+ #
162
+ # ─── PASTE THIS BLOCK AT THE TOP OF YOUR LOOP SCRIPT ────────────────────────
163
+
164
+ # 1. Flag: 0 = no terminal push has gone out yet; 1 = push already sent.
165
+ _LOOP_EXITED=0
166
+
167
+ # 2. Crash-safe trap — fires on EXIT (normal fall-through, `exit N`, SIGTERM,
168
+ # SIGINT, unhandled ERR). Sends loop_halted if no controlled push went out.
169
+ _loop_crash_trap() {
170
+ if [[ "${_LOOP_EXITED:-0}" -eq 0 ]]; then
171
+ # Unexpected exit — best-effort push, never block.
172
+ gocode-notify send --kind loop_halted --source ralph \
173
+ --project "$(basename "$PWD")" \
174
+ --title "Loop exited unexpectedly — check logs" || true
175
+ fi
176
+ }
177
+ trap '_loop_crash_trap' EXIT
15
178
 
16
- # --- At loop completion (the loop finished all work cleanly) -----------------
17
- gocode-notify send --kind loop_completed --source ralph --project "$(basename "$PWD")" || true
179
+ # ─── END OF TOP-OF-SCRIPT BLOCK ──────────────────────────────────────────────
18
180
 
19
- # --- At loop halt (paused_max_failures / awaiting_human / question raised) ---
20
- gocode-notify send --kind loop_halted --source ralph --project "$(basename "$PWD")" \
181
+ # ─── AT EACH NORMAL TERMINAL EXIT PATH, set the flag THEN send the push ──────
182
+
183
+ # Example — loop completed cleanly:
184
+ _LOOP_EXITED=1
185
+ gocode-notify send --kind loop_completed --source ralph \
186
+ --project "$(basename "$PWD")" || true
187
+
188
+ # Example — loop halted (user_blocking / max-failures):
189
+ _LOOP_EXITED=1
190
+ gocode-notify send --kind ralph_halted --source ralph \
191
+ --project "$(basename "$PWD")" \
21
192
  --title "Ralph halted — needs you" || true
193
+
194
+ # Example — explicit clean exit (no push needed, but mark so trap is silent):
195
+ _LOOP_EXITED=1
196
+
197
+ # ─────────────────────────────────────────────────────────────────────────────
198
+ # MINIMAL COPY-PASTE (drop-in for any loop script)
199
+ # ─────────────────────────────────────────────────────────────────────────────
200
+ #
201
+ # Add these 6 lines to the top of your script and the trap handles the rest:
202
+ #
203
+ # _LOOP_EXITED=0
204
+ # _loop_crash_trap() {
205
+ # [[ "${_LOOP_EXITED:-0}" -eq 0 ]] && \
206
+ # gocode-notify send --kind loop_halted --source ralph \
207
+ # --project "$(basename "$PWD")" \
208
+ # --title "Loop exited unexpectedly — check logs" || true
209
+ # }
210
+ # trap '_loop_crash_trap' EXIT
211
+ #
212
+ # Then at every terminal exit path, set _LOOP_EXITED=1 before the final push.
213
+
214
+ # ─────────────────────────────────────────────────────────────────────────────
215
+ # PATTERN 6 — `terminal_halt` helper (PRD §8.7 R12)
216
+ # ─────────────────────────────────────────────────────────────────────────────
217
+ #
218
+ # PROBLEM this solves:
219
+ # When a loop has many exit paths (task-complete, user_blocking halt,
220
+ # max-consecutive-failures, stall, explicit stop) each one must:
221
+ # 1. Set _LOOP_EXITED=1 (so the crash trap stays silent)
222
+ # 2. Send the right push kind
223
+ # 3. Break out of the main loop
224
+ # Duplicating those 3 steps at every exit point is error-prone — it is easy
225
+ # to add a new `break` or `exit` path and forget the push.
226
+ #
227
+ # SOLUTION — one-call `terminal_halt <kind> <detail>`:
228
+ # Centralise the 3 steps in a single helper. Every exit path calls the
229
+ # helper; the push is guaranteed by construction.
230
+ #
231
+ # TERMINAL KINDS — the three categories an exit can belong to:
232
+ #
233
+ # ┌──────────────────────┬────────────────────────────────────────────────┐
234
+ # │ Kind │ When to use │
235
+ # ├──────────────────────┼────────────────────────────────────────────────┤
236
+ # │ loop_halted │ Loop stopped; a HUMAN is needed. │
237
+ # │ (ralph_halted) │ • user_blocking question raised │
238
+ # │ │ • max consecutive failures reached │
239
+ # │ │ • explicit operator stop (.homer/STOP file) │
240
+ # ├──────────────────────┼────────────────────────────────────────────────┤
241
+ # │ loop_completed │ Loop finished all work cleanly — all tasks │
242
+ # │ (ralph_completed) │ checked off or tracker fully satisfied. │
243
+ # ├──────────────────────┼────────────────────────────────────────────────┤
244
+ # │ ralph_waiting │ Loop cannot proceed right now (offline / │
245
+ # │ │ quota stall) — emit on the EDGE only (first │
246
+ # │ │ stall). Use PATTERN 3's _stall_notified guard │
247
+ # │ │ before calling terminal_halt with this kind. │
248
+ # └──────────────────────┴────────────────────────────────────────────────┘
249
+ #
250
+ # SIGNATURE:
251
+ # terminal_halt <kind> <detail>
252
+ #
253
+ # <kind> — one of the terminal kinds above (loop_halted, loop_completed,
254
+ # ralph_halted, ralph_completed, ralph_waiting, …)
255
+ # <detail> — short human-readable description that becomes --title / body text
256
+ # on the push notification. Keep it under 80 chars.
257
+ #
258
+ # HOW TO USE — paste the function definition at the top of your loop script
259
+ # (right after the _LOOP_EXITED + trap block), then call `terminal_halt` at
260
+ # every exit point instead of inline _LOOP_EXITED=1 + gocode-notify + break.
261
+ #
262
+ # ─── PASTE THIS FUNCTION DEFINITION AT THE TOP OF YOUR LOOP SCRIPT ───────────
263
+
264
+ terminal_halt() {
265
+ # terminal_halt <kind> <detail>
266
+ # Marks the loop as exiting (silences crash trap), sends the push, then
267
+ # breaks out of the enclosing loop. Safe to call from inside `while`/`for`.
268
+ local _th_kind="${1:-loop_halted}"
269
+ local _th_detail="${2:-Loop exited}"
270
+ _LOOP_EXITED=1
271
+ gocode-notify send --kind "$_th_kind" --source ralph \
272
+ --project "$(basename "$PWD")" \
273
+ --title "$_th_detail" || true
274
+ # `break` exits the innermost loop. If you call terminal_halt from a nested
275
+ # function inside the main loop, use `return` + then call `break` in the
276
+ # caller, or replace this with `exit 0` for a top-level script.
277
+ break
278
+ }
279
+
280
+ # ─── END OF FUNCTION DEFINITION ──────────────────────────────────────────────
281
+
282
+ # ─── USAGE EXAMPLES — replace the inline _LOOP_EXITED=1 + send + break ───────
283
+
284
+ # Example — loop completed cleanly:
285
+ # terminal_halt loop_completed "All tasks finished"
286
+
287
+ # Example — loop halted (user_blocking question):
288
+ # terminal_halt ralph_halted "User action required — see .homer/questions/"
289
+
290
+ # Example — loop halted (max consecutive failures):
291
+ # terminal_halt ralph_halted "Max consecutive failures reached — check logs"
292
+
293
+ # Example — stall edge (wrap in the _stall_notified guard from PATTERN 3):
294
+ # if [[ "$_stall_notified" -eq 0 ]]; then
295
+ # terminal_halt ralph_waiting "Offline / quota stall — retrying when ready"
296
+ # # NOTE: terminal_halt breaks the loop here. For a stall you may prefer
297
+ # # to stay in the loop and retry — in that case emit ralph_waiting inline
298
+ # # (PATTERN 3) and do NOT call terminal_halt (which would exit the loop).
299
+ # fi
300
+
301
+ # ─── COMPLETE BOILERPLATE (combine PATTERN 5 + PATTERN 6) ────────────────────
302
+ #
303
+ # Minimal drop-in for a new loop script — covers crash-safe trap + terminal_halt:
304
+ #
305
+ # _LOOP_EXITED=0
306
+ #
307
+ # _loop_crash_trap() {
308
+ # [[ "${_LOOP_EXITED:-0}" -eq 0 ]] && \
309
+ # gocode-notify send --kind loop_halted --source ralph \
310
+ # --project "$(basename "$PWD")" \
311
+ # --title "Loop exited unexpectedly — check logs" || true
312
+ # }
313
+ # trap '_loop_crash_trap' EXIT
314
+ #
315
+ # terminal_halt() {
316
+ # local _th_kind="${1:-loop_halted}"
317
+ # local _th_detail="${2:-Loop exited}"
318
+ # _LOOP_EXITED=1
319
+ # gocode-notify send --kind "$_th_kind" --source ralph \
320
+ # --project "$(basename "$PWD")" \
321
+ # --title "$_th_detail" || true
322
+ # break
323
+ # }
324
+ #
325
+ # # Main loop
326
+ # while true; do
327
+ # # ... your loop body ...
328
+ #
329
+ # if all_tasks_done; then
330
+ # terminal_halt loop_completed "All tasks finished"
331
+ # fi
332
+ # if user_blocking_halt; then
333
+ # terminal_halt ralph_halted "User action required"
334
+ # fi
335
+ # if max_failures_exceeded; then
336
+ # terminal_halt ralph_halted "Max consecutive failures reached"
337
+ # fi
338
+ # done
339
+ #
340
+ # Any path that exits without calling terminal_halt is caught by the trap.
341
+ # Any path that calls terminal_halt gets exactly one push, then breaks cleanly.