harnex 0.29.1 → 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 +64 -1
- data/TECHNICAL.md +10 -2
- data/agents/gpt.md +16 -8
- data/docs/agents.md +5 -2
- data/docs/configuration.md +22 -0
- data/docs/dispatch-telemetry.md +12 -5
- data/docs/lanes.md +66 -8
- 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 +13 -1
- data/lib/harnex/commands/delegate.rb +24 -3
- data/lib/harnex/commands/lanes.rb +1 -1
- data/lib/harnex/commands/orchestration.rb +2 -1
- data/lib/harnex/commands/reply.rb +2 -1
- data/lib/harnex/commands/result.rb +1 -1
- data/lib/harnex/commands/send.rb +2 -1
- data/lib/harnex/commands/wait.rb +2 -1
- data/lib/harnex/config.rb +21 -0
- data/lib/harnex/core.rb +11 -0
- data/lib/harnex/lanes.rb +277 -17
- data/lib/harnex/mcp.rb +5 -3
- data/lib/harnex/orchestration.rb +2 -1
- data/lib/harnex/runtime/api_server.rb +13 -2
- data/lib/harnex/runtime/inbox.rb +3 -1
- data/lib/harnex/runtime/message.rb +1 -1
- data/lib/harnex/runtime/session.rb +88 -12
- data/lib/harnex/version.rb +2 -2
- 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,71 @@
|
|
|
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
|
+
|
|
23
|
+
## [0.30.0] - 2026-10-25 | 02:55 AM | IST
|
|
24
|
+
|
|
25
|
+
Holm's first nights with lanes (Holm Queue 167): #89–#92. Published to RubyGems.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Lane names collided** (#89). Identical briefs in sibling worktrees of a
|
|
30
|
+
bare repo all became `pi-read-carry-out-brief`, and `harnex result NAME`
|
|
31
|
+
then failed with `several lanes are named …`.
|
|
32
|
+
- A bare layout (`repo/.bare` plus sibling worktrees) is one repo: its main
|
|
33
|
+
checkout is the worktree on the bare repo's `HEAD` branch, so every
|
|
34
|
+
worktree shares one lane namespace and `harnex lanes` shows them all.
|
|
35
|
+
- Generated names skip every name another lane still holds in any repo
|
|
36
|
+
(working, uncollected, or an unlanded worktree); `--name`, `--worktree`
|
|
37
|
+
and `ask --id` refuse a name while another repo's worker runs under it;
|
|
38
|
+
`result`/`reply` find a working lane from anywhere.
|
|
39
|
+
- A lane working in another worktree is named after it
|
|
40
|
+
(`pi-night-1024_s3`); brief openers such as "You are a … lane" and "carry
|
|
41
|
+
out the brief" no longer make the name.
|
|
42
|
+
- `delegate --worktree` refuses a `--cwd` that is already another worktree
|
|
43
|
+
of the repo (it would have branched from the main checkout instead).
|
|
44
|
+
- **The `gpt` agent invented a `worktree`** from brief prose (#89). It now
|
|
45
|
+
sets `worktree`, `name` and `cwd` only from header lines, takes a new
|
|
46
|
+
`name:` header, and passes `cwd:` on to `result` and `reply`.
|
|
47
|
+
- **A lane in place reported other people's commits** (#91). Its footer
|
|
48
|
+
counts only commits that appeared while the worker ran a git command, and
|
|
49
|
+
names the rest: `0 commits (HEAD also moved 6 commits by others)`.
|
|
50
|
+
- **Messages sent straight to a worker were invisible** to the session that
|
|
51
|
+
started its lane (#92). `harnex send` now records who sent a message, and a
|
|
52
|
+
lane's result lists messages from anyone but the lane's owner and its own
|
|
53
|
+
`harnex reply` (`interjected: 1 message … from a terminal (harnex send):
|
|
54
|
+
"…"`). When a message reached the worker after it had answered, the result
|
|
55
|
+
shows the answers it replaced first, the original task's report among them.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- `.harnex/config.json` `lanes` templates put worktree lanes where the repo
|
|
60
|
+
keeps its worktrees (#90): `{"lanes": {"worktree_path": "../{name}",
|
|
61
|
+
"branch": "night/{mm}{dd}_{name}"}}` (`{name}`, `{yyyy}`, `{mm}`, `{dd}`).
|
|
62
|
+
`land` and `discard` follow each lane's own path and branch.
|
|
63
|
+
- MCP `ask` takes `name`.
|
|
64
|
+
|
|
3
65
|
## [0.29.1] - 2026-10-23 | 10:31 AM | IST
|
|
4
66
|
|
|
5
|
-
|
|
67
|
+
Fix release for #88: lanes in repos with a phase allowlist. Published to
|
|
68
|
+
RubyGems on 2026-10-25 | 02:14 AM | IST.
|
|
6
69
|
|
|
7
70
|
### Fixed
|
|
8
71
|
|
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/agents/gpt.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gpt
|
|
3
|
-
description: "Hand a task or question to a GPT model (a Pi worker run through harnex) and get its reply back verbatim. Use when the user asks for GPT, for another model's opinion or review, or to delegate work to Pi/GPT workers; launch several at once for parallel GPT work, and SendMessage the same agent for follow-ups (the worker keeps its context). While it runs, the harnex `lanes` tool or `harnex lanes` shows what each worker is doing right now (its current tool). Questions and reviews go to a read-only worker; a brief that asks to run commands, tests or benchmarks, or to change files, goes to a worker that can (in the current checkout). The prompt may open with header lines, then a blank line, then the brief: `worktree: NAME` (edit in a new git worktree on branch harnex/NAME; afterwards land it with the harnex `land` tool or `harnex land NAME`, which merges and removes the worktree and branch; don't git merge lane branches by hand), `mode: write` (run commands or edit the current checkout in place), `mode: read` (read-only even if the brief mentions commands), `model: provider/model`, `effort: low|medium|high`, `fallback: provider/model` (read-only asks), `cwd: <dir
|
|
3
|
+
description: "Hand a task or question to a GPT model (a Pi worker run through harnex) and get its reply back verbatim. Use when the user asks for GPT, for another model's opinion or review, or to delegate work to Pi/GPT workers; launch several at once for parallel GPT work, and SendMessage the same agent for follow-ups (the worker keeps its context). While it runs, the harnex `lanes` tool or `harnex lanes` shows what each worker is doing right now (its current tool). Questions and reviews go to a read-only worker; a brief that asks to run commands, tests or benchmarks, or to change files, goes to a worker that can (in the current checkout). The prompt may open with header lines, then a blank line, then the brief: `worktree: NAME` (edit in a new git worktree on branch harnex/NAME; afterwards land it with the harnex `land` tool or `harnex land NAME`, which merges and removes the worktree and branch; don't git merge lane branches by hand), `mode: write` (run commands or edit the current checkout in place), `mode: read` (read-only even if the brief mentions commands), `model: provider/model`, `effort: low|medium|high`, `fallback: provider/model` (read-only asks), `cwd: <dir>` (work in that directory, e.g. a sibling worktree), `name: NAME` (the lane's name; default: the cwd worktree's name or the brief's first words). Write a self-contained brief: the GPT worker has not seen this conversation."
|
|
4
4
|
tools: mcp__harnex__ask, mcp__harnex__delegate, mcp__harnex__reply, mcp__harnex__result
|
|
5
5
|
mcpServers:
|
|
6
6
|
- harnex
|
|
@@ -16,11 +16,16 @@ return its text.
|
|
|
16
16
|
## 1. Split the prompt
|
|
17
17
|
|
|
18
18
|
The prompt may open with header lines `key: value` using only these keys:
|
|
19
|
-
`worktree`, `mode`, `model`, `effort`, `fallback`, `cwd`. The header
|
|
20
|
-
the first line that is not one of them; drop one blank line after it.
|
|
19
|
+
`worktree`, `mode`, `model`, `effort`, `fallback`, `cwd`, `name`. The header
|
|
20
|
+
ends at the first line that is not one of them; drop one blank line after it.
|
|
21
21
|
Everything after the header is the brief, kept exactly as written. No header
|
|
22
22
|
lines means the whole prompt is the brief.
|
|
23
23
|
|
|
24
|
+
Only header lines set tool arguments. Never take a `worktree`, `name` or
|
|
25
|
+
`cwd` from the brief's wording, even when the brief names a worktree, a
|
|
26
|
+
branch or a directory: the brief goes to the worker as it is, and the worker
|
|
27
|
+
reads it.
|
|
28
|
+
|
|
24
29
|
## 2. Call one tool
|
|
25
30
|
|
|
26
31
|
Pick the tool:
|
|
@@ -35,23 +40,26 @@ Then:
|
|
|
35
40
|
|
|
36
41
|
- **`delegate`:** call it with
|
|
37
42
|
- `task` = the brief;
|
|
38
|
-
- `worktree` = NAME, if
|
|
43
|
+
- `worktree` = NAME, only if the header has a `worktree:` line;
|
|
44
|
+
- `name` = NAME, if the header has a `name:` line and no `worktree:` line;
|
|
39
45
|
- and `model`, `effort`, `cwd` if given.
|
|
40
46
|
- **`ask`:** call it with
|
|
41
47
|
- `question` = the brief;
|
|
42
|
-
- and `model`, `effort`, `cwd` if given, plus `fallback_model` from
|
|
48
|
+
- and `model`, `effort`, `cwd`, `name` if given, plus `fallback_model` from
|
|
43
49
|
`fallback:`.
|
|
44
50
|
- **A later message in this conversation** (a follow-up) goes to `reply`, with
|
|
45
51
|
- `name` = the lane named in the last result's `── NAME:` footer;
|
|
46
|
-
- `message` = the new message, exactly as written
|
|
52
|
+
- `message` = the new message, exactly as written;
|
|
53
|
+
- and `cwd` if the first prompt's header gave one.
|
|
47
54
|
|
|
48
55
|
Pass every string exactly as written.
|
|
49
56
|
|
|
50
57
|
## 3. Still working
|
|
51
58
|
|
|
52
59
|
If the tool's text ends with a footer saying `still working`, call `result`
|
|
53
|
-
with `names` = [that lane name]
|
|
54
|
-
times. Never start a second lane for
|
|
60
|
+
with `names` = [that lane name], and `cwd` if the header gave one. Repeat
|
|
61
|
+
while it says still working, up to 12 times. Never start a second lane for
|
|
62
|
+
the same brief.
|
|
55
63
|
|
|
56
64
|
## 4. Return
|
|
57
65
|
|
data/docs/agents.md
CHANGED
|
@@ -59,10 +59,13 @@ Optional header lines, then a blank line, then the brief:
|
|
|
59
59
|
| `mode: read` | Read-only, even if the brief mentions commands |
|
|
60
60
|
| `model: provider/model`, `effort: low\|medium\|high` | Pick the worker's model |
|
|
61
61
|
| `fallback: provider/model` | Retry a read-only ask on another model |
|
|
62
|
-
| `cwd: DIR` | Work in another directory |
|
|
62
|
+
| `cwd: DIR` | Work in another directory, e.g. a sibling worktree (follow-ups stay there) |
|
|
63
|
+
| `name: NAME` | Name the lane (default: the `cwd` worktree's name, else the brief's first words) |
|
|
63
64
|
|
|
64
65
|
Without a header, questions and reviews go to a read-only worker, and a brief
|
|
65
|
-
that asks to run something or change files goes to a worker that can.
|
|
66
|
+
that asks to run something or change files goes to a worker that can. Only
|
|
67
|
+
header lines set these: a brief that mentions a worktree or a branch is passed
|
|
68
|
+
on as it is, never turned into a `worktree`.
|
|
66
69
|
The definition ships with harnex as [`agents/gpt.md`](../agents/gpt.md).
|
|
67
70
|
|
|
68
71
|
### The CLI from Claude's Bash tool
|
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
|
@@ -5,7 +5,8 @@ its follow-ups. You start one with `harnex ask` or `harnex delegate`, collect
|
|
|
5
5
|
it with `harnex result`, continue it with `harnex reply`, and finish it with
|
|
6
6
|
`harnex land` or `harnex discard`. Every lane has a name, runs in a visible
|
|
7
7
|
tmux window named after it, and leaves a small record so any later command can
|
|
8
|
-
find it by name from anywhere in the repository
|
|
8
|
+
find it by name from anywhere in the repository (from any worktree of it, a
|
|
9
|
+
bare-repo layout included).
|
|
9
10
|
|
|
10
11
|
Lanes are the agent-facing layer of harnex. Everything here is also available
|
|
11
12
|
as native tools through [`harnex mcp`](mcp.md), and the low-level `run`, `send`,
|
|
@@ -65,10 +66,19 @@ cost, commits and the next commands.
|
|
|
65
66
|
`harnex/NAME` from `HEAD`. The worker is told to commit there. Parallel lanes
|
|
66
67
|
never collide, and your checkout is untouched until you land. If your checkout
|
|
67
68
|
has uncommitted changes, harnex warns that the worktree starts from `HEAD`.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
A repo that keeps its worktrees elsewhere says so in `.harnex/config.json`
|
|
70
|
+
(see [Where things live](#where-things-live)).
|
|
71
|
+
- **Without `--worktree`** the worker works in the current directory, or in
|
|
72
|
+
`--cwd DIR`, which is right for tasks that only run or inspect things (tests,
|
|
73
|
+
a benchmark, a long command) and for a worktree you made yourself.
|
|
74
|
+
- **Names.** `--name NAME` names the lane. Otherwise a lane working in another
|
|
75
|
+
worktree of the repo is named after it (`--cwd ../night-1024_s3` gives
|
|
76
|
+
`pi-night-1024_s3`), and any other lane after the task's first words
|
|
77
|
+
(`pi-slow-bench`, then `pi-slow-bench-2`). Generated names skip every name
|
|
78
|
+
another lane still holds anywhere (working, uncollected, or a worktree not
|
|
79
|
+
yet landed), and a name you choose is refused while another repo's worker
|
|
80
|
+
is running under it, so `harnex result NAME` and `reply NAME` find a
|
|
81
|
+
working lane from anywhere.
|
|
72
82
|
- **`--file PATH`** reads a long brief from a file. Briefs must be
|
|
73
83
|
self-contained: the worker has not seen your conversation. Name the files, the
|
|
74
84
|
expected behavior and how to verify it.
|
|
@@ -126,6 +136,11 @@ or bare `land`.
|
|
|
126
136
|
`harnex discard NAME` stops the lane if it is still working and deletes its
|
|
127
137
|
worktree and branch without merging.
|
|
128
138
|
|
|
139
|
+
**Lanes in place.** For a lane without a worktree, the footer counts only the
|
|
140
|
+
commits the worker made (harnex watches `HEAD` around each git command the
|
|
141
|
+
worker runs). Commits someone else made in the same checkout meanwhile are
|
|
142
|
+
named apart: `0 commits (HEAD also moved 3 commits by others)`.
|
|
143
|
+
|
|
129
144
|
## Parallel lanes
|
|
130
145
|
|
|
131
146
|
```bash
|
|
@@ -153,6 +168,26 @@ mid-task as a follow-up; a lane that has finished is relaunched on its own Pi
|
|
|
153
168
|
session (`pi --session`), so it remembers everything from the earlier turns.
|
|
154
169
|
The footer's cost is the lane's total so far.
|
|
155
170
|
|
|
171
|
+
**Messages from someone else.** Anyone can `harnex send` to a lane's worker
|
|
172
|
+
(the owner from another terminal, a buddy). The lane's result then says so,
|
|
173
|
+
so the session that started the lane is not left guessing:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
[earlier reply this turn, before another message reached the worker]
|
|
177
|
+
Review: PASS (0 P1, 2 P2) ...
|
|
178
|
+
|
|
179
|
+
[latest reply]
|
|
180
|
+
Understood; the report is now in /tmp/review.md.
|
|
181
|
+
|
|
182
|
+
── review: done in 6m10s · gpt-6.1-sol · $0.41 · 0 commits
|
|
183
|
+
interjected: 1 message reached the worker from outside this lane (the reply above may answer it; all: `harnex events --id review`):
|
|
184
|
+
23:26:04 from a terminal (harnex send): "Write the report to /tmp/review.md instead"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The worker's earlier answers in the same turn (the original task's report
|
|
188
|
+
among them) come first; your own `harnex reply` follow-ups are never listed as
|
|
189
|
+
interjections.
|
|
190
|
+
|
|
156
191
|
## When a model fails
|
|
157
192
|
|
|
158
193
|
```text
|
|
@@ -164,7 +199,14 @@ The footer's cost is the lane's total so far.
|
|
|
164
199
|
For provider and model failures (not found, rate limited, out of capacity) the
|
|
165
200
|
footer suggests continuing on Pi's default model. `reply --model` resumes the
|
|
166
201
|
same session, so the task is not lost. For `ask`, `--fallback-model` does this
|
|
167
|
-
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.
|
|
168
210
|
|
|
169
211
|
## Watching a worker
|
|
170
212
|
|
|
@@ -179,7 +221,12 @@ truncate done 2m03s gpt-5.1 harnex/truncate harnex land tr
|
|
|
179
221
|
```
|
|
180
222
|
|
|
181
223
|
A working lane shows the tool it is running right now (tool names only; harnex
|
|
182
|
-
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
|
|
183
230
|
worker's tmux window, and the window itself is always there to watch.
|
|
184
231
|
|
|
185
232
|
## Where things live
|
|
@@ -188,7 +235,18 @@ worker's tmux window, and the window itself is always there to watch.
|
|
|
188
235
|
repo, worktree, branch, model, owner, Pi session, cost, last result). Closed
|
|
189
236
|
lanes are pruned after 30 days; open lanes never are.
|
|
190
237
|
- **Worktrees:** `.harnex/worktrees/NAME` inside your repository, on branch
|
|
191
|
-
`harnex/NAME`.
|
|
238
|
+
`harnex/NAME`. To keep them beside the main checkout instead, set templates
|
|
239
|
+
in the repo's `.harnex/config.json` (`{name}`, `{yyyy}`, `{mm}`, `{dd}`;
|
|
240
|
+
the path is relative to the main checkout). `land` and `discard` follow the
|
|
241
|
+
lane's own path and branch:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{ "lanes": { "worktree_path": "../night-{mm}{dd}_{name}", "branch": "night/{mm}{dd}_{name}" } }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- **Bare-repo layouts** (`repo/.bare` plus sibling worktrees): the main
|
|
248
|
+
checkout is the worktree on the bare repo's `HEAD` branch (`master`), and
|
|
249
|
+
every worktree of the repo shares its lanes.
|
|
192
250
|
- **`.harnex/`** is added to the clone's `.git/info/exclude` the first time
|
|
193
251
|
harnex writes there, so lane worktrees and dispatch rows never show up as
|
|
194
252
|
untracked work. Nothing tracked changes.
|
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
|
@@ -112,6 +112,9 @@ module Harnex
|
|
|
112
112
|
|
|
113
113
|
def register_lane(id, model)
|
|
114
114
|
cwd = File.expand_path(@options[:cwd] || Dir.pwd)
|
|
115
|
+
if (other = Lanes.live_holder(id))
|
|
116
|
+
raise OptionParser::InvalidArgument, "a worker named #{id} is working in #{other['repo_root']}; pick another --id"
|
|
117
|
+
end
|
|
115
118
|
Lanes.save(
|
|
116
119
|
"name" => id, "kind" => "ask", "repo_root" => Lanes.main_root(cwd) || cwd, "run_root" => run_root,
|
|
117
120
|
"cwd" => cwd, "read_only" => !@options[:write], "owner" => Harnex.ambient_owner_id,
|
|
@@ -204,7 +207,8 @@ module Harnex
|
|
|
204
207
|
answer = full_answer(turn)
|
|
205
208
|
case exit_code
|
|
206
209
|
when 0
|
|
207
|
-
|
|
210
|
+
lane = answered_lane(result["id"])
|
|
211
|
+
@out.puts(lane ? Lanes.render_answer(lane, turn.merge("final_message" => answer.to_s, "final_message_truncated" => false)) : answer.to_s)
|
|
208
212
|
@err.puts("harnex ask: #{result['id']} answered in #{elapsed.round}s (model #{turn['model'] || 'Pi default'})")
|
|
209
213
|
when Waiter::DONE_EXIT_TIMEOUT
|
|
210
214
|
@err.puts("harnex ask: #{result['id']} is still working; keep waiting with: #{resume_command(result['id'])}")
|
|
@@ -212,10 +216,18 @@ module Harnex
|
|
|
212
216
|
@out.puts answer unless answer.to_s.empty?
|
|
213
217
|
reason = turn["last_error"] || turn["outcome_class"] || turn["status"] || result["error"] || "no result"
|
|
214
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
|
|
215
221
|
end
|
|
216
222
|
exit_code
|
|
217
223
|
end
|
|
218
224
|
|
|
225
|
+
def answered_lane(id)
|
|
226
|
+
id && Lanes.resolve(id, from: File.expand_path(@options[:cwd] || Dir.pwd))
|
|
227
|
+
rescue OptionParser::InvalidArgument
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
219
231
|
def full_answer(turn)
|
|
220
232
|
path = turn["final_message_path"].to_s
|
|
221
233
|
return File.read(path).chomp if turn["final_message_truncated"] && !path.empty? && File.file?(path)
|
|
@@ -17,8 +17,12 @@ module Harnex
|
|
|
17
17
|
Options:
|
|
18
18
|
--worktree NAME Work in a new git worktree (.harnex/worktrees/NAME, branch
|
|
19
19
|
harnex/NAME); finish with `harnex land NAME` or
|
|
20
|
-
`harnex discard NAME
|
|
21
|
-
|
|
20
|
+
`harnex discard NAME`. The repo's .harnex/config.json
|
|
21
|
+
can move it: {"lanes": {"worktree_path": "../{name}",
|
|
22
|
+
"branch": "night/{mm}{dd}_{name}"}}
|
|
23
|
+
--name NAME Lane name without a worktree (default: the --cwd
|
|
24
|
+
worktree's name, else from the task). A working
|
|
25
|
+
lane's name is unique across every repo
|
|
22
26
|
--cwd DIR Directory the worker works in (default: current directory)
|
|
23
27
|
--file PATH Read the task from PATH
|
|
24
28
|
--model NAME Pi model (provider/model); default: Pi's own default
|
|
@@ -86,9 +90,10 @@ module Harnex
|
|
|
86
90
|
root = Lanes.main_root(workdir)
|
|
87
91
|
raise OptionParser::InvalidArgument, "--worktree needs a git repository (#{workdir} is not in one)" if @options[:worktree] && root.nil?
|
|
88
92
|
|
|
93
|
+
refuse_nested_worktree!(root, workdir) if @options[:worktree]
|
|
89
94
|
Lanes.sweep_merged(root)
|
|
90
95
|
root ||= workdir
|
|
91
|
-
name = Lanes.validate_name!(@options[:worktree] || @options[:name] || Lanes.generate_name(root, task))
|
|
96
|
+
name = Lanes.validate_name!(@options[:worktree] || @options[:name] || Lanes.generate_name(root, task, cwd: workdir))
|
|
92
97
|
ensure_free!(root, name)
|
|
93
98
|
lane = {
|
|
94
99
|
"name" => name, "kind" => "delegate", "repo_root" => root, "run_root" => root, "cwd" => workdir,
|
|
@@ -125,11 +130,27 @@ module Harnex
|
|
|
125
130
|
"lane #{name} already exists (#{existing['state']}): follow up with `harnex reply #{name} \"...\"`, " \
|
|
126
131
|
"or `harnex land #{name}` / `harnex discard #{name}` first"
|
|
127
132
|
end
|
|
133
|
+
if (other = Lanes.live_holder(name)) && other["repo_root"] != root
|
|
134
|
+
raise OptionParser::InvalidArgument,
|
|
135
|
+
"a worker named #{name} is working in #{other['repo_root']}; working lanes' names are unique, " \
|
|
136
|
+
"so pick another name (or wait: `harnex result #{name}`)"
|
|
137
|
+
end
|
|
128
138
|
return unless Harnex.read_registry(root, name)
|
|
129
139
|
|
|
130
140
|
raise OptionParser::InvalidArgument, "a harnex session named #{name} is running; pick another name"
|
|
131
141
|
end
|
|
132
142
|
|
|
143
|
+
# #89: --cwd in a sibling worktree plus --worktree would branch a new
|
|
144
|
+
# worktree from the main checkout, not from --cwd; say so instead.
|
|
145
|
+
def refuse_nested_worktree!(root, workdir)
|
|
146
|
+
top = Lanes.toplevel(workdir)
|
|
147
|
+
return if top.nil? || root.nil? || Lanes.same_dir?(top, root)
|
|
148
|
+
|
|
149
|
+
raise OptionParser::InvalidArgument,
|
|
150
|
+
"--cwd #{top} is already a worktree of #{root}: drop --worktree to work there in place, " \
|
|
151
|
+
"or drop --cwd to start a new worktree from the main checkout"
|
|
152
|
+
end
|
|
153
|
+
|
|
133
154
|
def warn_uncommitted(root)
|
|
134
155
|
dirty = Lanes.dirty_paths(root)
|
|
135
156
|
return if dirty.empty?
|
|
@@ -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.
|
|
@@ -61,7 +61,8 @@ module Harnex
|
|
|
61
61
|
def follow_up(lane, message)
|
|
62
62
|
argv = @harnex_command + ["send", "--id", lane["name"], "--repo", lane["run_root"], "--message", message]
|
|
63
63
|
argv += ["--wait-done"] + (@options[:timeout] ? ["--timeout", @options[:timeout].to_s] : []) if @options[:wait]
|
|
64
|
-
|
|
64
|
+
# Marked as the lane's own follow-up, not an interjection (#92).
|
|
65
|
+
stdout, stderr, status = Open3.capture3({ "HARNEX_SEND_VIA" => "reply" }, *argv)
|
|
65
66
|
result = Lanes.last_json(stdout)
|
|
66
67
|
raise Lanes::LaunchError, "harnex could not reach lane #{lane['name']}: #{stderr.strip.lines.last}" if result.empty?
|
|
67
68
|
|
|
@@ -71,7 +71,7 @@ module Harnex
|
|
|
71
71
|
text = if @options[:json]
|
|
72
72
|
JSON.generate(lane: lane["name"], exit_code: exit_code, state: lane["state"], result: turn)
|
|
73
73
|
elsif lane["kind"] == "ask" && exit_code.zero?
|
|
74
|
-
Lanes.
|
|
74
|
+
Lanes.render_answer(lane, turn)
|
|
75
75
|
else
|
|
76
76
|
Lanes.render(lane, turn, exit_code)
|
|
77
77
|
end
|
data/lib/harnex/commands/send.rb
CHANGED
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -488,7 +488,8 @@ module Harnex
|
|
|
488
488
|
}
|
|
489
489
|
payload[:turn] = event["turn"] if event["turn"]
|
|
490
490
|
payload[:follow_ups] = event["follow_ups"] if event["follow_ups"]
|
|
491
|
-
%w[end_sha commits turn_commits model final_message final_message_path final_message_truncated
|
|
491
|
+
%w[end_sha commits turn_commits model final_message final_message_path final_message_truncated
|
|
492
|
+
earlier_replies].each do |key|
|
|
492
493
|
payload[key.to_sym] = event[key] if event.key?(key)
|
|
493
494
|
end
|
|
494
495
|
exit_code = 0
|