harnex 0.2.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/GUIDE.md +242 -0
  3. data/LICENSE +21 -0
  4. data/README.md +119 -0
  5. data/TECHNICAL.md +595 -0
  6. data/bin/harnex +18 -0
  7. data/lib/harnex/adapters/base.rb +134 -0
  8. data/lib/harnex/adapters/claude.rb +105 -0
  9. data/lib/harnex/adapters/codex.rb +112 -0
  10. data/lib/harnex/adapters/generic.rb +14 -0
  11. data/lib/harnex/adapters.rb +32 -0
  12. data/lib/harnex/cli.rb +115 -0
  13. data/lib/harnex/commands/guide.rb +23 -0
  14. data/lib/harnex/commands/logs.rb +184 -0
  15. data/lib/harnex/commands/pane.rb +251 -0
  16. data/lib/harnex/commands/recipes.rb +104 -0
  17. data/lib/harnex/commands/run.rb +384 -0
  18. data/lib/harnex/commands/send.rb +415 -0
  19. data/lib/harnex/commands/skills.rb +163 -0
  20. data/lib/harnex/commands/status.rb +171 -0
  21. data/lib/harnex/commands/stop.rb +127 -0
  22. data/lib/harnex/commands/wait.rb +165 -0
  23. data/lib/harnex/core.rb +286 -0
  24. data/lib/harnex/runtime/api_server.rb +187 -0
  25. data/lib/harnex/runtime/file_change_hook.rb +111 -0
  26. data/lib/harnex/runtime/inbox.rb +207 -0
  27. data/lib/harnex/runtime/message.rb +23 -0
  28. data/lib/harnex/runtime/session.rb +380 -0
  29. data/lib/harnex/runtime/session_state.rb +55 -0
  30. data/lib/harnex/version.rb +3 -0
  31. data/lib/harnex/watcher/inotify.rb +43 -0
  32. data/lib/harnex/watcher/polling.rb +92 -0
  33. data/lib/harnex/watcher.rb +24 -0
  34. data/lib/harnex.rb +25 -0
  35. data/recipes/01_fire_and_watch.md +82 -0
  36. data/recipes/02_chain_implement.md +115 -0
  37. data/skills/chain-implement/SKILL.md +234 -0
  38. data/skills/close/SKILL.md +47 -0
  39. data/skills/dispatch/SKILL.md +171 -0
  40. data/skills/harnex/SKILL.md +304 -0
  41. data/skills/open/SKILL.md +32 -0
  42. metadata +88 -0
