@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.
- package/README.md +181 -2
- package/dist/src/cli.js +207 -1
- package/dist/src/creds.js +32 -3
- package/dist/src/dedup_lock.js +0 -0
- package/dist/src/doctor.js +262 -0
- package/dist/src/launch.js +218 -0
- package/dist/src/mcp.js +93 -10
- package/dist/src/on_stop.js +323 -7
- package/dist/src/rule-content.js +52 -1
- package/dist/src/send.js +14 -3
- package/dist/src/setup.js +20 -0
- package/dist/src/status.js +18 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/snippets/ralph-homer.sh +329 -9
package/snippets/ralph-homer.sh
CHANGED
|
@@ -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
|
|
5
|
-
#
|
|
6
|
-
# `ralph`/`homer` skills, Geoffrey Huntley's `while :; do … done`
|
|
7
|
-
# or your own driver) to get
|
|
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
|
-
#
|
|
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
|
-
#
|
|
17
|
-
gocode-notify send --kind loop_completed --source ralph --project "$(basename "$PWD")" || true
|
|
179
|
+
# ─── END OF TOP-OF-SCRIPT BLOCK ──────────────────────────────────────────────
|
|
18
180
|
|
|
19
|
-
#
|
|
20
|
-
|
|
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.
|