harnex 0.6.0 → 0.6.3

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.
@@ -48,6 +48,17 @@ module Harnex
48
48
  CLIs with smart prompt detection: #{Adapters.known.join(', ')}
49
49
  Any other CLI name is launched with generic wrapping.
50
50
  Wrapper options may appear before or after <cli>.
51
+
52
+ Common patterns:
53
+ #{program_name} codex --id cx-i-42 --tmux cx-i-42 --context "Read /tmp/task-impl-42.md"
54
+ #{program_name} codex --id cx-i-42 --watch --preset impl --context "Read /tmp/task-impl-42.md"
55
+ #{program_name} claude --id cl-r-42 --tmux cl-r-42 --description "Review task 42"
56
+
57
+ Gotchas:
58
+ Always pair --id and --tmux with the same value for delegated work.
59
+ Passing --tmux without --id creates a random harnex session ID.
60
+ --watch is foreground-only; do not combine it with --tmux or --detach.
61
+ Use -- before child CLI flags when a flag could be parsed by harnex.
51
62
  TEXT
52
63
  end
53
64
 
@@ -38,7 +38,19 @@ module Harnex
38
38
  end
39
39
 
40
40
  def self.usage(program_name = "harnex send")
41
- build_parser({}, program_name).to_s
41
+ <<~TEXT
42
+ #{build_parser({}, program_name)}
43
+ Common patterns:
44
+ #{program_name} --id cx-i-42 --message "Read /tmp/task-impl-42.md" --wait-for-idle --timeout 900
45
+ #{program_name} --id cx-i-42 --message "Continue with the current task." --force
46
+ #{program_name} --id "$HARNEX_ID" --message "Worker finished; tests passed."
47
+
48
+ Gotchas:
49
+ --wait-for-idle is a turn fence, not proof that the whole task is done.
50
+ Use --no-wait for fire-and-forget delivery only when another monitor owns completion.
51
+ Long prompts are more reliable when written to a file and referenced by path.
52
+ Messages between harnex sessions get relay headers unless --no-relay is used.
53
+ TEXT
42
54
  end
43
55
 
44
56
  def initialize(argv)
@@ -19,6 +19,16 @@ module Harnex
19
19
  --all List sessions across all repos
20
20
  --json Output JSON instead of a table
21
21
  -h, --help Show this help
22
+
23
+ Common patterns:
24
+ #{program_name}
25
+ #{program_name} --all
26
+ #{program_name} --id cx-i-42 --json
27
+
28
+ Gotchas:
29
+ By default, status filters to the current repo root.
30
+ Use --all when supervising workers launched from sibling worktrees.
31
+ A prompt-like state is not a completion signal by itself.
22
32
  TEXT
23
33
  end
24
34
 
@@ -23,6 +23,16 @@ module Harnex
23
23
 
24
24
  Sends the adapter stop sequence to the session.
25
25
  Use `harnex wait --id ID` afterward to block until the session finishes.
26
+
27
+ Common patterns:
28
+ #{program_name} --id cx-i-42
29
+ #{program_name} --id cx-i-42 --repo /path/to/repo
30
+ #{program_name} --id cx-i-42 --timeout 15
31
+
32
+ Gotchas:
33
+ Stop only after verifying the worker's result landed.
34
+ For tmux sessions, stop targets the harnex session ID, not the tmux name.
35
+ If a session is in another repo/worktree, pass --repo or run status --all.
26
36
  TEXT
27
37
  end
28
38
 
@@ -24,6 +24,16 @@ module Harnex
24
24
  --repo PATH Resolve session using PATH's repo root (default: current repo)
25
25
  --timeout SECS Maximum time to wait in seconds (default: unlimited)
26
26
  -h, --help Show this help
27
+
28
+ Common patterns:
29
+ #{program_name} --id cx-i-42 --until task_complete --timeout 900
30
+ #{program_name} --id cx-i-42 --until prompt --timeout 120
31
+ #{program_name} --id cx-i-42
32
+
33
+ Gotchas:
34
+ task_complete is an event predicate; prompt/busy are live state polls.
35
+ Prompt state alone does not prove work acceptance. Verify artifacts/tests.
36
+ Without --timeout, wait can block indefinitely.
27
37
  TEXT