data/TECHNICAL.md ADDED
@@ -0,0 +1,595 @@
1
+ # Technical Reference
2
+
3
+ For what harnex is and whether you'd want it, see
4
+ [README.md](README.md). This document covers commands, patterns,
5
+ and internals.
6
+
7
+ ## Commands
8
+
9
+ ### `harnex run` — Start an agent
10
+
11
+ ```bash
12
+ harnex run codex
13
+ harnex run claude --id review
14
+ harnex run codex -- --cd ~/other/repo
15
+ ```
16
+
17
+ | Flag | What it does |
18
+ |-----------------|---------------------------------------|
19
+ | `--id ID` | Name this session (default: random) |
20
+ | `--description` | Store a short session description |
21
+ | `--detach` | Run in background, no terminal |
22
+ | `--tmux [NAME]` | Run in a tmux window you can watch |
23
+ | `--host HOST` | Bind a specific API host |
24
+ | `--port PORT` | Force a specific API port |
25
+ | `--context TXT` | Give the agent a task on startup |
26
+ | `--watch PATH` | Notify agent when a file changes |
27
+ | `--timeout SEC` | Wait budget for detached registration |
28
+
29
+ ### `harnex send` — Talk to a running agent
30
+
31
+ ```bash
32
+ harnex send --id worker --message "implement plan A"
33
+ ```
34
+
35
+ | Flag | What it does |
36
+ |-------------------|----------------------------------------|
37
+ | `--id ID` | Which agent to talk to |
38
+ | `--message` | The message text |
39
+ | `--wait-for-idle` | Block until agent finishes processing |
40
+ | `--repo PATH` | Resolve session from a repo root |
41
+ | `--cli CLI` | Filter by CLI type |
42
+ | `--submit-only` | Just press Enter (no new text) |
43
+ | `--no-submit` | Type the text but don't press Enter |
44
+ | `--force` | Send even if the agent looks busy |
45
+ | `--no-wait` | Don't wait for delivery confirmation |
46
+ | `--relay` | Force relay header formatting |
47
+ | `--no-relay` | Suppress automatic relay headers |
48
+ | `--port` / `--token` | Send directly to a known API port |
49
+ | `--timeout` | Overall wait budget |
50
+
51
+ ### `harnex stop` — Ask an agent to stop
52
+
53
+ ```bash
54
+ harnex stop --id worker
55
+ harnex stop --id worker --timeout 5
56
+ ```
57
+
58
+ Sends the adapter-specific stop sequence. Retries transient
59
+ failures for up to the given timeout.
60
+
61
+ ### `harnex status` — See running agents
62
+
63
+ ```
64
+ ID CLI AGE STATE
65
+ worker codex 36s ago prompt
66
+ review claude 8s ago busy
67
+ ```
68
+
69
+ Use `--json` for full payloads. Use `--all` for all repos.
70
+
71
+ ### `harnex wait` — Wait for an agent to finish
72
+
73
+ ```bash
74
+ harnex wait --id worker
75
+ harnex wait --id worker --until prompt --timeout 300
76
+ ```
77
+
78
+ ### `harnex logs` — Read session transcripts
79
+
80
+ ```bash
81
+ harnex logs --id worker --lines 50
82
+ harnex logs --id worker --follow
83
+ ```
84
+
85
+ ### `harnex pane` — Capture a tmux screen snapshot
86
+
87
+ ```bash
88
+ harnex pane --id worker --lines 40
89
+ harnex pane --id worker --follow
90
+ harnex pane --id worker --json
91
+ ```
92
+
93
+ Notes:
94
+ - Works only for tmux-backed sessions
95
+ - Resolves against the live tmux pane target, not just the harnex session ID
96
+ - If a session was started from another worktree, `pane` can fall back to a
97
+ unique cross-repo ID match; use `--repo` when the same ID exists in more
98
+ than one repo root
99
+
100
+ ## Usage Patterns
101
+
102
+ ### Atomic send+wait
103
+
104
+ Use `--wait-for-idle` instead of separate send + sleep + wait:
105
+
106
+ ```bash
107
+ # Replaces: send → sleep 5 → wait --until prompt
108
+ harnex send --id cx-1 --message "implement the plan" --wait-for-idle --timeout 600
109
+ ```
110
+
111
+ ### Agents talking to each other
112
+
113
+ When one harnex agent sends to another, the receiver sees a
114
+ relay header automatically:
115
+
116
+ ```
117
+ [harnex relay from=claude id=supervisor at=2026-03-14T12:00]
118
+ implement A
119
+ ```
120
+
121
+ Messages queue when the agent is busy and auto-deliver when ready.
122
+
123
+ ### Background agents
124
+
125
+ **tmux** (observable):
126
+
127
+ ```bash
128
+ harnex run codex --id worker --tmux cx-w1
129
+ ```
130
+
131
+ **Headless** (no terminal):
132
+
133
+ ```bash
134
+ harnex run codex --id worker --detach
135
+ ```
136
+
137
+ ### Supervisor pattern
138
+
139
+ One agent manages others:
140
+
141
+ ```bash
142
+ harnex run codex --id impl-1 --tmux cx-p1 -- --cd ~/wt-a
143
+ harnex run codex --id impl-2 --tmux cx-p2 -- --cd ~/wt-b
144
+
145
+ harnex send --id impl-1 --message "implement feature A" --wait-for-idle --timeout 600
146
+ harnex send --id impl-2 --message "implement feature B" --wait-for-idle --timeout 600
147
+
148
+ harnex run claude --id review --tmux cl-r1
149
+ harnex send --id review --message "review changes" --wait-for-idle --timeout 300
150
+ ```
151
+
152
+ ### Context on startup
153
+
154
+ ```bash
155
+ harnex run codex --id impl-1 --tmux cx-p1 \
156
+ --context "Implement the auth feature. Commit when done." \
157
+ -- --cd ~/repo/worktree
158
+ ```
159
+
160
+ ### File watching
161
+
162
+ ```bash
163
+ harnex run codex --id worker --watch ./tmp/status.jsonl
164
+ ```
165
+
166
+ Agent gets notified when the file changes. File doesn't need to
167
+ exist at startup.
168
+
169
+ ## Architecture
170
+
171
+ ```
172
+ bin/harnex
173
+
174
+ └── lib/harnex.rb (loader)
175
+
176
+ ├── CLI dispatch: run / send / status / wait / stop
177
+ ├── Runner spawn sessions (foreground/detach/tmux)
178
+ ├── Sender resolve target, inject text
179
+ ├── Status list live sessions
180
+ ├── Waiter block until session exits
181
+ ├── Stopper send stop sequence
182
+
183
+ ├── Session PTY lifecycle, HTTP server, registry
184
+ ├── SessionState state machine (prompt/busy/blocked)
185
+ ├── Inbox per-session message queue
186
+ ├── Message queued message struct
187
+
188
+ ├── FileChangeHook inotify file watcher
189
+ └── LinuxInotify raw inotify via Fiddle
190
+
191
+ lib/harnex/adapters/
192
+ ├── base.rb adapter interface
193
+ ├── generic.rb fallback adapter for any CLI
194
+ ├── codex.rb codex-specific behavior
195
+ └── claude.rb claude-specific behavior
196
+ ```
197
+
198
+ ## Session Lifecycle
199
+
200
+ Foreground execution is the default operating mode for a human directly using
201
+ `harnex run`. For agent-to-agent delegation, a visible tmux session is the
202
+ preferred interactive mode because it keeps the peer's work observable.
203
+
204
+ Headless/background execution (`--detach`) should still be treated as opt-in.
205
+
206
+ When you run `harnex run codex --id worker`:
207
+
208
+ ```
209
+ 1. Parse wrapper options, build adapter
210
+ 2. Validate the target binary before spawn
211
+ 3. Spawn the agent CLI under a PTY (pseudoterminal)
212
+ 4. Generate a random bearer token for API auth
213
+ 5. Pick a port:
214
+ hash(repo_root + id) % port_span + base_port
215
+ walk forward until a free port is found
216
+ 6. Start HTTP server on 127.0.0.1:<port>
217
+ 7. Write registry file:
218
+ ~/.local/state/harnex/sessions/<repo_hash>--<id>.json
219
+ and open transcript file:
220
+ ~/.local/state/harnex/output/<repo_hash>--<id>.log
221
+ 8. Start background threads:
222
+ - PTY reader (screen buffer)
223
+ - State machine (adapter parses screen for state)
224
+ - Inbox delivery (queue -> inject when prompt)
225
+ - File watcher (if --watch was given)
226
+ 9. Relay terminal I/O between user and agent
227
+ 10. On exit: clean up registry, write exit status
228
+ ```
229
+
230
+ ## PTY and Screen Parsing
231
+
232
+ Harnex spawns the agent inside a pseudoterminal (PTY). This
233
+ preserves the agent's full terminal UI while letting harnex:
234
+
235
+ - Read screen output into a ring buffer
236
+ - Feed the buffer to the adapter for state detection
237
+ - Inject text by writing to the PTY input
238
+
239
+ The screen text includes raw terminal escape sequences. The
240
+ adapter's `normalized_screen_text` strips ANSI and OSC codes
241
+ before parsing.
242
+
243
+ ## Adapter Contract
244
+
245
+ Each adapter in `lib/harnex/adapters/` implements:
246
+
247
+ | Method | Purpose |
248
+ |---------------------------|---------------------------------|
249
+ | `base_command` | CLI args to launch the agent |
250
+ | `input_state(screen)` | Parse screen -> state hash |
251
+ | `build_send_payload(...)` | Build injection with submit |
252
+ | `inject_exit(writer)` | Send adapter-specific stop text |
253
+ | `infer_repo_path(argv)` | Extract repo path from CLI args |
254
+ | `wait_for_sendable(...)` | Wait strategy before sending |
255
+
256
+ ### Input States
257
+
258
+ The adapter reads the screen and returns a state hash:
259
+
260
+ | State | `input_ready` | Meaning |
261
+ |--------------------------|---------------|-----------------------|
262
+ | `prompt` | `true` | Ready for input |
263
+ | `session` | `nil` | Agent is working |
264
+ | `workspace-trust-prompt` | `false` | Needs Enter to confirm|
265
+ | `confirmation` | `false` | Modal confirmation |
266
+ | `unknown` | `nil` | Can't determine |
267
+
268
+ ### Codex Adapter
269
+
270
+ - Launches with `--no-alt-screen` for inline screen output
271
+ - Detects prompt by looking for `›` prefix in recent lines
272
+ - Multi-step submit: types text, then sends Enter after a
273
+ 75ms delay (lets the UI process the input)
274
+
275
+ ### Claude Adapter
276
+
277
+ - Detects workspace trust prompt ("Quick safety check")
278
+ - Allows `--submit-only` to clear the trust prompt
279
+ - Detects prompt via `--INSERT--` marker or `›` prefix
280
+ - Multi-step submit: types text, then sends Enter after a
281
+ short delay so pasted prompts are actually submitted
282
+
283
+ ## State Machine
284
+
285
+ `SessionState` tracks the agent's readiness:
286
+
287
+ ```
288
+ ┌──────────┐
289
+ ┌─────────│ unknown │──────────┐
290
+ │ └──────────┘ │
291
+ ▼ ▼
292
+ ┌──────────┐ screen change ┌──────────┐
293
+ │ prompt │ <───────────────── │ busy │
294
+ │ │ ────────────────> │ │
295
+ └──────────┘ screen change └──────────┘
296
+ │ │
297
+ ▼ ▼
298
+ ┌──────────┐ ┌──────────┐
299
+ │ blocked │ │ blocked │
300
+ └──────────┘ └──────────┘
301
+ ```
302
+
303
+ - Uses a Mutex + ConditionVariable for thread-safe access
304
+ - `wait_for_prompt(timeout)` blocks until state == `:prompt`
305
+ - State is updated by the PTY reader thread calling the
306
+ adapter's `input_state` on each screen change
307
+
308
+ ## Inbox and Message Queue
309
+
310
+ Each session has an `Inbox` with a background delivery thread:
311
+
312
+ ```
313
+ harnex send ──> POST /send ──> Inbox#enqueue
314
+
315
+ ┌─────────────┴──────────────┐
316
+ │ │
317
+ prompt + empty queue? otherwise
318
+ │ │
319
+ deliver_now() push to @queue
320
+ return 200 return 202
321
+
322
+ delivery_loop thread
323
+ wait_for_prompt()
324
+ deliver_now()
325
+ ```
326
+
327
+ ### Message Lifecycle
328
+
329
+ | Status | Meaning |
330
+ |-------------|--------------------------------------|
331
+ | `queued` | In the queue, waiting for delivery |
332
+ | `delivered` | Injected into agent's terminal |
333
+ | `failed` | Injection raised an error |
334
+
335
+ Poll status with `GET /messages/:id`.
336
+
337
+ ## HTTP API
338
+
339
+ All endpoints are on `127.0.0.1:<port>`. Every request needs
340
+ the bearer token from the registry file.
341
+
342
+ ### `GET /status`
343
+
344
+ Returns JSON:
345
+
346
+ ```json
347
+ {
348
+ "ok": true,
349
+ "session_id": "abc123",
350
+ "repo_root": "/path/to/repo",
351
+ "cli": "codex",
352
+ "id": "worker",
353
+ "description": "implement auth module",
354
+ "pid": 12345,
355
+ "host": "127.0.0.1",
356
+ "port": 43123,
357
+ "command": ["codex", "--no-alt-screen"],
358
+ "started_at": "2026-03-13T20:45:00Z",
359
+ "last_injected_at": null,
360
+ "injected_count": 0,
361
+ "input_state": {
362
+ "state": "prompt",
363
+ "input_ready": true
364
+ },
365
+ "agent_state": "prompt",
366
+ "inbox": {
367
+ "pending": 0,
368
+ "delivered_total": 3
369
+ }
370
+ }
371
+ ```
372
+
373
+ ### `POST /send`
374
+
375
+ Send JSON body:
376
+
377
+ ```json
378
+ {
379
+ "text": "implement plan A",
380
+ "submit": true,
381
+ "enter_only": false,
382
+ "force": false
383
+ }
384
+ ```
385
+
386
+ - **200** with `"status": "delivered"` — sent immediately
387
+ - **202** with `"status": "queued"` and `message_id` — agent
388
+ is busy, message will auto-deliver
389
+ - **400** — missing text or bad request
390
+ - **409** — agent not ready (use `--force` to override)
391
+ - **503** — inbox is full
392
+
393
+ ### `POST /stop`
394
+
395
+ Tells the session adapter to inject its stop sequence.
396
+ The `harnex stop` CLI retries transient API failures for up
397
+ to its `--timeout` budget before returning exit code 124.
398
+
399
+ ### `GET /messages/:id`
400
+
401
+ Check delivery status of a queued message.
402
+
403
+ ### `GET /health`
404
+
405
+ Alias for `/status`.
406
+
407
+ ## Session Registry
408
+
409
+ Registry files at `~/.local/state/harnex/sessions/`:
410
+
411
+ ```
412
+ <repo_hash>--<normalized_id>.json
413
+ ```
414
+
415
+ The repo hash is a hex digest of the git root path. The ID is
416
+ normalized: lowercased, non-alphanumeric chars replaced with
417
+ dashes, leading/trailing dashes stripped.
418
+
419
+ Contents: port, PID, token, CLI, repo root, timestamps,
420
+ injection counters, agent state, inbox stats.
421
+
422
+ Cleaned up on normal exit. Stale files (dead PID) are ignored
423
+ during lookups.
424
+
425
+ ## Exit Status
426
+
427
+ When a session exits, harnex writes:
428
+
429
+ ```
430
+ ~/.local/state/harnex/exits/<repo_hash>--<normalized_id>.json
431
+ ```
432
+
433
+ This lets `harnex wait` return exit info even if the session
434
+ registry entry is already gone.
435
+
436
+ ## Output Transcript
437
+
438
+ Every session also writes a repo-keyed transcript:
439
+
440
+ ```
441
+ ~/.local/state/harnex/output/<repo_hash>--<normalized_id>.log
442
+ ```
443
+
444
+ - PTY output is appended as bytes are read
445
+ - The transcript is opened in append mode, so reusing an ID
446
+ does not wipe prior output
447
+ - `output_log_path` is exposed via `status --json` and
448
+ detached `run` responses
449
+ - The transcript is the source of truth for the planned
450
+ `harnex logs` operator interface
451
+
452
+ ## Relay Headers
453
+
454
+ When `harnex send` detects it is running inside a harnex
455
+ session (via `HARNEX_SESSION_ID` env) and the target is a
456
+ different session, it prepends:
457
+
458
+ ```
459
+ [harnex relay from=<cli> id=<sender_id> at=<timestamp>]
460
+ ```
461
+
462
+ Control with `--relay` (force) or `--no-relay` (suppress).
463
+
464
+ Already-wrapped messages (starting with `[harnex relay`) are
465
+ not double-wrapped.
466
+
467
+ ## File Watching
468
+
469
+ `--watch PATH` creates a `FileChangeHook` using Linux inotify
470
+ (via Fiddle, no gem needed).
471
+
472
+ - Watches the file's parent directory for write/create events
473
+ - 1 second debounce: waits for quiet time before triggering
474
+ - Injects `file-change-hook: read <path>` when triggered
475
+ - Creates the parent directory if needed
476
+ - File does not need to exist at startup
477
+
478
+ ## Port Selection
479
+
480
+ Ports are deterministic to keep things predictable:
481
+
482
+ ```
483
+ hash = Digest::SHA256.hexdigest(repo_root + id)
484
+ start = (hash[0..7].to_i(16) % port_span) + base_port
485
+ ```
486
+
487
+ Then walks forward from `start` until a free port is found.
488
+
489
+ Defaults: base `43000`, span `4000` (range 43000–46999).
490
+
491
+ ## Concurrency
492
+
493
+ Harnex uses Ruby threads, not processes:
494
+
495
+ - **PTY reader** — reads agent output into screen buffer
496
+ - **State poller** — feeds screen to adapter for state
497
+ - **Inbox delivery** — dequeues messages when prompt detected
498
+ - **HTTP server** — one thread per connection
499
+ - **File watcher** — inotify read loop (if enabled)
500
+
501
+ All shared state is protected by `Mutex`. The `SessionState`
502
+ and `Inbox` classes use `ConditionVariable` for signaling.
503
+
504
+ ## Skill Files
505
+
506
+ Harnex ships a skill file that teaches AI agents how to use
507
+ harnex commands. The file lives at:
508
+
509
+ ```
510
+ skills/harnex/SKILL.md
511
+ ```
512
+
513
+ ### What's in the skill
514
+
515
+ The skill tells agents:
516
+
517
+ - How to detect they're inside a harnex session (env vars)
518
+ - How to send messages, check status, spawn workers
519
+ - How to use `--context`, `--force`, `--no-wait`
520
+ - Relay header format and behavior
521
+ - Collaboration patterns (reply, supervisor, file watch)
522
+ - Safety rules (confirm before sending, no auto-loops)
523
+
524
+ ### How agents load skills
525
+
526
+ Claude and Codex both support a `skills/` directory. When a
527
+ skill is present, the agent can use harnex commands without
528
+ being told how — the skill provides the instructions.
529
+
530
+ Skill files use YAML frontmatter:
531
+
532
+ ```yaml
533
+ ---
534
+ name: harnex
535
+ description: Collaborate with other AI agents...
536
+ allowed-tools: Bash(harnex *)
537
+ ---
538
+ ```
539
+
540
+ The `allowed-tools` field grants the agent permission to run
541
+ `harnex` commands without asking for approval each time.
542
+
543
+ ### Symlinking the skill
544
+
545
+ To make the skill available globally (not just in the harnex
546
+ repo), symlink it into each agent's skill directory:
547
+
548
+ ```bash
549
+ # For Claude Code
550
+ ln -s /path/to/harnex/skills/harnex \
551
+ ~/.claude/skills/harnex
552
+
553
+ # For Codex
554
+ ln -s /path/to/harnex/skills/harnex \
555
+ ~/.codex/skills/harnex
556
+ ```
557
+
558
+ After symlinking, any Claude or Codex session — in any repo —
559
+ can use harnex commands. The skill activates automatically
560
+ when the user mentions agent collaboration or when a relay
561
+ message arrives.
562
+
563
+ ### Skill directory structure
564
+
565
+ ```
566
+ ~/.claude/skills/
567
+ └── harnex -> /path/to/harnex/skills/harnex
568
+ └── SKILL.md
569
+
570
+ ~/.codex/skills/
571
+ └── harnex -> /path/to/harnex/skills/harnex
572
+ └── SKILL.md
573
+ ```
574
+
575
+ The symlink points to the `skills/harnex/` directory (not the
576
+ file directly), so updates to `SKILL.md` in the repo are
577
+ picked up immediately.
578
+
579
+ ## Known Limitations
580
+
581
+ - Adapter prompt detection is heuristic-based. It works well
582
+ but can misread unusual screen output.
583
+ - No read-only HTTP output endpoint for local dashboards (use
584
+ `harnex pane --follow` or `harnex logs --follow` instead).
585
+ - File watching: inotify on Linux, stat-polling fallback on
586
+ macOS/other (works everywhere, inotify is just faster).
587
+ - The HTTP server is a simple socket server, not a full
588
+ framework. One thread per connection, no keep-alive.
589
+
590
+ ## Dependencies
591
+
592
+ - Ruby 3.x standard library only
593
+ - No gems required
594
+ - Uses: `io/console`, `pty`, `socket`, `json`, `net/http`,
595
+ `digest`, `fileutils`, `shellwords`, `fiddle` (for inotify)
data/bin/harnex ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/harnex"
4
+
5
+ begin
6
+ exit(Harnex::CLI.new(ARGV).run)
7
+ rescue OptionParser::ParseError => e
8
+ trace = ENV["HARNEX_TRACE"] == "1"
9
+ warn "harnex: #{e.message}"
10
+ warn "Try: harnex --help" unless trace
11
+ warn e.backtrace.join("\n") if trace
12
+ exit 2
13
+ rescue StandardError => e
14
+ trace = ENV["HARNEX_TRACE"] == "1"
15
+ warn "harnex: #{e.message}"
16
+ warn e.backtrace.join("\n") if trace
17
+ exit 1
18
+ end