harnex 0.12.0 → 0.29.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +592 -0
  3. data/GUIDE.md +222 -68
  4. data/README.md +78 -48
  5. data/TECHNICAL.md +235 -98
  6. data/agents/gpt.md +60 -0
  7. data/docs/agents.md +119 -0
  8. data/docs/dispatch-telemetry.md +184 -50
  9. data/docs/events.md +20 -7
  10. data/docs/lanes.md +202 -0
  11. data/docs/mcp.md +102 -0
  12. data/docs/pi-rpc.md +81 -7
  13. data/docs/upgrading-to-0.29.md +87 -0
  14. data/guides/01_dispatch.md +101 -34
  15. data/guides/04_monitoring.md +223 -25
  16. data/guides/05_naming.md +0 -1
  17. data/guides/06_claude.md +96 -0
  18. data/lib/harnex/adapters/base.rb +4 -8
  19. data/lib/harnex/adapters/pi.rb +92 -12
  20. data/lib/harnex/adapters.rb +1 -16
  21. data/lib/harnex/artifact_report.rb +206 -12
  22. data/lib/harnex/cli.rb +68 -50
  23. data/lib/harnex/commands/ask.rb +278 -0
  24. data/lib/harnex/commands/delegate.rb +202 -0
  25. data/lib/harnex/commands/doctor.rb +34 -36
  26. data/lib/harnex/commands/events.rb +3 -3
  27. data/lib/harnex/commands/history.rb +2 -8
  28. data/lib/harnex/commands/land.rb +150 -0
  29. data/lib/harnex/commands/lanes.rb +84 -0
  30. data/lib/harnex/commands/logs.rb +3 -3
  31. data/lib/harnex/commands/mcp.rb +208 -0
  32. data/lib/harnex/commands/orchestration.rb +88 -9
  33. data/lib/harnex/commands/pane.rb +3 -3
  34. data/lib/harnex/commands/preflight.rb +566 -0
  35. data/lib/harnex/commands/reply.rb +125 -0
  36. data/lib/harnex/commands/result.rb +108 -0
  37. data/lib/harnex/commands/run.rb +248 -64
  38. data/lib/harnex/commands/send.rb +121 -16
  39. data/lib/harnex/commands/status.rb +32 -4
  40. data/lib/harnex/commands/stop.rb +14 -4
  41. data/lib/harnex/commands/telemetry.rb +19 -1
  42. data/lib/harnex/commands/wait.rb +421 -56
  43. data/lib/harnex/commands/watch.rb +134 -21
  44. data/lib/harnex/context_file.rb +85 -0
  45. data/lib/harnex/core.rb +100 -15
  46. data/lib/harnex/dispatch_history.rb +157 -10
  47. data/lib/harnex/lanes.rb +513 -0
  48. data/lib/harnex/mcp.rb +362 -0
  49. data/lib/harnex/orchestration.rb +128 -5
  50. data/lib/harnex/owned_process_group.rb +50 -0
  51. data/lib/harnex/owner_stream.rb +399 -0
  52. data/lib/harnex/pricing.rb +4 -9
  53. data/lib/harnex/runtime/activity_tracker.rb +107 -0
  54. data/lib/harnex/runtime/api_server.rb +11 -1
  55. data/lib/harnex/runtime/completion_notifier.rb +90 -11
  56. data/lib/harnex/runtime/inbox.rb +26 -7
  57. data/lib/harnex/runtime/message.rb +5 -2
  58. data/lib/harnex/runtime/runtime_budget.rb +96 -0
  59. data/lib/harnex/runtime/session.rb +684 -294
  60. data/lib/harnex/runtime/stop_request.rb +39 -0
  61. data/lib/harnex/stall.rb +46 -0
  62. data/lib/harnex/telemetry_reconciler.rb +42 -20
  63. data/lib/harnex/terminal_status.rb +5 -5
  64. data/lib/harnex/version.rb +2 -2
  65. data/lib/harnex.rb +16 -0
  66. data/recipes/01_fire_and_watch.md +14 -14
  67. data/recipes/02_chain_implement.md +35 -31
  68. data/recipes/03_buddy.md +1 -1
  69. metadata +34 -12
  70. data/docs/codex-appserver.md +0 -175
  71. data/lib/harnex/adapters/codex.rb +0 -160
  72. data/lib/harnex/adapters/codex_appserver.rb +0 -407
  73. data/lib/harnex/codex/app_server/client.rb +0 -347