28
38
  end
29
39
 
@@ -229,7 +229,7 @@ module Harnex
229
229
 
230
230
  def inject_via_adapter(text:, submit:, enter_only:, force: false)
231
231
  if adapter.transport == :stdio_jsonrpc
232
- return inject_via_jsonrpc(text: text, force: force)
232
+ return inject_via_jsonrpc(text: text, submit: submit, enter_only: enter_only, force: force)
233
233
  end
234
234
 
235
235
  snapshot = adapter.wait_for_sendable(method(:screen_snapshot), submit: submit, enter_only: enter_only, force: force)
@@ -256,25 +256,36 @@ module Harnex
256
256
  .tap { emit_send_event(text, force: payload[:force]) }
257
257
  end
258
258
 
259
- def inject_via_jsonrpc(text:, force: false)
259
+ def inject_via_jsonrpc(text:, submit:, enter_only:, force: false)
260
+ payload = adapter.build_send_payload(
261
+ text: text,
262
+ submit: submit,
263
+ enter_only: enter_only,
264
+ screen_text: nil,
265
+ force: force
266
+ )
267
+ dispatch = payload.fetch(:dispatch).dup
268
+ dispatch[:model] = meta_hash["model"] if meta_hash["model"] && !dispatch.key?(:model)
269
+ dispatch[:effort] = meta_hash["effort"] if meta_hash["effort"] && !dispatch.key?(:effort)
270
+
260
271
  turn_id = nil
261
272
  @inject_mutex.synchronize do
262
- turn_id = adapter.dispatch(prompt: text)
273
+ turn_id = adapter.dispatch(**dispatch)
263
274
  @state_machine.force_busy!
264
275
  @injected_count += 1
265
276
  @last_injected_at = Time.now
266
277
  persist_registry
267
278
  end
268
279
 
269
- emit_send_event(text, force: force)
280
+ emit_send_event(dispatch.fetch(:prompt, text), force: payload[:force])
270
281
  {
271
282
  ok: true,
272
283
  cli: adapter.key,
273
- bytes_written: text.to_s.bytesize,
284
+ bytes_written: dispatch.fetch(:prompt, text).to_s.bytesize,
274
285
  injected_count: @injected_count,
275
286
  newline: false,
276
- input_state: adapter.input_state(nil),
277
- force: force,
287
+ input_state: payload[:input_state],
288
+ force: payload[:force],
278
289
  turn_id: turn_id
279
290
  }
280
291
  end
@@ -300,6 +311,7 @@ module Harnex
300
311
 
301
312
  adapter.start_rpc(env: child_env, cwd: repo_root)
302
313
  @pid = adapter.pid
314
+ @state_machine.force_prompt!
303
315
  emit_started_event
304
316
  emit_git_start_event
305
317
 
@@ -310,6 +322,7 @@ module Harnex
310
322
 
311
323
  watch_thread = start_watch_thread
312
324
  @inbox.start
325
+ dispatch_initial_prompt
313
326
 
314
327
  if @pid
315
328
  begin
@@ -364,9 +377,11 @@ module Harnex
364
377
  when "thread/started"
365
378
  @rpc_thread_id = params["threadId"] || params["thread_id"]
366
379
  when "turn/started"
380
+ @state_machine.force_busy!
367
381
  emit_event("turn_started", turnId: params["turnId"] || params["turn_id"])
368
382
  when "turn/completed"
369
383
  @last_completed_at = Time.now
384
+ @state_machine.force_prompt!
370
385
  payload = { turnId: params["turnId"] || params["turn_id"] }
371
386
  payload[:status] = params["status"] if params["status"]
372
387
  payload[:tokenUsage] = params["tokenUsage"] if params["tokenUsage"]
@@ -386,6 +401,7 @@ module Harnex
386
401
  when "account/rateLimits/updated"
387
402
  @rate_limits = params
388
403
  when "error"
404
+ @state_machine.force_busy!
389
405
  emit_event("disconnected", source: "error_notification", message: params["message"])
