harnex 0.30.0 → 0.30.1
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 +21 -1
- data/TECHNICAL.md +10 -2
- data/docs/configuration.md +22 -0
- data/docs/dispatch-telemetry.md +12 -5
- data/docs/lanes.md +14 -2
- data/docs/mcp.md +7 -3
- data/docs/pi-rpc.md +9 -0
- data/guides/04_monitoring.md +4 -1
- data/guides/06_claude.md +6 -1
- data/lib/harnex/commands/ask.rb +2 -0
- data/lib/harnex/commands/lanes.rb +1 -1
- data/lib/harnex/commands/orchestration.rb +2 -1
- data/lib/harnex/lanes.rb +67 -2
- data/lib/harnex/orchestration.rb +2 -1
- data/lib/harnex/runtime/session.rb +16 -3
- data/lib/harnex/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3869562568a9927b6a413b5536ee9c489940ce0a8cf9ac6c001072b005ae760
|
|
4
|
+
data.tar.gz: 1203d0e9e327d56ccb63a119bc27489c417dce551e6be530772b2477e6f592b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f445333438f47be5c9f8f8eef6a90c3ce4844d6d4c398ae9f9a1568d4750de3313f741266b325b07d01fb941599473b6b143c827b34aa8608cab47a7f2bd1db
|
|
7
|
+
data.tar.gz: f5bc3dde8e4ab3b666989418293ce304d53dbcbd4752baf7d17ad1afad86e39af2d8b56bcf28ace274caf5440892ac7111e4fb8dfd02581e9de38757a6659d7e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.30.1] - 2026-10-28 | 11:16 AM | +04
|
|
4
|
+
|
|
5
|
+
Holm Queue 174 lost a night to dead lanes reading `running` and refusals
|
|
6
|
+
reading "Unknown error": #93.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Settled lanes no longer read `running` in machine listings** (#93).
|
|
11
|
+
`harnex lanes --json` reports `done`/`failed` independently of
|
|
12
|
+
`collected`; listing does not collect results. The table, which MCP
|
|
13
|
+
`lanes` also returns, keeps its wording, including `finished, uncollected`.
|
|
14
|
+
- **Pi provider errors have actionable outcomes** (#93). Refusal errors
|
|
15
|
+
(`cyber_policy`, content filter/policy, cybersecurity-risk flags) settle as
|
|
16
|
+
`policy_refusal`; retry guidance requires a rephrased brief or another
|
|
17
|
+
route. Pi's exact `Unknown error (no error details in response)` settles as
|
|
18
|
+
`provider_error_unexplained`, with guidance to inspect provider/gateway
|
|
19
|
+
logs before retrying. Neither suggests continuing the same task unchanged.
|
|
20
|
+
Both classes remain intact in owner-watch events and count toward the
|
|
21
|
+
default orchestration failure budget. `provider_capacity` is unchanged.
|
|
22
|
+
|
|
3
23
|
## [0.30.0] - 2026-10-25 | 02:55 AM | IST
|
|
4
24
|
|
|
5
|
-
Holm's first nights with lanes (Holm Queue 167): #89–#92.
|
|
25
|
+
Holm's first nights with lanes (Holm Queue 167): #89–#92. Published to RubyGems.
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
8
28
|
|
data/TECHNICAL.md
CHANGED
|
@@ -423,7 +423,10 @@ not a new runtime.
|
|
|
423
423
|
base sha, owner (`CLAUDE_CODE_SESSION_ID`/`PI_SESSION_ID`), requested model,
|
|
424
424
|
Pi `agent_session_id`, accumulated cost, launch time and the last turn's result.
|
|
425
425
|
States: `running` (a turn is out and uncollected), `done`, `failed`, `landed`,
|
|
426
|
-
`discarded`.
|
|
426
|
+
`discarded`. `lanes --json` and MCP `lanes` project current settlement from
|
|
427
|
+
task events (or fresh terminal evidence): `done`/`failed` even before
|
|
428
|
+
collection, with separate `collected` and process-liveness `live` fields.
|
|
429
|
+
Listing is read-only; pending results and the table wording are unchanged.
|
|
427
430
|
- **Launching:** the verbs shell out to the low-level CLI. `delegate` runs
|
|
428
431
|
`harnex run pi --id NAME --tmux NAME --cwd WORKDIR --root ROOT --auto-stop`
|
|
429
432
|
with a brief file (`--context-file`); with `--wait-done` it blocks on the
|
|
@@ -436,7 +439,12 @@ not a new runtime.
|
|
|
436
439
|
<agent_session_id>`, so Pi resumes the conversation.
|
|
437
440
|
- **Settling:** after a turn, the lane reads the launch's `usage` event (cost,
|
|
438
441
|
`agent_session_id`) and records the result; the footer adds commits and a
|
|
439
|
-
diffstat from git (`base_sha..HEAD`).
|
|
442
|
+
diffstat from git (`base_sha..HEAD`). Pi error text can classify
|
|
443
|
+
`policy_refusal` (retry with a rephrased brief or another route) or
|
|
444
|
+
`provider_error_unexplained` (inspect provider/gateway logs first). These
|
|
445
|
+
failure classes are preserved in `watch --owner`; `provider_capacity`
|
|
446
|
+
retains its adapter-retries-exhausted meaning. Full vocabulary:
|
|
447
|
+
[dispatch telemetry](docs/dispatch-telemetry.md).
|
|
440
448
|
- **Worktrees:** `.harnex/worktrees/NAME` on branch `harnex/NAME`; `.harnex/`
|
|
441
449
|
is added to `<git-common-dir>/info/exclude` on the first write.
|
|
442
450
|
`land` merges with `git merge --no-edit`, leaves conflicts in progress, and
|
data/docs/configuration.md
CHANGED
|
@@ -50,6 +50,28 @@ The effective value is the first-class `harnex run --phase TEXT` value, or
|
|
|
50
50
|
The allowlist must be an array of non-empty strings. Policy must be `warn` or
|
|
51
51
|
`reject`.
|
|
52
52
|
|
|
53
|
+
## Lane worktrees
|
|
54
|
+
|
|
55
|
+
`harnex delegate --worktree NAME` creates `.harnex/worktrees/NAME` on branch
|
|
56
|
+
`harnex/NAME`. Repositories that keep worktrees elsewhere can set templates:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"lanes": {
|
|
61
|
+
"worktree_path": "../night-{mm}{dd}_{name}",
|
|
62
|
+
"branch": "night/{mm}{dd}_{name}"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Placeholders: `{name}` (the lane), `{yyyy}`, `{mm}`, `{dd}` (the day the
|
|
68
|
+
lane starts). Each template must contain `{name}`.
|
|
69
|
+
- `worktree_path` is relative to the main checkout; `branch` must be a valid
|
|
70
|
+
branch name.
|
|
71
|
+
- Either key may be left out to keep its default. `land` and `discard` follow
|
|
72
|
+
each lane's recorded path and branch, so changing the templates does not
|
|
73
|
+
strand lanes already started. See [Lanes](lanes.md).
|
|
74
|
+
|
|
53
75
|
## Events, output, and receipt retention
|
|
54
76
|
|
|
55
77
|
Per-session event JSONL, output transcripts, and generated proof receipts live
|
data/docs/dispatch-telemetry.md
CHANGED
|
@@ -345,10 +345,16 @@ facts prove the delta, not semantic quality or human authorship.
|
|
|
345
345
|
- **Completion:** `completed_with_proof`, `completed_with_activity`.
|
|
346
346
|
- **Proof defects:** `completed_no_activity`, `report_missing`,
|
|
347
347
|
`report_invalid`, `report_rejected`.
|
|
348
|
-
- **
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
348
|
+
- **Provider:** `provider_capacity` (the turn failed after the adapter's own
|
|
349
|
+
provider retries ran out, from Pi's structured `auto_retry_end success:false`;
|
|
350
|
+
capacity or rate limits rather than the work), `policy_refusal` (Pi's error
|
|
351
|
+
text names `cyber_policy`, content filter/policy, or a cybersecurity-risk
|
|
352
|
+
flag; retry needs a rephrased brief or another route),
|
|
353
|
+
`provider_error_unexplained` (Pi's exact `Unknown error (no error details in
|
|
354
|
+
response)`; inspect provider/gateway logs before retrying). Explicit error
|
|
355
|
+
classes take precedence over retry exhaustion; they do not infer a refusal
|
|
356
|
+
from an unexplained error. `watch --owner` carries these classes unchanged.
|
|
357
|
+
- **Process:** `task_failed` (the turn failed), `boot_failure` (structured
|
|
352
358
|
transport died before its first turn), `permission_prompt` (ended on a
|
|
353
359
|
trust/confirmation prompt the adapter recognizes), `timeout` (`--max-runtime`
|
|
354
360
|
stopped running work, or exit 124), `stopped_by_caller` (a manual stop
|
|
@@ -375,7 +381,8 @@ initial workers do not join families. Start rows are not counted. An optional
|
|
|
375
381
|
adds a `failure_budget` verdict and exits 3 once N fatal failures are counted
|
|
376
382
|
(1 is an error, 2 a usage error; a missing `--dispatch` file is an error).
|
|
377
383
|
By default the fatal classes are `completed_no_activity`, `task_failed`,
|
|
378
|
-
`provider_capacity`, `
|
|
384
|
+
`provider_capacity`, `policy_refusal`, `provider_error_unexplained`,
|
|
385
|
+
`boot_failure`, `permission_prompt`, `timeout`, and `process_failed`; proof
|
|
379
386
|
defects, caller stops and `unknown` are not fatal.
|
|
380
387
|
Harnex supplies the ledger only: the caller decides what to do on a breach.
|
|
381
388
|
|
data/docs/lanes.md
CHANGED
|
@@ -199,7 +199,14 @@ interjections.
|
|
|
199
199
|
For provider and model failures (not found, rate limited, out of capacity) the
|
|
200
200
|
footer suggests continuing on Pi's default model. `reply --model` resumes the
|
|
201
201
|
same session, so the task is not lost. For `ask`, `--fallback-model` does this
|
|
202
|
-
automatically.
|
|
202
|
+
automatically on `provider_capacity` only.
|
|
203
|
+
|
|
204
|
+
A provider refusal is different: `policy_refusal` means the provider refused
|
|
205
|
+
the content, so a retry needs a rephrased brief or another route. Do not send
|
|
206
|
+
"Continue the same task" unchanged. `provider_error_unexplained` means Pi
|
|
207
|
+
reported exactly `Unknown error (no error details in response)`; read the
|
|
208
|
+
provider/gateway logs before retrying. Harnex does not infer a policy refusal
|
|
209
|
+
when Pi supplies no details.
|
|
203
210
|
|
|
204
211
|
## Watching a worker
|
|
205
212
|
|
|
@@ -214,7 +221,12 @@ truncate done 2m03s gpt-5.1 harnex/truncate harnex land tr
|
|
|
214
221
|
```
|
|
215
222
|
|
|
216
223
|
A working lane shows the tool it is running right now (tool names only; harnex
|
|
217
|
-
never records tool arguments). `harnex
|
|
224
|
+
never records tool arguments). `harnex lanes --json` and MCP `lanes` return
|
|
225
|
+
current settlement (`done`/`failed` even before collection), a separate
|
|
226
|
+
`collected` boolean, and process-liveness `live`. `collected: false` means
|
|
227
|
+
`harnex result` still needs to collect the turn; listing does not do that.
|
|
228
|
+
The table keeps `finished, uncollected` for those finished workers.
|
|
229
|
+
`harnex pane --id NAME --lines 40` captures the
|
|
218
230
|
worker's tmux window, and the window itself is always there to watch.
|
|
219
231
|
|
|
220
232
|
## Where things live
|
data/docs/mcp.md
CHANGED
|
@@ -17,8 +17,12 @@ servers when a session starts. `harnex doctor` reports the registrations under
|
|
|
17
17
|
|
|
18
18
|
## Tools
|
|
19
19
|
|
|
20
|
-
Each tool runs the matching CLI command and returns its text: the
|
|
21
|
-
reply followed by the `── NAME:` footer.
|
|
20
|
+
Each tool runs the matching CLI command and returns its text: normally the
|
|
21
|
+
worker's reply followed by the `── NAME:` footer. `lanes` returns the
|
|
22
|
+
`harnex lanes` table, with each worker's live activity; a settled worker reads
|
|
23
|
+
"finished, uncollected" until its result is collected. Scripts use
|
|
24
|
+
`harnex lanes --json`, whose `state` is `done`/`failed` for a settled worker
|
|
25
|
+
(never `running`), with separate `collected` and `live` fields.
|
|
22
26
|
|
|
23
27
|
| Tool | Arguments | Does |
|
|
24
28
|
| --- | --- | --- |
|
|
@@ -28,7 +32,7 @@ reply followed by the `── NAME:` footer.
|
|
|
28
32
|
| `reply` | `name`, `message`; `model`, `wait`, `cwd`, `wait_minutes` | Follow-up to the same worker, which keeps its context |
|
|
29
33
|
| `land` | `names` (optional); `cwd` | Merge worktree lanes, remove worktrees and branches |
|
|
30
34
|
| `discard` | `names`; `cwd` | Drop lanes without merging |
|
|
31
|
-
| `lanes` | `all`; `cwd` | Open
|
|
35
|
+
| `lanes` | `all`; `cwd` | Open lane JSON: settled state, collection, liveness, model and branch |
|
|
32
36
|
|
|
33
37
|
Arguments are forgiving: `prompt`, `brief` or `message` work for `task`;
|
|
34
38
|
`prompt` or `query` for `question`; `lane` or `id` for `name`. `cwd` defaults
|
data/docs/pi-rpc.md
CHANGED
|
@@ -107,6 +107,15 @@ Harnex maps Pi events as follows:
|
|
|
107
107
|
| `agent_settled` without an authoritative final stop reason | fail closed |
|
|
108
108
|
| EOF, malformed JSON, or request timeout | disconnect/failure |
|
|
109
109
|
|
|
110
|
+
A final Pi `error` with refusal text (`cyber_policy`, content filter/policy,
|
|
111
|
+
"flagged for possible cybersecurity risk") settles as `policy_refusal`.
|
|
112
|
+
A retry needs a rephrased brief or another route, not the same task unchanged.
|
|
113
|
+
The exact `Unknown error (no error details in response)` settles as
|
|
114
|
+
`provider_error_unexplained`: inspect provider/gateway logs before retrying;
|
|
115
|
+
it is not evidence of a refusal. Both override exhausted adapter retries;
|
|
116
|
+
otherwise `provider_capacity` keeps its existing retry-exhaustion behavior.
|
|
117
|
+
These classes survive status, dispatch rows and `watch --owner` events.
|
|
118
|
+
|
|
110
119
|
A forced send while Pi is busy is translated to a `prompt` with
|
|
111
120
|
`streamingBehavior="steer"`; ordinary sends wait for the settled prompt state.
|
|
112
121
|
This avoids sending a plain prompt that current Pi correctly rejects while
|
data/guides/04_monitoring.md
CHANGED
|
@@ -238,7 +238,10 @@ harnex watch --owner --stall-after 25m
|
|
|
238
238
|
`session_id`, `repo`, `ts`, `work_state`, plus `outcome_class`,
|
|
239
239
|
`receipt_path`, `elapsed_s`, `stall`, `reason` where they apply, and a
|
|
240
240
|
readable `msg`. No heartbeats, so Claude Code's `Monitor` wakes only when
|
|
241
|
-
something needs a decision.
|
|
241
|
+
something needs a decision. Provider failures preserve `policy_refusal`
|
|
242
|
+
(rephrase the brief or use another route), `provider_error_unexplained`
|
|
243
|
+
(read provider/gateway logs before retrying), and `provider_capacity`
|
|
244
|
+
(adapter retries exhausted) as distinct outcome classes.
|
|
242
245
|
- **Restarts:** a restarted stream re-announces `started` for running
|
|
243
246
|
dispatches and delivers only results it has not delivered before (cursor
|
|
244
247
|
under the owner's notifications directory). Delivery is not acceptance:
|
data/guides/06_claude.md
CHANGED
|
@@ -70,7 +70,12 @@ harnex reply word-count "Also add TextKit.char_count(text) with a test."
|
|
|
70
70
|
A worker still on its turn takes the message as a follow-up. A finished lane
|
|
71
71
|
is relaunched on its own Pi session, so the worker keeps everything it knew.
|
|
72
72
|
After a provider-capacity failure, reply "Continue the same task from where
|
|
73
|
-
you stopped", optionally with `--model OTHER`.
|
|
73
|
+
you stopped", optionally with `--model OTHER`. Do not do that for
|
|
74
|
+
`policy_refusal`: the provider refused the content, so retry with a rephrased
|
|
75
|
+
brief or another route. For `provider_error_unexplained`, read the
|
|
76
|
+
provider/gateway logs before retrying. `harnex lanes --json` and MCP `lanes`
|
|
77
|
+
separate settled `state` (`done`/`failed`) from `collected` and process `live`;
|
|
78
|
+
uncollected results are still available through `result`.
|
|
74
79
|
|
|
75
80
|
## Rules For Claude Code
|
|
76
81
|
|
data/lib/harnex/commands/ask.rb
CHANGED
|
@@ -216,6 +216,8 @@ module Harnex
|
|
|
216
216
|
@out.puts answer unless answer.to_s.empty?
|
|
217
217
|
reason = turn["last_error"] || turn["outcome_class"] || turn["status"] || result["error"] || "no result"
|
|
218
218
|
@err.puts("harnex ask: #{result['id'] || 'the worker'} did not answer (#{reason})")
|
|
219
|
+
hint = Lanes.provider_failure_hint("outcome" => turn["outcome_class"])
|
|
220
|
+
@err.puts("next: #{hint}") if hint
|
|
219
221
|
end
|
|
220
222
|
exit_code
|
|
221
223
|
end
|
|
@@ -34,7 +34,7 @@ module Harnex
|
|
|
34
34
|
Lanes.sweep_merged(Lanes.main_root(Dir.pwd))
|
|
35
35
|
lanes = selected
|
|
36
36
|
if @argv.include?("--json")
|
|
37
|
-
@out.puts JSON.generate(lanes.map { |lane|
|
|
37
|
+
@out.puts JSON.generate(lanes.map { |lane| Lanes.snapshot(lane) })
|
|
38
38
|
elsif lanes.empty?
|
|
39
39
|
@out.puts "no lanes#{@argv.include?('--all') ? '' : ' in this repo'} (start one with `harnex delegate`)"
|
|
40
40
|
else
|
|
@@ -51,7 +51,8 @@ module Harnex
|
|
|
51
51
|
Worker outcome classes (dispatch row outcome.class):
|
|
52
52
|
completed completed_with_proof completed_with_activity
|
|
53
53
|
proof completed_no_activity* report_missing report_invalid report_rejected
|
|
54
|
-
|
|
54
|
+
provider provider_capacity* policy_refusal* provider_error_unexplained*
|
|
55
|
+
process task_failed* boot_failure* permission_prompt*
|
|
55
56
|
timeout* process_failed* stopped_by_caller
|
|
56
57
|
fallback unknown
|
|
57
58
|
The budget is a ledger, not a breaker: enforcement stays with the caller.
|
data/lib/harnex/lanes.rb
CHANGED
|
@@ -11,8 +11,9 @@ module Harnex
|
|
|
11
11
|
# harnex/<name>. A record under STATE_DIR/lanes lets `result`, `reply`,
|
|
12
12
|
# `land` and `discard` find a lane by name from anywhere in its repo.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# Internal record states: running (a turn is out and uncollected), done,
|
|
15
|
+
# failed, landed, discarded. Machine listings project the worker's settled
|
|
16
|
+
# state independently, with `collected` indicating the record's boundary.
|
|
16
17
|
module Lanes
|
|
17
18
|
NAME = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,47}\z/
|
|
18
19
|
BRANCH_PREFIX = "harnex/".freeze
|
|
@@ -154,6 +155,57 @@ module Harnex
|
|
|
154
155
|
!registry.nil? && Harnex.alive_pid?(registry["pid"])
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
# #93: listing is read-only, not result collection. The record's running
|
|
159
|
+
# state still drives pending/result and the table's "finished, uncollected".
|
|
160
|
+
# Fence evidence to this launch and follow-up so a reused name cannot pick
|
|
161
|
+
# up an earlier turn. Settlement can precede process exit (Pi at prompt).
|
|
162
|
+
def snapshot(lane)
|
|
163
|
+
live = live?(lane)
|
|
164
|
+
view = lane.merge("live" => live, "collected" => lane["state"] != "running")
|
|
165
|
+
return view unless lane["state"] == "running"
|
|
166
|
+
|
|
167
|
+
%w[outcome last_error settled_at last_turn].each { |key| view.delete(key) }
|
|
168
|
+
event = events(lane).reverse.find do |row|
|
|
169
|
+
row.is_a?(Hash) && %w[task_complete task_failed work_turn agent_start].include?(row["type"]) &&
|
|
170
|
+
after?(row["ts"], lane["launched_at"]) &&
|
|
171
|
+
(!lane["after_seq"] || row["seq"].to_i > lane["after_seq"].to_i)
|
|
172
|
+
end
|
|
173
|
+
if event && %w[task_complete task_failed].include?(event["type"])
|
|
174
|
+
failed = event["type"] == "task_failed" ||
|
|
175
|
+
(!event["status"].to_s.empty? && !%w[completed success succeeded].include?(event["status"]))
|
|
176
|
+
view.merge!("state" => failed ? "failed" : "done", "outcome" => event["outcome_class"] || event["status"],
|
|
177
|
+
"settled_at" => event["ts"])
|
|
178
|
+
view["last_error"] = event["message"] || event["error"] if failed
|
|
179
|
+
elsif !live
|
|
180
|
+
# Process failures may exit without a task event. The canonical end
|
|
181
|
+
# row supplies their class; ignore old rows from a previous launch.
|
|
182
|
+
terminal = TerminalStatus.resolve(id: lane["name"], repo_root: lane["run_root"])
|
|
183
|
+
if terminal && terminal["terminal"] && after?(terminal["ended_at"], lane["launched_at"])
|
|
184
|
+
view.merge!("state" => terminal["work_state"] == "completed" ? "done" : "failed",
|
|
185
|
+
"outcome" => terminal["outcome_class"], "settled_at" => terminal["ended_at"])
|
|
186
|
+
elsif (exit_status = fresh_exit_status(lane))
|
|
187
|
+
done = exit_status["task_complete"] == true ||
|
|
188
|
+
(exit_status["task_failed"] != true && exit_status["exit_code"] == 0)
|
|
189
|
+
view.merge!("state" => done ? "done" : "failed", "outcome" => exit_status["outcome_class"],
|
|
190
|
+
"settled_at" => exit_status["exited_at"])
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
view
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def fresh_exit_status(lane)
|
|
197
|
+
path = Harnex.exit_status_path(lane["run_root"], lane["name"])
|
|
198
|
+
data = JSON.parse(File.read(path))
|
|
199
|
+
return nil unless data.is_a?(Hash)
|
|
200
|
+
|
|
201
|
+
# Exit timestamps have second precision; mtime also fences same-second
|
|
202
|
+
# relaunches so an earlier dispatch's leftover file cannot settle one.
|
|
203
|
+
since = Time.iso8601(lane["launched_at"].to_s)
|
|
204
|
+
data if File.mtime(path) >= since && after?(data["exited_at"], since.iso8601)
|
|
205
|
+
rescue Errno::ENOENT, JSON::ParserError, ArgumentError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
157
209
|
# The live worker's activity snapshot (see ActivityTracker), or nil.
|
|
158
210
|
def activity(lane)
|
|
159
211
|
registry = Harnex.read_registry(lane["run_root"], lane["name"])
|
|
@@ -678,6 +730,8 @@ module Harnex
|
|
|
678
730
|
def details(lane, exit_code)
|
|
679
731
|
name = lane["name"]
|
|
680
732
|
return ["keep waiting: harnex result #{name}"] if exit_code == Waiter::DONE_EXIT_TIMEOUT
|
|
733
|
+
provider_hint = provider_failure_hint(lane)
|
|
734
|
+
return ["next: #{provider_hint}"] if lane["kind"] == "ask" && !exit_code.zero? && provider_hint
|
|
681
735
|
return ["more: harnex reply #{name} \"...\""] if lane["kind"] == "ask"
|
|
682
736
|
|
|
683
737
|
out = []
|
|
@@ -697,6 +751,8 @@ module Harnex
|
|
|
697
751
|
else
|
|
698
752
|
out << "worked in place in #{lane['cwd']}; more work: `harnex reply #{name} \"...\"`"
|
|
699
753
|
end
|
|
754
|
+
elsif provider_hint
|
|
755
|
+
out << "next: #{provider_hint}#{lane['branch'] ? "; or `harnex discard #{name}`" : ''}"
|
|
700
756
|
else
|
|
701
757
|
retry_model = model_problem?(lane) ? other_model(lane) : nil
|
|
702
758
|
out << "next: `harnex reply #{name} \"Continue the same task from where you stopped\"" \
|
|
@@ -707,6 +763,15 @@ module Harnex
|
|
|
707
763
|
out
|
|
708
764
|
end
|
|
709
765
|
|
|
766
|
+
def provider_failure_hint(lane)
|
|
767
|
+
case lane["outcome"]
|
|
768
|
+
when "policy_refusal"
|
|
769
|
+
"provider refused the content; a retry needs a rephrased brief or another route"
|
|
770
|
+
when "provider_error_unexplained"
|
|
771
|
+
"read the provider/gateway logs before retrying; the provider returned no error details"
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
710
775
|
def commit_count(lane)
|
|
711
776
|
count, _err, ok = git(lane["worktree"] || lane["cwd"], "rev-list", "--count", "#{lane['base_sha']}..HEAD")
|
|
712
777
|
ok ? count.to_i : 0
|
data/lib/harnex/orchestration.rb
CHANGED
|
@@ -23,7 +23,8 @@ module Harnex
|
|
|
23
23
|
# are not fatal unless a caller names them (#57). provider_capacity was
|
|
24
24
|
# task_failed before #82 and stays fatal; pass fatal classes to exempt it.
|
|
25
25
|
DEFAULT_FATAL_CLASSES = %w[
|
|
26
|
-
completed_no_activity task_failed provider_capacity
|
|
26
|
+
completed_no_activity task_failed provider_capacity policy_refusal provider_error_unexplained
|
|
27
|
+
boot_failure permission_prompt timeout process_failed
|
|
27
28
|
].freeze
|
|
28
29
|
|
|
29
30
|
module_function
|
|
@@ -54,7 +54,7 @@ module Harnex
|
|
|
54
54
|
# rate limits, not the work. Structured signal only, never text.
|
|
55
55
|
OUTCOME_CLASSES = (
|
|
56
56
|
%w[completed_with_proof completed_with_activity] + PROOF_REJECTION_CLASSES +
|
|
57
|
-
%w[task_failed provider_capacity] + PROCESS_OUTCOME_CLASSES + %w[unknown]
|
|
57
|
+
%w[task_failed provider_capacity policy_refusal provider_error_unexplained] + PROCESS_OUTCOME_CLASSES + %w[unknown]
|
|
58
58
|
).freeze
|
|
59
59
|
# Pi tools whose structured results carry an integer exit code.
|
|
60
60
|
PI_COMMAND_TOOLS = %w[bash].freeze
|
|
@@ -1263,6 +1263,10 @@ module Harnex
|
|
|
1263
1263
|
nil
|
|
1264
1264
|
end
|
|
1265
1265
|
|
|
1266
|
+
# #93: these are Pi's structured errorMessage/finalError, not transcript
|
|
1267
|
+
# text. Explicit provider errors take precedence over exhausted retries.
|
|
1268
|
+
PI_POLICY_REFUSAL = /\bcyber_policy\b|content[\s_-]*(?:filter|policy)|flagged for possible cybersecurity risk/i
|
|
1269
|
+
|
|
1266
1270
|
def settle_pi_task!
|
|
1267
1271
|
return if @stop_requested || @pi_transport_failed
|
|
1268
1272
|
|
|
@@ -1273,8 +1277,16 @@ module Harnex
|
|
|
1273
1277
|
when "error", "aborted", "length"
|
|
1274
1278
|
error = @pi_last_assistant_error || @pi_last_retry_error
|
|
1275
1279
|
error = "Pi agent settled with stopReason=#{reason}" if error.to_s.empty?
|
|
1276
|
-
|
|
1277
|
-
|
|
1280
|
+
outcome_class = if reason == "error"
|
|
1281
|
+
if [error, @pi_last_retry_error].any? { |text| text.to_s.match?(PI_POLICY_REFUSAL) }
|
|
1282
|
+
"policy_refusal"
|
|
1283
|
+
elsif error == "Unknown error (no error details in response)"
|
|
1284
|
+
"provider_error_unexplained"
|
|
1285
|
+
elsif !@pi_last_retry_error.to_s.empty?
|
|
1286
|
+
"provider_capacity"
|
|
1287
|
+
end
|
|
1288
|
+
end
|
|
1289
|
+
mark_task_failed(status: reason, error: error, outcome_class: outcome_class)
|
|
1278
1290
|
when nil, ""
|
|
1279
1291
|
mark_task_failed(
|
|
1280
1292
|
status: "missing_final_message",
|
|
@@ -2549,6 +2561,7 @@ module Harnex
|
|
|
2549
2561
|
# Status/source derive from the proof verdict, not the refined label.
|
|
2550
2562
|
proof_failure = %w[
|
|
2551
2563
|
completed_no_activity report_missing report_invalid report_rejected task_failed provider_capacity
|
|
2564
|
+
policy_refusal provider_error_unexplained
|
|
2552
2565
|
].include?(base_class)
|
|
2553
2566
|
harness_receipt = artifact_payload&.dig("receipt", "author") == "harnex"
|
|
2554
2567
|
status = receipt_outcome["status"]
|
data/lib/harnex/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: harnex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.30.
|
|
4
|
+
version: 0.30.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jikku Jose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'Hand questions and tasks to Pi workers in visible tmux windows and get
|
|
14
14
|
the result back in one call: lanes in git worktrees, follow-ups that keep context,
|