@@ -0,0 +1,39 @@
1
+ require "time"
2
+
3
+ module Harnex
4
+ # Caller-declared provenance is deliberately an enum, never a free-text log.
5
+ # Session owns first-request-wins synchronization and work acceptance.
6
+ class StopRequest
7
+ REASONS = %w[manual completion runtime_budget].map(&:freeze).freeze
8
+ ORIGINS = %w[api cli auto_stop watch runtime].map(&:freeze).freeze
9
+ WORK_STATES = %w[running completed failed unknown].freeze
10
+
11
+ def self.validate!(reason:, origin:)
12
+ raise ArgumentError, "stop reason must be one of #{REASONS.join(', ')}" unless REASONS.include?(reason)
13
+ raise ArgumentError, "stop origin must be one of #{ORIGINS.join(', ')}" unless ORIGINS.include?(origin)
14
+ end
15
+
16
+ def initialize(reason:, origin:, work_state:, requested_at: Time.now, runtime_limit_s: nil)
17
+ self.class.validate!(reason: reason, origin: origin)
18
+ raise ArgumentError, "invalid stop work state" unless WORK_STATES.include?(work_state)
19
+
20
+ @payload = {
21
+ reason: reason.dup.freeze,
22
+ origin: origin.dup.freeze,
23
+ requested_at: requested_at.getutc.iso8601(3).freeze,
24
+ work_state: work_state.dup.freeze
25
+ }
26
+ unless runtime_limit_s.nil?
27
+ limit = Float(runtime_limit_s)
28
+ raise ArgumentError, "invalid stop runtime limit" unless limit.finite? && limit.positive?
29
+
30
+ @payload[:runtime_limit_s] = limit
31
+ end
32
+ @payload.freeze
33
+ end
34
+
35
+ def to_h
36
+ @payload.dup
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ module Harnex
2
+ # #76: one "no progress" predicate over a live status payload, shared by
3
+ # `wait/watch --stall-after` and later observers (#77). It only reads clocks;
4
+ # it never nudges, stops, or judges the worker.
5
+ module Stall
6
+ STRUCTURED_CLOCKS = %w[turn_age_s model_idle_s tool_idle_s].freeze
7
+
8
+ module_function
9
+
10
+ # Seconds since the session last showed progress, with the clock used:
11
+ # activity - the structured Pi adapter: the most
12
+ # recent of turn start, model event, or tool event, and
13
+ # only while a turn is active;
14
+ # output_log - PTY adapters: seconds since the transcript last changed.
15
+ # nil when no clock applies: settled work, no active structured turn, or
16
+ # nothing observable.
17
+ def progress_idle(payload)
18
+ return nil unless payload.is_a?(Hash)
19
+ return nil if payload["done"] == true || payload["task_complete"] == true || payload["task_failed"] == true
20
+
21
+ activity = payload["activity"]
22
+ if activity.is_a?(Hash) && activity["status"] == "observed"
23
+ return nil unless activity["turn_active"] == true
24
+
25
+ idle = STRUCTURED_CLOCKS.filter_map { |key| non_negative(activity[key]) }.min
26
+ return idle && { "clock" => "activity", "idle_s" => idle }
27
+ end
28
+
29
+ idle = non_negative(payload["log_idle_s"])
30
+ idle && { "clock" => "output_log", "idle_s" => idle }
31
+ end
32
+
33
+ def stalled(payload, threshold_s)
34
+ progress = progress_idle(payload)
35
+ return nil unless progress && progress["idle_s"] >= threshold_s
36
+
37
+ progress.merge("threshold_s" => threshold_s.to_f)
38
+ end
39
+
40
+ def non_negative(value)
41
+ return nil unless value.is_a?(Numeric) && value.finite?
42
+
43
+ [value.to_f, 0.0].max
44
+ end
45
+ end
46
+ end
@@ -37,9 +37,9 @@ module Harnex
37
37
 
38
38
  private
39
39
 
40
- def analyze
40
+ def analyze(canonical_locked: false)
41
41
  reset_diagnostics
42
- canonical = load_canonical(@canonical)
42
+ canonical = load_canonical(@canonical, locked: canonical_locked)
43
43
  sources = discover_sources(@sources, @canonical)
44
44
  source_rows = sources[:rows]
45
45
  conflict_rows = sources[:conflicts]
@@ -115,7 +115,7 @@ module Harnex
115
115
 
116
116
  def writable?(state) = state[:fatal].empty? && state[:conflict_rows].empty? && state[:missing_rows].any?
117
117
 