390
406
  signal_rpc_done!
391
407
  end
@@ -395,10 +411,20 @@ module Harnex
395
411
 
396
412
  def handle_rpc_disconnect(error)
397
413
  msg = error.is_a?(Hash) ? error["message"] : error&.message
414
+ @state_machine.force_busy!
398
415
  emit_event("disconnected", source: "transport", message: msg) rescue nil
399
416
  signal_rpc_done!
400
417
  end
401
418
 
419
+ def dispatch_initial_prompt
420
+ return unless adapter.respond_to?(:initial_prompt)
421
+
422
+ prompt = adapter.initial_prompt
423
+ return if prompt.to_s.empty?
424
+
425
+ inject_via_jsonrpc(text: prompt, submit: true, enter_only: false, force: false)
426
+ end
427
+
402
428
  def render_item_text(item)
403
429
  return nil unless item.is_a?(Hash)
404
430
 
@@ -36,6 +36,13 @@ module Harnex
36
36
  end
37
37
  end
38
38
 
39
+ def force_prompt!
40
+ @mutex.synchronize do
41
+ @state = :prompt
42
+ @condvar.broadcast
43
+ end
44
+ end
45
+
39
46
  def wait_for_prompt(timeout)
40
47
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
41
48
  @mutex.synchronize do
@@ -1,4 +1,4 @@
1
1
  module Harnex
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.3"
3
3
  RELEASE_DATE = "2026-05-06"
4
4
  end
data/lib/harnex.rb CHANGED
@@ -24,6 +24,6 @@ require_relative "harnex/commands/events"
24
24
  require_relative "harnex/commands/pane"
25
25
  require_relative "harnex/commands/recipes"
26
26
  require_relative "harnex/commands/guide"
27
- require_relative "harnex/commands/skills"
27
+ require_relative "harnex/commands/agents_guide"
28
28
  require_relative "harnex/commands/doctor"
29
29
  require_relative "harnex/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harnex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jikku Jose
@@ -26,6 +26,11 @@ files:
26
26
  - TECHNICAL.md
27
27
  - bin/gem-push
28
28
  - bin/harnex
29
+ - guides/01_dispatch.md
30
+ - guides/02_chain.md
31
+ - guides/03_buddy.md
32
+ - guides/04_monitoring.md
33
+ - guides/05_naming.md
29
34
  - lib/harnex.rb
30
35
  - lib/harnex/adapters.rb
31
36
  - lib/harnex/adapters/base.rb
@@ -34,6 +39,7 @@ files:
34
39
  - lib/harnex/adapters/codex_appserver.rb
35
40
  - lib/harnex/adapters/generic.rb
36
41
  - lib/harnex/cli.rb
42
+ - lib/harnex/commands/agents_guide.rb
37
43
  - lib/harnex/commands/doctor.rb
38
44
  - lib/harnex/commands/events.rb
39
45
  - lib/harnex/commands/guide.rb
@@ -42,7 +48,6 @@ files:
42
48
  - lib/harnex/commands/recipes.rb
43
49
  - lib/harnex/commands/run.rb
44
50
  - lib/harnex/commands/send.rb
45
- - lib/harnex/commands/skills.rb
46
51
  - lib/harnex/commands/status.rb
47
52
  - lib/harnex/commands/stop.rb
48
53
  - lib/harnex/commands/wait.rb
@@ -62,12 +67,6 @@ files:
62
67
  - recipes/01_fire_and_watch.md
63
68
  - recipes/02_chain_implement.md
64
69
  - recipes/03_buddy.md
65
- - skills/close/SKILL.md
66
- - skills/harnex-buddy/SKILL.md
67
- - skills/harnex-chain/SKILL.md
68
- - skills/harnex-dispatch/SKILL.md
69
- - skills/harnex/SKILL.md
70
- - skills/open/SKILL.md
71
70
  homepage: https://github.com/jikkuatwork/harnex
72
71
  licenses:
73
72
  - MIT
