File size: 20,816 Bytes
24fe873
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
"""Reflective walk loop: train, watch the end of the video, reflect, small change, repeat.

This is the agentic loop for /pp (TwoLeg biped). Each round:
  1. resume-train a bounded iteration budget from the current checkpoint,
  2. render a rollout and run scripts/verify_walk.py (which now judges the
     LAST frames: upright + both feet sharing load = walking),
  3. if WALKS  -> STOP (success),
  4. if NO-WALK -> the REFLECTOR reads the verdict + end-frame pixels, writes
     ONE small reversible change as a new strategy, and the loop repeats.

There is no fixed strategy table and no STRATEGIES_EXHAUSTED stop: the loop
only ends on WALKS or a hard training failure. The reflector is the brain; it
must keep each change small (one reward term, one servo sign, one hyper) so
the search stays local and reversible. State persists in
.audit/walk_loop_state.json so a later /pp call resumes.

    uv run python scripts/walk_loop.py --rounds 1
    uv run python scripts/walk_loop.py --reflect-only --from-run <r> --from-checkpoint <pt>
"""

import argparse
import json
import os
import subprocess
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
TRAINING = os.path.dirname(HERE)
REPO = os.path.dirname(TRAINING)
AUDIT = os.path.join(REPO, ".audit")
STATE = os.path.join(AUDIT, "walk_loop_state.json")
STRAT = os.path.join(AUDIT, "walk_loop_strategies.jsonl")  # append-only reflector log
LOG_TSV = os.path.join(AUDIT, "twoleg-walk.tsv")
LOG_SH = ("/home/kenpeter/work/pp/.opencode/skills/show-me-your-work/scripts/log.sh")
TASK = "Mjlab-Velocity-Flat-TwoLeg"

sys.path.insert(0, HERE)
import loop_changes  # whitelisted, git-committed, test-gated code edits

# Reflection registry lives in code as the FIRST hypothesis; after that the
# reflector appends. Seed with the standing open questions so round 1 has a
# direction if state is fresh.
SEED_STRATEGY = {
    "name": "fresh-symmetry-gate-1000", "kind": "resume-train", "iters": 1000, "envs": 4096,
    "why": "H12: last verdict left_duty 0.0 / right_duty 0.936 -> collapses onto "
           "right leg, left leg does nothing. Hypothesis: leg servos asymmetric "
           "(sign/scale mismatch or unmirrored position target). Add a left/right "
           "duty-balance reward term so both legs must bear load; predict symmetric "
           "stance and stepping. Revert if it flails.",
}


def sh(cmd, **kw):
    return subprocess.run(cmd, capture_output=True, text=True, **kw)


def log_row(phase, decision, why, evidence, result):
    sh(["bash", LOG_SH, LOG_TSV, phase, decision, why, evidence, result])


def load_state():
    if os.path.exists(STATE):
        with open(STATE) as fh:
            return json.load(fh)
    return {"rounds_done": 0, "run": None, "checkpoint": None,
            "last_verdict": None, "last_video": None}


def save_state(s):
    os.makedirs(AUDIT, exist_ok=True)
    with open(STATE, "w") as fh:
        json.dump(s, fh, indent=1)


def append_strategy(st):
    os.makedirs(AUDIT, exist_ok=True)
    with open(STRAT, "a") as fh:
        fh.write(json.dumps(st) + "\n")


def run_dirs():
    root = os.path.join(TRAINING, "logs", "rsl_rl", "velocity")
    return sorted(d for d in os.listdir(root)
                  if os.path.isdir(os.path.join(root, d)))


def newest_model(run):
    d = os.path.join(TRAINING, "logs", "rsl_rl", "velocity", run)
    if not os.path.isdir(d):
        return None
    pts = sorted(f for f in os.listdir(d)
                 if f.startswith("model_") and f.endswith(".pt"))
    if not pts:
        return None
    return max(pts, key=lambda f: os.path.getmtime(os.path.join(d, f)))