118
- def load_canonical(path)
118
+ def load_canonical(path, locked: false)
119
119
  rows = []
120
120
  fatal = []
121
121
  families = FAMILY_KEYS.to_h { |key| [key, 0] }
@@ -123,7 +123,19 @@ module Harnex
123
123
  ends = {}
124
124
  recoverable = []
125
125
 
126
- read_jsonl(path).each do |entry|
126
+ begin
127
+ entries = read_jsonl(path, complete: true, locked: locked)
128
+ rescue DispatchHistory::HistoryUnavailable => e
129
+ return {
130
+ rows: 0,
131
+ families: families,
132
+ recoverable: [],
133
+ fatal: [e.message],
134
+ open_starts: 0
135
+ }
136
+ end
137
+
138
+ entries.each do |entry|
127
139
  if entry[:error]
128
140
  fatal << "malformed canonical JSON at #{path}:#{entry[:line]}"
129
141
  next
@@ -281,18 +293,27 @@ module Harnex
281
293
  records.each_with_index.map { |record, index| { record: record, line: index + 1, path: path } }
282
294
  end
283
295
 
284
- def read_jsonl(path)
285
- return [] unless File.file?(path)
296
+ def read_jsonl(path, complete: false, locked: false)
297
+ return [] unless File.file?(path) || complete
286
298
 
287
- File.readlines(path, chomp: true).each_with_index.filter_map do |line, index|
299
+ entries = []
300
+ index = 0
301
+ consume = proc do |line|
302
+ index += 1
288
303
  next if line.strip.empty?
289
304
 
290
305
  begin
291
- { record: JSON.parse(line), line: index + 1, path: path }
306
+ entries << { record: JSON.parse(line), line: index, path: path }
292
307
  rescue JSON::ParserError => error
293
- { error: error, line: index + 1, path: path }
308
+ entries << { error: error, line: index, path: path }
294
309
  end
295
310
  end
311
+ if complete
312
+ DispatchHistory.each_history_line(path, locked: locked, &consume)
313
+ else
314
+ File.foreach(path, &consume)
315
+ end
316
+ entries
296
317
  end
297
318
 
298
319
  def canonical_family(record)
@@ -410,23 +431,22 @@ module Harnex
410
431
 
411
432
  def append_missing_under_lock(rows)
412
433
  FileUtils.mkdir_p(File.dirname(@canonical))
413
- File.open(@canonical, File::RDWR | File::CREAT, 0o644) do |file|
414
- file.flock(File::LOCK_EX)
415
- file.rewind
416
- existing = file.read
417
- locked = analyze
434
+ DispatchHistory.with_lock(@canonical) do
435
+ locked = analyze(canonical_locked: true)
418
436
  return [0, locked] unless writable?(locked)
419
437
 
420
438
  still_missing = locked[:missing_rows].select { |row| rows.any? { |wanted| wanted.identity == row.identity } }
421
- payload = still_missing.map { |row| JSON.generate(row.record) }.join("\n")
439
+ payload = still_missing.map { |row| JSON.generate(DispatchHistory.portable_record(row.record)) }.join("\n")
440
+ existing = File.file?(@canonical) ? File.binread(@canonical) : ""
422
441
  payload = "\n#{payload}" unless existing.empty? || existing.end_with?("\n")
423
442
  payload = "#{payload}\n" unless payload.empty?
