harnex 0.3.3 → 0.4.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.
- checksums.yaml +4 -4
- data/GUIDE.md +11 -0
- data/README.md +37 -5
- data/TECHNICAL.md +95 -42
- data/lib/harnex/cli.rb +7 -0
- data/lib/harnex/commands/events.rb +212 -0
- data/lib/harnex/commands/run.rb +128 -14
- data/lib/harnex/commands/skills.rb +30 -10
- data/lib/harnex/commands/status.rb +17 -3
- data/lib/harnex/commands/watch.rb +209 -0
- data/lib/harnex/commands/watch_presets.rb +17 -0
- data/lib/harnex/core.rb +33 -0
- data/lib/harnex/runtime/session.rb +75 -2
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +3 -0
- data/skills/harnex/SKILL.md +9 -337
- data/skills/harnex-buddy/SKILL.md +20 -15
- data/skills/harnex-chain/SKILL.md +90 -192
- data/skills/harnex-dispatch/SKILL.md +115 -9
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4d7c194ba2ba10ee075e6e53aa3eaf291cbfc08eca1d89cd291955834230c01
|
|
4
|
+
data.tar.gz: e3bafc087ae74a484be3ee121cce0d8f351c6b381ce244d5da032e48c85da4db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3737c2aa49fb223c75cfc0d997a52bb72029b4c1a7a11affdd05a16c685e4edc42384ff0cfc03e63e4b4abcc9228c7f1d37c408c074538392c75d13be4e1e5e
|
|
7
|
+
data.tar.gz: 0b096fb9d95f22b812fc43fc69b4f8546e090d76c669383b8d71b34a048aa8eed46510f7ce9be27e0da3eab449a610c13fd6a338d8e1c202649d235df7052154
|
data/GUIDE.md
CHANGED
|
@@ -49,6 +49,17 @@ automatically when the agent is ready. You don't have to wait
|
|
|
49
49
|
or retry. Queueing exists, but the default workflow should still be
|
|
50
50
|
one task per fresh worker.
|
|
51
51
|
|
|
52
|
+
For unattended dispatch, prefer built-in monitoring over external poll loops:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
harnex run codex --id impl --tmux impl --watch --preset impl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This adds a foreground watcher that checks idle activity and performs bounded
|
|
59
|
+
force-resume nudges. For full flag behavior and event-stream consumers, see
|
|
60
|
+
[TECHNICAL.md](TECHNICAL.md) and the built-in monitoring section in
|
|
61
|
+
[README.md](README.md).
|
|
62
|
+
|
|
52
63
|
## Seeing what's running
|
|
53
64
|
|
|
54
65
|
```bash
|
data/README.md
CHANGED
|
@@ -105,12 +105,43 @@ Install skills so agents can use them:
|
|
|
105
105
|
harnex skills install
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
## Built-in dispatch monitoring
|
|
109
|
+
|
|
110
|
+
For unattended dispatches, use `--watch` instead of writing a bash poll loop:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
harnex run codex --id cx-impl-42 --tmux cx-impl-42 --watch --preset impl \
|
|
114
|
+
--context "Implement koder/plans/42_plan.md. Run tests and commit when done."
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`--watch` runs a foreground babysitter that checks session activity every 60s,
|
|
118
|
+
force-resumes on stall up to a cap, and exits when the target session exits or
|
|
119
|
+
the resume cap is reached.
|
|
120
|
+
|
|
121
|
+
Presets map to stall policy defaults:
|
|
122
|
+
|
|
123
|
+
- `impl` -> `--stall-after 8m --max-resumes 1`
|
|
124
|
+
- `plan` -> `--stall-after 3m --max-resumes 2`
|
|
125
|
+
- `gate` -> `--stall-after 15m --max-resumes 0`
|
|
126
|
+
|
|
127
|
+
Explicit `--stall-after` and `--max-resumes` flags override preset defaults.
|
|
128
|
+
|
|
129
|
+
For structured subscriptions, stream JSONL events:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
harnex events --id cx-impl-42 | jq -c '.'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Schema details and compatibility policy are documented in
|
|
136
|
+
[docs/events.md](docs/events.md).
|
|
137
|
+
|
|
108
138
|
## Long-running and overnight work
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
For plain "force-resume on stall" recovery, use
|
|
141
|
+
`harnex run --watch --preset impl`.
|
|
142
|
+
|
|
143
|
+
A **buddy** is for richer reasoning: doc drift checks, semantic sanity checks,
|
|
144
|
+
and multi-session correlation. It's still just another harnex session.
|
|
114
145
|
|
|
115
146
|
### Example: keep a worker from stalling
|
|
116
147
|
|
|
@@ -168,12 +199,13 @@ See [recipes/03_buddy.md](recipes/03_buddy.md) for the full pattern.
|
|
|
168
199
|
|
|
169
200
|
| Command | What it does |
|
|
170
201
|
|---------|-------------|
|
|
171
|
-
| `harnex run <cli>` | Start an agent (`--tmux`
|
|
202
|
+
| `harnex run <cli>` | Start an agent (`--tmux` visible, `--detach` background, `--watch` built-in monitoring) |
|
|
172
203
|
| `harnex send --id <id>` | Send a message (queues if busy, `--wait-for-idle` to block until done) |
|
|
173
204
|
| `harnex stop --id <id>` | Send the agent's native exit sequence |
|
|
174
205
|
| `harnex status` | List running sessions (`--json` for full payloads) |
|
|
175
206
|
| `harnex pane --id <id>` | Capture the agent's tmux screen (`--follow` for live) |
|
|
176
207
|
| `harnex logs --id <id>` | Read session transcript (`--follow` to tail) |
|
|
208
|
+
| `harnex events --id <id>` | Stream structured session events (`--snapshot` for non-blocking dump) |
|
|
177
209
|
| `harnex wait --id <id>` | Block until exit or a target state |
|
|
178
210
|
| `harnex guide` | Getting started walkthrough |
|
|
179
211
|
| `harnex recipes` | Tested workflow patterns |
|
data/TECHNICAL.md
CHANGED
|
@@ -14,17 +14,21 @@ harnex run claude --id review
|
|
|
14
14
|
harnex run codex -- --cd ~/other/repo
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
| Flag
|
|
18
|
-
|
|
19
|
-
| `--id ID`
|
|
20
|
-
| `--description`
|
|
21
|
-
| `--detach`
|
|
22
|
-
| `--tmux [NAME]`
|
|
23
|
-
| `--host HOST`
|
|
24
|
-
| `--port PORT`
|
|
25
|
-
| `--
|
|
26
|
-
| `--
|
|
27
|
-
| `--
|
|
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
|
+
| `--watch` | Enable blocking babysitter mode (foreground only) |
|
|
26
|
+
| `--stall-after DUR` | Idle threshold before force-resume (default: `480s`) |
|
|
27
|
+
| `--max-resumes N` | Max forced resumes before escalation (default: `1`) |
|
|
28
|
+
| `--preset NAME` | Watch preset (`impl`, `plan`, `gate`), requires `--watch` |
|
|
29
|
+
| `--watch-file PATH` | Auto-send a file-change hook (`--watch PATH`/`--watch=PATH` legacy) |
|
|
30
|
+
| `--context TXT` | Give the agent a task on startup |
|
|
31
|
+
| `--timeout SEC` | Wait budget for detached registration |
|
|
28
32
|
|
|
29
33
|
### `harnex send` — Talk to a running agent
|
|
30
34
|
|
|
@@ -61,12 +65,20 @@ failures for up to the given timeout.
|
|
|
61
65
|
### `harnex status` — See running agents
|
|
62
66
|
|
|
63
67
|
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
ID CLI PID PORT AGE IDLE STATE REPO DESC
|
|
69
|
+
worker codex 12345 43123 36s 12s prompt ~/... -
|
|
70
|
+
review claude 12346 43124 8s - busy ~/... -
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
Text mode includes an `IDLE` column derived from `log_idle_s` (`-` means no
|
|
74
|
+
transcript activity yet).
|
|
75
|
+
|
|
76
|
+
Use `--json` for full payloads. JSON includes:
|
|
77
|
+
|
|
78
|
+
- `log_mtime` (ISO8601 or `null`) — transcript file mtime
|
|
79
|
+
- `log_idle_s` (Integer or `null`) — seconds since last transcript write
|
|
80
|
+
|
|
81
|
+
Use `--all` for all repos.
|
|
70
82
|
|
|
71
83
|
### `harnex wait` — Wait for an agent to finish
|
|
72
84
|
|
|
@@ -160,12 +172,54 @@ harnex run codex --id impl-1 --tmux cx-p1 \
|
|
|
160
172
|
### File watching
|
|
161
173
|
|
|
162
174
|
```bash
|
|
163
|
-
harnex run codex --id worker --watch ./tmp/status.jsonl
|
|
175
|
+
harnex run codex --id worker --watch-file ./tmp/status.jsonl
|
|
164
176
|
```
|
|
165
177
|
|
|
166
178
|
Agent gets notified when the file changes. File doesn't need to
|
|
167
179
|
exist at startup.
|
|
168
180
|
|
|
181
|
+
Legacy compatibility: `--watch PATH` and `--watch=PATH` still configure
|
|
182
|
+
file-hook mode.
|
|
183
|
+
|
|
184
|
+
## harnex events
|
|
185
|
+
|
|
186
|
+
`harnex events` streams structured per-session JSONL for orchestrators and
|
|
187
|
+
monitoring tooling.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
harnex events --id worker
|
|
191
|
+
harnex events --id worker --snapshot
|
|
192
|
+
harnex events --id worker --from 2026-04-29T10:00:00Z
|
|
193
|
+
harnex events --id worker | jq -c '.'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
| Flag | What it does |
|
|
197
|
+
|------|---------------|
|
|
198
|
+
| `--id ID` | Session ID to inspect (required) |
|
|
199
|
+
| `--[no-]follow` | Stream appended events (default: follow) |
|
|
200
|
+
| `--snapshot` | Print current event file and exit (`--no-follow`) |
|
|
201
|
+
| `--from TS` | Replay floor (ISO-8601, inclusive; `ts >= from`) |
|
|
202
|
+
| `--repo PATH` | Resolve ID from a specific repo root |
|
|
203
|
+
| `--cli CLI` | Filter active-session resolution by CLI |
|
|
204
|
+
|
|
205
|
+
Exit codes:
|
|
206
|
+
|
|
207
|
+
- `0` — snapshot completed, or follow mode observed `type: "exited"`
|
|
208
|
+
- `1` — operational error (missing stream/session, invalid `--from`,
|
|
209
|
+
stream truncated/disappeared, lookup failure)
|
|
210
|
+
|
|
211
|
+
Transport file (append-only JSONL):
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
~/.local/state/harnex/events/<repo_hash>--<id>.jsonl
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Each row uses schema v1 with envelope fields `schema_version`, `seq`, `ts`,
|
|
218
|
+
`id`, and `type`. Emitted today: `started`, `send`, `exited`. `send.msg` is a
|
|
219
|
+
200-character preview with `msg_truncated` when shortened.
|
|
220
|
+
|
|
221
|
+
Schema details and compatibility guarantees are in [docs/events.md](docs/events.md).
|
|
222
|
+
|
|
169
223
|
## Architecture
|
|
170
224
|
|
|
171
225
|
```
|
|
@@ -218,6 +272,8 @@ When you run `harnex run codex --id worker`:
|
|
|
218
272
|
~/.local/state/harnex/sessions/<repo_hash>--<id>.json
|
|
219
273
|
and open transcript file:
|
|
220
274
|
~/.local/state/harnex/output/<repo_hash>--<id>.log
|
|
275
|
+
and open events file:
|
|
276
|
+
~/.local/state/harnex/events/<repo_hash>--<id>.jsonl
|
|
221
277
|
8. Start background threads:
|
|
222
278
|
- PTY reader (screen buffer)
|
|
223
279
|
- State machine (adapter parses screen for state)
|
|
@@ -503,19 +559,21 @@ and `Inbox` classes use `ConditionVariable` for signaling.
|
|
|
503
559
|
|
|
504
560
|
## Skill Files
|
|
505
561
|
|
|
506
|
-
Harnex ships
|
|
507
|
-
|
|
562
|
+
Harnex ships bundled skills that teach agents the orchestration workflow and
|
|
563
|
+
dispatch discipline. The canonical collaboration skill is:
|
|
508
564
|
|
|
509
565
|
```
|
|
510
|
-
skills/harnex/SKILL.md
|
|
566
|
+
skills/harnex-dispatch/SKILL.md
|
|
511
567
|
```
|
|
512
568
|
|
|
513
569
|
### What's in the skill
|
|
514
570
|
|
|
515
|
-
The skill tells agents:
|
|
571
|
+
The dispatch skill tells agents:
|
|
516
572
|
|
|
517
573
|
- How to detect they're inside a harnex session (env vars)
|
|
518
|
-
- How to
|
|
574
|
+
- How to define return channels before delegation
|
|
575
|
+
- How to send short, file-referenced tasks with explicit reply instructions
|
|
576
|
+
- How to send messages, check status, spawn workers, and stop safely
|
|
519
577
|
- How to use `--context`, `--force`, `--no-wait`
|
|
520
578
|
- Relay header format and behavior
|
|
521
579
|
- Collaboration patterns (reply, supervisor, file watch)
|
|
@@ -531,8 +589,8 @@ Skill files use YAML frontmatter:
|
|
|
531
589
|
|
|
532
590
|
```yaml
|
|
533
591
|
---
|
|
534
|
-
name: harnex
|
|
535
|
-
description:
|
|
592
|
+
name: harnex-dispatch
|
|
593
|
+
description: Fire & Watch dispatch pattern...
|
|
536
594
|
allowed-tools: Bash(harnex *)
|
|
537
595
|
---
|
|
538
596
|
```
|
|
@@ -540,41 +598,36 @@ allowed-tools: Bash(harnex *)
|
|
|
540
598
|
The `allowed-tools` field grants the agent permission to run
|
|
541
599
|
`harnex` commands without asking for approval each time.
|
|
542
600
|
|
|
543
|
-
###
|
|
601
|
+
### Installing bundled skills
|
|
544
602
|
|
|
545
|
-
|
|
546
|
-
repo), symlink it into each agent's skill directory:
|
|
603
|
+
Use the installer command instead of manual symlinks:
|
|
547
604
|
|
|
548
605
|
```bash
|
|
549
|
-
#
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
# For Codex
|
|
554
|
-
ln -s /path/to/harnex/skills/harnex \
|
|
555
|
-
~/.codex/skills/harnex
|
|
606
|
+
harnex skills install # all canonical skills
|
|
607
|
+
harnex skills install harnex # compatibility alias -> harnex-dispatch
|
|
608
|
+
harnex skills install --local # install into current repo only
|
|
556
609
|
```
|
|
557
610
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
611
|
+
Compatibility aliases accepted by the installer:
|
|
612
|
+
|
|
613
|
+
- `harnex` -> `harnex-dispatch`
|
|
614
|
+
- `dispatch` -> `harnex-dispatch`
|
|
615
|
+
- `chain-implement` -> `harnex-chain`
|
|
562
616
|
|
|
563
617
|
### Skill directory structure
|
|
564
618
|
|
|
565
619
|
```
|
|
566
620
|
~/.claude/skills/
|
|
567
|
-
└── harnex
|
|
621
|
+
└── harnex-dispatch
|
|
568
622
|
└── SKILL.md
|
|
569
623
|
|
|
570
624
|
~/.codex/skills/
|
|
571
|
-
└── harnex -> /
|
|
625
|
+
└── harnex-dispatch -> ~/.claude/skills/harnex-dispatch
|
|
572
626
|
└── SKILL.md
|
|
573
627
|
```
|
|
574
628
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
picked up immediately.
|
|
629
|
+
Deprecated installed names (`harnex`, `dispatch`, `chain-implement`) are
|
|
630
|
+
cleaned automatically during install and uninstall.
|
|
578
631
|
|
|
579
632
|
## Known Limitations
|
|
580
633
|
|
data/lib/harnex/cli.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Harnex
|
|
|
21
21
|
Status.new(@argv.drop(1)).run
|
|
22
22
|
when "logs"
|
|
23
23
|
Logs.new(@argv.drop(1)).run
|
|
24
|
+
when "events"
|
|
25
|
+
Events.new(@argv.drop(1)).run
|
|
24
26
|
when "pane"
|
|
25
27
|
Pane.new(@argv.drop(1)).run
|
|
26
28
|
when "recipes"
|
|
@@ -59,6 +61,8 @@ module Harnex
|
|
|
59
61
|
Status.usage
|
|
60
62
|
when "logs"
|
|
61
63
|
Logs.usage
|
|
64
|
+
when "events"
|
|
65
|
+
Events.usage
|
|
62
66
|
when "pane"
|
|
63
67
|
Pane.usage
|
|
64
68
|
when "recipes"
|
|
@@ -81,6 +85,7 @@ module Harnex
|
|
|
81
85
|
harnex stop --id ID [options]
|
|
82
86
|
harnex status [options]
|
|
83
87
|
harnex logs --id ID [options]
|
|
88
|
+
harnex events --id ID [options]
|
|
84
89
|
harnex pane --id ID [options]
|
|
85
90
|
harnex help [command]
|
|
86
91
|
|
|
@@ -91,6 +96,7 @@ module Harnex
|
|
|
91
96
|
stop Send the adapter stop sequence to a session
|
|
92
97
|
status List live sessions
|
|
93
98
|
logs Read session output transcripts
|
|
99
|
+
events Stream per-session JSONL runtime events
|
|
94
100
|
pane Capture the current tmux pane for a live session
|
|
95
101
|
recipes List and read workflow recipes
|
|
96
102
|
guide Show the getting started guide
|
|
@@ -109,6 +115,7 @@ module Harnex
|
|
|
109
115
|
harnex run codex -- --cd /path/to/repo
|
|
110
116
|
harnex status
|
|
111
117
|
harnex logs --id main --follow
|
|
118
|
+
harnex events --id main --snapshot
|
|
112
119
|
harnex pane --id main --lines 40
|
|
113
120
|
harnex send --id main --message "Summarize current progress."
|
|
114
121
|
harnex skills install
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Harnex
|
|
6
|
+
class Events
|
|
7
|
+
POLL_INTERVAL = 0.1
|
|
8
|
+
|
|
9
|
+
def self.usage(program_name = "harnex events")
|
|
10
|
+
<<~TEXT
|
|
11
|
+
Usage: #{program_name} [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--id ID Session ID to inspect (required)
|
|
15
|
+
--repo PATH Resolve using PATH's repo root (default: current repo)
|
|
16
|
+
--cli CLI Filter the active session by CLI
|
|
17
|
+
--[no-]follow Keep streaming appended events (default: true)
|
|
18
|
+
--snapshot Print current events and exit (alias for --no-follow)
|
|
19
|
+
--from TS Replay floor (ISO-8601, inclusive)
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
TEXT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(argv)
|
|
25
|
+
@argv = argv.dup
|
|
26
|
+
@options = {
|
|
27
|
+
id: nil,
|
|
28
|
+
repo_path: Dir.pwd,
|
|
29
|
+
cli: nil,
|
|
30
|
+
follow: true,
|
|
31
|
+
from: nil,
|
|
32
|
+
help: false
|
|
33
|
+
}
|
|
34
|
+
@tail_buffer = +""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run
|
|
38
|
+
parser.parse!(@argv)
|
|
39
|
+
if @options[:help]
|
|
40
|
+
puts self.class.usage
|
|
41
|
+
return 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
raise "--id is required for harnex events" unless @options[:id]
|
|
45
|
+
|
|
46
|
+
target = resolve_target
|
|
47
|
+
return 1 unless target
|
|
48
|
+
|
|
49
|
+
offset = print_snapshot(target.fetch(:path))
|
|
50
|
+
return 0 unless @options[:follow] && target[:live]
|
|
51
|
+
|
|
52
|
+
follow(target.fetch(:path), offset)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def parser
|
|
58
|
+
@parser ||= OptionParser.new do |opts|
|
|
59
|
+
opts.banner = "Usage: harnex events [options]"
|
|
60
|
+
opts.on("--id ID", "Session ID to inspect") { |value| @options[:id] = Harnex.normalize_id(value) }
|
|
61
|
+
opts.on("--repo PATH", "Resolve using PATH's repo root") { |value| @options[:repo_path] = value }
|
|
62
|
+
opts.on("--cli CLI", "Filter the active session by CLI") { |value| @options[:cli] = value }
|
|
63
|
+
opts.on("--[no-]follow", "Keep streaming appended events") { |value| @options[:follow] = value }
|
|
64
|
+
opts.on("--snapshot", "Print current events and exit") { @options[:follow] = false }
|
|
65
|
+
opts.on("--from TS", "Replay floor (ISO-8601, inclusive)") { |value| @options[:from] = parse_from(value) }
|
|
66
|
+
opts.on("-h", "--help", "Show help") { @options[:help] = true }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_from(value)
|
|
71
|
+
Time.iso8601(value.to_s)
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
raise OptionParser::InvalidArgument, "--from must be an ISO-8601 timestamp"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve_target
|
|
77
|
+
repo_root = Harnex.resolve_repo_root(@options[:repo_path])
|
|
78
|
+
session = Harnex.read_registry(repo_root, @options[:id], cli: @options[:cli])
|
|
79
|
+
|
|
80
|
+
if session
|
|
81
|
+
path = session["events_log_path"].to_s
|
|
82
|
+
path = Harnex.events_log_path(repo_root, @options[:id]) if path.empty?
|
|
83
|
+
return event_target(path, live: true, repo_root: repo_root)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if cli_filter_mismatch?(repo_root)
|
|
87
|
+
warn("harnex events: no active session found with id #{@options[:id].inspect} and cli #{@options[:cli].inspect}")
|
|
88
|
+
return nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
event_target(Harnex.events_log_path(repo_root, @options[:id]), live: false, repo_root: repo_root)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cli_filter_mismatch?(repo_root)
|
|
95
|
+
return false unless @options[:cli]
|
|
96
|
+
|
|
97
|
+
session = Harnex.read_registry(repo_root, @options[:id])
|
|
98
|
+
return false unless session
|
|
99
|
+
|
|
100
|
+
Harnex.cli_key(Harnex.session_cli(session)) != Harnex.cli_key(@options[:cli])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def event_target(path, live:, repo_root:)
|
|
104
|
+
return { path: path, live: live, repo_root: repo_root } if File.file?(path)
|
|
105
|
+
|
|
106
|
+
if live
|
|
107
|
+
warn("harnex events: stream not found at #{path}")
|
|
108
|
+
else
|
|
109
|
+
warn("harnex events: no session or event stream found with id #{@options[:id].inspect} for #{repo_root}")
|
|
110
|
+
end
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def print_snapshot(path)
|
|
115
|
+
offset = 0
|
|
116
|
+
File.open(path, "rb") do |file|
|
|
117
|
+
file.each_line do |line|
|
|
118
|
+
next unless emit_line?(line)
|
|
119
|
+
|
|
120
|
+
$stdout.write(line)
|
|
121
|
+
end
|
|
122
|
+
offset = file.pos
|
|
123
|
+
end
|
|
124
|
+
$stdout.flush
|
|
125
|
+
offset
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def follow(path, offset)
|
|
129
|
+
current_offset = offset
|
|
130
|
+
|
|
131
|
+
loop do
|
|
132
|
+
streamed = stream_growth(path, current_offset)
|
|
133
|
+
return streamed.fetch(:code) if streamed[:code]
|
|
134
|
+
|
|
135
|
+
current_offset = streamed.fetch(:offset)
|
|
136
|
+
sleep POLL_INTERVAL
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def stream_growth(path, offset)
|
|
141
|
+
unless File.file?(path)
|
|
142
|
+
warn("harnex events: stream source disappeared at #{path}")
|
|
143
|
+
return { code: 1, offset: offset }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
size = File.size(path)
|
|
147
|
+
if size < offset
|
|
148
|
+
warn("harnex events: stream source was truncated at #{path}")
|
|
149
|
+
return { code: 1, offset: size }
|
|
150
|
+
end
|
|
151
|
+
return { offset: offset } if size == offset
|
|
152
|
+
|
|
153
|
+
chunk = +""
|
|
154
|
+
File.open(path, "rb") do |file|
|
|
155
|
+
file.seek(offset)
|
|
156
|
+
chunk = file.read(size - offset).to_s
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@tail_buffer << chunk
|
|
160
|
+
lines = @tail_buffer.split("\n", -1)
|
|
161
|
+
@tail_buffer = lines.pop || +""
|
|
162
|
+
wrote = false
|
|
163
|
+
|
|
164
|
+
lines.each do |line|
|
|
165
|
+
json_line = "#{line}\n"
|
|
166
|
+
event = parse_event(json_line)
|
|
167
|
+
if emit_line?(json_line, parsed: event)
|
|
168
|
+
$stdout.write(json_line)
|
|
169
|
+
wrote = true
|
|
170
|
+
end
|
|
171
|
+
return { code: 0, offset: size } if target_exited?(event)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
$stdout.flush if wrote
|
|
175
|
+
{ offset: size }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def emit_line?(line, parsed: nil)
|
|
179
|
+
return true unless @options[:from]
|
|
180
|
+
|
|
181
|
+
event = parsed || parse_event(line)
|
|
182
|
+
return false unless event
|
|
183
|
+
|
|
184
|
+
timestamp = event_timestamp(event)
|
|
185
|
+
return false unless timestamp
|
|
186
|
+
|
|
187
|
+
timestamp >= @options[:from]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def parse_event(line)
|
|
191
|
+
JSON.parse(line)
|
|
192
|
+
rescue JSON::ParserError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def event_timestamp(event)
|
|
197
|
+
value = event["ts"].to_s
|
|
198
|
+
return nil if value.empty?
|
|
199
|
+
|
|
200
|
+
Time.iso8601(value)
|
|
201
|
+
rescue ArgumentError
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def target_exited?(event)
|
|
206
|
+
return false unless event
|
|
207
|
+
return false unless event["type"] == "exited"
|
|
208
|
+
|
|
209
|
+
Harnex.id_key(event["id"].to_s) == Harnex.id_key(@options[:id])
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|