def train_round(strategy, run, ckpt, fresh=False):
    """Launch a bounded training run detached, poll until it exits. Returns the
    new run dir, or None on failure.

    fresh=True  -> train from random init (NO --agent.resume / load flags).
    fresh=False -> resume from (run, ckpt). ANY run we launch is always
    recorded in state with its newest checkpoint, so it can be resumed later.
    """
    iters, envs = strategy["iters"], strategy["envs"]
    before = set(run_dirs())
    logf = f"/tmp/opencode/loop_{strategy['name']}.log"
    if os.path.exists(logf):
        os.remove(logf)
    resume_part = ""
    if not fresh:
        resume_part = (f"--agent.resume True --agent.load-run {run} "
                       f"--agent.load-checkpoint {ckpt} ")
    # xvfb-run wraps train: --video True needs a GL context, and this host has
    # no real display. Without it the child dies at renderer init (caught as
    # "no new run dir appeared").
    cmd = (f"setsid nohup xvfb-run -a -s \"-screen 0 800x600x24\" "
           f"uv run --no-sync train {TASK} "
           f"--env.scene.num-envs {envs} --agent.max-iterations {iters} "
           f"{resume_part}--video True "
           f"> {logf} 2>&1 < /dev/null &")
    subprocess.Popen(cmd, shell=True, cwd=TRAINING,
                     stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    # Generous wall-clock budget: ~1.0s/iter on this 150W-capped 4070 Ti plus
    # startup (xvfb+uv+cuda) slack. Never pkill a healthy run that is still
    # making progress -- only give up if the proc actually died. The earlier
    # iters*2.0+600 budget was too tight and killed a resume at iter 3255.
    deadline = time.time() + iters * 1.0 + 1800.0
    launched = False
    while time.time() < deadline:
        time.sleep(20)
        r = sh(["pgrep", "-f", "train Mjlab"])
        alive = bool(r.stdout.strip())
        if alive:
            launched = True
        elif launched:
            break  # proc exited on its own -> run finished (or crashed)
    if time.time() >= deadline and launched:
        # Only reached if the proc is STILL alive past budget. Log but do not
        # kill; let it keep training and pick up the newer checkpoint.
        print(f"train round still running past budget ({iters} iters); "
              f"continuing to poll for checkpoint")
    after = set(run_dirs())
    new = sorted(after - before)
    if not new:
        print("no new run dir appeared; check", logf)
        return None
    print(f"round done, new run dir: {new[-1]}")
    return new[-1]


def verify(run, ckpt):
    """Run the metric verifier (headless, no GL) which reads the same contact
    duties + last-frame altitude the video verifier used. Returns (passed, sidecar).
    Returns (passed, sidecar). Returns a dict with 'verdict' in sidecar."""
    if not ckpt:
        raise RuntimeError(f"verify: no checkpoint in run {run} (training died?)")
    ckpt_path = os.path.join(TRAINING, "logs", "rsl_rl", "velocity", run, ckpt)
    if not os.path.exists(ckpt_path):
        raise RuntimeError(f"verify: checkpoint missing: {ckpt_path}")
    env = dict(os.environ)  # no MUJOCO_GL needed (verify_metrics has no render)
    sidecar_path = os.path.join(TRAINING, "logs", "rsl_rl", "velocity", run,
                                "verify_metrics_cmd0.10.json")
    before = os.path.getmtime(sidecar_path) if os.path.exists(sidecar_path) else 0.0
    r = subprocess.run(
        ["uv", "run", "--no-sync", "python", "scripts/verify_metrics.py",
         "--checkpoint", ckpt_path, "--command", "0.1",
         "--steps", "250", "--envs", "4", "--device", "cuda:0"],
        cwd=TRAINING, capture_output=True, text=True, env=env, timeout=300)
    print(r.stdout[-1500:] if r.stdout else "")
    if r.stderr:
        print("ERR:", r.stderr[-800:])
    if not os.path.exists(sidecar_path):
        raise RuntimeError(f"verify: no sidecar written for {ckpt}")
    if os.path.getmtime(sidecar_path) <= before:
        raise RuntimeError(
            f"verify: sidecar is stale (not rewritten for {ckpt}); refusing to "
            "gate on an older checkpoint's numbers")
    with open(sidecar_path) as fh:
        sidecar = json.load(fh)
    REQUIRED = ("verdict", "valid_frac", "left_duty_mean", "right_duty_mean",
                "speed_mean", "last_frame_alt_mean", "switch_hz_mean",
                "double_support_frac", "knee_flex_min_rad", "hip_swing_min_rad",
                "stance_knee_bend_rad", "articulated_frac", "rolling_pass_frac")
    missing = [k for k in REQUIRED if k not in sidecar]
    if missing:
        raise RuntimeError(f"verify: sidecar missing keys {missing}; the gate "
                           f"would read defaults, not measurements")
    return sidecar.get("verdict") == "WALKS", sidecar


def reflect(state):
    """The brain. Read the last verdict + end-frame pixels, propose ONE small
    reversible change. Returns a strategy dict. Never invents a rewrite."""
    v = state.get("last_verdict") or {}
    reasons = "; ".join(v.get("reasons", [])) or "no verdict"
    left = v.get("left_duty", "?")
    right = v.get("right_duty", "?")
    speed = v.get("speed", "?")
    last_min = v.get("last_min_duty", "?")
    last_alt = v.get("last_frame_alt", "?")
    video = state.get("last_video") or v.get("video") or ""
    round_n = state["rounds_done"] + 1

    prompt = (
        f"You are the reflection brain for a TwoLeg biped RL training loop. "
        f"Round {round_n} FAILED to walk.\n\n"
        f"VERIFIER VERDICT (last-frames aware):\n"
        f"  reasons: {reasons}\n"
        f"  duty left={left} right={right}  speed={speed}\n"
        f"  LAST-frames min duty={last_min}  last frame_alt={last_alt}\n\n"
        f"WATCH THE VIDEO before deciding: {video}\n"
        f"Open it and look at the FINAL frames. Is the robot upright? Are BOTH "
        f"feet on the ground bearing load, or did it collapse onto one leg / fall? "
        f"Describe what the last frames actually show, then explain the most "
        f"likely cause.\n\n"
        f"Then propose EXACTLY ONE small, reversible change to make it walk on "
        f"two legs (no rewrites):\n"
        f" - add/adjust a reward term (duty-balance so both legs share load, "
        f"foot-contact alternation, torso-upright bonus)\n"
        f" - fix a leg servo sign/scale or mirror bug in the reference XML\n"
        f" - adjust one hyper (learning rate, iters, action clip)\n"
        f"Return JSON only: {{\"name\": str, \"kind\": \"resume-train\", \"iters\": 2000, "
        f"\"envs\": 2048, \"why\": str, \"change\": str}}"
    )

    # Use the running opencode instance in poteto mode to reflect + watch the
    # end-frame video (it owns the model/XML context). Pin muse-spark-1.3
    # (free) per direction. If opencode is unavailable, fall back to a
    # duty-balance nudge. NOTE: opencode can hang (keeps pipe open past
    # timeout) so we launch it in its own process group and killpg hard.
    import subprocess as _sp
    try:
        proc = _sp.Popen(
            ["opencode", "run", "--model",
             "opencode/muse-spark-1.3-contributor-free", prompt],
            cwd="/home/kenpeter/work/pp",
            stdout=_sp.PIPE, stderr=_sp.PIPE, text=True,
            start_new_session=True)
        try:
            out, err = proc.communicate(timeout=180)
        except _sp.TimeoutExpired:
            proc.kill(); _sp.run(["pkill", "-9", "-f", "opencode run"],
                                  capture_output=True)
            raise RuntimeError("opencode timed out after 180s")
        txt = (out or "") + (err or "")
        j = txt[txt.find("{"):txt.rfind("}")+1]
        st = json.loads(j)
        st.setdefault("iters", 1000); st.setdefault("envs", 4096)
        st.setdefault("kind", "resume-train")
        return st
    except Exception as e:
        print(f"reflect: opencode unavailable ({e}); fallback duty-balance")
        return {"name": f"duty-balance-{round_n}", "kind": "resume-train",
                "iters": 1000, "envs": 4096,
                "why": f"fallback: still collapsing (last min duty {last_min}). "
                       f"Add duty-balance reward so both legs bear load.",
                "change": "add left/right duty-balance term to reward"}


def decide(state, new_run, new_ckpt):
    """Agentic loop-condition. The GROUND TRUTH is the rigorous verify_metrics.py
    verdict (human-gait gate: upright + both feet load + signed speed + switch_hz
    + double-support + knee/hip articulation + smoothness). We MUST trust it,
    because a weaker local gate here already false-declared WALKS on a symmetric
    squat-freeze (upright + both feet down + tiny motion) and stopped the loop.

    - STOP   if verdict == "WALKS" (verify_metrics said >=60% of envs pass the
             full human-gait gate).
    - CONTINUE otherwise -- the rigorous gate already encoded the evidence; we do
             NOT re-derive a weaker local gate that can be gamed.
    """
    info = state.get("last_verdict") or {}
    verdict = info.get("verdict", "NO-WALK")

    if verdict == "WALKS":
        # Cross-check the evidence the verifier already measured (defense in
        # depth, but the verifier's verdict is authoritative).
        fa = info.get("last_frame_alt_mean", -1.0)
        l_d = info.get("left_duty_mean", 0.0)
        r_d = info.get("right_duty_mean", 0.0)
        sp = info.get("speed_mean", 0.0)
        roll = info.get("rolling_pass_frac", 0.0)
        return "stop", (f"RIGOROUS VERDICT WALKS: frame_alt={fa} L={l_d} "
                        f"R={r_d} spd={sp} roll={roll} -> genuine human gait")
    return "continue", (f"RIGOROUS VERDICT NO-WALK "
                        f"(verdict={verdict}) -> keep training")


def propose_change(state, info):
    """Turn a NO-WALK verdict into ONE whitelisted code change.

    The reflector (disabled here) would read the verdict + video and return a
    structured change dict. When disabled we use a CAUSAL fallback: inspect the
    measured verdict and pick the change that addresses the actual failure
    mode, falling back to a safe rotation over the whitelist. Every applied
    change is git-committed + test-gated inside loop_changes.apply_change(); a
    bad change auto-reverts and we just retrain identical weights.
    """
    info = info or {}
    l_d = info.get("left_duty_mean", 0.0)
    r_d = info.get("right_duty_mean", 0.0)
    c_alt = info.get("contact_alt_mean", 0.0)
    switch = info.get("switch_hz_mean", 0.0)
    # One-leg dominance / dead leg: the failing foot barely contacts while the
    # other is always down (high contact asymmetry). Break it with duty_balance.
    min_duty = min(l_d, r_d)
    if min_duty < 0.2 or c_alt > 0.5:
        return {"target": "reward_weight", "term": "duty_balance", "weight": 3.0}
    # Hopping came back (high switch_hz but no real double-support / duty fails):
    # strengthen the anti-hop term.
    if switch > 2.0:
        return {"target": "reward_weight", "term": "both_feet_air_time",
                "weight": 6.0}
    # SQUAT-FREEZE (round-2 failure): both feet planted, no stance transfer,
    # no double-support, tiny speed, deep knee bend. The planted double-stance
    # earns neither no_fly (needs exactly-one-down) nor feet_moving (needs any
    # lift), so raise both to push the policy into alternating single support.
    dsup = info.get("double_support_frac", 1.0)
    knee = info.get("knee_flex_min_rad", 0.0)
    spd = info.get("speed_mean", 0.0)
    if switch < 0.4 and dsup < 0.1 and spd < 0.1 and knee > 0.5:
        return {"target": "reward_weight", "term": "no_fly", "weight": 4.0}
    if switch < 0.4 and dsup < 0.1:
        return {"target": "reward_weight", "term": "feet_moving", "weight": 3.0}
    # Otherwise rotate over safe structural nudges.
    if not getattr(propose_change, "_rot", None):
        propose_change._rot = iter([
            {"target": "reward_weight", "term": "air_time", "weight": 4.0},
            {"target": "reward_weight", "term": "foot_clearance", "weight": -0.3},
            {"target": "symmetry", "field": "mirror_loss_coeff", "value": 0.8},
            {"target": "reward_weight", "term": "upright", "weight": 3.0},
            {"target": "reward_weight", "term": "duty_balance", "weight": 3.0},
            {"target": "env_param", "param": "fell_over_limit_angle", "value": 35.0},
        ])
    try:
        change = next(propose_change._rot)
    except StopIteration:
        propose_change._rot = None
        return propose_change(state, info)  # restart rotation
    return change


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--from-run", default=None)
    ap.add_argument("--from-checkpoint", default=None)
    ap.add_argument("--rounds", type=int, default=0,
                    help="max rounds; 0 = infinite loop until WALKS (default)")
    ap.add_argument("--reflect-only", action="store_true")
    ap.add_argument("--no-reflect", action="store_true",
                    help="skip the opencode reflector; always use the fallback "
                         "strategy (pure resume, no blocking on muse)")
    args = ap.parse_args()

    state = load_state()
    run = args.from_run or state["run"]
    ckpt = args.from_checkpoint or state["checkpoint"]
    if not run or not ckpt:
        if state.get("rounds_done", 0) == 0 and state.get("run") is None:
            run, ckpt = None, None  # first-ever round trains fresh from init
        else:
            print("need --from-run and --from-checkpoint (or a saved state)")
            sys.exit(2)

    if args.reflect_only:
        st = reflect(state)
        print("REFLECT ->", json.dumps(st))
        sys.exit(0)

    def pick_strategy(state):
        if args.no_reflect:
            # Reflector disabled: use the safe fallback (continue the same
            # microduck-exact config, pure resume). Never blocks on muse.
            return {"name": f"continue-{state['rounds_done'] + 1}",
                    "kind": "resume-train", "iters": 1000, "envs": 4096,
                    "why": "reflector disabled; continuing microduck-exact resume",
                    "change": "none (no reflector)"}
        if state.get("rounds_done", 0) == 0 and "strategy" not in state:
            return SEED_STRATEGY
        return reflect(state)

    for step in range(args.rounds) if args.rounds and args.rounds > 0 else iter(int, 1):
        st = pick_strategy(state)
        append_strategy(st)
        fresh = (state.get("run") is None)  # first ever run = fresh from init
        log_row("loop", f"round {state['rounds_done'] + 1}: {st['name']} "
                f"{'FRESH' if fresh else 'from ' + ckpt}",
                st.get("why", ""), f"logs/rsl_rl/velocity/{run}/{ckpt}",
                "training")
        new_run = train_round(st, run, ckpt, fresh=fresh)
        if new_run is None:
            log_row("loop", f"{st['name']} failed to produce a run", "see loop log",
                    f"/tmp/opencode/loop_{st['name']}.log", "failed")
            sys.exit(2)
        new_ckpt = newest_model(new_run)
        if new_ckpt is None:
            log_row("loop", f"{st['name']} produced no checkpoint (training died)",
                    "see loop log", f"/tmp/opencode/loop_{st['name']}.log", "failed")
            sys.exit(2)
        try:
            passed, info = verify(new_run, new_ckpt)
        except RuntimeError as e:
            log_row("loop", f"verify failed: {e}", "training died early",
                    f"logs/rsl_rl/velocity/{new_run}", "failed")
            sys.exit(2)
        # ANY run we launched is now recorded so it can always be resumed.
        state.update({"rounds_done": state["rounds_done"] + 1,
                      "run": new_run, "checkpoint": new_ckpt,
                      "last_verdict": info, "last_video": info.get("video", "")})
        save_state(state)
        run, ckpt = new_run, new_ckpt
        # Agentic loop-condition: subagent decides STOP (walks) vs CONTINUE.
        decision, why = decide(state, new_run, new_ckpt)
        if decision == "stop":
            print(f"LOOP VERDICT: WALKS at {new_run}/{new_ckpt} ({why})")
            log_row("loop", f"WALKS at {new_ckpt}",
                    why, f"logs/rsl_rl/velocity/{new_run}/{new_ckpt}", "done")
            sys.exit(0)
        print(f"round {state['rounds_done']} NO-WALK -> {why} -> continuing (infinite loop)")
        # Agentic code-change: propose ONE whitelisted edit from the verdict and
        # apply it (git-committed + test-gated; auto-reverts on failure) so the
        # next round trains on a nudged config instead of identical weights.
        change = propose_change(state, info)
        res = loop_changes.apply_change(change)
        print(f"  code-change: {res}")
        # brief pause so we never spin on a bad state; then loop forever.
        time.sleep(5)
    # Round budget exhausted without a WALKS verdict -> continue (not a win).
    # The supervisor must NOT read this as WALKS. Exit 1 = keep looping.
    print(f"round budget spent ({state['rounds_done']} rounds) -> NO-WALK, continuing")
    sys.exit(1)


if __name__ == "__main__":
    main()