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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +151 -0
- data/GUIDE.md +11 -11
- data/README.md +22 -16
- data/TECHNICAL.md +25 -59
- data/guides/01_dispatch.md +139 -0
- data/guides/02_chain.md +113 -0
- data/guides/03_buddy.md +94 -0
- data/guides/04_monitoring.md +130 -0
- data/guides/05_naming.md +106 -0
- data/lib/harnex/adapters/codex_appserver.rb +43 -8
- data/lib/harnex/cli.rb +13 -6
- data/lib/harnex/commands/agents_guide.rb +109 -0
- data/lib/harnex/commands/doctor.rb +8 -0
- data/lib/harnex/commands/events.rb +10 -0
- data/lib/harnex/commands/guide.rb +9 -0
- data/lib/harnex/commands/logs.rb +10 -0
- data/lib/harnex/commands/pane.rb +10 -0
- data/lib/harnex/commands/recipes.rb +9 -0
- data/lib/harnex/commands/run.rb +11 -0
- data/lib/harnex/commands/send.rb +13 -1
- data/lib/harnex/commands/status.rb +10 -0
- data/lib/harnex/commands/stop.rb +10 -0
- data/lib/harnex/commands/wait.rb +10 -0
- data/lib/harnex/runtime/session.rb +33 -7
- data/lib/harnex/runtime/session_state.rb +7 -0
- data/lib/harnex/version.rb +1 -1
- data/lib/harnex.rb +1 -1
- metadata +7 -8
- data/lib/harnex/commands/skills.rb +0 -226
- data/skills/close/SKILL.md +0 -47
- data/skills/harnex/SKILL.md +0 -20
- data/skills/harnex-buddy/SKILL.md +0 -104
- data/skills/harnex-chain/SKILL.md +0 -132
- data/skills/harnex-dispatch/SKILL.md +0 -294
- data/skills/open/SKILL.md +0 -32
data/lib/harnex/commands/run.rb
CHANGED
|
@@ -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
|
|
data/lib/harnex/commands/send.rb
CHANGED
|
@@ -38,7 +38,19 @@ module Harnex
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def self.usage(program_name = "harnex send")
|
|
41
|
-
|
|
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
|
|
data/lib/harnex/commands/stop.rb
CHANGED
|
@@ -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
|
|
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -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(
|
|
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:
|
|
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
|
data/lib/harnex/version.rb
CHANGED
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/
|
|
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.
|
|
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
|
data/skills/close/SKILL.md
DELETED
|
@@ -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
|
data/skills/harnex/SKILL.md
DELETED
|
@@ -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
|