@@ -1,226 +0,0 @@
1
- require "fileutils"
2
-
3
- module Harnex
4
- class Skills
5
- SKILLS_ROOT = File.expand_path("../../../../skills", __FILE__)
6
- INSTALL_SKILLS = %w[harnex-dispatch harnex-chain harnex-buddy].freeze
7
- DEPRECATED_SKILLS = %w[harnex dispatch chain-implement].freeze
8
- SKILL_ALIASES = {
9
- "harnex" => "harnex-dispatch",
10
- "dispatch" => "harnex-dispatch",
11
- "chain-implement" => "harnex-chain"
12
- }.freeze
13
-
14
- def self.usage
15
- <<~TEXT
16
- Usage: harnex skills <subcommand> [SKILL...] [--local]
17
-
18
- Subcommands:
19
- install Install bundled skills (globally by default; optional skill names)
20
- uninstall Remove installed skills (globally by default)
21
-
22
- Options:
23
- --local Target the current repo instead of global ~/.claude/
24
-
25
- Installs: #{INSTALL_SKILLS.join(', ')}
26
- Aliases: harnex|dispatch -> harnex-dispatch, chain-implement -> harnex-chain
27
-
28
- By default, copies each skill to ~/.claude/skills/<skill>/
29
- and symlinks ~/.codex/skills/<skill> to it.
30
-
31
- With --local, copies each skill to .claude/skills/<skill>/
32
- in the current repo and symlinks .codex/skills/<skill> to it.
33
- TEXT
34
- end
35
-
36
- def initialize(argv)
37
- @argv = argv.dup
38
- end
39
-
40
- def run
41
- subcommand = @argv.shift
42
- case subcommand
43
- when "install"
44
- local, help, requested_skills = parse_args(@argv, allow_positional: true)
45
- return (puts self.class.usage; 0) if help
46
-
47
- remove_deprecated(local)
48
- install_skills = requested_skills.empty? ? INSTALL_SKILLS : canonical_skill_names(requested_skills)
49
-
50
- install_skills.each do |skill_name|
51
- skill_source = resolve_skill_source(skill_name)
52
- unless skill_source
53
- return missing_skill(skill_name)
54
- end
55
-
56
- result = local ? install_local(skill_name, skill_source) : install_global(skill_name, skill_source)
57
- return result unless result == 0
58
- end
59
- 0
60
- when "uninstall"
61
- local, help, = parse_args(@argv)
62
- return (puts self.class.usage; 0) if help
63
-
64
- (INSTALL_SKILLS + DEPRECATED_SKILLS).each do |skill_name|
65
- local ? uninstall_local(skill_name) : uninstall_global(skill_name)
66
- end
67
- 0
68
- when "-h", "--help", nil
69
- puts self.class.usage
70
- 0
71
- else
72
- warn("harnex skills: unknown subcommand #{subcommand.inspect}")
73
- puts self.class.usage
74
- 1
75
- end
76
- end
77
-
78
- private
79
-
80
- def parse_args(args, allow_positional: false)
81
- local = false
82
- help = false
83
- positional = []
84
-
85
- args.each do |arg|
86
- case arg
87
- when "--local"
88
- local = true
89
- when "-h", "--help"
90
- help = true
91
- when /\A-/
92
- raise "harnex skills: unknown option #{arg.inspect}"
93
- else
94
- if allow_positional
95
- positional << arg
96
- else
97
- warn("harnex skills: unexpected argument #{arg.inspect}")
98
- raise "harnex skills takes no positional arguments"
99
- end
100
- end
101
- end
102
-
103
- [local, help, positional]
104
- end
105
-
106
- def resolve_skill_source(skill_name)
107
- path = File.join(SKILLS_ROOT, skill_name)
108
- File.directory?(path) ? path : nil
109
- end
110
-
111
- def missing_skill(skill_name)
112
- warn("harnex skills: bundled skill #{skill_name.inspect} not found at #{SKILLS_ROOT}")
113
- 1
114
- end
115
-
116
- def remove_deprecated(local)
117
- DEPRECATED_SKILLS.each do |skill_name|
118
- local ? uninstall_local(skill_name) : uninstall_global(skill_name)
119
- end
120
- end
121
-
122
- def canonical_skill_names(skill_names)
123
- skill_names.map { |name| canonical_skill_name(name) }.uniq
124
- end
125
-
126
- def canonical_skill_name(skill_name)
127
- SKILL_ALIASES.fetch(skill_name, skill_name)
128
- end
129
-
130
- def install_local(skill_name, skill_source)
131
- repo_root = Harnex.resolve_repo_root(Dir.pwd)
132
- claude_dir = File.join(repo_root, ".claude", "skills", skill_name)
133
- codex_dir = File.join(repo_root, ".codex", "skills", skill_name)
134
-
135
- # Copy skill to .claude/skills/<skill>/
136
- if Dir.exist?(claude_dir)
137
- warn("harnex skills: #{claude_dir} already exists, overwriting")
138
- FileUtils.rm_rf(claude_dir)
139
- end
140
- FileUtils.mkdir_p(File.dirname(claude_dir))
141
- FileUtils.cp_r(skill_source, claude_dir)
142
- puts "installed #{claude_dir}"
143
-
144
- # Symlink .codex/skills/<skill> -> .claude/skills/<skill>
145
- codex_parent = File.dirname(codex_dir)
146
- FileUtils.mkdir_p(codex_parent)
147
- FileUtils.rm_rf(codex_dir) if File.exist?(codex_dir) || File.symlink?(codex_dir)
148
-
149
- # Relative symlink so it works if the repo moves
150
- relative = relative_path(from: codex_parent, to: claude_dir)
151
- File.symlink(relative, codex_dir)
152
- puts "symlinked #{codex_dir} -> #{relative}"
153
-
154
- 0
155
- end
156
-
157
- def install_global(skill_name, skill_source)
158
- claude_dir = File.expand_path("~/.claude/skills/#{skill_name}")
159
- codex_dir = File.expand_path("~/.codex/skills/#{skill_name}")
160
-
161
- # Copy skill to ~/.claude/skills/<skill>/
162
- if Dir.exist?(claude_dir)
163
- warn("harnex skills: #{claude_dir} already exists, overwriting")
164
- FileUtils.rm_rf(claude_dir)
165
- end
166
- FileUtils.mkdir_p(File.dirname(claude_dir))
167
- FileUtils.cp_r(skill_source, claude_dir)
168
- puts "installed #{claude_dir}"
169
-
170
- # Symlink ~/.codex/skills/<skill> -> ~/.claude/skills/<skill>
171
- codex_parent = File.dirname(codex_dir)
172
- FileUtils.mkdir_p(codex_parent)
173
- FileUtils.rm_rf(codex_dir) if File.exist?(codex_dir) || File.symlink?(codex_dir)
174
- File.symlink(claude_dir, codex_dir)
175
- puts "symlinked #{codex_dir} -> #{claude_dir}"
176
-
177
- 0
178
- end
179
-
180
- def uninstall_local(skill_name)
181
- repo_root = Harnex.resolve_repo_root(Dir.pwd)
182
- claude_dir = File.join(repo_root, ".claude", "skills", skill_name)
183
- codex_dir = File.join(repo_root, ".codex", "skills", skill_name)
184
-
185
- removed = false
186
- if File.exist?(codex_dir) || File.symlink?(codex_dir)
187
- FileUtils.rm_rf(codex_dir)
188
- removed = true
189
- end
190
- if File.exist?(claude_dir) || File.symlink?(claude_dir)
191
- FileUtils.rm_rf(claude_dir)
192
- removed = true
193
- end
194
- puts "removed #{skill_name}" if removed
195
- end
196
-
197
- def uninstall_global(skill_name)
198
- claude_dir = File.expand_path("~/.claude/skills/#{skill_name}")
199
- codex_dir = File.expand_path("~/.codex/skills/#{skill_name}")
200
-
201
- removed = false
202
- if File.exist?(codex_dir) || File.symlink?(codex_dir)
203
- FileUtils.rm_rf(codex_dir)
204
- removed = true
205
- end
206
- if File.exist?(claude_dir) || File.symlink?(claude_dir)
207
- FileUtils.rm_rf(claude_dir)
208
- removed = true
209
- end
210
- puts "removed #{skill_name}" if removed
211
- end
212
-
213
- def relative_path(from:, to:)
214
- from_parts = File.expand_path(from).split("/")
215
- to_parts = File.expand_path(to).split("/")
216
-
217
- # Drop common prefix
218
- while from_parts.first == to_parts.first && !from_parts.empty?
219
- from_parts.shift
220
- to_parts.shift
221
- end
222
-
223
- ([".."] * from_parts.length + to_parts).join("/")
224
- end
225
- end
226
- end
@@ -1,47 +0,0 @@
1
- ---
2
- name: close
3
- description: Close a work session in this repo — update koder/STATE.md with what changed and the next step, clean up accidental or temporary repo artifacts, and leave a clear handoff. Use when the user says "close session", "wrap up", "end session", "handoff", or invokes "/close".
4
- ---
5
-
6
- # Close Session Workflow
7
-
8
- When the user asks to wrap up or close the current session, run this sequence:
9
-
10
- ## 1. Review the session changes
11
-
12
- - Check `git status --short` and `git diff --stat`
13
- - Separate the work from this session from unrelated user changes
14
- - Do not revert unrelated changes you did not make
15
-
16
- ## 2. Update `koder/STATE.md`
17
-
18
- - Update the `Updated:` date
19
- - Add or adjust concise lines in `Current snapshot` for completed work
20
- - Update test count if it changed
21
- - Update issue or plan statuses only when work was actually completed or a new blocker was clearly discovered
22
- - Rewrite `Next step` so the next agent can resume without reconstructing context
23
-
24
- ## 3. Clean up repo artifacts
25
-
26
- - Remove temporary files, scratch notes, or mistaken tracking docs created during the session
27
- - Keep durable artifacts that are part of the intended result
28
- - If cleanup would discard ambiguous work, ask the user instead of guessing
29
-
30
- ## 4. Commit and clean the repo
31
-
32
- - Stage all session changes (code, docs, STATE.md updates) and commit with a clear message summarizing the session's work
33
- - Do NOT stage files that look like secrets, credentials, or large binaries — flag them to the user
34
- - After committing, run `git status --short` to confirm a clean working tree
35
- - If unrelated uncommitted changes remain, leave them alone — do not revert or commit work you did not produce
36
-
37
- ## 5. Verify the handoff
38
-
39
- - Run the relevant tests or verification commands if code or docs changed
40
- - Give the user a concise summary of what changed, the commit, and any remaining follow-up
41
- - The goal: the next `/open` should see a clean repo and an accurate `koder/STATE.md`
42
-
43
- ## Notes
44
-
45
- - Do NOT create or close issue docs unless the user explicitly asks
46
- - Do NOT build, install, or publish anything unless the user explicitly asks
47
- - If `koder/STATE.md` is already accurate, keep the update minimal rather than churning it
@@ -1,20 +0,0 @@
1
- ---
2
- name: harnex
3
- description: Deprecated alias for harnex-dispatch. Use harnex-dispatch for active harnex collaboration guidance.
4
- ---
5
-
6
- # Deprecated Skill Alias
7
-
8
- `harnex` is deprecated.
9
-
10
- Use `harnex-dispatch` as the canonical skill for harnex collaboration,
11
- Fire & Watch dispatching, relay handling, and return-channel discipline.
12
-
13
- If you are installing skills, use:
14
-
15
- ```bash
16
- harnex skills install harnex-dispatch
17
- ```
18
-
19
- Compatibility note: `harnex skills install harnex` remains supported and
20
- installs `harnex-dispatch`.
@@ -1,104 +0,0 @@
1
- ---
2
- name: harnex-buddy
3
- description: Spawn an accountability partner for long-running harnex sessions. Use when the user asks to run something overnight, unattended, or for any work expected to take more than 30 minutes without supervision.
4
- ---
5
-
6
- # Buddy — Accountability Partner for Long-Running Work
7
-
8
- For any long-running or unattended work, spawn a **buddy** — a second harnex
9
- agent that watches the worker and nudges it if it stalls.
10
-
11
- For plain stall recovery (force-resume on inactivity), prefer
12
- `harnex run --watch --preset impl`. Use a buddy when you need reasoning that
13
- policy checks cannot provide (doc drift, semantic checks, multi-session
14
- correlation).
15
-
16
- The buddy is an LLM, so it has intelligence for free. It reads the worker's
17
- screen, reasons about whether it's stuck, and composes a meaningful nudge.
18
-
19
- Dispatch workers via `harnex-dispatch` first. This skill owns only buddy
20
- behavior after the worker is already running.
21
-
22
- ## When to activate
23
-
24
- - User says "do this overnight" or "run this while I'm away"
25
- - Task is expected to take more than 30 minutes unattended
26
- - User explicitly asks for a buddy, accountability partner, or monitoring
27
- - User asks to "keep an eye on" a dispatched worker
28
-
29
- ## Spawn the buddy
30
-
31
- After dispatching the worker with `harnex-dispatch`, spawn a buddy alongside
32
- it. Keep ID/tmux naming consistent with `harnex-dispatch` (`--tmux` matches
33
- `--id`):
34
-
35
- ```bash
36
- # Spawn its buddy
37
- harnex run claude --id buddy-42 --tmux buddy-42
38
- ```
39
-
40
- ## Write the buddy prompt
41
-
42
- Write a task file with the watching instructions, then send it:
43
-
44
- ```bash
45
- cat > /tmp/buddy-42.md <<'EOF'
46
- You are an accountability partner for harnex session `cx-impl-42`.
47
-
48
- Your job:
49
- 1. Every 5 minutes, check on the worker:
50
- - `harnex pane --id cx-impl-42 --lines 20`
51
- - `harnex status --id cx-impl-42 --json`
52
- 2. If the worker appears stuck at a prompt for more than 10 minutes
53
- with no progress, nudge it:
54
- - `harnex send --id cx-impl-42 --message "You appear to have stalled. Continue with your current task."`
55
- 3. If the worker has exited, report back to the invoker:
56
- - `tmux send-keys -t "$HARNEX_SPAWNER_PANE" "cx-impl-42 has exited. Check results." Enter`
57
- 4. Keep watching until the worker finishes or is stopped.
58
-
59
- Do not interfere with work in progress. Only nudge when clearly stalled.
60
- EOF
61
-
62
- harnex send --id buddy-42 --message "Read and execute /tmp/buddy-42.md"
63
- ```
64
-
65
- Adjust the polling interval (5 min), stall threshold (10 min), and nudge
66
- message to match the workload.
67
-
68
- ## Return channel
69
-
70
- The buddy can reach back to the invoker (your raw Claude session) via
71
- `$HARNEX_SPAWNER_PANE` — the stable tmux pane ID set automatically by
72
- harnex at spawn time:
73
-
74
- ```bash
75
- # Read the invoker's screen
76
- tmux capture-pane -t "$HARNEX_SPAWNER_PANE" -p
77
-
78
- # Type into the invoker
79
- tmux send-keys -t "$HARNEX_SPAWNER_PANE" "worker-42 finished" Enter
80
- ```
81
-
82
- The invoker does NOT need to be a harnex session. It just needs to be in tmux.
83
-
84
- ## Naming convention
85
-
86
- Use naming from `harnex-dispatch`: set `--tmux <same-as-id>` for every
87
- session and keep the buddy ID paired with the worker step ID.
88
-
89
- ## Cleanup
90
-
91
- Stop the buddy after the worker finishes:
92
-
93
- ```bash
94
- harnex stop --id buddy-42
95
- ```
96
-
97
- ## Notes
98
-
99
- - For chain orchestration, phase gates, and the 5-concurrent parallel planning
100
- cap, see `harnex-chain`.
101
- - One buddy per worker, or one buddy watching multiple sessions
102
- - The buddy is a regular harnex session — stop, inspect, log it like any other
103
- - Tune polling and thresholds in the buddy's prompt, not in harnex config
104
- - See `recipes/03_buddy.md` for the full recipe