424
- file.seek(0, IO::SEEK_END)
425
- file.write(payload)
426
- file.flush
443
+ DispatchHistory.prepare_archive(@canonical, payload.bytesize)
444
+ File.open(@canonical, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |file|
445
+ file.write(payload)
446
+ file.flush
447
+ file.fsync
448
+ end
427
449
  [still_missing.length, locked.merge(missing_rows: [])]
428
- ensure
429
- file.flock(File::LOCK_UN) unless file.closed?
430
450
  end
431
451
  end
432
452
 
@@ -459,6 +479,8 @@ module Harnex
459
479
  end
460
480
  when Array
461
481
  value.map { |item| deep_normalize(item) }
482
+ when String
483
+ DispatchHistory.portable_path(value)
462
484
  else
463
485
  value
464
486
  end
@@ -64,8 +64,7 @@ module Harnex
64
64
  summary_record = nil
65
65
  history_record = nil
66
66
 
67
- File.foreach(path) do |line|
68
- record = JSON.parse(line)
67
+ DispatchHistory.each_record(path) do |record|
69
68
  next unless record.is_a?(Hash)
70
69
  next if DispatchHistory.start_record?(record)
71
70
 
@@ -83,8 +82,6 @@ module Harnex
83
82
  elsif history_record?(record) && record["id"].to_s == id
84
83
  history_record = record
85
84
  end
86
- rescue JSON::ParserError
87
- next
88
85
  end
89
86
 
90
87
  [summary_record, history_record]
@@ -139,7 +136,7 @@ module Harnex
139
136
  terminal = state != "unknown"
140
137
  {
141
138
  "id" => meta["id"].to_s,
142
- "repo_root" => meta["repo"] || fallback_repo_root,
139
+ "repo_root" => DispatchHistory.expand_portable_path(meta["repo"] || fallback_repo_root),
143
140
  "state" => state,
144
141
  "process_state" => Harnex.process_state_for(state, terminal: terminal),
145
142
  "terminal" => terminal,
@@ -149,6 +146,9 @@ module Harnex
149
146
  "work_state" => Harnex.work_state_for(state, task_complete: task_complete),
150
147
  "outcome_class" => blank_to_nil(outcome["class"]),
151
148
  "artifact_report_status" => blank_to_nil(outcome["report_status"]),
149
+ "activity" => record["activity"],
150
+ "stop" => record["stop"],
151
+ "runtime_budget" => record["runtime_budget"],
152
152
  "exit" => blank_to_nil(actual["exit"]),
153
153
  "exit_code" => actual["exit_code"],
154
154
  "started_at" => meta["started_at"],
@@ -1,4 +1,4 @@
1
1
  module Harnex
2
- VERSION = "0.12.0"
3
- RELEASE_DATE = "2026-09-03"
2
+ VERSION = "0.29.0"
3
+ RELEASE_DATE = "2026-10-22"
4
4
  end
data/lib/harnex.rb CHANGED
@@ -9,10 +9,16 @@ require_relative "harnex/config"
9
9
  require_relative "harnex/retention"
10
10
  require_relative "harnex/pricing"
11
11
  require_relative "harnex/terminal_status"
12
+ require_relative "harnex/runtime/stop_request"
12
13
  require_relative "harnex/artifact_report"
14
+ require_relative "harnex/context_file"
13
15
  require_relative "harnex/context_telemetry"
14
16
  require_relative "harnex/telemetry_reconciler"
15
17
  require_relative "harnex/orchestration"
18
+ require_relative "harnex/stall"
19
+ require_relative "harnex/owner_stream"
20
+ require_relative "harnex/lanes"
21
+ require_relative "harnex/mcp"
16
22
  require_relative "harnex/watcher"
17
23
  require_relative "harnex/adapters"
18
24
  require_relative "harnex/runtime/session_state"
@@ -20,6 +26,8 @@ require_relative "harnex/runtime/message"
20
26
  require_relative "harnex/runtime/inbox"
21
27
  require_relative "harnex/runtime/file_change_hook"
22
28
  require_relative "harnex/runtime/completion_notifier"
29
+ require_relative "harnex/runtime/activity_tracker"
30
+ require_relative "harnex/runtime/runtime_budget"
23
31
  require_relative "harnex/runtime/api_server"
24
32
  require_relative "harnex/runtime/session"
25
33
  require_relative "harnex/commands/watch"
@@ -37,6 +45,14 @@ require_relative "harnex/commands/recipes"
37
45
  require_relative "harnex/commands/guide"
38
46
  require_relative "harnex/commands/agents_guide"
39
47
  require_relative "harnex/commands/doctor"
48
+ require_relative "harnex/commands/preflight"
49
+ require_relative "harnex/commands/ask"
50
+ require_relative "harnex/commands/delegate"
51
+ require_relative "harnex/commands/result"
52
+ require_relative "harnex/commands/reply"
53
+ require_relative "harnex/commands/land"
54
+ require_relative "harnex/commands/lanes"
55
+ require_relative "harnex/commands/mcp"
40
56
  require_relative "harnex/commands/orchestration"
41
57
  require_relative "harnex/commands/artifact_report"
42
58
  require_relative "harnex/commands/telemetry"
@@ -11,7 +11,7 @@ repeating this pattern with file handoffs between steps.
11
11
  ### 1. Spawn the worker
12
12
 
13
13
  ```bash
14
- harnex run codex --id cx-23 --tmux
14
+ harnex run pi --id pi-i-23 --tmux pi-i-23
15
15
  ```
16
16
 
17
17
  ### 2. Send the task
@@ -19,22 +19,22 @@ harnex run codex --id cx-23 --tmux
19
19
  If the plan file is self-contained, reference it directly:
20
20
 
21
21
  ```bash
22
- harnex send --id cx-23 --message "Implement koder/plans/plan_23.md. Run tests when done."
23
- harnex wait --id cx-23 --until done --timeout 1200
22
+ harnex send --id pi-i-23 --message "Implement koder/plans/plan_23.md. Run tests when done."
23
+ harnex wait --id pi-i-23 --until done --timeout 1200
24
24
  ```
25
25
 
26
26
  For tasks that need structured output, tell the worker to write a
27
27
  file and inspect the screen separately:
28
28
 
29
29
  ```bash
30
- cat > /tmp/task-cx-23.md <<'EOF'
30
+ cat > /tmp/task-impl-23.md <<'EOF'
31
31
  Implement koder/plans/plan_23.md.
32
32
  Run the full test suite when done.
33
33
  Write a short summary to /tmp/impl-23.md.
34
34
  EOF
35
35
 
36
- harnex send --id cx-23 --message "Read and execute /tmp/task-cx-23.md"
37
- harnex wait --id cx-23 --until done --timeout 1200
36
+ harnex send --id pi-i-23 --message "Read and execute /tmp/task-impl-23.md"
37
+ harnex wait --id pi-i-23 --until done --timeout 1200
38
38
  ```
39
39
 
40
40
  ### 3. Watch until done
@@ -42,25 +42,25 @@ harnex wait --id cx-23 --until done --timeout 1200
42
42
  Use `harnex wait --until done` as the work-level fence, then read the worker's screen:
43
43
 
44
44
  ```bash
45
- harnex pane --id cx-23 --lines 25
45
+ harnex pane --id pi-i-23 --lines 25
46
46
  ```
47
47
 
48
48
  If you prefer to watch while it runs:
49
49
 
50
50
  ```bash
51
- harnex pane --id cx-23 --follow
51
+ harnex pane --id pi-i-23 --follow
52
52
  ```
53
53
 
54
54
  When the worker looks idle, capture a larger snapshot:
55
55
 
56
56
  ```bash
57
- harnex pane --id cx-23 --lines 80
57
+ harnex pane --id pi-i-23 --lines 80
58
58
  ```
59
59
 
60
60
  ### 4. Stop the worker
61
61
 
62
62
  ```bash
63
- harnex stop --id cx-23
63
+ harnex stop --id pi-i-23
64
64
  ```
65
65
 
66
66
  Start a fresh worker for the next step instead of reusing this one.
@@ -69,10 +69,10 @@ Start a fresh worker for the next step instead of reusing this one.
69
69
 
70
70
  Use the same pattern for every role:
71
71
 
72
- - Codex planning: "Write the plan to `/tmp/plan-23.md`. Do not change code."
73
- - Codex implementation: "Read `/tmp/plan-23.md`, implement it, run tests, write `/tmp/impl-23.md`."
74
- - Claude review: "Review current changes against `/tmp/plan-23.md` and write findings to `/tmp/review-23.md`."
75
- - Codex fix: "Read `/tmp/review-23.md`, fix the issues, run tests, write `/tmp/fix-23.md`."
72
+ - Pi planning: "Write the plan to `/tmp/plan-23.md`. Do not change code."
73
+ - Pi implementation: "Read `/tmp/plan-23.md`, implement it, run tests, write `/tmp/impl-23.md`."
74
+ - Claude (or fresh Pi) review: "Review current changes against `/tmp/plan-23.md` and write findings to `/tmp/review-23.md`."
75
+ - Pi fix: "Read `/tmp/review-23.md`, fix the issues, run tests, write `/tmp/fix-23.md`."
76
76
 
77
77
  ## Rationale
78
78
 
@@ -5,16 +5,16 @@ This is repeated fire-and-watch for a batch of jobs.
5
5
  Process plans in series. For each plan, use fresh instances for
6
6
  every step:
7
7
 
8
- - Codex plans or implements
9
- - Claude reviews
10
- - Codex fixes
8
+ - Pi plans or implements
9
+ - Claude (or a fresh Pi lane) reviews
10
+ - Pi fixes
11
11
  - Repeat review and fix until clean, then move to the next plan
12
12
 
13
13
  ## Trigger
14
14
 
15
15
  User says something like:
16
16
  - "implement plans 23 to 27, review each, fix issues"
17
- - "implement plans 23 to 27, use fresh Codex for implementation
17
+ - "implement plans 23 to 27, use fresh Pi for implementation
18
18
  and fresh Claude for review on every plan"
19
19
 
20
20
  ## Procedure
@@ -24,55 +24,58 @@ For each plan in the batch:
24
24
  ### Step 1: Plan or confirm the plan
25
25
 
26
26
  If the plan file does not already exist or needs refinement, spawn a
27
- fresh Codex planner and tell it to write a plan artifact.
27
+ fresh Pi planner and tell it to write a plan artifact.
28
28
 
29
29
  ```bash
30
- harnex run codex --id cx-plan-23 --tmux
31
- harnex send --id cx-plan-23 --message "Write a concrete implementation plan for plan 23 to /tmp/plan-23.md. Do not change code." --wait-for-idle --timeout 600
30
+ harnex run pi --id pi-p-23 --tmux pi-p-23
31
+ harnex send --id pi-p-23 --message "Write a concrete implementation plan for plan 23 to /tmp/plan-23.md. Do not change code." --wait-for-idle --timeout 600
32
32
  ```
33
33
 
34
- Inspect with `harnex pane --id cx-plan-23 --lines 60`, then stop it.
34
+ Inspect with `harnex pane --id pi-p-23 --lines 60`, then stop it.
35
35
 
36
36
  If the plan is already written and trusted, skip this step and use the
37
37
  existing plan file directly.
38
38
 
39
39
  ### Step 2: Implement
40
40
 
41
- Spawn a fresh Codex worker, send it the plan, watch it, stop it.
41
+ Spawn a fresh Pi worker, send it the plan, watch it, stop it.
42
42
 
43
43
  ```bash
44
- harnex run codex --id cx-impl-23-r1 --tmux
45
- harnex send --id cx-impl-23-r1 --message "Read /tmp/plan-23.md, implement it, run tests, and write a summary to /tmp/impl-23-r1.md." --wait-for-idle --timeout 1200
44
+ harnex run pi --id pi-i-23-r1 --tmux pi-i-23-r1
45
+ harnex send --id pi-i-23-r1 --message "Read /tmp/plan-23.md, implement it, run tests, and write a summary to /tmp/impl-23-r1.md." --wait-for-idle --timeout 1200
46
46
  ```
47
47
 
48
- Inspect with `harnex pane --id cx-impl-23-r1 --lines 80`, then stop it.
48
+ Inspect with `harnex pane --id pi-i-23-r1 --lines 80`, then stop it.
49
49
 
50
50
  ### Step 3: Review
51
51
 
52
- Spawn a fresh Claude reviewer. Claude is only used for reviews in this
53
- recipe.
52
+ Spawn a fresh reviewer: a Claude session, or a fresh Pi lane that has
53
+ not seen the implementation. Reviewers only review in this recipe.
54
54
 
55
55
  ```bash
56
- harnex run claude --id cl-rev-23-r1 --tmux
57
- harnex send --id cl-rev-23-r1 --message "Review the current changes against /tmp/plan-23.md. Write findings to /tmp/review-23-r1.md. If there are no issues, say clean." --wait-for-idle --timeout 900
56
+ harnex run claude --id cl-r-23-r1 --tmux cl-r-23-r1
57
+ harnex send --id cl-r-23-r1 --message "Review the current changes against /tmp/plan-23.md. Write findings to /tmp/review-23-r1.md. If there are no issues, say clean." --wait-for-idle --timeout 900
58
58
  ```
59
59
 
60
- Inspect with `harnex pane --id cl-rev-23-r1 --lines 80`, then stop it.
60
+ For a Pi reviewer, use `harnex run pi --id pi-r-23-r1 --tmux pi-r-23-r1`
61
+ with the same message.
62
+
63
+ Inspect with `harnex pane --id cl-r-23-r1 --lines 80`, then stop it.
61
64
 
62
65
  ### Step 4: Fix and repeat if needed
63
66
 
64
67
  If the review is clean, move to the next plan.
65
68
 
66
- If the review finds issues, spawn a fresh Codex fixer:
69
+ If the review finds issues, spawn a fresh Pi fixer:
67
70
 
68
71
  ```bash
69
- harnex run codex --id cx-fix-23-r1 --tmux
70
- harnex send --id cx-fix-23-r1 --message "Read /tmp/review-23-r1.md, fix every issue, run tests, and write a summary to /tmp/fix-23-r1.md." --wait-for-idle --timeout 1200
72
+ harnex run pi --id pi-f-23-r1 --tmux pi-f-23-r1
73
+ harnex send --id pi-f-23-r1 --message "Read /tmp/review-23-r1.md, fix every issue, run tests, and write a summary to /tmp/fix-23-r1.md." --wait-for-idle --timeout 1200
71
74
  ```
72
75
 
73
- Inspect with `harnex pane --id cx-fix-23-r1 --lines 80`, then stop it.
76
+ Inspect with `harnex pane --id pi-f-23-r1 --lines 80`, then stop it.
74
77
 
75
- Then spawn another fresh Claude reviewer and repeat the review and fix
78
+ Then spawn another fresh reviewer and repeat the review and fix
76
79
  loop until clean or until you decide the plan needs manual attention.
77
80
 
78
81
  ### Step 5: Next plan
@@ -84,12 +87,12 @@ Move to plan 24. Repeat from step 1.
84
87
  Use the plan number in every worker ID so you can tell them apart in
85
88
  `harnex status`:
86
89
 
87
- | Step | ID pattern | Example |
88
- | --- | --- | --- |
89
- | Plan | `cx-plan-NN` | `cx-plan-23` |
90
- | Implement | `cx-impl-NN-rM` | `cx-impl-23-r1` |
91
- | Review | `cl-rev-NN-rM` | `cl-rev-23-r1` |
92
- | Fix | `cx-fix-NN-rM` | `cx-fix-23-r1` |
90
+ | Step | ID pattern | Example |
91
+ | --- | --- | --- |
92
+ | Plan | `pi-p-NN` | `pi-p-23` |
93
+ | Implement | `pi-i-NN-rM` | `pi-i-23-r1` |
94
+ | Review | `cl-r-NN-rM` / `pi-r-NN-rM` | `cl-r-23-r1` |
95
+ | Fix | `pi-f-NN-rM` | `pi-f-23-r1` |
93
96
 
94
97
  ## Notes
95
98
 
@@ -98,8 +101,9 @@ Use the plan number in every worker ID so you can tell them apart in
98
101
  implement, review, and fix.
99
102
  - The batch is serial. Finish plan 23 completely before starting
100
103
  plan 24. Plans often build on each other.
101
- - Claude only reviews. Do not use Claude as the planner or fixer in
102
- this recipe.
104
+ - The reviewer only reviews. Do not use Claude as the planner or
105
+ fixer in this recipe, and never let the implementing Pi lane review
106
+ its own work.
103
107
  - Pass artifacts between steps as files (`/tmp/plan-23.md`,
104
108
  `/tmp/review-23-r1.md`), not as harnex reply messages.
105
109
  - If the review finds no issues, skip the fix step and move on.
@@ -111,5 +115,5 @@ fire-and-watch repeated with stronger discipline:
111
115
 
112
116
  - one worker per step
113
117
  - one artifact per handoff
114
- - one reviewer role, always Claude
118
+ - one reviewer role, always a fresh Claude or Pi lane
115
119
  - one serial job stream, so later plans see earlier fixes
data/recipes/03_buddy.md CHANGED
@@ -22,7 +22,7 @@ session using existing primitives.
22
22
  ### 1. Spawn the worker
23
23
 
24
24
  ```bash
25
- harnex run codex --id worker-42 --tmux worker-42
25
+ harnex run pi --id worker-42 --tmux worker-42
26
26
  harnex send --id worker-42 --message "Read and execute /tmp/task-42.md"
27
27
  ```
28
28
 
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harnex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jikku Jose
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: A local PTY harness that wraps terminal AI agents (Claude, Codex, Pi)
14
- and adds a control plane for discovery, messaging, and coordination.
13
+ description: 'Hand questions and tasks to Pi workers in visible tmux windows and get
14
+ the result back in one call: lanes in git worktrees, follow-ups that keep context,
15
+ an MCP server for Claude Code and Pi, and a local harness for any terminal agent.'
15
16
  email:
16
17
  - jikkujose@gmail.com
17
18
  executables:
@@ -24,40 +25,50 @@ files:
24
25
  - LICENSE
25
26
  - README.md
26
27
  - TECHNICAL.md
28
+ - agents/gpt.md
27
29
  - bin/gem-push
28
30
  - bin/harnex
29
- - docs/codex-appserver.md
31
+ - docs/agents.md
30
32
  - docs/configuration.md
31
33
  - docs/dispatch-telemetry.md
32
34
  - docs/events.md
35
+ - docs/lanes.md
36
+ - docs/mcp.md
33
37
  - docs/pi-rpc.md
38
+ - docs/upgrading-to-0.29.md
34
39
  - guides/01_dispatch.md
35
40
  - guides/02_chain.md
36
41
  - guides/03_buddy.md
37
42
  - guides/04_monitoring.md
38
43
  - guides/05_naming.md
44
+ - guides/06_claude.md
39
45
  - lib/harnex.rb
40
46
  - lib/harnex/adapters.rb
41
47
  - lib/harnex/adapters/base.rb
42
48
  - lib/harnex/adapters/claude.rb
43
- - lib/harnex/adapters/codex.rb
44
- - lib/harnex/adapters/codex_appserver.rb
45
49
  - lib/harnex/adapters/generic.rb
46
50
  - lib/harnex/adapters/opencode.rb
47
51
  - lib/harnex/adapters/pi.rb
48
52
  - lib/harnex/artifact_report.rb
49
53
  - lib/harnex/cli.rb
50
- - lib/harnex/codex/app_server/client.rb
51
54
  - lib/harnex/commands/agents_guide.rb
52
55
  - lib/harnex/commands/artifact_report.rb
56
+ - lib/harnex/commands/ask.rb
57
+ - lib/harnex/commands/delegate.rb
53
58
  - lib/harnex/commands/doctor.rb
54
59
  - lib/harnex/commands/events.rb
55
60
  - lib/harnex/commands/guide.rb
56
61
  - lib/harnex/commands/history.rb
62
+ - lib/harnex/commands/land.rb
63
+ - lib/harnex/commands/lanes.rb
57
64
  - lib/harnex/commands/logs.rb
65
+ - lib/harnex/commands/mcp.rb
58
66
  - lib/harnex/commands/orchestration.rb
59
67
  - lib/harnex/commands/pane.rb
68
+ - lib/harnex/commands/preflight.rb
60
69
  - lib/harnex/commands/recipes.rb
70
+ - lib/harnex/commands/reply.rb
71
+ - lib/harnex/commands/result.rb
61
72
  - lib/harnex/commands/run.rb
62
73
  - lib/harnex/commands/send.rb
63
74
  - lib/harnex/commands/status.rb
@@ -67,19 +78,28 @@ files:
67
78
  - lib/harnex/commands/watch.rb
68
79
  - lib/harnex/commands/watch_presets.rb
69
80
  - lib/harnex/config.rb
81
+ - lib/harnex/context_file.rb
70
82
  - lib/harnex/context_telemetry.rb
71
83
  - lib/harnex/core.rb
72
84
  - lib/harnex/dispatch_history.rb
85
+ - lib/harnex/lanes.rb
86
+ - lib/harnex/mcp.rb
73
87
  - lib/harnex/orchestration.rb
88
+ - lib/harnex/owned_process_group.rb
89
+ - lib/harnex/owner_stream.rb
74
90
  - lib/harnex/pricing.rb
75
91
  - lib/harnex/retention.rb
92
+ - lib/harnex/runtime/activity_tracker.rb
76
93
  - lib/harnex/runtime/api_server.rb
77
94
  - lib/harnex/runtime/completion_notifier.rb
78
95
  - lib/harnex/runtime/file_change_hook.rb
79
96
  - lib/harnex/runtime/inbox.rb
80
97
  - lib/harnex/runtime/message.rb
98
+ - lib/harnex/runtime/runtime_budget.rb
81
99
  - lib/harnex/runtime/session.rb
82
100
  - lib/harnex/runtime/session_state.rb
101
+ - lib/harnex/runtime/stop_request.rb
102
+ - lib/harnex/stall.rb
83
103
  - lib/harnex/telemetry_reconciler.rb
84
104
  - lib/harnex/terminal_status.rb
85
105
  - lib/harnex/version.rb
@@ -96,7 +116,9 @@ metadata:
96
116
  homepage_uri: https://github.com/jikkuatwork/harnex
97
117
  source_code_uri: https://github.com/jikkuatwork/harnex
98
118
  bug_tracker_uri: https://github.com/jikkuatwork/harnex/issues
99
- post_install_message:
119
+ documentation_uri: https://harnex.zyt.app
120
+ changelog_uri: https://github.com/jikkuatwork/harnex/blob/main/CHANGELOG.md
121
+ post_install_message:
100
122
  rdoc_options: []
101
123
  require_paths:
102
124
  - lib
@@ -112,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
134
  version: '0'
113
135
  requirements: []
114
136
  rubygems_version: 3.5.3
115
- signing_key:
137
+ signing_key:
116
138
  specification_version: 4
117
- summary: PTY harness for terminal AI agents
139
+ summary: Pi workers (GPT models) as tools for you and your coding agents
118
140
  test_files: []